flauwekeul / honeycomb

Create hex grids easily, in node or the browser.
https://abbekeultjes.nl/honeycomb
MIT License
622 stars 57 forks source link

[Discussion] How to couple instances of the grid and its hexes nicely? #18

Closed grandsong closed 5 years ago

grandsong commented 5 years ago

I was making a map of hexes. Sometimes, I need to start with a given hex and interact with its grid, or, other peer hexes.

For example, to get neighbors of the hex. My workaround now is as follows:


var main_grid = null;

Hex = Honeycomb.extendHex({
  .
  .
  neighbors: function() {
    return main_grid.neighborsOf(this);
  },
  .
  .
}
.
.
grid = Grid.rectangle({ ... });

main_grid = grid

I know it is "neat" to decouple grid and hexes, as not all hexes must belong / be bound to a grid.

But when hexes ARE bound to a grid and they seldom or never jump around, they are fixed logically.

In this case, it is natural to couple the grid with them as to have a more "object-oriented" way of coding.

That's why I made a Hex()::neighbors() instead of using grid.neighborsOf(hex).

And a thought experiment: If I had two grids, it would be wrong to calculate grid2.neighborsOf(hex) but the "freedom" of decoupling would "allow" me to make such mistakes, since there was no relation fixed in code and the correct relation exists only in my mind.

That's why, when dealing with frontend programming, I am not a big fan of "functional programming"...


Back to the topic.

My workaround (with a semi-global variale main_grid) works for me, but not in a nice way, I'm afraid.

I'm not sure how to improve it.

What do you think?

And moreover, perhaps, someday you may offer an out-of-box solution in your lib?

flauwekeul commented 5 years ago

This idea of having both a Grid#neighborsOf() and Hex#neighbors() crossed my mind a few times when working on Honeycomb. Some time ago I only had Hex#neighbors() (not Grid#neighborsOf()), but I moved it to Grid because it would add an extra feature: when Grid#neighborsOf() was called with a hex that was on the border of a grid, only the hexes present in the grid would be returned. This is actually how Grid#neighborsOf() works: it only returns hexes present in the grid it's called on.

Having Hex#neighbors() only return its neighbors that are present in the grid wouldn't make much sense, because a hex might not be "living" in a grid. It has no notion of a grid, it's just a hex.

I could add a Hex#neighbors() that would always return all its neighbors, regardless if they exist in the grid the hex might live in. That might be confusing for users though. A user might expect it to work the same as Grid#neighborsOf(). That's why I've decided earlier to only have a Grid#neighborsOf(). But I'm open for discussion 👍

grandsong commented 5 years ago

Hex()#neighbors() here is just an example in real practice.

It immediately calls main_grid.neighborsOf(this). It does not do anything different. Nor should it do.

You can replace neighborsOf with other relational methods like distance, hexesBetween.

The core issue here is: When we (well, the program) is given a hex and need to process with its grid, where and how to get to know the grid?

I used main_grid and my hexes so far are present in it. So Hex()#neighbors() can include it, then call its neighborsOf.

If later I have more Hex factories and grids, which are very likely, I will have trouble to make sure neighborsOf/hexesBetween/etc is called correctly.

Eg, I may happen to write grid2.neighborsOf(hex1) but hex1 are present in grid1.

(BTW, what will happen in this case?)


A bit ideas might help to show the differences & probably benefits.

You may provide Hex()::getGrid(). It will read prop grid of a hex. Users can read grid directly only if they are completely sure it is not undefined and is a valid grid.

When a grid is created, it sets each hex member with grid to it, before the event/callback onCreate is triggered.

No more need to relate it with these hexes later, again and again.

No chores. And no room for mistakes.

As to methods like Hex():neighbors(), they are just save some otherwise repetitive code, and may include some validations. They are not essential for your lib. As long as hexes have prop grid, this.grid.xxxx() is already safe and simple enough in users own code.

grandsong commented 5 years ago

Two things came to my mind for comparision:

Playlist and Songs

In a music (or video) player, songs have very loose relations to playlists:

So, a song cannot nor need to have a prop about playlist.

If you need to know which song is next to a given song, always ask the playlist.

HTML Elements

In the other end of spectrum, we have these elements (nodes) nesting one and another, composing very big and complex trees.

Every element has prop parentElement.

Without it, all frontend developers would be damned.

Besides, they have nextSibling/nextElementSibling/previousElementSibling.


I think the relations between hexes and grid is somewhere in the middle of the spectrum.

grandsong commented 5 years ago

I'm making a turn-based game.

In my vague plans, everything is positioned in hexes, and there will be 5 + x layers of grids.

1. The bottommost one is the ground grid.

All hexes of it are fixed. They never change or move.

But if there is a map editor, well, this layer will be like the following ones.

2. Fixtures

Building or enviromental objects like forests/woods.

3. Loots

Things are dropped here. They can be collected.

4. Characters

Player's heroes versus enemies.

5. The path

For movement preview, shows a path for a selected character to walk from one tile (position) to another.

x: the road layers

Actually, they are a special category.

Every character has its own road layer. Its hexes are solely markers about whether the respective tiles allow that character to walk into. A tile can be passable for some characters while not for others. For example, birds can fly over water pools and amphibians can swim but others have to walk around.

These grids are virtual. The player don't see them.


All grids are the same size.

For one tile, hexes from different layers overlap.

It will be no joke if I fail to manage all the relations well.

flauwekeul commented 5 years ago

Grid#neighborsOf() requires a hex as its first argument, it doesn't have to be a hex present in that particular grid. It would be better if the method accepted a cube: an object with the 3 cube coordinates (q, r and s). Or even better: accept a cube or point, basically anything that is a position in the grid. I'm planning to make that improvement in the next major update.

I see the advantages of having some property in each hex that points to the grid it lives in. But I still see some issues:

  1. When a hex is created outside the context of a grid, that property doesn't make much sense and seems out-of-place. However, I expect > 95% of all hex creation to be done in the context of a grid.
  2. When a hex is moved/copied to another grid, its grid property needs to be updated too. I'm afraid this would add quite some "magic" to the library to keep track of each hex's parent grid. This might impact performance too (when dealing with very large grids).
  3. Similar to the previous problem: a hex could belong to more than 1 grid:
    const grid1 = Grid.rectangle({ width: 3, height: 3 })
    const grid2 = Grid(...grid1)
    console.log(grid1[0], grid2[0], grid1[0] === grid2[0]);
    // {x: 0, y: 0} {x: 0, y: 0} true

    What value would the grid property have? Should I add change it into a grids (plural) property?

grandsong commented 5 years ago

Yes, I vote for filtering returned hexes by "grid-presence". Or, better, give Grid a prop allowsNonePresentHexes, which is false by default. So, if, which I believe will be very rare, a lib user really needs to skip validations and enjoy freedom, they will be happy.

So here's the platitude: the less rules/validation, the more freedom, but, more errors as well.

Grids needs to do enough validations and throw errors to avoid bug huntering.

And as I said, if hexes are not bound with grids, in many-to-one relations, it is developers' responsibility to remember and tell which hexes belongs to which grids. My attempt to save me was to use semi-global varibles as a "bridge", but I prefer more direct way. (However, if I set hex.grid = grid, the grid instance will lose all methods magically... so the "bridge" main_grid is the one way right now.) When in the future I have more grids, I still need to remember if a hex is in grid-2 etc whenever I call methods. I won't enjoy a future like this.

grandsong commented 5 years ago

As to your reasons against prop grid ...

  1. A standalone hex of course doesn't have prop grid.
  2. When a hex is remove, of course it loses its grid.

Think of DOM element. You can

So, your worries are not strong enough.

However, it is true some job is required for you to automatically delete hex.grid in a new method delete of Grid as well as in mutating methods like splice.

  1. Ehh, more than one grid? really?

What practical value can you think of?

And if such rare case happens, cannot the user find a workaround?

Functional programming clones and maps arrays/objects fanatically. Why not borrow as little from their ways when fit?

And, in my demo, Hexes can emit events and thus let many things (like Grids) to know changes.

flauwekeul commented 5 years ago

However, it is true some job is required for you to automatically delete hex.grid in a new method delete of Grid as well as in mutating methods like splice.

Exactly. I think "some job" is actually "a lot of magic" 😅Currently, Grid extends Array. So removing a hex from a grid is as simple as grid[3] = null or grid.splice(3, 1). Before finally deciding to let Grid extend Array, I've experimented with (native) proxies. Using a proxy is the only way of detecting the aforementioned ways to remove hexes from grids. But this means a proxy would be the API for users and that would be a very poor user experience.

I can't think of a reason why a hex would belong to multiple grids, but the possibility is there. It's easy for anyone to share the same hex with multiple grids. Someone else might have valid reasons to do so, reason we can't think of right now. Isn't honeycomb responsible for keeping the grid property work properly even when users share hexes among multiple grids?