18 KiB
Hex Geometry — Center + 6 Edge Slots
The single differentiator of this game's hex grid. Most hex 4Xs treat a tile as one indivisible cell. We do not. Every hex carries one center slot plus six edge slots, where each edge slot is shared with the neighbour on the other side of that edge. The shared edge is the negotiable space — it can hold a third unit, between two centre-occupants, and it is also the geometric location where combat resolves.
Every other doc that touches formations, flanking, biome boundaries, or ZOC is downstream of this one.
1. The model
___ N edge ___
╱ ╲
NW edge ┌─────────┐ NE edge
│ central │
│ area │
│ (1) │
SW edge └─────────┘ SE edge
╲ ╱
‾‾‾ S edge ‾‾‾
- Inner hexagon = the central area; holds 1 unit (the home / leader).
- Ring of 6 trapezoidal regions between the inner hex and the outer perimeter = the 6 edge slots. Each trapezoid corresponds to one of the outer hex's six edges.
- Each edge slot is shared with exactly one neighbour — the neighbour on the other side of that outer edge.
Counts:
- Per hex (exclusive): 1 centre + 0 fully-owned edges = 1 slot.
- Per hex (neighbourhood): 1 centre + 6 shared edges = 7 slot positions.
- Per map of N hexes: N centres + 3N edges (since each edge is shared 1:1, total edges = 6N/2 = 3N) = 4N slots.
2. Why "edge slot" and not "sub-tile partition"
Earlier framings tried to slice a hex into halves (top/bottom trapezoids) or thirds (square + 2 triangles). Both fail to support the actual design intent — namely, a third unit positioned between two centre-occupants. That position is not inside either tile; it is geometrically on the shared boundary. Modelling the shared boundary as a slot that both adjacent hexes can reach is what gives the design its leverage.
The edge slot is simultaneously a unit position and the combat-resolution location. That dual role is the whole reason this geometry buys us anything — see §4 and §5.
3. Direction-to-edge mapping
The simulator's neighbour directions are indexed 0..5 in mc-core/src/algorithms/hex.rs:11-19:
| Direction index | Name | Axial vector | Edge slot |
|---|---|---|---|
| 0 | E | (+1, 0) | E edge (shared with E neighbour) |
| 1 | NE | (+1, −1) | NE edge (shared with NE neighbour) |
| 2 | NW | (0, −1) | NW edge (shared with NW neighbour) |
| 3 | W | (−1, 0) | W edge (shared with W neighbour) |
| 4 | SW | (−1, +1) | SW edge (shared with SW neighbour) |
| 5 | SE | (0, +1) | SE edge (shared with SE neighbour) |
Each direction names exactly one edge of the hex; that edge is the slot shared with the neighbour in that direction. The 6 slots are 1:1 with the 6 directions.
Edge identity (canonical form). An edge is shared between two hexes. To address an edge unambiguously across both sides, use the canonical form (min(hex_a, hex_b), direction_from_min) where hex_a < hex_b by some stable id ordering. The two adjacent hexes refer to the same edge slot via different direction labels (E from one side is W from the other).
4. Slot semantics
| Slot | Owned by | Occupants | Purpose |
|---|---|---|---|
| Centre | Host hex exclusively | 1 unit | Home position; main body / leader |
| Edge | Shared between 2 hexes | 1 unit (default — see §10) | Forward picket, skirmisher, liaison, OR an attacker engaged at the boundary |
A unit at a centre slot is "in" its hex unambiguously. A unit at an edge slot is between two hexes — it has been deployed forward from its parent hex but has not entered the neighbour's hex.
Provenance. An edge unit is aligned with the hex it was deployed from. That alignment determines:
- Which player owns it
- Which terrain bonus applies (it draws cover from its parent tile — see §7)
- Which formation it belongs to (the formation in its parent hex's centre, when relevant)
5. Combat — the edge is the engagement membrane
When the unit at hex A's centre attacks the unit at hex B's centre, the engagement resolves at the shared A↔B edge. This is true whether or not that edge slot is occupied by a third unit.
| Combat type | Engagement point | Whose terrain applies |
|---|---|---|
| Melee | At the shared A↔B edge | Attacker rolls with A's terrain; defender rolls with B's terrain |
| Ranged | Projectile passes over the shared edge from A-centre to B-centre | Defender's edge cover applies (B's terrain on the A-facing edge) |
| Magic | Same as ranged unless the spell is contact |
Same |
Edge occupants are in the engagement. If a third unit γ stands on the shared A↔B edge, an A-vs-B melee clash necessarily passes through γ's position. The default is:
- γ's owner = A → γ joins A's attack; defender hits γ first, then α (A's centre)
- γ's owner = B → γ joins B's defence; attacker hits γ first, then β (B's centre)
- γ is unaligned → γ is overrun: it takes damage from both sides on the first contact
This is the geometric basis for forward pickets, skirmisher screens, and taking the enemy's edge before assaulting their centre.
Forest cover on the attack — explained naturally. A forest unit at A's centre attacking B's plains is engaging at A's forest-perimeter edge. The unit never leaves the trees. Cover applies. Other hex games handwave this rule; here it is geometric.
6. Flanking
Flanking is discrete and edge-counted. A defender at hex B is flanked when attackers approach from two or more non-adjacent edges simultaneously.
| Attacker pattern | Effect |
|---|---|
| 1 attacker at one edge | No flanking |
| 2 attackers at adjacent edges (e.g., NE and E) | Mild flanking — overlapping engagement zones |
| 2 attackers at non-adjacent edges (e.g., E and W, or N and SE) | Full flanking trigger |
| 3+ attackers across 3+ edges | Encirclement |
The defender at hex B fights effectively in one edge direction at a time. Forcing it to face two simultaneously costs combat efficiency.
7. Movement
- Units move centre → adjacent centre, traversing the shared edge. Movement cost is per-hex (terrain-derived), not per-edge.
- An edge slot is not a pathfinding stop (default — confirm or revise). The edge is a positional/combat object, not a transit waypoint.
- A separate deploy action allows a unit at a centre to move forward to its own hex's edge slot (one step of "stepping out" without leaving its tile's domain). A withdraw action reverses this.
- An edge unit blocks centre-to-centre movement through that edge (default — confirm or revise). Cheap units used as pickets become real ZOC.
Implication: pathfinding (mc-core/src/algorithms/pathfinding.rs) keeps its existing per-hex cost model. Edge-occupancy adds blockage as a new cost factor at the per-edge level.
8. Biome boundaries — edges as derived blend zones
Each hex carries one terrain at its centre (plains, forest, hill, …). Each of its 6 edges carries its own terrain, derived as a blend of the host's centre terrain and the neighbour's centre terrain. Edges are not terrain-less geometric objects; they are transitional ecotones whose biome reflects what happens at the boundary between two adjacent tiles.
Example — a plains tile with neighbours [mountain, water, water, plains, plains, forest] carries:
| Edge | Centre terrain | Neighbour terrain | Edge terrain (blend) |
|---|---|---|---|
| 1 mountain edge | plains | mountain | foothills |
| 2 water edges | plains | water | shore |
| 2 plains edges | plains | plains | plains (no transition) |
| 1 forest edge | plains | forest | grass-fringe |
This is per-edge ecotone modelling. A hex adjacent to a coast has a shoreline. A hex below a mountain has foothills on one side. Same hex, different edges, different biomes.
Symmetry
blend(A, B) == blend(B, A). The same physical edge has the same character viewed from either side. The mountain tile sees its plains-facing edge as foothills; the plains tile sees its mountain-facing edge as foothills.
Combat and movement at edges
- Edge unit's terrain bonus comes from the edge terrain (the blend), not from the parent tile alone. A unit on a forest↔plains edge gets grass-fringe cover — partial, not full forest cover.
- Edge crossing cost in pathfinding can incorporate the edge terrain (foothills cost more to cross than plains).
- Combat at an empty edge — each combatant rolls with their own centre's terrain (unchanged from §5); the blend only applies when a unit occupies the edge.
Where the blends are defined
The blend table is data, not code: public/resources/tiles/terrain_blends.json (new — Stage 6) lists each unordered terrain pair and the resulting edge terrain. The blend terrains themselves (foothills, shore, etc.) live alongside other tile definitions in public/resources/tiles/land_blends.json per the post-p1-40 unified data architecture. Pairs not in the table default to the centre terrain unchanged.
River and other features layer on top of the blend
Existing river edges (mc-core/src/grid/mod.rs:97 river_edges) modify the edge further. A forest↔plains edge with a river on it is grass-fringe + river, accruing both biome and feature effects. This unifies the existing edge-feature data (rivers, roads — Stage 6) with the new edge-terrain data.
9. Zone of Control (ZOC)
A unit at a centre projects ZOC into its 6 edge slots. A unit at an edge slot projects ZOC into:
- Both adjacent centres (it sits between two centres and contests both)
- The two edges adjacent to its edge (sharing a vertex with it) — (default — confirm or revise)
Edge units therefore meaningfully extend a tile's reach. A picket on the NE edge denies an enemy's NE-direction approach.
10. Stacking and edge occupancy
Centres: Single unit per centre. No stacking.
Edges: Single unit per edge slot (default — confirm or revise). The edge is a single occupant slot; the first unit to deploy there holds it. An adjacent enemy must displace it (via combat) before the slot becomes available.
A two-occupant edge ("opposed engagement" / "duel") is conceivable but doubles bookkeeping. Default is single-occupant; we can lift this later if a duel mechanic is wanted.
11. Formations
A formation occupies one centre + a subset of its 6 edge slots, all anchored on a single hex.
| Pattern | Centre | Edges occupied | Total units |
|---|---|---|---|
| Solitary | 1 | 0 | 1 |
| Picket | 1 | 1 | 2 |
| Vanguard | 1 | 3 (one half) | 4 |
| Encircled | 1 | 6 | 7 |
The existing FormationShape::{Line, Column, Wedge, Diamond} variants in mc-core::Formation map to occupation patterns over (centre + chosen edges). The mapping in combat/FORMATIONS.md walks the four shapes through the edge-set they fill.
A formation pivots by rotating which edges are occupied. Rotation re-binds edge slot identities relative to the formation's facing — the same N units now hold a different set of edges.
12. Considered alternative — sub-tile partition (the previous misstep)
An earlier design framed the hex as two trapezoid halves sharing a 2-cell "spine" (4 + 4 − 2 = 6). That model was geometrically valid but solved a different problem — it partitioned the interior of one hex into two zones rather than modelling the boundary between two hexes. It could not represent the third-unit-between-two-centres case directly. It is documented here so the next person to suggest it finds the answer.
The centre + edge-slots model wins on three counts:
- The third-unit slot is geometrically on the boundary, where it should be
- Combat-space and unit-position collapse into one slot
- ZOC and forest-cover-on-attack derive naturally from the same geometry
13. Implementation surface
The model in this doc is the target spec; the code is partial.
| Concern | File | Status |
|---|---|---|
| Hex coord math (axial / cube / odd-q offset) | src/simulator/crates/mc-core/src/algorithms/hex.rs |
✅ Implemented; matches HexUtils.gd and HexGrid.ts |
Direction indices 0..5 |
mc-core/src/algorithms/hex.rs:11-19 |
✅ Single source of truth (AXIAL_DIRECTIONS) |
| Odd-q offset neighbour table | mc-core/src/algorithms/hex.rs::ODD_Q_NEIGHBORS |
✅ Derived from AXIAL_DIRECTIONS; pinned by odd_q_table_agrees_with_axial_directions. Tile fields that store axial direction indices (e.g., wind_direction, flow_out) round-trip through either table without drift. |
| Edge identity + occupancy | mc-core/src/grid/edge.rs |
✅ EdgeId(min_hex, dir_from_min), EdgeOccupant { unit_id, aligned_to, owner_player_id }, EdgeFeatures { river, road, bridge, wall_owner }. Sparse storage in GridState::edges and GridState::edge_features. |
| Edge passability + move validation | mc-core/src/grid/mod.rs |
✅ GridState::is_edge_passable_for(edge, player_id), validate_centre_to_centre_move(from, to, player_id) -> Result<EdgeId, MoveBlockedReason>. Wall and occupant rules compose. |
| Edge terrain (blends — §8) | mc-core/src/grid/terrain_blend.rs + public/resources/tiles/terrain_blends.json + public/resources/tiles/land_blends.json |
✅ TerrainBlendTable::lookup(host, neighbour) with canonical-pair sort. 10 canonical Game 1 ecotones. |
| River generation | mc-mapgen/src/lib.rs::generate_rivers (Stage 7.5) |
✅ Flow downhill from high-moisture / high-elevation sources to the sea. Symmetric edge marking. Deterministic via PCG32. |
River-edges → edge_features migration |
mc-core/src/grid/mod.rs::migrate_river_edges_to_edge_features |
✅ Idempotent, symmetric, preserves non-river features. Called by mc-mapgen post-generation. |
| Formation slot model | mc-core/src/formation.rs |
✅ FormationSlot { Centre, Edge { dir } }, Formation::slot_assignments, assign_slot, centre_unit, edge_unit(dir), occupied_edges(). Additive — legacy formations still work. |
| Engagement interceptor lookup | mc-core/src/grid/mod.rs::engagement_interceptor |
✅ Option<&EdgeOccupant> between attacker and defender centres. Symmetric across attack direction. Combat resolver wiring (Stage 2b) is the remaining work. |
| ZOC reach geometry | mc-core/src/grid/edge.rs::zoc_from_centre / zoc_from_edge |
✅ Centre projects into 6 own edges; edge projects into 2 centres + 4 vertex-adjacent edges. mc-turn overlay pass (Stage 4 wiring) is the remaining work. |
| Combat resolver damage routing | src/simulator/crates/mc-combat/src/resolver.rs:65-81 |
⚠️ HP scales by formation_count; does not yet call engagement_interceptor to route first-tick damage through edge occupants. Stage 2b. |
| ZOC overlay (per-player projection) | mc-turn |
⚠️ Pure-geometry primitives ready; mc-turn pass that builds the per-player projection map from Formation + GridState::edges is pending. Stage 4 wiring. |
| Pathfinding (Rust A*) | (does not yet exist in Rust) | ⚠️ No Rust A* found in mc-core/src/algorithms/; existing pathfinding lives GDScript-side. validate_centre_to_centre_move is ready for the future Rust pathfinder to call. |
| Renderer | src/packages/guide/src/components/climate-sim/HexGLRenderer.tsx |
✅ Hex tile rendering correct; no inner-hex / edge-slot overlay (debug-only feature pending) |
| AI evaluator | src/simulator/crates/mc-ai/src/evaluator.rs:414-480 |
⚠️ Scores formations by size + threat; does not yet model edge-slot positioning |
| GDExtension bridge | src/simulator/api-gdext/src/lib.rs::GdGameState::engagement_interceptor / validate_centre_to_centre_move |
✅ Two Godot-callable primitives expose the edge model to GDScript. Combat preview UI consults engagement_interceptor; movement preview consults validate_centre_to_centre_move and branches on reason (not_adjacent / wall_blocks / edge_occupied / adjacent_clean). |
Test coverage (cargo test -p mc-core --lib): 70 tests (was 30 before this work) covering edge identity, passability, move validation, engagement interception, ZOC reach (centre + edge), formation slot model, blend-table lookup with production-JSON round-trip, cross-file schema guard (every blend → defined terrain), river-edges migration. cargo test -p mc-mapgen --lib: 24 tests including 3 river-determinism guards.
Successor work: combat resolver wiring (Stage 2b), ZOC overlay pass in mc-turn (Stage 4 wiring), formation reflow rewrite (Stage 5 follow-up), Stage 6 wiring (combat / pathfinding consult TerrainBlendTable::lookup).
14. Defaults to confirm or revise
These eight decisions are baked into the spec above as defaults. They are mine, not the user's expressed wishes — flag any to revise before the spec propagates further.
| # | Decision | Default | Section |
|---|---|---|---|
| 1 | Edge slot occupancy | Single occupant | §10 |
| 2 | Edge unit blocks centre-to-centre movement | Yes | §7 |
| 3 | Edge slots are pathfinding stops | No (deploy/withdraw is a separate action) | §7 |
| 4 | Edge unit's terrain bonus source | Edge terrain (blend of host + neighbour) | §8 |
| 5 | Edge unit damage order in combat | Hit first (before centre) | §5 |
| 6 | Edge ZOC reach | Two adjacent centres + two adjacent edges | §9 |
| 7 | Flanking trigger | Two attackers from non-adjacent edges | §6 |
| 8 | Hex episode scope | Universal across Game 1, 2, 3 | (page placement) |
See also
combat/COMBAT_SYSTEM.md— armor/attack matrix; references this doc for positional damagecombat/FORMATIONS.md— how the four formation shapes map to edge-set occupationsterrain/TERRAIN_SYSTEM.md— references this doc for biome-boundary semantics.project/designs/hex-edge-slots.md— annotated diagram for engineers/designers.project/designs/app/src/pages/HexFormation.tsx— interactive React mockup at route/hex