A faithful, fully-playable digital adaptation of Betrayal at House on the Hill (2nd Edition) for 3–6 players, built with ASP.NET Core 10 Blazor Server and SignalR.
This project was generated by the jc_attractor pipeline system using a 22-node DAG (betrayal.dot) that orchestrated multiple LLM agents (Claude Opus 4.6 + Sonnet 4.6) to build the entire codebase from an architecture doc through implementation.
- This README — project overview, structure, how to run
- ARCHITECTURE.md — deep dive into data models, engine design, all 12 characters' stat arrays, SignalR hub methods, component tree
- RULEBOOK.md — complete game rules for Betrayal at House on the Hill 2nd Edition
- All 50 haunt scenarios implemented with behaviors, win conditions, and tests
- All game content: 45 room tiles, 22 items, 22 omens, 44 events, 12 characters
- Full multiplayer over SignalR with lobby/character select/game flow
- Gothic-themed Blazor UI with board grid, dice roller, card reveals, combat overlays
- ~1,200 unit tests across the engine
- Known gaps: The
scaffoldnode hit a tool-round limit (may have incomplete stubs in some files). Theui_cards_dicenode had a transient network error. Both were compensated by later pipeline nodes but there may be rough edges in card reveal animations or dice UI.
When you make changes to this repo, update this README to reflect them. Specifically:
- If you add/remove/rename files or directories, update the Project Structure section
- If you change how to build or run the app, update Setup & Running
- If you add new haunt scenarios or game content, update Data Verification counts
- If you fix known issues listed above, remove them from the "Current state" section
- If you add new features, document them in the appropriate section
- If you change test counts, update the expected count in Testing
- Add a brief changelog entry to the Changelog section at the bottom
- Tech Stack
- Project Structure
- Setup & Running
- How to Play
- Data Verification
- Development Guide
- Testing
- Multiplayer & Networking
- Accessibility & UX Features
- Audio
- Architecture Notes
- Changelog
| Layer | Technology |
|---|---|
| Backend | ASP.NET Core 10 (Blazor Server + SignalR) |
| Game Engine | Pure C# class library (BetrayalEngine) |
| Real-time | ASP.NET Core SignalR |
| Frontend | Blazor Server Components + Vanilla JS interop |
| Audio | Web Audio API (procedural synthesis — no audio files) |
| CSS | Custom CSS (no Tailwind/SASS dependency) |
| Fonts | Google Fonts — Cinzel Decorative, Crimson Text, EB Garamond |
| Testing | xUnit + .NET 10 Test SDK |
BetrayalWeb.sln
├── src/
│ ├── BetrayalEngine/ # Pure game logic (no UI deps)
│ │ ├── Data/ # Static data definitions
│ │ │ ├── CharacterData.cs # All 12 characters with stat arrays
│ │ │ ├── EventCardData.cs # 44 event cards
│ │ │ ├── HauntScenarioData.cs # 50 haunt scenario definitions
│ │ │ ├── ItemCardData.cs # 22 item cards
│ │ │ ├── OmenCardData.cs # 22 omen cards
│ │ │ └── RoomTileData.cs # 45 room tiles + 5 starting tiles
│ │ ├── Engine/ # Core game systems
│ │ │ ├── BoardGrid.cs # 2D sparse grid, tile placement, pathfinding
│ │ │ ├── CardDeck.cs # Generic deck: shuffle, draw, discard
│ │ │ ├── CardEffectResolver.cs# Applies card effects to game state
│ │ │ ├── CombatSystem.cs # Physical + mental combat resolution
│ │ │ ├── DiceRoller.cs # 0/1/2 dice via IRandom
│ │ │ ├── ExplorationEngine.cs # Tile discovery + card triggers
│ │ │ ├── GameEngine.cs # Top-level orchestrator (IGameEngine)
│ │ │ ├── HauntEngine.cs # Dispatches to haunt behaviors
│ │ │ ├── HauntRevelation.cs # Omen count vs dice, haunt table lookup
│ │ │ ├── HauntTable.cs # 990-entry cross-reference (omen × room → scenario)
│ │ │ ├── HauntTurnManager.cs # Haunt-phase turn order (heroes then traitor)
│ │ │ ├── TurnManager.cs # Exploration-phase turn sequencing
│ │ │ └── Haunts/ # 50 haunt behavior classes
│ │ │ ├── BaseHauntBehavior.cs
│ │ │ ├── Haunt01Behavior.cs # The Mummy Walks
│ │ │ ├── ...
│ │ │ └── Haunt50Behavior.cs # Treasure Hunt
│ │ ├── Models/ # Domain models + enums
│ │ ├── Helpers/ # Shuffle, door alignment utilities
│ │ └── Validation/ # Move/action/tile placement validators
│ └── BetrayalWeb/ # Blazor Server web app
│ ├── Components/
│ │ ├── Pages/
│ │ │ ├── Home.razor # Landing: create/join game
│ │ │ ├── GameLobby.razor # Character selection + ready up
│ │ │ └── GameBoard.razor # Main game view
│ │ ├── Layout/
│ │ │ ├── GothicLayout.razor # Dark themed layout
│ │ │ ├── MainLayout.razor
│ │ │ └── ReconnectModal.razor # Auto-reconnect UI
│ │ └── App.razor
│ ├── Hubs/
│ │ └── GameHub.cs # All SignalR methods (create/join/move/attack/etc.)
│ ├── Services/
│ │ ├── GameService.cs # IGameService — validates + delegates to engine
│ │ ├── GameSessionStore.cs # ConcurrentDictionary of active games
│ │ └── AudioService.cs # Scoped JS interop for sound
│ ├── Shared/ # Reusable Blazor components
│ │ ├── BoardGrid.razor # Tile map with zoom/pan
│ │ ├── CardReveal.razor # Card draw modal
│ │ ├── CombatOverlay.razor # Attack/defend UI
│ │ ├── DiceRoller.razor # Animated dice with 0/1/2 faces
│ │ ├── HauntReveal.razor # Dramatic haunt announcement
│ │ ├── HauntDashboard.razor # Haunt-phase objectives + actions
│ │ ├── PlayerDashboard.razor# Stats, inventory, actions
│ │ ├── GameLog.razor # Scrollable action log
│ │ ├── RulesReference.razor # Searchable rules sidebar
│ │ ├── TutorialOverlay.razor# First-time player walkthrough
│ │ ├── SettingsPanel.razor # Volume, speed, color-blind mode
│ │ └── UndoToast.razor # Undo last move
│ └── wwwroot/
│ ├── css/ # Gothic theme, board, tiles, animations
│ └── js/
│ ├── audio-interop.js # Web Audio API procedural sound engine
│ └── board-interop.js # Board zoom/pan, localStorage persistence
├── tests/
│ └── BetrayalEngine.Tests/ # ~1,200 unit tests
├── ARCHITECTURE.md # Detailed architecture document
├── RULEBOOK.md # Complete game rules
└── GNUmakefile # Build/test/run shortcuts
- .NET 10 SDK
- A modern browser (Chrome 90+, Firefox 90+, Safari 15+, Edge 90+)
dotnet restore BetrayalWeb.sln
dotnet run --project src/BetrayalWeb/BetrayalWeb.csprojThen open:
- http://localhost:5109 (HTTP)
- https://localhost:7289 (HTTPS)
For hot-reload during development:
dotnet watch run --project src/BetrayalWeb/BetrayalWeb.csproj# Find your IP
ipconfig getifaddr en0
# Bind to all interfaces
dotnet run --project src/BetrayalWeb/BetrayalWeb.csproj --urls "http://0.0.0.0:5109"Other players on the same network go to http://<your-ip>:5109.
Use a tunnel:
ngrok http 5109Share the generated public URL.
dotnet test BetrayalWeb.slnExpected: ~1,200 tests — all passing.
dotnet publish src/BetrayalWeb/BetrayalWeb.csproj -c Release --output ./publish
./publish/BetrayalWeb- Host enters their name and clicks Host a Game — gets a game code (e.g.
HOUSE-7X4M) - Other players enter the code and click Join Game (3–6 players required)
- Each player picks a character from the selection grid (12 characters in 6 color pairs)
- When all players are ready, the host clicks Start Game
| Action | How |
|---|---|
| Move | Click an open door on your current tile (up to Speed value in rooms) |
| Explore | Step through an unexplored door to draw and place a new room tile |
| Draw a Card | Automatic when entering a room with a symbol (spiral=Event, bull's-eye=Item, raven=Omen) |
| Use Item | Click an item in your inventory panel |
| End Turn | Click End Turn in your dashboard |
Every Omen card drawn triggers a haunt check — roll 6 dice, if total < number of omens revealed, the haunt begins. One player becomes the Traitor with secret objectives opposing the heroes.
Both combatants roll their relevant stat dice. Higher total wins. Loser takes damage = difference, distributed across stats of their choice.
- Heroes win by fulfilling the hero victory condition (varies per haunt)
- Traitor wins by fulfilling the traitor victory condition
See RULEBOOK.md for complete rules.
The following counts match the official 2nd Edition component list:
| Component | Expected | Actual |
|---|---|---|
| Haunt scenarios | 50 | 50 |
| Room tiles | 45 drawable + 5 starting = 50 | 50 |
| Item cards | 22 | 22 |
| Omen cards | 22 | 22 |
| Event cards | 44 | 44 |
| Characters | 12 (6 pairs) | 12 |
| Haunt table entries | 990 (22 omens x 45 rooms) | 990 |
Verification is enforced by unit tests.
| Interface | Purpose | Implementation |
|---|---|---|
IGameEngine |
Top-level game orchestration | GameEngine |
IHauntBehavior |
Per-scenario haunt logic | Haunt01Behavior ... Haunt50Behavior |
IGameService |
Web-layer game session management | GameService |
IRandom |
Abstraction over randomness for testability | DeterministicRandom (tests), system random (prod) |
- Create
src/BetrayalEngine/Engine/Haunts/HauntNNBehavior.csextendingBaseHauntBehavior - Register in
src/BetrayalWeb/Program.cs:builder.Services.AddSingleton<IHauntBehavior, HauntNNBehavior>();
- Add scenario definition to
src/BetrayalEngine/Data/HauntScenarioData.cs - Add omen x room entries to
src/BetrayalEngine/Engine/HauntTable.cs - Add tests in
tests/BetrayalEngine.Tests/
- Implement synthesis in
wwwroot/js/audio-interop.jsunder thesoundsobject - Add a
public Task PlayXxxAsync()toAudioService.cs - Call it from the relevant Blazor component
All colors, spacing, and typography values are CSS custom properties in gothic-theme.css (:root block). Edit these to retheme the entire app.
The test project (tests/BetrayalEngine.Tests/) covers:
- Data integrity — all 50 haunts, 45 tiles, 22 items, 22 omens, 44 events loaded and valid
- Haunt table — 990-entry cross-reference, all omens and rooms covered
- Board grid — tile placement, door alignment, adjacency, floor restrictions
- Card decks — shuffle, draw, discard, empty-deck reshuffling
- Dice roller — distribution, stat-based count, seeded determinism via
IRandom - Combat system — roll resolution, damage distribution, monster combat, death
- Turn manager — phase sequencing, haunt turn order, condition effects
- Exploration engine — door discovery, tile draw, card trigger logic
- Haunt revelation — haunt number lookup, traitor assignment
- All 50 haunt behaviors — setup, per-turn hooks, win conditions, scenario actions
- Validators — move legality, tile placement, staircase rules
- Multiplayer integration — lobby → game start → haunt trigger flow
| Method | Direction | Purpose |
|---|---|---|
CreateGame(name) |
Client → Server | Host creates a game, returns code |
JoinGame(code, name) |
Client → Server | Player joins by code |
SelectCharacter(id) |
Client → Server | Pick a character in lobby |
ReadyUp() |
Client → Server | Mark ready |
StartGame() |
Client → Server | Host starts (requires 3-6 ready) |
MoveToTile(x, y) |
Client → Server | Move explorer |
ExploreDoor(direction) |
Client → Server | Discover new room |
DrawCard() |
Client → Server | Resolve room card |
RollDice(count, purpose) |
Client → Server | Roll dice |
UseItem(itemId, targetId?) |
Client → Server | Use an item |
Attack(targetId) |
Client → Server | Initiate combat |
EndTurn() |
Client → Server | Pass turn |
HauntAction(type, params) |
Client → Server | Scenario-specific action |
{code}— all players in a game (broadcasts state changes){code}-traitor— traitor only (secret objectives){code}-heroes— heroes only (secret objectives)
SignalR is configured with WithAutomaticReconnect(). On reconnect, the client re-syncs full game state from the server. A ReconnectModal.razor overlay shows connection status.
| Feature | Implementation |
|---|---|
| Gothic horror theme | Dark palette, CSS noise texture overlay, flickering candle animations |
| Tooltips | data-tip CSS attribute on all interactive elements |
| Rules reference | Slide-out sidebar with searchable quick-reference |
| Tutorial overlay | 7-step first-time walkthrough; persisted to localStorage |
| Undo last move | Toast bar after each move; dismissed on dice roll or turn end |
| Color-blind mode | Distinct SVG patterns on player tokens (toggle in Settings) |
| Animation speed | Slow / Normal / Fast — persisted to localStorage |
| Responsive layout | Mobile / tablet / desktop grid; pinch-to-zoom on touch |
| ARIA labels | All interactive elements labelled; live regions for log and alerts |
| Rate limiting | JS-side sliding-window limiter per action; server-side ASP.NET rate limiter |
| Auto-reconnect | SignalR reconnect + full state re-sync |
All sounds are synthesised procedurally via the Web Audio API — no audio files bundled. The engine lives in wwwroot/js/audio-interop.js.
| Key | Description |
|---|---|
ambience_exploration |
Low sawtooth drone loop |
ambience_haunt_hero |
Tense triangle pad |
ambience_haunt_traitor |
Sinister low sawtooth |
sting_haunt_reveal |
Dramatic descending harmonic cascade |
sfx_dice_roll |
Filtered noise bursts |
sfx_card_draw |
High-frequency noise sweep + sine ping |
sfx_tile_placed |
Low thud + filtered noise |
sfx_combat_hit |
Sharp noise crack |
sfx_player_death |
Descending sine tones |
sfx_omen_found |
Eerie ascending chimes |
sfx_victory |
Bright ascending arpeggio |
sfx_defeat |
Slow descending minor tones |
Volume and mute state persist to localStorage. Audio context initialises lazily on first user gesture (browser autoplay policy).
- Server-authoritative — all game state lives on the server (Blazor Server model). No client-side cheating possible.
GameInstance— each active game gets its own instance wrapping shared engine services, with a per-sessionSemaphoreSlim(1,1)for thread safety.IRandomabstraction — all randomness flows through a single interface. Tests useDeterministicRandomfor reproducibility.- 50 haunt behaviors — each is a singleton implementing
IHauntBehavior, dispatched byHauntEnginevia scenario number. - Information hiding — during the haunt, traitor and hero objectives are sent only to their respective SignalR groups.
- No database — game state is in-memory only. Games are lost on server restart. For persistence, add a backing store to
GameSessionStore.
See ARCHITECTURE.md for the full design document including all character stat arrays, data model schemas, and component hierarchy.
| Date | Change |
|---|---|
| 2025-02-17 | Initial generation via jc_attractor pipeline (22-node DAG, Opus 4.6 + Sonnet 4.6) |
| 2025-02-18 | Added RULEBOOK.md, .gitignore, removed build artifacts from tracking |