semantique / levels /_base.py
Ben Blaker
feat(game): escalating, per-target hint ladder (#73)
66ebc4d unverified
Raw
History Blame Contribute Delete
3.03 kB
"""The Level schema shared by every board.
A level is self-contained: the grid geometry the browser draws plus the
candidate `labels` the judge scores among. Adding a board is one file under
levels/ that defines `LEVEL = Level(...)` β€” see levels/__init__.py for the
auto-discovery.
Grid words are plain strings, with four reserved values:
"start" the home tile (rendered blank, appends no word)
"" a walkable empty tile (rendered blank, appends no word)
"⏎" a submit tile β€” hopping onto one sends the sentence to the judge
"portal" a spinning-spiral teleport tile (appends no word); hopping onto one
whisks the doodle to the next portal clockwise around the board.
A board needs >= 2 portals for the links to mean anything.
A board may have several "⏎" tiles; every other cell appends its word.
"""
from dataclasses import dataclass, field
@dataclass(frozen=True)
class Level:
id: str # stable key used by the judge payload and nav
title: str # shown on the nav buttons
grid: list[list[str]] # rows of word tiles (see reserved values above)
start: tuple[int, int] # [row, col] the doodle spawns on
targets: list[str] # labels to collect (a win checks one off)
labels: list[str] # full candidate set the judge scores among
budget: int # hops before the doodle falls off the board
order: int = 100 # play order across boards (lower comes first)
home: bool = False # the board the app boots into
glitch: bool = False # bonus "glitch mode": the board inverts to a dark
# negative; "?" becomes a shrink key and file tiles blow the context (see game.js)
portal_to: str = "" # if set, this board's portal tiles are a cross-level
# link to that level id (instead of an in-board teleport)
solutions: dict[str, str] = field(default_factory=dict) # target -> a known
# answer the hint modal reveals (a literal tile path for critters; a cryptic
# riddle for emotions). Only the targets you want hintable need an entry.
music: str = "" # basename of this board's background loop under static/
# (e.g. "farm.mp3"); "" plays the default loop. game.js crossfades between
# boards' tracks on transition.
music_gain: float = 1.0 # linear trim applied to this track so every board
# sits at the same loudness as the default loop (measured by EBU R128 / LUFS);
# nothing jumps in volume across a transition.
def client_value(self) -> dict:
"""The subset shipped to the browser β€” no judge internals leave the server."""
return {
"id": self.id,
"title": self.title,
"grid": self.grid,
"start": list(self.start),
"targets": self.targets,
"labels": self.labels,
"budget": self.budget,
"glitch": self.glitch,
"portal_to": self.portal_to,
"solutions": self.solutions,
"music": self.music,
"music_gain": self.music_gain,
}