veratown-prototype

Shared Tile And Region Geometry

Status: Implemented; production configuration rollout remains operator-reviewed. Date: 2026-10-21

Goal

Support exact irregular map regions while retaining simple single-tile references and all existing rectangle documents. Location checks, helper task checks, and door occupancy checks must agree on membership.

This is a cross-cutting geometry change, not a new observation authority, pathfinder, or door permission system.

Related documentation:

Current behavior and limitations

  1. Keep direct coordinates for doors, switches, destinations, and other points.
  2. Keep existing rectangle storage unchanged.
  3. Add exact tile regions represented by canonical row spans.
  4. Share transport-independent geometry contracts and containment in core/.
  5. Treat a one-tile region as valid, but do not automatically convert points into regions or regions into points.
  6. Defer arbitrary polygon input and interactive drawing. Exact regions already cover L-shapes, holes, and disconnected pieces without polygon mathematics.

No new dependencies, environment variables, collections, per-tile documents, or BC packets are required.

Geometry contract

Introduce a small core geometry module containing readonly discriminated types for points, rectangles, and tile regions, plus a pure containsTile(geometry, position) operation.

Use a core-owned tile position type rather than importing BcMapPosition into core. Adapt persisted shapes at the feature boundary. BC positions can satisfy the core coordinate contract structurally.

Keep persistence validation in the map model/repository boundary. Do not make core depend on a feature’s Zod schema or MongoDB types. Share pure shape invariants where necessary instead of maintaining a second handwritten rectangle guard in the admin service.

Coordinate semantics

Persisted forms

Point locations keep their existing fields:

{ "x": 20, "y": 10 }

Existing rectangle values in region and insideRegion remain valid:

{
    "TopLeft": { "X": 20, "Y": 10 },
    "BottomRight": { "X": 25, "Y": 13 }
}

Add this alternative value for those same region fields:

{
    "kind": "tiles",
    "rows": [
        { "y": 10, "spans": [[20, 25]] },
        { "y": 11, "spans": [[20, 25]] },
        { "y": 12, "spans": [[20, 22]] },
        { "y": 13, "spans": [[20, 22]] }
    ]
}

This example is an L-shaped region. Multiple spans in a row represent gaps; omitting enclosed tiles represents a hole. A one-tile region is one row with one span whose start equals its end.

The public JSON marker is kind: "tiles"; row spans are its storage encoding, not a separate geometry type. Runtime normalization gives legacy rectangles an internal discriminator without rewriting their stored representation.

Validation and bounds

Start with direct row/span lookup: the map is only 1,600 tiles. Do not add a global geometry cache. If profiling later justifies compiled membership sets, scope them to a configuration revision and replace them on reload.

Integration and behavior preservation

Surface Required change
Map model and repository Validate both region forms on reads and writes; preserve legacy normalization, document limits, room scoping, and reload errors.
Map runtime Replace rectangle-only checks with shared containment for location transitions and door insideRegion.
Helper task executor Use the same location-to-geometry conversion and core containment; preserve fresh observation and task authorization requirements.
Location administration Add optional region fields to draft/read/edit contracts; region: null clears a region on edit. Include geometry in find() results.
World location commands Add explicit region create/edit/clear support using existing command/session patterns; retain room-admin checks, input limits, and private replies. Do not require pasting a 1,600-tile payload into chat.
Door administration Accept both region forms for insideRegion through the authoritative validator; preserve existing null/removal behavior.
Display/help Show shape, tile count, and bounds; label bounds as a summary, not membership. Retain bounded whisper output.
Location-event consumers Keep the existing point/region event classification. A one-tile region remains a region. Verify casino/shop and other access gates still revoke access on exits.
Offline analysis Convert reviewed exact cell lists to canonical row spans without altering the source analysis or treating inferred labels as policy.
Operator tooling Add a bounded, dry-run-first manifest import path so large shapes need not travel through chat. Reuse repository validation and isolation guards.

Preserve region precedence when a location also has x/y. Point coordinates may remain an explicit destination/anchor; never infer a teleport destination from a region’s first tile or bounding-box center. A region alone must not silently become a teleportable point.

Configuration reload must recompute membership using the existing observed positions and emit the corresponding entry/exit changes. Geometry changes must not synthesize fresh BC position evidence, mutate BC snapshots, or relax helper scope/lease checks. Unknown positions remain unknown under existing policies.

Door occupancy continues to use the existing tracked-character policy. An excluded courtyard or adjacent street must not defer closure merely because it falls within the region’s bounding rectangle. The door itself remains one tile; coordinating multi-tile doors is a separate design.

Offline export and review

Extend the analysis workflow with an explicit region-candidate export:

  1. Convert exact cells arrays to row spans and verify round-trip membership.
  2. Report tile count, bounds, disconnected components, source catalog revision, and a map-content fingerprint.
  3. Require a reviewed mapping from transient analysis IDs such as R06 to stable room-scoped location keys or door keys. Never derive durable identities solely from flood-fill numbering.
  4. Preview overlays and document changes before applying a bounded manifest to the selected runtime database with explicit --apply.
  5. Reject missing/ambiguous target keys and fingerprint mismatches against the explicitly selected source map. Explain that this verifies configuration provenance, not the current live BC room.

The importer updates only reviewed geometry fields and normal update metadata. It must not infer permissions, keypad codes, unlock durations, enabled state, or which side of a door is inside. No database writes occur in the analyzer itself. Keep production/test mappings exact and do not extend the legacy source-database copy allowlist as part of this change.

Backward compatibility and rollout

No bulk data migration is required. Existing points, rectangles, null door regions, stable location keys, permissions, and action outcomes retain their meaning.

  1. Deploy dual-format readers and administration support to every supervisor and helper before writing tile regions.
  2. Qualify reviewed regions in veratown_prototype_test with dedicated accounts.
  3. Back up affected configuration documents and review exact tile overlays.
  4. Apply only explicitly reviewed geometry to veratown_prototype.

Old binaries do not understand tile regions. Before rolling back to them, restore the backed-up documents or remove the new region configuration through a deliberate reviewed change. Never approximate an irregular region with its bounds as a rollback shortcut. Mixed-version deployments must not receive new tile-region documents.

Implementation phases

  1. Implemented — shared geometry and dual-format reads: core contracts, canonical span validation, persistence parsing, runtime/helper containment, admin display, and regression tests. Existing rectangles and point destinations retain their stored meaning.
  2. Implemented — configuration surfaces: location contracts and guided commands, door validator reuse, region clearing, bounded manifest import, and authorization/error tests.
  3. Implemented — analysis integration: candidate export, stable-key review mappings, fingerprint checks, exact overlay preview, and round-trip tests.
  4. Operational rollout remains: qualify in the test runtime, verify every supervisor/helper binary before configuring tile regions, review backups and overlays, then explicitly approve production configuration changes.

Keep each unit tested and use a PR for changes to core or operator scripts. Update the architecture guide, map/helper guides, command help, analysis guide, root README, and documentation index as their implemented contracts change.

Acceptance criteria

Deferred work and effort

This is a moderate cross-layer change: the membership algorithm is small; configuration validation, consistent authorization, and safe rollout are the larger parts. Analysis export and import are separate deliverables, not reasons to delay shared runtime correctness.

Arbitrary polygon input is deferred. If added later, convert polygons to exact tile membership at configuration time. Define tile-edge coordinates (0-40), tile-center inclusion, boundary behavior, self-intersection rejection, and holes before accepting vertices. Do not reuse the 0-39 tile-position validator for polygon corners.

Also out of scope: visual polygon editors, GeoJSON/geospatial indexes, collision or pathfinding changes, automatic region tracking after map edits, automatic door policy generation, and per-tile database documents.