townlet / game /world.py
Budlee's picture
Named-place movement: atomic move_to, drop coord reasoning, 10/gather, per-archetype reward, default llama-3b, smart-mock updated
fda78e0 verified
Raw
History Blame Contribute Delete
4.28 kB
"""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}