"""The World — tile grid, building positions, message board, shell-lock semaphore. Owns WorldState. The map matches the user's sketch: - Cave (top-left) - Store / locker row (top-centre) - Well (top-right) - Town hall + message board (bottom-left) - Shell building + queue tile (bottom-right) Tile coords are (col, row), origin top-left. Movement is atomic: `move_to('cave')` puts the character onto a deterministic stand-tile adjacent to the cave in one tick (CSS transition handles the visual sweep). BFS pathing is gone — the LLM never reasons about coords. """ from __future__ import annotations from dataclasses import dataclass, field from .character import Character GRID_W = 20 GRID_H = 12 # Named tile anchors. Building tiles are non-walkable; the LLM only ever # names a place — code picks which adjacent tile the character stands on. PLACES: dict[str, tuple[int, int]] = { "cave": (2, 1), "well": (17, 1), "town_hall": (3, 10), "shell": (16, 10), "shell_queue": (15, 10), "locker_row": (10, 1), } # Targets the LLM may pass to `move_to`. `shell_queue` is kept in PLACES # as a coordinate anchor but is hidden from the move tool — agents reach # the queue by trying to enter the shell while someone else holds the lock. LLM_TARGETS: tuple[str, ...] = ("cave", "well", "locker_row", "town_hall", "shell") BUILDING_TILES: set[tuple[int, int]] = { PLACES["cave"], PLACES["well"], PLACES["town_hall"], PLACES["shell"], } # Walkable tiles a character can stand on when at a named place. Order is # stable; selection within the list is by hash(name) for deterministic per- # character placement. Multiple chars at the same place may land on the # same tile — z-index handles visual overlap, no functional impact. STAND_TILES: dict[str, list[tuple[int, int]]] = { "cave": [(1, 1), (3, 1), (2, 2)], "well": [(16, 1), (17, 2), (18, 1)], "locker_row": [(9, 1), (11, 1), (10, 2)], "town_hall": [(2, 10), (4, 10), (3, 9)], "shell": [(15, 10), (17, 10), (16, 9), (16, 11)], } def stand_tile_for(place: str, character_name: str) -> tuple[int, int]: """Pick a stable stand-tile for `character_name` at `place`. Same character at the same place always lands on the same tile.""" slots = STAND_TILES.get(place) if not slots: # Unknown place — fall back to PLACES coords if present. return PLACES.get(place, (1, 1)) return slots[hash(character_name) % len(slots)] def location_of(pos: tuple[int, int]) -> str: """Return the named place this position is at (one of the stand-tiles) or "wandering" if the position isn't a recognised stand-tile.""" for place, slots in STAND_TILES.items(): if tuple(pos) in {tuple(s) for s in slots}: return place return "wandering" @dataclass class BoardPost: author: str text: str tick: int @dataclass class ShellLine: author: str kind: str # "in" | "out" | "err" | "system" text: str tick: int @dataclass class WorldState: tick: int = 0 characters: dict[str, Character] = field(default_factory=dict) board: list[BoardPost] = field(default_factory=list) shell_lines: list[ShellLine] = field(default_factory=list) shell_lock_holder: str | None = None shell_queue: list[str] = field(default_factory=list) board_sentiment: str = "neutral" # "positive" | "neutral" | "negative" paused: bool = False def in_bounds(pos: tuple[int, int]) -> bool: x, y = pos return 0 <= x < GRID_W and 0 <= y < GRID_H def is_walkable(pos: tuple[int, int]) -> bool: return in_bounds(pos) and pos not in BUILDING_TILES def resolve_place(name: str) -> tuple[int, int] | None: return PLACES.get(name) def at_place(pos: tuple[int, int], place: str) -> bool: """True if `pos` is one of the named place's stand-tiles. Used by action guards (mine_coal must be `at_place(pos, 'cave')`, etc.). Since movement is now atomic via `stand_tile_for`, this is equivalent to `location_of(pos) == place`, but the function survives in this name for clarity at the call site.""" slots = STAND_TILES.get(place) if not slots: return False return tuple(pos) in {tuple(s) for s in slots}