995 lines
34 KiB
Markdown
995 lines
34 KiB
Markdown
# Twungeon MVP Technical Specification
|
|
|
|
**Status:** Approved implementation baseline
|
|
|
|
**Date:** 2026-08-17
|
|
|
|
**Approved:** 2026-08-17
|
|
|
|
**Product source of truth:** [Twungeon MVP PRD](Twungeon_MVP_PRD_Current.md)
|
|
|
|
**Execution and verification:** [Twungeon MVP Acceptance-Test and Build Checklist](Twungeon_MVP_Acceptance_Test_and_Build_Checklist.md)
|
|
|
|
**Intended implementation:** TypeScript, RPGJS, Twurple, and a Twitch Extension
|
|
|
|
## 1. Purpose
|
|
|
|
This approved document translates the Twungeon MVP Product Requirements
|
|
Document into an implementation-ready system design. It defines component responsibilities,
|
|
authoritative state, command handling, game-state transitions, spatial rules,
|
|
integration boundaries, failure behavior, and verification requirements.
|
|
|
|
The PRD remains authoritative for product scope and acceptance. If this
|
|
technical specification conflicts with the PRD, the PRD wins and this document
|
|
must be corrected.
|
|
|
|
This specification intentionally does not add post-MVP features. Decisions made
|
|
here resolve behavior that the PRD requires but does not define precisely enough
|
|
for deterministic implementation.
|
|
|
|
## 2. Design Goals
|
|
|
|
The implementation must optimize for the following goals, in order:
|
|
|
|
1. Prove the complete Twitch viewer-to-gameplay loop with real viewers.
|
|
2. Keep all game and identity decisions authoritative on the backend.
|
|
3. Make command processing deterministic and testable.
|
|
4. Keep Twungeon domain rules independent of RPGJS where practical.
|
|
5. Make Twitch setup reproducible from a clean checkout.
|
|
6. Prefer observable, recoverable failure over silent state corruption.
|
|
7. Avoid systems outside the MVP, including long-term progression and
|
|
production-grade persistence.
|
|
|
|
## 3. Scope and Constraints
|
|
|
|
### 3.1 Included
|
|
|
|
- Twitch chat `!spawn` handling and follower validation
|
|
- Stable Twitch user identity binding between chat and Extension
|
|
- One character per Twitch user
|
|
- Twitch Extension controls
|
|
- Shared stream-facing game view and chronological action log
|
|
- Dormant, Player Phase, Enemy Phase, floor transition, and run reset states
|
|
- AP, movement, combat, AutoGuard, self-heal, death, and resurrection
|
|
- One Goblin Guard with guarding, pursuit, and return behavior
|
|
- Varied, navigable two-room floors
|
|
- Runtime reconnect and refresh handling
|
|
- Clean-checkout Twitch integration documentation
|
|
- Automated domain tests and live integration verification
|
|
|
|
### 3.2 Excluded
|
|
|
|
All non-goals in PRD section 22 remain excluded. In particular, this design does
|
|
not introduce accounts beyond Twitch identity, inventory, progression, multiple
|
|
classes or enemies, sophisticated AI, long-term persistence, or anti-exploit
|
|
systems.
|
|
|
|
### 3.3 Runtime persistence boundary
|
|
|
|
The authoritative run exists in backend memory. Browser refreshes and temporary
|
|
client disconnects do not remove or duplicate characters because clients
|
|
reconnect to that backend state using the same Twitch user ID.
|
|
|
|
Surviving a backend process restart is not an MVP requirement. On an intentional
|
|
or unintentional backend restart, Twungeon starts a new run on Floor 1 and logs
|
|
the reset. Configuration and secrets persist outside the run state.
|
|
|
|
## 4. System Architecture
|
|
|
|
Twungeon uses one authoritative backend process for game decisions. Other
|
|
components translate external input into authenticated commands or render
|
|
backend state; they never modify game state directly.
|
|
|
|
```text
|
|
Twitch chat ----> Twitch adapter -----------+
|
|
|
|
|
Twitch events --> Twitch adapter -----------+--> Authoritative backend
|
|
| | |
|
|
Extension ------> Extension API/WebSocket --+ | +--> Action log
|
|
|
|
|
+--> RPGJS adapter
|
|
|
|
|
+--> State snapshots
|
|
|
|
|
+------------------+----------------+
|
|
| |
|
|
Stream view Extension view
|
|
```
|
|
|
|
### 4.1 Authoritative backend
|
|
|
|
The backend owns:
|
|
|
|
- the current run, floor, map, phase, and phase deadline;
|
|
- all participating player records and character state;
|
|
- Twitch identity bindings and follower eligibility results;
|
|
- the Goblin state and AI decisions;
|
|
- command ordering, validation, and idempotency;
|
|
- random outcomes;
|
|
- floor generation and transitions;
|
|
- the canonical action log; and
|
|
- state snapshots distributed to clients.
|
|
|
|
Only this component may accept or reject a command or perform a state
|
|
transition.
|
|
|
|
### 4.2 Twitch adapter
|
|
|
|
The Twitch adapter uses Twurple to:
|
|
|
|
- receive chat messages needed for `!spawn`;
|
|
- extract the stable Twitch user ID and display data from the chat event;
|
|
- verify follower eligibility using the configured broadcaster identity;
|
|
- receive the configured Channel Points custom reward redemption event;
|
|
- normalize Twitch events into internal messages; and
|
|
- reconnect without submitting the same external event more than once.
|
|
|
|
Version-specific Twitch scopes, event types, and transport configuration must
|
|
be confirmed against the official Twitch and Twurple documentation during the
|
|
integration task and recorded in the setup guide. They must not be guessed or
|
|
hard-coded into domain logic.
|
|
|
|
### 4.3 Extension gateway
|
|
|
|
The Extension gateway:
|
|
|
|
- accepts Twitch-authenticated Extension sessions;
|
|
- verifies the credential server-side;
|
|
- resolves the credential to a stable Twitch user ID;
|
|
- binds that ID to the same player created from chat;
|
|
- accepts controller commands;
|
|
- adds a server-issued connection and request identity;
|
|
- forwards normalized commands to the game core; and
|
|
- distributes personalized state and command results.
|
|
|
|
The Extension never supplies an authoritative player ID, AP value, position,
|
|
target validity, damage result, or follower result.
|
|
|
|
### 4.4 Domain game core
|
|
|
|
The domain core is a framework-independent TypeScript module containing:
|
|
|
|
- state types;
|
|
- command validation and reducers;
|
|
- phase and floor state machines;
|
|
- movement and combat rules;
|
|
- Goblin AI;
|
|
- dungeon generation contracts;
|
|
- deterministic random interfaces; and
|
|
- structured domain events.
|
|
|
|
It must not import RPGJS, Twurple, browser APIs, or Twitch SDK types.
|
|
|
|
### 4.5 RPGJS adapter
|
|
|
|
The RPGJS adapter maps domain state and events to prototype rendering and any
|
|
engine-specific map representation. It must not decide whether movement,
|
|
attacks, spawning, resurrection, or floor completion are valid.
|
|
|
|
### 4.6 Views
|
|
|
|
The presentation is implemented as one shared UI system with two modes:
|
|
|
|
- **Broadcast mode:** a read-only stream source that renders the three-section
|
|
layout. Its upper-left area shows shared status and a visible controller
|
|
legend, the upper-right area shows the dungeon, and the bottom area shows the
|
|
running log.
|
|
- **Extension mode:** an authenticated controller using the same visual
|
|
language. It renders interactive controls and personalized player status,
|
|
while consuming the same authoritative shared state.
|
|
|
|
This resolves the difference between a personalized controller and a shared
|
|
broadcast: controls are visibly represented on the stream, but only an
|
|
authenticated viewer's Extension controls are interactive.
|
|
|
|
## 5. Trust and Security Model
|
|
|
|
### 5.1 Stable identity
|
|
|
|
`twitchUserId` is the canonical player key. Display name, chat name, Extension
|
|
connection ID, and browser storage are metadata only and cannot establish
|
|
ownership.
|
|
|
|
### 5.2 Identity-binding sequence
|
|
|
|
1. The Twitch adapter receives `!spawn` with a stable chat user ID.
|
|
2. The backend verifies that user follows the configured broadcaster.
|
|
3. The backend creates or rejects the character using `twitchUserId`.
|
|
4. The Extension sends its Twitch-authenticated credential to the gateway.
|
|
5. The gateway verifies the credential and extracts its stable user ID.
|
|
6. Controls become enabled only when that ID equals the character owner ID.
|
|
7. Every later control command uses the verified server session; client-sent
|
|
ownership fields are ignored.
|
|
|
|
### 5.3 Secrets and configuration
|
|
|
|
Secrets must be supplied through environment variables or ignored local secret
|
|
files. They must never be committed, logged, returned to clients, included in
|
|
URLs, or embedded in the Extension bundle.
|
|
|
|
Configuration validation must fail startup with a specific error when required
|
|
values are missing or inconsistent. Logs may name a missing variable but must
|
|
not print its value.
|
|
|
|
### 5.4 Input safety
|
|
|
|
All external messages are schema-validated. Invalid, unauthorized, stale, or
|
|
duplicate messages are rejected without changing game state. Display names and
|
|
other viewer text are escaped before rendering or logging.
|
|
|
|
## 6. Authoritative Data Model
|
|
|
|
The following types are conceptual contracts. Exact TypeScript syntax may vary,
|
|
but the fields and ownership boundaries must remain equivalent.
|
|
|
|
### 6.1 Run state
|
|
|
|
```ts
|
|
interface RunState {
|
|
runId: string
|
|
floorNumber: number
|
|
floor: FloorState
|
|
phase: PhaseState
|
|
players: Map<TwitchUserId, PlayerState>
|
|
goblin: GoblinState
|
|
actionLog: ActionLogEntry[]
|
|
nextEventSequence: number
|
|
}
|
|
```
|
|
|
|
### 6.2 Player state
|
|
|
|
```ts
|
|
interface PlayerState {
|
|
twitchUserId: string
|
|
displayName: string
|
|
followerVerified: boolean
|
|
characterCreated: boolean
|
|
extensionBound: boolean
|
|
connectionState: 'connected' | 'disconnected'
|
|
position: TilePosition
|
|
hp: 0 | 1 | 2 | 3
|
|
lifeState: 'alive' | 'dead'
|
|
ap: 0 | 1 | 2
|
|
guard: 0 | 1 | 2
|
|
healAvailable: boolean
|
|
participatingFloor: number
|
|
diedOnFloor: number | null
|
|
eligibleThisPhase: boolean
|
|
}
|
|
```
|
|
|
|
Connection state does not determine whether a living character participates in
|
|
the turn. A disconnected living character remains in the dungeon, receives AP,
|
|
and converts unused AP to AutoGuard.
|
|
|
|
### 6.3 Phase state
|
|
|
|
```ts
|
|
type PhaseState =
|
|
| { kind: 'dormant' }
|
|
| {
|
|
kind: 'player'
|
|
phaseId: string
|
|
startedAt: number
|
|
deadlineAt: number
|
|
initialEligiblePlayerIds: string[]
|
|
}
|
|
| { kind: 'enemy'; phaseId: string }
|
|
| { kind: 'transition'; reason: 'floor-advance' | 'party-wipe' }
|
|
```
|
|
|
|
The backend uses a monotonic clock for elapsed phase timing. Wall-clock values
|
|
may be included for display but cannot determine acceptance ordering.
|
|
|
|
### 6.4 Floor state
|
|
|
|
```ts
|
|
interface FloorState {
|
|
floorId: string
|
|
width: number
|
|
height: number
|
|
tiles: TileKind[][]
|
|
spawnTiles: TilePosition[]
|
|
exitPosition: TilePosition
|
|
goblinGuardPosition: TilePosition
|
|
generationSeed: string
|
|
}
|
|
|
|
type TileKind = 'wall' | 'floor' | 'exit'
|
|
```
|
|
|
|
### 6.5 Goblin state
|
|
|
|
```ts
|
|
interface GoblinState {
|
|
hp: 0 | 1 | 2
|
|
position: TilePosition
|
|
guardPosition: TilePosition
|
|
mode: 'guarding' | 'pursuing' | 'returning' | 'dead'
|
|
targetPlayerId: string | null
|
|
}
|
|
```
|
|
|
|
### 6.6 Structured action log
|
|
|
|
```ts
|
|
interface ActionLogEntry {
|
|
sequence: number
|
|
runId: string
|
|
floorNumber: number
|
|
phaseId: string | null
|
|
type: string
|
|
actorId: string | null
|
|
targetId: string | null
|
|
message: string
|
|
occurredAt: string
|
|
}
|
|
```
|
|
|
|
The backend creates log messages from domain events. Clients do not submit log
|
|
text. Sequence numbers establish canonical ordering.
|
|
|
|
## 7. Spatial and Interaction Rules
|
|
|
|
These rules make movement and combat deterministic while preserving the PRD's
|
|
simple cooperative intent.
|
|
|
|
### 7.1 Grid
|
|
|
|
- Movement is orthogonal: up, down, left, or right by one tile.
|
|
- Diagonal movement and attacks are not permitted.
|
|
- Walls are impassable.
|
|
- The exit tile is traversable by living players.
|
|
- A Goblin blocks player movement into its tile.
|
|
- A player blocks Goblin movement into its tile.
|
|
- Multiple players may share a tile. This prevents spawn-room crowding and
|
|
cooperative path blocking from becoming an accidental MVP system.
|
|
|
|
### 7.2 Spawn placement
|
|
|
|
New characters spawn on the first spawn tile chosen by deterministic ordering.
|
|
Players may share that tile. A late join never changes the current phase timer
|
|
or the set of players required to finish the phase.
|
|
|
|
### 7.3 Attack range and targeting
|
|
|
|
- Player and Goblin attacks target an orthogonally adjacent tile.
|
|
- A player attack requires a selected living Goblin adjacent to the player.
|
|
- A Goblin attack requires its living pursuit target to be adjacent.
|
|
- Invalid or stale targets do not consume AP.
|
|
- A valid attack consumes AP even when the hit roll misses.
|
|
- A successful unblocked hit applies exactly 1 HP of damage.
|
|
|
|
### 7.4 Exit behavior
|
|
|
|
When a living player successfully moves onto the exit tile, the move consumes
|
|
1 AP and the floor transition starts immediately after that command resolves.
|
|
No later queued command for the old floor is processed.
|
|
|
|
## 8. Command Model
|
|
|
|
### 8.1 Player commands
|
|
|
|
```ts
|
|
type PlayerCommand =
|
|
| { type: 'move'; direction: 'up' | 'down' | 'left' | 'right' }
|
|
| { type: 'attack'; targetId: string }
|
|
| { type: 'heal-self' }
|
|
| { type: 'pass' }
|
|
```
|
|
|
|
Each Extension request includes:
|
|
|
|
- a client-generated `requestId`;
|
|
- the current `runId`, `floorId`, and `phaseId` last observed by the client; and
|
|
- exactly one command payload.
|
|
|
|
The gateway supplies the authenticated `twitchUserId`; it is not accepted from
|
|
the payload.
|
|
|
|
### 8.2 Command ordering
|
|
|
|
The backend processes accepted messages serially in arrival order. Each message
|
|
is fully validated against the state produced by all earlier messages before it
|
|
is applied.
|
|
|
|
This rule resolves simultaneous actions:
|
|
|
|
- the first accepted player to enter the exit ends the floor;
|
|
- the first accepted attack may invalidate a later attack by killing the
|
|
target;
|
|
- duplicate request IDs return the original result without applying again; and
|
|
- commands for an old run, floor, or phase are rejected as stale.
|
|
|
|
### 8.3 Player command validation
|
|
|
|
A command is accepted only when:
|
|
|
|
- the session is authenticated and bound to the character owner;
|
|
- the character exists, is alive, and is eligible to act;
|
|
- the current phase is Player Phase;
|
|
- the command reaches the authoritative queue before the deadline;
|
|
- the player has at least 1 AP;
|
|
- the supplied run, floor, and phase IDs match current state; and
|
|
- command-specific spatial or resource rules pass.
|
|
|
|
Rejected commands consume no AP and return a machine-readable reason plus a
|
|
safe display message.
|
|
|
|
### 8.4 Pass semantics
|
|
|
|
Pass costs exactly 1 AP. A player with 2 AP who wants to finish immediately
|
|
must pass twice. There is no separate zero-cost end-turn command in the MVP.
|
|
|
|
## 9. Turn and State Machines
|
|
|
|
### 9.1 Dormant state
|
|
|
|
The game starts with a generated Floor 1 and no participating characters.
|
|
While no active character exists:
|
|
|
|
- phase is `dormant`;
|
|
- no phase deadline exists;
|
|
- no Enemy Phase can run;
|
|
- the Goblin cannot act; and
|
|
- the spawn banner is visible.
|
|
|
|
The first accepted `!spawn` creates a living character, removes the banner, and
|
|
starts a fresh Player Phase.
|
|
|
|
The MVP has no voluntary leave or character-removal command. Temporary
|
|
disconnects therefore do not create dormancy. A total-party death triggers a
|
|
run reset rather than dormancy.
|
|
|
|
### 9.2 Starting Player Phase
|
|
|
|
For every living participant present when the phase starts:
|
|
|
|
1. set AP to 2;
|
|
2. set Guard to 0;
|
|
3. set `eligibleThisPhase` to true; and
|
|
4. include the player in `initialEligiblePlayerIds`.
|
|
|
|
The duration is:
|
|
|
|
```text
|
|
min(living-player-count * 25 seconds, 120 seconds)
|
|
```
|
|
|
|
The count includes disconnected living characters, matching the PRD's AFK
|
|
AutoGuard behavior.
|
|
|
|
Players who spawn or resurrect after the phase starts receive 0 AP, are not
|
|
added to the phase completion set, and may act beginning with the next Player
|
|
Phase.
|
|
|
|
### 9.3 Ending Player Phase
|
|
|
|
The phase ends when either:
|
|
|
|
- every player in the initial eligible set has 0 AP or is dead; or
|
|
- the authoritative deadline is reached.
|
|
|
|
At the transition, each living player's remaining AP becomes Guard one-for-one,
|
|
then AP becomes 0. Dead players receive no Guard.
|
|
|
|
Commands already waiting in the serialized backend queue are accepted only if
|
|
the gateway recorded their arrival before the deadline. A command arriving at
|
|
or after the deadline is rejected even if the phase-transition task has not yet
|
|
run.
|
|
|
|
### 9.4 Enemy Phase
|
|
|
|
The living Goblin receives 2 AP and repeatedly selects one deterministic action
|
|
until it has no AP or no valid action.
|
|
|
|
After the Goblin finishes, Guard is discarded and a new Player Phase begins,
|
|
unless the floor advanced or the run reset during the Enemy Phase.
|
|
|
|
### 9.5 AutoGuard
|
|
|
|
When a Goblin attack hits:
|
|
|
|
1. if the target has Guard greater than 0, decrement Guard and apply no damage;
|
|
2. otherwise, subtract 1 HP;
|
|
3. if HP reaches 0, perform player death handling.
|
|
|
|
A miss never consumes Guard. Guard cannot carry into another round.
|
|
|
|
## 10. Player Lifecycle
|
|
|
|
### 10.1 `!spawn`
|
|
|
|
An accepted spawn requires:
|
|
|
|
- a stable Twitch chat user ID;
|
|
- verified follower status;
|
|
- no existing character for that ID; and
|
|
- no death marker for that ID on the current floor.
|
|
|
|
The backend creates one character at full HP with heal available. If the game is
|
|
already active, the player has 0 AP until the next Player Phase.
|
|
|
|
Repeated `!spawn` attempts for an existing character return an informative chat
|
|
response or log result and never create another character.
|
|
|
|
### 10.2 Heal
|
|
|
|
A valid self-heal:
|
|
|
|
- costs 1 AP;
|
|
- requires the player to be alive;
|
|
- requires `healAvailable` to be true;
|
|
- restores HP to 3 even if already full; and
|
|
- sets `healAvailable` to false.
|
|
|
|
### 10.3 Death
|
|
|
|
When HP reaches 0:
|
|
|
|
- life state becomes dead;
|
|
- AP and Guard become 0;
|
|
- `diedOnFloor` becomes the current floor number;
|
|
- controls are disabled; and
|
|
- a death event is logged.
|
|
|
|
The dead character remains associated with the Twitch user ID and cannot be
|
|
replaced with `!spawn`.
|
|
|
|
### 10.4 Channel Points resurrection
|
|
|
|
The Channel Points custom reward ID is required configuration and may change
|
|
without changing domain code. Its cost is owned by Twitch. An accepted
|
|
resurrection event must identify the configured reward, the same Twitch user ID
|
|
as the dead character, and a unique redemption ID.
|
|
|
|
Resurrection:
|
|
|
|
- restores the player to 3 HP;
|
|
- places the player on the deterministic spawn tile;
|
|
- clears the current-floor death marker;
|
|
- preserves whether the floor heal was already used;
|
|
- grants 0 AP during the current phase; and
|
|
- enables action beginning with the next Player Phase.
|
|
|
|
Other-reward, duplicate, unknown-user, or non-dead-user events do not alter game
|
|
state and must produce a diagnostic result without exposing secrets. Bits and
|
|
Cheers are outside the MVP and are not normalized into domain messages.
|
|
|
|
### 10.5 Refresh and reconnect
|
|
|
|
On Extension refresh or reconnection, the gateway re-verifies identity and
|
|
returns the existing character and current personalized snapshot. It never
|
|
creates a character from browser state. Multiple connections for the same user
|
|
may display state, but all share one player command budget and idempotency
|
|
history.
|
|
|
|
## 11. Goblin Guard Behavior
|
|
|
|
### 11.1 Guarding
|
|
|
|
The Goblin begins at its guard position in `guarding` mode. It spends no AP and
|
|
does not attack until it is attacked by a player.
|
|
|
|
### 11.2 Aggro and pursuit
|
|
|
|
The first valid player attack changes the Goblin to `pursuing` and sets that
|
|
attacker as its target, whether the attack hits or misses. Later attacks do not
|
|
retarget it while the target lives.
|
|
|
|
During each Enemy Phase, the Goblin:
|
|
|
|
1. attacks its target if orthogonally adjacent;
|
|
2. otherwise moves one tile along a shortest path to the target; and
|
|
3. repeats until its 2 AP are spent or no path/action exists.
|
|
|
|
Shortest paths use breadth-first search. Equal choices use a fixed direction
|
|
priority of up, left, right, then down so tests are deterministic.
|
|
|
|
### 11.3 Target death and return
|
|
|
|
If the pursued player dies, the Goblin enters `returning`, clears its target,
|
|
and uses later Enemy Phases to take shortest-path movement toward its original
|
|
guard position. It does not attack players while returning. On reaching the
|
|
guard position, it becomes passive `guarding` again.
|
|
|
|
### 11.4 Goblin death
|
|
|
|
At 0 HP the Goblin becomes `dead`, is removed as a blocking entity, and takes no
|
|
further actions. The exit remains usable whether the Goblin is alive or dead.
|
|
|
|
## 12. Floor Generation and Progression
|
|
|
|
### 12.1 Generator contract
|
|
|
|
For a supplied seed, the generator must return the same floor and guarantee:
|
|
|
|
- exactly one rectangular spawn room;
|
|
- exactly one rectangular exit room that does not overlap the spawn room;
|
|
- one exit tile in the exit room;
|
|
- one Goblin guard position adjacent to or near the exit;
|
|
- an orthogonally navigable floor path between a spawn tile and the exit; and
|
|
- enclosing impassable walls.
|
|
|
|
Variation must affect room sizes, room positions, or connecting corridor shape
|
|
across seeds. Generation retries with a derived seed when validation fails and
|
|
fails startup/transition visibly after a bounded retry count rather than loading
|
|
an invalid map.
|
|
|
|
### 12.2 Floor advancement
|
|
|
|
When a living player enters the exit:
|
|
|
|
1. enter transition state and reject remaining old-floor commands;
|
|
2. increment the floor counter;
|
|
3. generate and validate a new floor and Goblin;
|
|
4. place every participating player on the new spawn tile;
|
|
5. restore every player to 3 HP and alive;
|
|
6. clear death markers, AP, and Guard;
|
|
7. refresh every self-heal;
|
|
8. log the new floor; and
|
|
9. begin a fresh Player Phase.
|
|
|
|
The previous Goblin and map are discarded.
|
|
|
|
### 12.3 Total-party wipe
|
|
|
|
After any death resolution, if participating characters exist and none is
|
|
alive, the backend immediately:
|
|
|
|
1. enters transition state;
|
|
2. creates a new run ID;
|
|
3. sets the floor counter to 1;
|
|
4. generates a new starting floor and Goblin;
|
|
5. restores all participating players alive at 3 HP;
|
|
6. clears death markers, AP, and Guard;
|
|
7. refreshes all self-heals;
|
|
8. places all players at the spawn tile;
|
|
9. logs the wipe and reset; and
|
|
10. begins a fresh Player Phase.
|
|
|
|
Because the reset is immediate, a Channel Points redemption cannot interrupt a
|
|
completed total-party wipe. Zero participating characters is explicitly not a
|
|
wipe.
|
|
|
|
## 13. Randomness
|
|
|
|
All random behavior goes through an injected pseudo-random interface. The
|
|
backend is the sole random authority.
|
|
|
|
Randomness is used for:
|
|
|
|
- player 65% attack hit rolls;
|
|
- Goblin 50% attack hit rolls; and
|
|
- procedural floor parameters.
|
|
|
|
Tests use fixed sequences or seeds. Production logs record the run/floor seed
|
|
and outcome event, but clients never provide rolls or seeds used to resolve a
|
|
command.
|
|
|
|
## 14. Backend Interfaces
|
|
|
|
Exact framework routing may change, but equivalent contracts are required.
|
|
|
|
### 14.1 Health
|
|
|
|
`GET /health`
|
|
|
|
Returns process readiness without credentials or game secrets. Readiness is
|
|
false when required Twitch configuration or the game core failed to initialize.
|
|
|
|
### 14.2 Extension session
|
|
|
|
`POST /api/extension/session`
|
|
|
|
Accepts the Twitch Extension authentication credential, verifies it, and
|
|
returns a short-lived Twungeon session plus personalized state. It does not
|
|
create a character.
|
|
|
|
### 14.3 Commands
|
|
|
|
`POST /api/commands`
|
|
|
|
Accepts an authenticated player command envelope and returns:
|
|
|
|
```json
|
|
{
|
|
"requestId": "client-generated-id",
|
|
"accepted": true,
|
|
"eventSequence": 42,
|
|
"reason": null
|
|
}
|
|
```
|
|
|
|
Rejected responses use a stable reason such as `UNAUTHENTICATED`,
|
|
`IDENTITY_NOT_BOUND`, `CHARACTER_DEAD`, `WRONG_PHASE`, `STALE_PHASE`,
|
|
`NO_AP`, `INVALID_TARGET`, `BLOCKED_TILE`, or `DUPLICATE`.
|
|
|
|
### 14.4 State stream
|
|
|
|
`GET /ws` upgrades to an authenticated WebSocket when personalized state is
|
|
needed or a read-only broadcast connection otherwise. Messages include:
|
|
|
|
- complete snapshot on connection;
|
|
- ordered state/event updates;
|
|
- current server time and phase deadline;
|
|
- action-log entries; and
|
|
- explicit resynchronization after sequence gaps.
|
|
|
|
Clients must replace local state with a complete snapshot after reconnect or a
|
|
sequence gap. UI animation state is local; game state is not.
|
|
|
|
### 14.5 Internal Twitch messages
|
|
|
|
Twitch events normalize into idempotent internal messages:
|
|
|
|
```ts
|
|
type TwitchMessage =
|
|
| {
|
|
type: 'spawn-requested'
|
|
externalEventId: string
|
|
twitchUserId: string
|
|
displayName: string
|
|
broadcasterId: string
|
|
}
|
|
| {
|
|
type: 'channel-point-resurrection-redeemed'
|
|
externalEventId: string
|
|
twitchUserId: string
|
|
rewardId: string
|
|
}
|
|
```
|
|
|
|
## 15. Presentation Requirements
|
|
|
|
### 15.1 Three-section layout
|
|
|
|
The default wide layout reserves:
|
|
|
|
- a compact upper-left control/status area;
|
|
- the largest upper-right area for the map; and
|
|
- a full-width bottom area for the action log.
|
|
|
|
Temporary shapes, colors, labels, and sprites are acceptable. Every required
|
|
entity, wall, traversable path, and exit must remain distinguishable.
|
|
|
|
### 15.2 Player status
|
|
|
|
The personalized Extension status includes at least identity/binding status,
|
|
HP, AP, Guard, heal availability, alive/dead status, current phase, and a clear
|
|
disabled reason when controls are unavailable.
|
|
|
|
### 15.3 Action log
|
|
|
|
The view displays canonical entries by sequence, oldest to newest, with the
|
|
latest entries visible. It must cover every event category required by PRD
|
|
section 4.2. Reconnection restores recent history from the backend rather than
|
|
starting an empty client-only log.
|
|
|
|
### 15.4 Dormant banner
|
|
|
|
When phase is dormant, both applicable views prominently display exactly:
|
|
|
|
`Type !spawn to spawn in the Twungeon!`
|
|
|
|
## 16. Failure and Recovery Behavior
|
|
|
|
### 16.1 Twitch unavailable
|
|
|
|
- Existing authenticated gameplay may continue if the backend remains healthy.
|
|
- New spawn, new identity binding, and Channel Points redemptions fail closed
|
|
when they cannot be verified.
|
|
- The operator view and logs identify the unavailable integration.
|
|
- Reconnect uses bounded exponential backoff and does not duplicate events.
|
|
|
|
### 16.2 Extension disconnected
|
|
|
|
The character remains participating. The current phase continues, unused AP
|
|
becomes Guard, and reconnection restores authoritative state.
|
|
|
|
### 16.3 View disconnected
|
|
|
|
Game simulation continues. The view requests a full snapshot on return and does
|
|
not replay commands.
|
|
|
|
### 16.4 Invalid floor
|
|
|
|
An invalid generated floor is never activated. Generation retries a bounded
|
|
number of times. If all retries fail, phase progression stops with an operator-
|
|
visible error rather than corrupting the active run.
|
|
|
|
### 16.5 Unexpected domain exception
|
|
|
|
The command is not partially applied. Command handling must compute and commit
|
|
one atomic state transition. The error is correlated with request, run, floor,
|
|
phase, and event sequence identifiers without logging secrets.
|
|
|
|
## 17. Observability
|
|
|
|
Structured operational logs must distinguish:
|
|
|
|
- Twitch connection and subscription state;
|
|
- authentication and identity-binding results;
|
|
- follower checks;
|
|
- accepted and rejected spawn attempts;
|
|
- accepted and rejected controller commands;
|
|
- phase transitions and timer expiry;
|
|
- Channel Points redemption deduplication and resurrection results;
|
|
- floor-generation seed and validation result;
|
|
- reconnect and snapshot resynchronization; and
|
|
- run reset or fatal errors.
|
|
|
|
Logs must use stable user IDs only where operationally required and must avoid
|
|
credentials and full authentication payloads.
|
|
|
|
## 18. Proposed Source Layout
|
|
|
|
```text
|
|
/
|
|
|-- apps/
|
|
| |-- backend/ # Process composition, APIs, WebSocket, scheduler
|
|
| |-- extension/ # Authenticated Twitch controller UI
|
|
| `-- stream-view/ # Read-only broadcast layout
|
|
|-- packages/
|
|
| |-- domain/ # Framework-independent game state and rules
|
|
| |-- dungeon-generator/ # Seeded two-room generation and validation
|
|
| |-- twitch-adapter/ # Twurple chat, follower, and Channel Points integration
|
|
| |-- rpgjs-adapter/ # Mapping between domain state and RPGJS
|
|
| |-- contracts/ # Shared validated API/event schemas
|
|
| `-- ui/ # Shared layout, status, controls, and log components
|
|
|-- tests/
|
|
| |-- domain/ # Deterministic rule and state-machine tests
|
|
| |-- integration/ # Backend, adapters, APIs, reconnect, deduplication
|
|
| `-- e2e/ # Multi-viewer and complete-loop scenarios
|
|
|-- docs/
|
|
| |-- twitch-setup.md # Clean-checkout Twitch setup and troubleshooting
|
|
| `-- testing.md # Local and live-channel verification procedure
|
|
|-- .env.example # Names and descriptions, never secret values
|
|
|-- Twungeon_MVP_PRD_Current.md
|
|
`-- Twungeon_MVP_Technical_Specification.md
|
|
```
|
|
|
|
The exact monorepo tooling may be selected during project scaffolding. Component
|
|
boundaries and dependency direction are mandatory even if folders are renamed.
|
|
|
|
## 19. Dependency Direction
|
|
|
|
Allowed dependency direction:
|
|
|
|
```text
|
|
apps -> adapters/contracts -> domain
|
|
UI apps -> contracts/UI components
|
|
domain -> no framework or external-service package
|
|
```
|
|
|
|
The domain package exposes commands, state, and events. Adapters translate
|
|
between those contracts and RPGJS, Twurple, HTTP, WebSocket, or browser APIs.
|
|
|
|
## 20. Verification Strategy
|
|
|
|
### 20.1 Domain unit tests
|
|
|
|
Deterministic tests must cover:
|
|
|
|
- all accepted and rejected command conditions;
|
|
- AP spending and early phase completion;
|
|
- timer duration and 120-second cap;
|
|
- deadline boundary behavior;
|
|
- AutoGuard conversion, hit blocking, misses, and reset;
|
|
- player and Goblin hit probabilities through injected rolls;
|
|
- heal availability and floor refresh;
|
|
- death, spawn lockout, resurrection, advancement, and wipe reset;
|
|
- Goblin guarding, aggro on hit or miss, pursuit, return, and death;
|
|
- exit entry with a living Goblin;
|
|
- dormant versus total-party-wipe conditions;
|
|
- serial ordering and stale/duplicate command handling; and
|
|
- deterministic floor generation and connectivity validation.
|
|
|
|
### 20.2 Integration tests
|
|
|
|
Integration tests use test doubles before live Twitch testing and cover:
|
|
|
|
- chat event to follower check to spawn;
|
|
- non-follower rejection;
|
|
- chat-to-Extension identity match and mismatch;
|
|
- two authenticated viewers controlling separate characters;
|
|
- refresh/reconnect without duplication;
|
|
- Twitch event and command deduplication;
|
|
- Channel Points redemption mapped to the correct dead character;
|
|
- backend snapshots, ordered events, and sequence-gap recovery; and
|
|
- RPGJS rendering updates without domain authority leakage.
|
|
|
|
### 20.3 End-to-end scenarios
|
|
|
|
At minimum, the live test plan must demonstrate:
|
|
|
|
1. clean setup and startup from repository documentation;
|
|
2. dormant banner before any player joins;
|
|
3. follower spawn and non-follower rejection;
|
|
4. matching Extension control and mismatched identity rejection;
|
|
5. two or more simultaneous viewer characters;
|
|
6. movement, two attacks, heal, pass, timer expiry, and AutoGuard;
|
|
7. Goblin aggro, pursuit, attack, target death, and return;
|
|
8. Channel Points resurrection of the correct dead viewer;
|
|
9. floor escape with the Goblin alive and dead-player revival;
|
|
10. total-party wipe and Floor 1 reset;
|
|
11. refresh and temporary disconnect recovery; and
|
|
12. repetition across multiple generated floors.
|
|
|
|
### 20.4 PRD success-criteria traceability
|
|
|
|
| PRD criteria | Primary verification |
|
|
|---|---|
|
|
| 1-6 | Clean-checkout, configuration, Twitch event, Extension, and layout E2E tests |
|
|
| 7-13 | Dormancy, spawn eligibility, duplicate prevention, and identity integration tests |
|
|
| 14-19 | Controller, phase, timer, AP, combat, heal, death, Guard, and log tests |
|
|
| 20-22 | Goblin AI, generator validation, and live-Goblin escape tests |
|
|
| 23-29 | Floor revival, death lockout, Channel Points, wipe, zero-player, late spawn, and reconnect tests |
|
|
| 30-33 | Multi-floor soak test, first-time-viewer test, setup troubleshooting review, and viewer-feedback session |
|
|
|
|
The test implementation should reference individual PRD criterion numbers in
|
|
test names or metadata so failures remain traceable to product acceptance.
|
|
|
|
## 21. Implementation Sequence
|
|
|
|
Implementation should proceed through vertical, independently verifiable
|
|
slices:
|
|
|
|
1. Scaffold the repository, shared contracts, configuration validation, and
|
|
framework-independent domain package.
|
|
2. Implement the deterministic floor generator and static stream rendering.
|
|
3. Implement player/Goblin state, commands, turn phases, timer, and action log
|
|
using local test drivers.
|
|
4. Implement death, resurrection messages, floor advancement, wipe reset,
|
|
dormancy, reconnect, and idempotency.
|
|
5. Add the RPGJS adapter without moving rules out of the domain package.
|
|
6. Add Twurple chat and follower verification for `!spawn`.
|
|
7. Add Extension authentication, identity binding, controller commands, and
|
|
personalized state.
|
|
8. Add the Twitch Channel Points redemption adapter and configured reward rule.
|
|
9. Complete clean-checkout Twitch setup and troubleshooting documentation.
|
|
10. Run automated, live-channel, multi-viewer, and repeated-floor acceptance
|
|
tests against all 33 PRD criteria.
|
|
|
|
## 22. Decisions Deferred to Integration Configuration
|
|
|
|
The following are intentionally configurable or version-dependent and do not
|
|
change the domain design:
|
|
|
|
- Channel Points resurrection reward ID and broadcaster-managed cost;
|
|
- concrete Twitch developer application and Extension identifiers;
|
|
- exact Twitch scopes, EventSub event names, and Twurple configuration required
|
|
by the versions selected at implementation time;
|
|
- callback, WebSocket, public endpoint, and HTTPS values for each environment;
|
|
- UI art, typography, and final dimensions; and
|
|
- retention limits for operational logs and the on-screen action-log window.
|
|
|
|
These values must be resolved and documented before live acceptance testing.
|
|
|
|
## 23. Definition of Ready for Implementation
|
|
|
|
Implementation may begin when:
|
|
|
|
- the PRD and this specification are accepted as the product and technical
|
|
baselines;
|
|
- the eight resolved behavior choices below are acknowledged;
|
|
- the initial repository/tooling scaffold is selected;
|
|
- a Twitch development channel and required developer access are available; and
|
|
- secrets can be supplied outside source control.
|
|
|
|
Resolved behavior choices:
|
|
|
|
1. Players may share tiles; the Goblin and players may not share a tile.
|
|
2. Movement and attacks are orthogonal, and attacks are adjacent-only.
|
|
3. The backend serializes commands by authoritative arrival order.
|
|
4. The arrival timestamp at the backend gateway decides timer-boundary commands.
|
|
5. Run state survives client reconnects but not backend process restarts.
|
|
6. Channel Points resurrection restores full HP at spawn and grants AP next Player Phase.
|
|
7. Broadcast and Extension modes share presentation, but only the authenticated
|
|
Extension is interactive.
|
|
8. The backend domain core is the sole authority for every state transition.
|
|
|
|
Changes to these decisions must update this specification and any affected
|
|
tests before or alongside implementation.
|