| """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 |
|
|
| |
| |
| 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), |
| } |
|
|
| |
| |
| |
| 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"], |
| } |
|
|
| |
| |
| |
| |
| 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: |
| |
| 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 |
| 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" |
| 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} |
|
|