This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
All commands run from the src/ directory:
dotnet build # Build entire solution
dotnet test # Run all tests
dotnet test --filter "FullyQualifiedName~BattleTest" # Run a single test class
dotnet test --filter "FullyQualifiedName~BattleTest.AttackEnemy_WithUnits_ReturnsResult" # Single test
dotnet run --project BrowserGameEngine.StatefulGameServer.Benchmarks # Run benchmarksLocal dev with Docker (requires GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET env vars):
docker-compose upCI runs dotnet build --configuration Release and dotnet test on push/PR to master.
Stateful monolith: per-game world state lives in WorldState (one per active game), cross-game state in GlobalState (users, game registry, achievements). Both are serialized to blob storage every 10 seconds. A GameRegistry singleton manages all active GameInstance objects. React SPA client communicates via REST API and SignalR.
See docs/ARCHITECTURE.md for the full architecture reference.
ReactClient → Shared (ViewModels via API types)
FrontendServer → everything (sole composition root)
StatefulGameServer → GameDefinition, GameModel, Persistence
Persistence.S3 → Persistence
Persistence → GameModel
Shared → GameDefinition, GameModel
GameDefinition.SCO → GameDefinition, GameModel
GameModel → GameDefinition
GameDefinition → (nothing)
- GameDefinition — Pure immutable game config (resources, units, assets, costs). No logic, no state.
- GameModel — Immutable snapshot types (
WorldStateImmutable,GlobalStateImmutable,GameRecordImmutable,PlayerAchievementImmutable, etc.) for serialization. - GameDefinition.SCO — StarCraft Online concrete game data. To make a different game, create a new
GameDefinition.XXXproject. - Persistence —
IBlobStorageabstraction.FileStorage(local dev) orS3Storage(prod). Per-game state atgames/{gameId}/state.json; global state atglobal/state.json. - StatefulGameServer — Core engine.
GameRegistryholds all activeGameInstanceobjects. Repositories injectIWorldStateAccessorto access the appropriateWorldState. Read/write repositories are strictly separated. Game tick modules implementIGameTickModule. - Shared — ViewModels/DTOs. The API contract between server and client.
- FrontendServer — ASP.NET Core host (composition root). Controllers, hosted services, middleware. All game endpoints require
[Authorize]and must checkCurrentUserContext.IsValid. - ReactClient — React SPA (Vite + TypeScript). Communicates with the server via REST API and SignalR.
- Immutable/mutable duality:
GameModeltypes are immutable snapshots;StatefulGameServer/GameModelInternal/has mutable counterparts. Convert viaToImmutable()/ToMutable(). IWorldStateAccessor: Repositories inject this interface (notWorldStatedirectly) so the same class works for any game instance.- Read/write repository separation: Different classes for queries vs mutations. Write repos use
lockfor thread safety. - Tick-based simulation: Each
GameInstancehas its ownGameTickEngine.GameTickTimerServiceiterates all active instances. New periodic behavior = newIGameTickModuleregistered inGameServerExtensions. - Commands as records: Player actions are record types in
Commands/(e.g.,BuildAssetCommand,SendUnitCommand). - Thread safety:
WorldState.PlayersandGlobalState.UsersuseConcurrentDictionary. Write repos uselock. Tick engine usesInterlockedguards.
- Add command record in
StatefulGameServer/Commands/ - Add or extend repository (read + write); inject
IWorldStateAccessor, notWorldState - Add ViewModel in
Shared - Add controller endpoint in
FrontendServer/Controllers/ - Add React page in
ReactClient/src/pages/and route inApp.tsx - If tick-based, add
IGameTickModuleand register inGameServerExtensions
Governed by .editorconfig:
- Tabs for C# indentation (4-space width), spaces for XML/JSON/YAML (2-space)
- LF line endings, UTF-8, file-scoped namespaces
varonly when type is apparent; braces preferred- Newline before open brace for types and methods only (not
else/catch/finally) - Sort
usingdirectives with System first
| Key | Purpose |
|---|---|
Bge:DevAuth |
Enable password-less dev login (default: true) |
Bge:S3BucketName |
S3 bucket; empty = local FileStorage |
Bge:S3KeyPrefix |
S3 key prefix |
GitHub:ClientId |
GitHub OAuth client ID |
GitHub:ClientSecret |
GitHub OAuth client secret |
Tests are in StatefulGameServer.Test (xUnit). Test game logic through repositories using the TestGame helper class, not through controllers.
All changes go through GitHub PRs — never push directly to master.
Each task gets an isolated git worktree so multiple branches can be active simultaneously:
# Create worktree for a new task
git worktree add /tmp/bge-work/<branch-name> -b <branch-name> master
cd /tmp/bge-work/<branch-name>
# ... implement, build, test, commit ...
# Push and open PR
git push -u origin <branch-name>
gh pr create --title "<title>" --body "..."
# Clean up worktree after PR is open
cd /home/chris/repos/my/bge
git worktree remove /tmp/bge-work/<branch-name>List all active worktrees: git worktree list