Something went wrong. Try again.
A Social Deduction / Deception Game
Something went wrong. Try again.
7.4 kB · 214 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215// Core domain types for the Sabotage Town game engine.//// The engine is a pure, deterministic reducer over a stream of actions (§3.7 of PLAN.md).// Nothing here reads the wall clock or generates randomness — time arrives via each action's// `serverTime`, and any random outcome is recorded in the action itself.
export type Did = stringexport type ResourceId = string
export type Role = 'worker' | 'saboteur'export type PlayerStatus = 'alive' | 'disconnected' | 'kicked'export type Facing = 'south' | 'west' | 'east' | 'north'
export type StationState = 'working' | 'broken' | 'sabotaged'
export type GamePhase = 'lobby' | 'round' | 'ended'export type Outcome = 'workers' | 'saboteur' | 'no-one' | 'aborted'
/** What a player is actively doing at a station (consumes a timer). */export type ActivityKind = // worker | 'collect' | 'repair' | 'inspect' // saboteur | 'pretendCollect' | 'breakStation' | 'fakeRepair' | 'sabotage' // special stations (all stateful: each rolls the failure dice on a real interaction when sabotaged) | 'deposit' | 'fakeDeposit' | 'scoreboard' | 'pretendScoreboard' | 'callVote'
export interface Vec { x: number y: number}
/** A rectangle in world pixels (top-left origin, y-down). */export interface Rect { x: number y: number w: number h: number}
/** A walkability grid for pathfinding (built from the level's collision rects — see nav.ts). */export interface NavGrid { cellSize: number cols: number rows: number /** world-px position of the top-left corner of cell (0,0) */ originX: number originY: number /** row-major flags, blocked[row*cols + col] === 1 means impassable */ blocked: number[]}
export interface GameConfig { /** Ordered resource ids, e.g. ['wood','stone','wheat','ore']. */ resources: ResourceId[] /** Collected amount required per resource for a worker win. */ targets: Record<ResourceId, number> roundDurationMs: number /** the 1-cost actions: collect / pretendCollect / breakStation / deposit / fakeDeposit / * scoreboard / pretendScoreboard / callVote */ timer1Ms: number /** repair / fakeRepair */ timer2Ms: number /** inspect / sabotage */ timer3Ms: number /** per-slot cooldown after a resolved action */ slotCooldownMs: number debrisTtlMs: number voteDurationMs: number /** walk speed along a path, world px per second */ moveSpeedPxPerSec: number /** grace window after the saboteur disconnects mid-round before the round aborts (§3.6) */ saboteurGraceMs: number}
export interface Slot { /** did of the player currently mid-action in this slot, else null */ occupantDid: Did | null /** a slot regenerates after a Collect: you can't Collect from it again until now >= this. Only * Collect sets it and only Collect is blocked by it; other activities ignore it entirely. * Secret — never exposed in projection. */ collectCooldownUntil: number}
export interface ActiveAction { kind: ActivityKind stationId: string slot: number startedAt: number endsAt: number /** which resource this action concerns — set for collect / pretendCollect / deposit, else null */ resourceId: ResourceId | null}
/** A carried resource. `authentic: false` is a saboteur's fake. */export interface Carry { authentic: boolean resourceId: ResourceId}
export interface Player { did: Did handle: string spriteRef: string | null /** null until the round starts; hidden from others by projection until game end */ role: Role | null status: PlayerStatus pos: Vec facing: Facing carrying: Carry | null active: ActiveAction | null /** current pathfinding destination (world px), or null when idle/arrived */ dest: Vec | null /** remaining waypoints (world px) from the next one to `dest`; player heads toward path[0] */ path: Vec[]}
export interface Station { id: string pos: Vec /** the station's footprint; a player must be inside it to occupy a slot (enforced at the API) */ area: Rect slots: Slot[] /** Capability set (a station may combine several — e.g. a restaurant table that is both a collect * and a deposit point). Resources are the in-play set: the map may author a superset, narrowed to * the scenario's resources record at room creation. */ /** in-play resources collectible here (also the saboteur's pretendCollect set); [] if not a collect station */ collect: ResourceId[] /** in-play resources this station accepts for deposit; [] if not a deposit station */ deposit: ResourceId[] /** in-play resource keys whose totals can be read here (Timer-3); [] if not a scoreboard station */ scoreboard: ResourceId[] /** can call a vote here */ meeting: boolean /** station-wide state (applies to all slots). Broken blocks collect AND deposit (repair only); * sabotaged secretly affects collect only. Meaningful when collect or deposit is non-empty. */ state: StationState /** how many collects have happened since this station was sabotaged; ramps the failure dice. * May be negative right after a fakeRepair (extra grace). */ collectionsSinceSabotage: number /** when the station entered the sabotaged state (ms), or null — feeds the time-based failure * chance alongside collectionsSinceSabotage */ sabotagedAt: number | null /** per-viewer knowledge: dids that have learned this station is sabotaged (inspectors + the * saboteur who set it) */ revealedSabotageTo: Did[]}
export interface Debris { id: string pos: Vec authentic: boolean resourceId: ResourceId expiresAt: number /** per-viewer knowledge: dids that have inspected this debris (and thus know authenticity) */ inspectedBy: Did[]}
export interface Vote { startedAt: number endsAt: number /** voter did -> target did */ ballots: Record<Did, Did>}
export interface GameState { code: string phase: GamePhase /** authoritative current time (ms epoch); advanced by every action's serverTime */ now: number config: GameConfig startedAt: number | null endsAt: number | null players: Record<Did, Player> stations: Record<string, Station> /** collected totals per resource */ resources: Record<ResourceId, number> debris: Record<string, Debris> vote: Vote | null outcome: Outcome | null /** when the round will abort because the saboteur disconnected and didn't return in time (§3.6), * or null when the saboteur is present. SERVER-ONLY — never projected: it would identify the * saboteur. Set on the saboteur's playerDisconnected, cleared on their playerReconnected. */ saboteurGraceEndsAt: number | null /** the level's navigation grid for pathfinding; null until provided at gameCreated */ nav: NavGrid | null /** count of actions applied — serves as the subscription cursor */ seq: number}
/** Seed used by `gameCreated` to lay out a station. Capabilities mirror Station; `slotCount` is * filled from the player-count balance at room creation (never authored on the map). */export interface StationSeed { id: string pos: Vec area: Rect slotCount: number collect: ResourceId[] deposit: ResourceId[] scoreboard: ResourceId[] meeting: boolean /** optional human label authored on the map (`town.deception.stationName`, e.g. "Chicken coop"), * used to make deduction prompts read naturally. A plain display string, not a record ref. */ name?: string}