This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
IMPORTANT: Always use Serena MCP tools when possible for code exploration and manipulation.
Serena provides semantic, symbol-aware operations that are more efficient and accurate than text-based tools:
- Code Exploration: Use
get_symbols_overview,find_symbol,search_for_patterninstead of Grep/Read for understanding code - Code Navigation: Use
find_referencing_symbolsto understand dependencies and usage - Code Editing: Use
replace_symbol_body,insert_after_symbol,insert_before_symbolfor precise code changes - Refactoring: Use
rename_symbolfor safe, project-wide renames
Serena has been initialized with project-specific memories about architecture, conventions, and workflows.
This project enforces StyleCop code quality rules. Always follow these guidelines when writing C# code:
-
SA1402: File may only contain a single type
- Each file must contain only ONE class, interface, enum, or struct
- Exception: Nested types within a parent type are allowed
- ❌ BAD: Two classes in one file
- ✅ GOOD: One class per file
-
SA1649: File name should match first type name
- File name must exactly match the class/type name
- Example: Class
UserRepository→ FileUserRepository.cs - Case-sensitive match required
-
SA1633: File should have header
- Use XML documentation comments (
///) for all public types and members - Document purpose, parameters, return values, and exceptions
- Use XML documentation comments (
-
SA1200: Using directives should be placed correctly
- Place all
usingstatements outside the namespace - Order: System namespaces first, then third-party, then project namespaces
- Place all
-
SA1309: Field names should not begin with underscore
- This rule is typically disabled in this project (private fields use
_fieldNameconvention) - If enabled, would require private fields without underscores
- This rule is typically disabled in this project (private fields use
// ✅ GOOD: Follows all StyleCop rules
using System;
using Microsoft.Extensions.Logging;
using Backgammon.Server.Models;
namespace Backgammon.Server.Services;
/// <summary>
/// Manages user authentication and authorization.
/// </summary>
public class AuthService
{
private readonly ILogger<AuthService> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="AuthService"/> class.
/// </summary>
/// <param name="logger">Logger for diagnostic output.</param>
public AuthService(ILogger<AuthService> logger)
{
_logger = logger;
}
}- One class per file - Create separate files for each class
- Match file name to class name exactly
- Add XML documentation for all public members
- Use proper namespace matching the folder structure
- Order usings correctly (System first, then alphabetically)
Before committing C# code:
- One type per file
- File name matches class name exactly
- XML documentation on public types/members
- Usings are outside namespace and ordered correctly
- Private fields use
_camelCaseconvention - No unused using statements
- Proper indentation (4 spaces, not tabs)
- Follow ESLint rules (enforced by build)
- No unused variables or imports
- Avoid constant expressions in conditionals (
false &&,true ||) - Use TypeScript strict mode patterns
- Prefer functional components with hooks
# Build backend
dotnet build
# Build frontend
cd Backgammon.WebClient && pnpm build
# Regenerate TypedSignalR client (after changing IGameHub or IGameHubClient interfaces)
cd Backgammon.WebClient && pnpm generate:signalr
# Run all tests
dotnet test
# Run tests with detailed output
dotnet test --verbosity normal
# Run with Aspire (all-in-one: Postgres + Redis + Backend + Frontend)
cd Backgammon.AppHost && dotnet run
# Run console game
cd Backgammon.Console && dotnet run
# Run web multiplayer (manual)
cd Backgammon.Server && dotnet run # Server on http://localhost:5000
cd Backgammon.WebClient && pnpm dev # Client dev server on http://localhost:3000
# Quick start web (script)
./start-web.sh
# Run AI simulations
cd Backgammon.AI && dotnet run
# Build/run documentation site
pnpm docs:dev # Start docs dev server
pnpm docs:build # Build docs for productionMulti-project solution for a Backgammon game with console UI, web multiplayer, and AI framework:
- Backgammon.Core - Pure game logic library (no dependencies). Contains
GameEngine,Board,Player,Dice,Move,DoublingCube,Match,GameHistory,TurnSnapshot. - Backgammon.Console - Text-based UI using Spectre.Console
- Backgammon.Server - SignalR multiplayer server. State lives in Orleans grains (
GameGrain,MatchGrain,PresenceGrain,MatchChatGrain,AnalysisSessionGrain) with grain state persisted to Postgres via Orleans AdoNet.GameHubis a thin dispatch layer over the grains. Postgres also stores game history / users / friendships via thePostgres*Repositoryclasses. Remaining services includeMatchService(read-only query facade),EloRatingService,DailyPuzzleService,AnalysisService. - Backgammon.WebClient - React + TypeScript + Vite frontend with real-time SignalR communication. Uses shadcn/ui, TailwindCSS, Zustand for state management.
- Backgammon.AI - Pluggable AI framework. Implements
IBackgammonAIinterface withRandomAI,GreedyAI, and heuristic-based bots. - Backgammon.Analysis - Position evaluation and analysis. Integrates with GNU Backgammon (
GnubgEvaluator) and providesHeuristicEvaluator. - Backgammon.Plugins - Plugin registry for bots and evaluators. Provides
IPluginRegistry,BotMetadata,EvaluatorMetadata. - Backgammon.AppHost - .NET Aspire orchestrator (manages Postgres, Redis, services)
- Backgammon.ServiceDefaults - Shared Aspire configuration for observability and health checks
- Backgammon.Tests - xUnit test project
The server uses Postgres for both EF Core domain persistence and Orleans grain state, plus Redis for caching and the SignalR backplane.
- Postgres + Redis run in Docker containers managed by Aspire
- EF Core migrations live under
Backgammon.Server/Data/Migrations(BackgammonDbContext) - Orleans schema (
OrleansQuery,OrleansStorage) is bootstrapped at startup byOrleansSchemaInitializer.EnsureSchemaAsyncfrom embedded SQL underBackgammon.Server/Data/OrleansSchema/ - Same Postgres instance backs both EF Core and Orleans AdoNet grain storage
All under Backgammon.Server/Services/Postgres/:
PostgresUserRepositoryPostgresGameRepository— game historyPostgresMatchRepository— match recordsPostgresFriendshipRepositoryPostgresPuzzleRepositoryPostgresThemeRepository
IPersistentState<TState>withAddAdoNetGrainStorageAsDefault(Invariant:Npgsql)- Grain state types are
[GenerateSerializer]-decorated with[Id(N)]on every field (runtime-validated — see Orleans serializer note) PubSubStore(for Orleans Streams) is in-memory; Streams aren't currently used
- Deployed to AWS Lightsail Instance running
docker-compose(seeDEPLOYMENT.md) - Images published to GitHub Container Registry (
ghcr.io/garrettbeatty/backgammon-*) - Postgres + Redis run as containers on the instance; data persists in Docker volumes
- Snapshots scheduled via Lightsail console
State and lifecycle that used to live in singleton services now live in Orleans grains. Hub methods dispatch into grains via IGrainFactory.
GameGrain(key = gameId) — owns the liveGameEnginefor an in-progress game. State persisted viaIPersistentState<GameGrainState>. Self-healing fallback toIGameRepositoryfor games created before grain-state migration.MatchGrain(key = matchId) — owns match lifecycle (CreateMatchAsync,CreateCorrespondenceMatchAsync,JoinAsync,EnsureNextGameAsync,CompleteGameAsync,AbandonAsync) plus correspondence turn/timeout handling. State persisted viaIPersistentState<MatchGrainState>. Dual-writes toIMatchRepositoryso HTTP query endpoints stay live.PresenceGrain(well-known key ="global") — owns presence (playerId↔connection set) and connection→game association. Ephemeral; no persisted state.MatchChatGrain(key = matchId) — FIFO chat history (capped at 500) plus per-connection rate-limit timestamps. Replaces the oldChatService+IMatchChatStorage.AnalysisSessionGrain(key = userId) — owns the user's analysis sessions and connection→session map. Single-threaded (grain serialization replaces the oldSemaphoreSlim).
Read-only query facades that still exist as services: MatchService (queries match data), GameService, CorrespondenceGameService (queries only — lifecycle moved into MatchGrain).
Tech Stack:
- React 18 - UI framework with functional components and hooks
- TypeScript 5.3 - Type-safe development
- Vite 7 - Fast build tool and dev server
- shadcn/ui - Accessible component library built on Radix UI
- TailwindCSS 3 - Utility-first CSS framework
- Zustand - Lightweight state management
- SignalR (@microsoft/signalr 8.0) - Real-time WebSocket communication
- React Router 6 - Client-side routing
- Recharts - Data visualization for statistics
Project Structure:
Backgammon.WebClient/
├── src/ # Source code (NOT committed build outputs)
│ ├── components/ # React components
│ │ ├── board/ # Board rendering (BoardSVG, checkers)
│ │ ├── game/ # Game components (GameStatus, PlayerCard, MoveList)
│ │ ├── puzzle/ # Daily puzzle (DailyPuzzleBoard, PuzzleStats)
│ │ ├── players/ # Player list and profiles
│ │ ├── friends/ # Friend list and social features
│ │ ├── home/ # Home page components
│ │ ├── themes/ # Theme selector and customization
│ │ ├── layout/ # Layout components
│ │ ├── modals/ # Modal dialogs
│ │ └── ui/ # shadcn/ui components (30+)
│ ├── contexts/ # React contexts (SignalRContext, AuthContext, MatchContext)
│ ├── hooks/ # Custom React hooks (useSignalREvents)
│ ├── lib/ # Utility libraries
│ ├── pages/ # Route pages (17 pages including Analysis, Puzzle, Profile)
│ ├── services/ # Service layer (signalr, auth, api, theme, audio)
│ ├── stores/ # Zustand stores (game, match, analysis, puzzle, theme, chat, etc.)
│ ├── styles/ # Global styles
│ ├── types/ # TypeScript type definitions (includes generated types)
│ ├── utils/ # Utility functions
│ └── main.tsx # App entry point
├── wwwroot/ # Build output directory (gitignored)
├── index.html # Vite HTML template
├── vite.config.ts # Vite configuration
├── tailwind.config.js # TailwindCSS configuration
├── components.json # shadcn/ui configuration
└── package.json # pnpm dependencies
Key Patterns:
-
Multi-Tab Support - Players can open the same game in multiple browser tabs. The server tracks multiple connections per player using
HashSet<string>for connection IDs. All tabs receive real-time updates and can make moves. -
SignalR Event Handling -
useSignalREventshook registers event handlers once on mount and uses refs to avoid constant cleanup/re-registration. Events are filtered by game ID to prevent cross-game interference. -
State Management - Multiple Zustand stores for different concerns:
gameStore- Current game state and player colormatchStore- Match-level state (scores, Crawford)analysisStore- Position analysis resultspuzzleStore- Daily puzzle statethemeStore- Theme preferenceschatStore- Chat messagesboardInteractionStore- User interactions (selected point, drag state)
-
Real-time Communication Flow:
- Client →
invoke(HubMethods.MakeMove, ...)→ Server - Server →
SendAsync("GameUpdate", state)→ All connected tabs useSignalREvents→ Updates Zustand store → React re-renders
- Client →
-
Connection Lifecycle:
- Page loads → SignalR connects → Sets
isConnected = true - GamePage waits for
isConnectedbefore callingJoinGame - On unmount, cleanup effect calls
LeaveGame - Prevents race conditions where join attempts before connection ready
- Page loads → SignalR connects → Sets
-
Build Process:
- Development:
npm run dev→ Vite dev server with HMR - Production:
npm run build→ TypeScript compile + Vite bundle →wwwroot/ - Output is gitignored, generated fresh on deploy
- Development:
The application supports multi-game matches with proper match scoring and Crawford rule:
Domain Layer (Backgammon.Core):
Match- Pure match logic, tracks score, Crawford state, game historyGame- Individual game within a match (wraps GameEngine result)GameResult- Captures win type (Normal/Gammon/Backgammon) and points scoredMatchStatusenum - InProgress, Completed, Abandoned
Server Layer (Backgammon.Server):
MatchGrain- Owns match lifecycle (create, join, start games, complete, abandon, correspondence turn handling). State viaIPersistentState<MatchGrainState>.MatchService- Read-only query facade overIMatchRepository(match lookups for HTTP endpoints; lifecycle lives onMatchGrain)PostgresMatchRepository- Persists matches and games to Postgres (dual-written byMatchGrain)Match(server model) - WrapsCore.Matchwith server metadata (lobby status, opponent type, duration)
Key Match Patterns:
- Match created → First game starts automatically
- Game completes →
MatchService.CompleteGameAsync()updates match score - Crawford rule enforced automatically when score reaches targetScore-1
- Match completes when a player reaches targetScore
Crawford Rule Implementation:
// Backgammon.Core.Match handles Crawford logic
match.RecordGameResult(gameResult); // Auto-activates Crawford if needed
if (match.IsCrawfordGame) {
// Doubling cube disabled for this game
}Match Lobby Flow:
- Player creates match lobby via
CreateMatchLobbyAsync()(friend/AI/open lobby) - Opponent joins via
JoinMatch()SignalR method - Creator starts match via
StartMatchFromLobby() - First game begins, players join game session
The server supports multiple browser tabs per player in the same game:
PresenceGraintracks the set of connection IDs per player and the connection→game associationGameGrainbroadcasts to all connections owned by each player, not just one- Reconnects: when a connection joins an in-progress game, the grain sends current state to the new connection only; new games broadcast
GameStartto everyone
End result: players can open the same game in multiple tabs, make moves from any tab, see updates in all tabs, and reconnect after network issues without manual recovery.
- 24 points (positions 1-24) as
Point[]array - Point 0 = bar, Point 25 = bear off
- White moves 24→1 (descending), Red moves 1→24 (ascending)
- Home boards: White = points 1-6, Red = points 19-24
1. Check game.RemainingMoves.Count > 0
2. If 0, call game.RollDice()
3. Call game.GetValidMoves() for legal moves
4. game.ExecuteMove(move) - automatically updates RemainingMoves
5. game.EndTurn() to switch playersNever manually manipulate RemainingMoves - ExecuteMove() manages it.
- Bar priority: checkers on bar must enter first
- Bearing off: only when all checkers in home board
- Forced moves: if only one die can be used, must use larger value
- Doubles: roll same number = 4 moves
Tests directly manipulate board state for scenarios:
var game = new GameEngine();
game.StartNewGame();
game.Board.GetPoint(1).Checkers.Clear();
game.Board.GetPoint(1).AddChecker(CheckerColor.White);
game.Dice.SetDice(3, 4);
game.RemainingMoves.Clear();
game.RemainingMoves.AddRange(game.Dice.GetMoves());Implement IBackgammonAI.ChooseMoves(GameEngine engine):
while (engine.RemainingMoves.Count > 0) {
var validMoves = engine.GetValidMoves();
if (validMoves.Count == 0) break;
var move = /* select */;
engine.ExecuteMove(move);
}GetValidMoves() returns new list each call after ExecuteMove() changes state.
The server implements an ELO-based rating system for player skill tracking:
Key Components:
EloRatingService- Calculates rating changes after matchesPlayerStatsService- Tracks wins, losses, rating history- Rating stored per user in Postgres
Rating Flow:
- Match completes →
EloRatingService.CalculateNewRatings() - Ratings updated based on outcome and opponent strength
- Stats persisted via
PlayerStatsService
See docs/elo-rating-implementation-plan.md for detailed algorithm.
The Analysis project provides move evaluation and suggestions:
Evaluators:
GnubgEvaluator- Uses GNU Backgammon for expert-level analysisHeuristicEvaluator- Fast built-in position scoring
Models:
PositionEvaluation- Win/gammon/backgammon probabilitiesMoveSequenceEvaluation- Ranked move suggestionsCubeDecision- Double/take/pass recommendations
Integration:
The server's AnalysisService exposes evaluation through SignalR for real-time hints.
See docs/GNUBG_SETUP.md for GNU Backgammon configuration.
The server supports turn-based asynchronous games:
Key Components:
MatchGrain.CreateCorrespondenceMatchAsync/HandleTurnCompletedAsync/HandleTimeoutAsync- Lifecycle lives on the grainCorrespondenceGameService- Read-only query facade ("my turn" / "waiting" lists)CorrespondenceTimeoutService- Background service checking for expired turns (slated to move to an Orleans reminder in a future phase)
Flow:
- Player creates correspondence match via
CreateCorrespondenceMatch()(dispatched toMatchGrain) - Grain state + game state persisted via Orleans grain storage (AdoNet → Postgres) after each turn
- Opponent notified via
CorrespondenceTurnNotificationevent GetCorrespondenceGames()queries Postgres viaCorrespondenceGameServiceto return games awaiting player's turn
Daily puzzles using GNU Backgammon for position generation:
Components:
DailyPuzzleService- Retrieves/validates daily puzzlesDailyPuzzleGenerationService- Background service generating puzzlesRandomPositionGenerator- Creates random positions for evaluationPostgresPuzzleRepository- Puzzle persistence
Puzzle Flow:
- Background service generates puzzle at configured time
- Position evaluated by GNU Backgammon for optimal moves
- Players solve via
SubmitPuzzleAnswer()with move comparison - Streak tracking via
PuzzleStreakInfo
HybridCache with two-tier architecture:
Configuration (CacheSettings):
{
"CacheSettings": {
"UserProfile": { "Expiration": "00:05:00", "LocalCacheExpiration": "00:01:00" },
"PlayerStats": { "Expiration": "00:15:00", "LocalCacheExpiration": "00:03:00" },
"FriendsList": { "Expiration": "00:05:00", "LocalCacheExpiration": "00:01:00" }
}
}Layers:
- L1 (Local): In-memory cache per server instance
- L2 (Distributed): Redis for cross-server sharing
Usage Pattern:
var user = await _cache.GetOrCreateAsync(
$"user:{userId}",
async ct => await _userRepo.GetByUserIdAsync(userId),
new HybridCacheEntryOptions { ... },
tags: [$"player:{userId}"]);Bots and evaluators registered via plugin architecture:
Components:
IPluginRegistry- Central registry for pluginsBotMetadata/EvaluatorMetadata- Plugin descriptorsIBotResolver- Creates bot instances by ID
Registration:
// In Program.cs
builder.Services.AddBackgammonPlugins(builder.Configuration);
builder.Services.AddStandardBots(); // Random, Greedy
builder.Services.AddHeuristicBot(); // Heuristic evaluator
builder.Services.AddAnalysisPlugins(); // GNU Backgammon (if available)Available Bots:
random- Random valid moves (~800 ELO)greedy- Prioritizes bearing off/hitting (~1100 ELO)heuristic- Position evaluation (~1300 ELO)gnubg- GNU Backgammon neural network (~1800+ ELO)
Runtime feature toggles in configuration:
{
"Features": {
"BotGamesEnabled": false,
"MaxBotGames": 5,
"BotGameRestartDelaySeconds": 30
}
}Accessed via IOptions<FeatureFlags> injection.
When working with Aspire orchestration:
- Run with
dotnet runfrom Backgammon.AppHost directory - Changes to AppHost Program.cs require restart
- Use Aspire MCP tools to check resource status and debug
- Avoid persistent containers during development
- See
AGENTS.mdfor Aspire-specific agent instructions