Closed grandsong closed 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 👍
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.
Two things came to my mind for comparision:
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.
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.
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.
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.
Building or enviromental objects like forests/woods.
Things are dropped here. They can be collected.
Player's heroes versus enemies.
For movement preview, shows a path for a selected character to walk from one tile (position) to another.
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.
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:
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).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?
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.
As to your reasons against prop grid
...
grid
.grid
.Think of DOM element. You can
document.createElement()
;remove()
method of an element
(which is new; in old browser, I had to call its parent's removeChild(thisElement)
...)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
.
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.
However, it is true some job is required for you to automatically
delete hex.grid
in a new methoddelete
ofGrid
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?
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:
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 usinggrid.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?