Spatial Coordination
The spatial-coordination layer provides game-agnostic derived indexes and coordination primitives. Applications supply semantic keys, scores, entity identities, and policies; tess owns deterministic grouping and bounded scratch. It does not define rooms, factions, combat meaning, or pawn AI.
Area Index
include/tess/spatial/area.h groups topology regions by a caller-supplied
nonzero std::uint64_t key. This deliberately builds on RegionGraphT
instead of adding a second tile flood:
flowchart LR
Graph["RegionGraphT"] --> Grouper["Caller region-to-key function"]
Grouper --> Areas["Area summaries"]
Graph --> Portals["Region portals"]
Portals --> Links["Undirected area connections"]
AreaId is 1-based within one built index; invalid_area_id is zero.
AreaSummary reports the semantic key, region and tile counts, and unioned
world bounds. AreaConnection canonicalizes an undirected pair of area IDs
and counts the directed region portals crossing between them. Multiple regions
with the same key become one area even when disconnected; connectivity remains
available from the underlying region graph. Returning key zero omits a region.
build_area_index(graph, grouper, scratch, index) assigns IDs by ascending
semantic key, so callback and graph traversal order cannot change identities.
It supports dense and sparse graphs and uses AreaIndexScratch for reusable
keys and edge sorting. AreaIndex::reserve plus AreaIndexScratch::reserve
make a warm rebuild allocation-free when capacities suffice.
AreaBuildResult reports the resulting counts and an AreaBuildStatus of
Built or TooManyAreas if 32-bit identifiers cannot represent every unique
key.
Lookups accept either a RegionRef or a graph plus world coordinate. An index
is bound to the exact graph object and its monotonic revision().
is_valid(graph) is O(1), and coordinate lookup adds only region lookup plus
the area's ordered-region lookup; neither rescans portals. Both reject an
index after the graph changes, so rebuild it after topology maintenance. The
graph-aware calls borrow that exact object: it must remain alive, and callers
must clear or rebuild the index before destroying the graph or constructing a
different graph in the same storage. Summaries and RegionRef lookup remain
index-owned.
This is an area substrate, not a room model. The application decides whether a key means a room, district, biome, work zone, tactical sector, or nothing at all, and owns names, membership policy, ownership, statistics, and lifecycle.
Tactical Assignment
include/tess/spatial/tactical_assignment.h provides the deliberately named
assign_tactical_candidates_greedy primitive. TacticalRequest carries a
stable requester ID, origin, and priority; TacticalCandidate carries a
stable candidate ID, position, and capacity. The caller's scorer returns a
TacticalScore whose feasibility flag filters illegal pairs and whose signed
value is lower-is-better.
The pass processes higher priorities first, then lower requester IDs. It picks
the lowest score with candidate ID as the final tie break and decrements
capacity. TacticalAssignment rows remain aligned with request input order,
including explicit unassigned rows. TacticalAssignmentResult reports the
assigned count and TacticalAssignmentStatus (Complete, Partial, or
InvalidInput for duplicate stable IDs). Returned rows borrow
TacticalAssignmentScratch until its next pass; reserve makes warm assignment
allocation-free.
The algorithm is intentionally greedy, not a claim of globally minimum-cost matching. It is suitable for cover slots, work positions, rally points, or other scarce candidates where applications want a predictable fast baseline. Applications needing a global optimum can use the same request, candidate, and score vocabulary with their own matching solver.
Local Move Coordination
include/tess/spatial/local_coordination.h resolves a caller-generated set of
nearby destination options without owning steering or path planning. Each
LocalMoveRequest identifies an agent, current coordinate, priority, and a
non-overlapping range in a flat LocalMoveOption array. The caller's predicate
decides whether each option is currently legal, so world bounds, movement
class, occupancy, reservation, clearance, and application rules stay outside
the generic resolver. The predicate must be deterministic and side-effect
free; the resolver evaluates it exactly once per referenced option.
Higher priority moves claim first, with stable agent ID as the final request
tie break. Each request chooses the lowest preference among unclaimed feasible
destinations and breaks equal preferences lexicographically by coordinate.
LocalMoveDecision rows remain aligned with input requests and explicitly
report LocalMoveDecisionStatus::Reserved or
LocalMoveDecisionStatus::Wait. resolve_local_moves returns a
LocalCoordinationResult whose LocalCoordinationStatus distinguishes
complete, partial, and invalid inputs. A reserved decision is permission for
the caller to form and validate its normal MovementIntent; it does not
mutate a world or bypass commit-time validation.
The same pass emits coordinate-sorted LocalCongestion summaries. demand
counts distinct requests with a feasible option for that coordinate and
reserved counts accepted claims. Callers may publish those counts into a
bounded congestion field or diagnostics without a hidden full-world product.
Returned spans borrow LocalCoordinationScratch; reserve makes the warm pass
allocation-free. The resolver targets bounded local cohorts: duplicate-option
filtering is quadratic in the largest per-request option count, and ordered
claim insertion is quadratic in the request count. Callers with unbounded
crowds or option lists must partition them before resolution.
This is deterministic local arbitration, not continuous steering, collision prediction, formation control, or globally optimal multi-agent pathfinding. The conservative caller predicate can reject currently occupied destinations, which also rejects swaps and move-through cycles under the existing movement commit contract.