This document specifies the business logic for the Automatafl game platform backend. It follows a Command Query Responsibility Segregation (CQRS) and Event Sourcing architecture.
- Commands: Represent an intent to change the system's state. They are handled by Aggregates.
- Events: Represent an immutable fact that has occurred in the system. They are the single source of truth for all state.
- Aggregates: Encapsulate the business logic and state for a domain entity (e.g., a GameorPlayer). They process Commands and produce Events.
- Processes & Sagas: Coordinate complex workflows by listening to Events and issuing new Commands.
- Projections: Read-models that listen to the event stream to build and maintain queryable state for clients.
type Uuid = universally unique identifier
type Pid = u8      // Player ID within a single game context (e.g., 0, 1)
type Timestamp = u64 // Unix epoch seconds
struct Coord { x: u8, y: u8 }
struct Move { who: Pid, from: Coord, to: Coord }
enum GameLifecycle { Waiting, InProgress, Finished, Aborted }
enum GameVariant { Standard, ColumnRule }
enum TimeControl { Unlimited, Realtime(seconds_initial: u16, seconds_increment: u8), Correspondence(days_per_move: u8) }
enum GameOutcome { Win, Loss, Draw, Aborted }
enum TerminationReason { Normal, Resignation, Timeout, Abandonment }
enum RoundState { Fresh, PartiallySubmitted, ResolvingConflict, GameOver }
enum JoinPolicy { Open, ApprovalRequired }
struct Player {
    id: Uuid,
    displayname: String,
    password_hash: String,
    is_admin: bool,
    bio: Option<String>,
    avatar_url: Option<String>,
    title: Option<String>, // e.g., "GM", "LM"
    elo_ratings: Map<GameVariant, i32>,
    stats: Map<GameVariant, PlayerStats>,
    settings: PlayerSettings,
    moderation_status: { is_muted: bool, is_banned: bool },
    created_at: Timestamp,
}
struct PlayerStats {
    games_played: u32,
    games_won: u32,
    total_playtime: u64, // seconds
}
struct PlayerSettings {
    theme: String,
    sound_on: bool,
    // ... other UI/gameplay preferences
}
struct Game {
    id: Uuid,
    is_rated: bool,
    variant: GameVariant,
    time_control: TimeControl,
    lifecycle: GameLifecycle,
    outcome: Option<{ winner: Option<Pid>, reason: TerminationReason }>,
    created_at: Timestamp,
    started_at: Option<Timestamp>,
    players: Map<Pid, Uuid>,
    clocks: Map<Pid, u64>, // Remaining time in milliseconds
    draw_offers: Map<Pid, bool>,
    round_state: RoundState,
    pending_moves: Map<Pid, Move>,
    locked_players: Vec<Pid>,
    core_logic_state: Opaque<automatafl_logic::Game>,
}
struct Session {
    id: Uuid,
    player_id: Uuid,
    expires_at: Timestamp,
}
struct Tournament {
    id: Uuid,
    name: String,
    creator_id: Uuid,
    is_rated: bool,
    variant: GameVariant,
    time_control: TimeControl,
    rules: TournamentRules,
    status: (Scheduled, InProgress, Finished),
    players: Vec<Uuid>,
    pairings: Vec<Pairing>,
    standings: Map<Uuid, Score>,
}
enum TournamentRules { Arena { duration_minutes: u16, berserkable: bool }, Swiss { num_rounds: u8 } }
struct Pairing { round: u8, game_id: Uuid, player1: Uuid, player2: Uuid }
struct Team {
    id: Uuid,
    name: String,
    leader_id: Uuid,
    description: String,
    members: Vec<Uuid>,
    join_policy: JoinPolicy,
}
struct Study {
    id: Uuid,
    name: String,
    owner_id: Uuid,
    contributors: Vec<Uuid>,
    chapters: Vec<Chapter>,
    chat_history: Vec<ChatMessage>,
}
struct Chapter { name: String, initial_state: Game, annotations: Map<Move, String> }
Commands represent intents to change the system state and are named in the imperative tense.
command RegisterPlayer { displayname: String, password: String }
command LogInPlayer { displayname: String, password: String }
command LogOutPlayer { session_id: Uuid }
command UpdatePlayerProfile { player_id: Uuid, bio: Option<String>, avatar_url: Option<String> }
command UpdatePlayerSettings { player_id: Uuid, settings: PlayerSettings }
command FollowPlayer { follower_id: Uuid, followed_id: Uuid }
command UnfollowPlayer { follower_id: Uuid, followed_id: Uuid }
command SendMessage { sender_id: Uuid, recipient_id: Uuid, body: String }
command PostChatMessage { player_id: Uuid, game_id: Uuid, message: String }
command CreateCustomGame { creator_id: Uuid, is_rated: bool, variant: GameVariant, time_control: TimeControl, rating_range: Option<(u16, u16)> }
command ChallengePlayer { challenger_id: Uuid, challenged_id: Uuid, is_rated: bool, variant: GameVariant, time_control: TimeControl }
command AcceptChallenge { challenge_id: Uuid }
command DeclineChallenge { challenge_id: Uuid }
command CancelChallenge { challenge_id: Uuid }
command JoinGame { player_id: Uuid, game_id: Uuid }
command SubmitMove { player_id: Uuid, game_id: Uuid, from: Coord, to: Coord }
command CompleteRound { player_id: Uuid, game_id: Uuid }
command ResignGame { player_id: Uuid, game_id: Uuid }
command OfferDraw { player_id: Uuid, game_id: Uuid }
command AcceptDraw { player_id: Uuid, game_id: Uuid }
command DeclineDraw { player_id: Uuid, game_id: Uuid }
command ClaimVictoryOnTime { player_id: Uuid, game_id: Uuid }
command JoinMatchmakingQueue { player_id: Uuid, variant: GameVariant, time_control: TimeControl }
command LeaveMatchmakingQueue { player_id: Uuid }
// Internal command issued by the Matchmaking Process
command CreateMatchmadeGame { players: Vec<Uuid>, variant: GameVariant, time_control: TimeControl }
command CreateTournament { creator_id: Uuid, name: String, is_rated: bool, variant: GameVariant, time_control: TimeControl, rules: TournamentRules }
command JoinTournament { player_id: Uuid, tournament_id: Uuid }
command LeaveTournament { player_id: Uuid, tournament_id: Uuid }
command StartTournament { admin_id: Uuid, tournament_id: Uuid }
command CreateTeam { creator_id: Uuid, name: String, description: String, join_policy: JoinPolicy }
command JoinTeam { player_id: Uuid, team_id: Uuid }
command LeaveTeam { player_id: Uuid, team_id: Uuid }
command ApproveJoinRequest { leader_id: Uuid, team_id: Uuid, player_to_approve: Uuid }
command KickTeamMember { leader_id: Uuid, team_id: Uuid, player_to_kick: Uuid }
command CreateStudy { owner_id: Uuid, name: String, is_public: bool }
command AddChapterToStudy { study_id: Uuid, chapter_name: String, initial_state: Game }
command AnnotateMoveInStudy { study_id: Uuid, chapter_index: usize, move: Move, text: String }
command RequestGameAnalysis { requester_id: Uuid, game_id: Uuid }
command ReportPlayer { reporter_id: Uuid, reported_id: Uuid, reason: String, context_game_id: Option<Uuid> }
command MutePlayer { moderator_id: Uuid, target_id: Uuid, reason: String }
command BanPlayer { moderator_id: Uuid, target_id: Uuid, reason: String }
command AdminDeleteGame { admin_id: Uuid, game_id: Uuid }
command AdminForceCompleteRound { admin_id: Uuid, game_id: Uuid }
Events are immutable facts named in the past tense, forming the single source of truth.
event PlayerRegistered { player_id: Uuid, displayname: String, timestamp: Timestamp }
event PlayerLoggedIn { player_id: Uuid, session_id: Uuid, expires_at: Timestamp }
event PlayerLoggedOut { session_id: Uuid }
event PlayerProfileUpdated { player_id: Uuid, bio: Option<String>, avatar_url: Option<String> }
event PlayerSettingsUpdated { player_id: Uuid, new_settings: PlayerSettings }
event PlayerStatsUpdated { player_id: Uuid, variant: GameVariant, outcome: GameOutcome, playtime: u64 }
event PlayerEloUpdated { player_id: Uuid, variant: GameVariant, old_elo: i32, new_elo: i32 }
event PlayerFollowedPlayer { follower_id: Uuid, followed_id: Uuid }
event PlayerUnfollowedPlayer { follower_id: Uuid, followed_id: Uuid }
event MessageSent { conversation_id: Uuid, sender_id: Uuid, recipient_id: Uuid, body: String, timestamp: Timestamp }
event ChatMessagePosted { game_id: Uuid, player_id: Uuid, displayname: String, message: String, timestamp: Timestamp }
event ChallengeIssued { challenge_id: Uuid, challenger_id: Uuid, challenged_id: Uuid, game_config: { is_rated, variant, time_control } }
event ChallengeAccepted { challenge_id: Uuid, game_id: Uuid }
event ChallengeDeclined { challenge_id: Uuid }
event ChallengeCancelled { challenge_id: Uuid }
event GameCreated { game_id: Uuid, is_rated: bool, variant: GameVariant, time_control: TimeControl }
event PlayerJoinedGame { game_id: Uuid, player_id: Uuid, pid: Pid }
event GameStarted { game_id: Uuid, players: Map<Pid, Uuid>, timestamp: Timestamp }
event MoveSubmitted { game_id: Uuid, pid: Pid, move: Move, remaining_time_ms: u64 }
event MoveInvalidated { game_id: Uuid, pid: Pid, reason: String }
event RoundCompleted { game_id: Uuid, move_results: Map<Pid, MoveResult>, automaton_location: Coord }
event ConflictsOccurred { game_id: Uuid, locked_players: Vec<Pid>, conflict_coords: Vec<Coord> }
event DrawOffered { game_id: Uuid, by_pid: Pid }
event DrawOfferResponded { game_id: Uuid, accepted: bool }
event GameEnded { game_id: Uuid, outcome: GameOutcome, reason: TerminationReason, elo_changes: Option<Vec<EloChange>> }
event PlayerJoinedMatchmakingQueue { player_id: Uuid, preferences: { variant, time_control }, timestamp: Timestamp }
event PlayerLeftMatchmakingQueue { player_id: Uuid, timestamp: Timestamp }
event MatchFound { game_id: Uuid, players: Vec<Uuid> }
event TournamentCreated { tournament_id: Uuid, name: String, creator_id: Uuid, config: { ... } }
event PlayerJoinedTournament { tournament_id: Uuid, player_id: Uuid }
event TournamentStarted { tournament_id: Uuid }
event PairingsGenerated { tournament_id: Uuid, round: u8, pairings: Vec<Pairing> }
event TournamentStandingUpdated { tournament_id: Uuid, new_standings: Map<Uuid, Score> }
event TournamentFinished { tournament_id: Uuid, final_standings: Map<Uuid, Score> }
event TeamCreated { team_id: Uuid, name: String, leader_id: Uuid }
event PlayerJoinedTeam { team_id: Uuid, player_id: Uuid }
event PlayerLeftTeam { team_id: Uuid, player_id: Uuid }
event TeamJoinRequestReceived { team_id: Uuid, requesting_player_id: Uuid }
event TeamLeaderChanged { team_id: Uuid, new_leader_id: Uuid }
event GameAnalysisRequested { game_id: Uuid, requested_at: Timestamp }
event GameAnalysisCompleted { game_id: Uuid, analysis_data: Opaque }
event PlayerReported { reporter_id: Uuid, reported_id: Uuid, reason: String, context_game_id: Option<Uuid> }
event PlayerMuted { target_id: Uuid, reason: String }
event PlayerBanned { target_id: Uuid, reason: String }
Aggregates process commands, enforce business rules, and produce events.
- State: Playerstruct.
- Handles: RegisterPlayer,UpdatePlayerProfile,UpdatePlayerSettings.
- Business Rules:
- On RegisterPlayer:- Validate displaynamefor uniqueness, length (1-50), and character set.
- Validate password strength (e.g., >= 12 chars).
- Hash password.
- emit PlayerRegisteredwith default ELO ratings and zeroed stats.
 
- Validate 
- On UpdatePlayerProfile:- Validate biolength (<= 500 chars).
- Validate avatar_urlformat (must be a secure HTTPS URL).
- emit PlayerProfileUpdated.
 
- Validate 
 
- On 
- Applies Events: Updates its state from PlayerRegistered,PlayerProfileUpdated,PlayerSettingsUpdated,PlayerStatsUpdated,PlayerEloUpdated.
- State: Gamestruct.
- Handles: CreateCustomGame,JoinGame,CreateMatchmadeGame,SubmitMove,CompleteRound,ResignGame,OfferDraw,AcceptDraw,ClaimVictoryOnTime.
- Business Rules:
- On CreateCustomGame:- emit GameCreated.
- emit PlayerJoinedGamefor the creator as- Pid(0).
 
- On CreateMatchmadeGame:- emit GameCreated.
- For each player provided, emit PlayerJoinedGame.
- emit GameStarted.
- emit MatchFound.
 
- On JoinGame:- Guard: lifecyclemust beWaiting.
- Guard: Player must not already be in the game.
- Guard: Game must not be full.
- Assign the lowest available Pid.
- emit PlayerJoinedGame.
- If the game is now full, emit GameStarted.
 
- Guard: 
- On SubmitMove:- Guard: lifecyclemust beInProgress.
- Translate player_idto in-gamePid.
- Use core_logic_state.propose_move(move).
- If the move is valid, emit MoveSubmitted.
- If the move is invalid, emit MoveInvalidated.
 
- Guard: 
- On CompleteRound:- Guard: lifecyclemust beInProgress.
- Guard: All players must have submitted moves.
- Use core_logic_state.try_complete_round().
- If successful, emit RoundCompleted. If a winner is determined,emit GameEndedand trigger ELO/stats updates.
- If conflicts occur, emit ConflictsOccurred.
 
- Guard: 
- On ResignGame:emit GameEndedwithreason: Resignation.
- On ClaimVictoryOnTime: Check opponent's clock. If expired,emit GameEndedwithreason: Timeout.
- On AcceptDraw: If a draw was offered by the other player,emit GameEndedwithoutcome: Draw.
 
- On 
- State: Tournamentstruct.
- Handles: CreateTournament,JoinTournament,LeaveTournament,StartTournament.
- Business Rules:
- On JoinTournament:- Guard: statusmust beScheduled.
- Guard: Player must not already be in the tournament.
- emit PlayerJoinedTournament.
 
- Guard: 
- On StartTournament:- Guard: statusmust beScheduled.
- emit TournamentStarted.
- Generate first-round pairings based on ELO.
- emit PairingsGenerated.
 
- Guard: 
 
- On 
Background processes that coordinate logic by reacting to events and issuing new commands.
- Trigger: Runs on a periodic timer (e.g., every 5 seconds).
- Logic:
- Query: Fetch players from the MatchmakingQueueView.
- Group: Group players by variantandtime_control.
- Match: For each group large enough, find players with ELO ratings within a defined tolerance (e.g., +/- 200). If no ELO-matched group is found after a timeout, match longest-waiting players.
- Command: If a match is found, issue a CreateMatchmadeGamecommand.
 
- Query: Fetch players from the 
- Listens to: ChallengeIssued.
- Handles: AcceptChallenge,DeclineChallenge,CancelChallenge.
- Logic:
- On AcceptChallenge:emit ChallengeAccepted, then issue aCreateCustomGamecommand using the configuration from the originalChallengeIssuedevent.
 
- On 
- Listens to: GameEnded.
- Logic:
- If the game belongs to an active tournament, update tournament standings and emit TournamentStandingUpdated.
- Check if all games in the current round are finished.
- If so, generate new pairings based on current standings (e.g., Swiss rules).
- emit PairingsGeneratedand issue- CreateCustomGamecommands for the new games.
 
- If the game belongs to an active tournament, update tournament standings and 
- Trigger: Runs on a periodic timer (e.g., every hour).
- Logic:
- Query: Find all sessions in the AuthenticationViewwhereexpires_at < now().
- Command: For each expired session, issue a LogOutPlayercommand.
 
- Query: Find all sessions in the 
- Listens to: GameAnalysisRequested.
- Logic:
- A background worker loads the game's move history from its event stream.
- It runs a game engine analysis on the moves.
- emit GameAnalysisCompletedwith the evaluation data.
 
Projections create and maintain denormalized views for fast queries.
- 
PlayerFullProfileView:{ id, displayname, bio, avatar, title, elo_ratings, stats, rating_history, recent_games, followed_players }- Subscribes to: Player*,GameEndedevents.
- Powers: Main player profile pages (/@/{displayname}).
 
- Subscribes to: 
- 
AuthenticationView:Map<displayname, {player_id, password_hash}>,Map<session_id, Session>- Subscribes to: PlayerRegistered,PlayerLoggedIn,PlayerLoggedOut.
- Powers: Login/logout logic and session validation.
 
- Subscribes to: 
- 
LobbyView:{ open_challenges: Vec<...>, waiting_custom_games: Vec<...> }- Subscribes to: ChallengeIssued,GameCreated.
- Powers: The main game lobby dashboard.
 
- Subscribes to: 
- 
GameSpectatorView: A rich, real-time game object including clocks, chat, and player info.- Subscribes to: All events for a single game_id.
- Powers: Spectator pages (/games/{id}/spectate) and WebSocket feeds.
 
- Subscribes to: All events for a single 
- 
GameListView:Vec<{ id, lifecycle, players, variant, time_control, created_at }>- Subscribes to: GameCreated,PlayerJoinedGame,GameStarted,GameEnded.
- Powers: Browsing public and active games (/games).
 
- Subscribes to: 
- 
TournamentPageView:{ name, status, standings, current_pairings, rules, chat }- Subscribes to: All events for a single tournament_id.
- Powers: Tournament home pages (/tournament/{id}).
 
- Subscribes to: All events for a single 
- 
TeamPageView:{ name, leader, description, members, recent_activity }- Subscribes to: All Team*events for a singleteam_id.
- Powers: Team profile pages (/team/{id}).
 
- Subscribes to: All 
- 
LeaderboardView:{ elo: Vec<{player_id, displayname, elo}>, wins: Vec<...> }- Subscribes to: PlayerRegistered,PlayerStatsUpdated,PlayerEloUpdated.
- Powers: Leaderboard pages (/leaderboard/*).
 
- Subscribes to: 
- 
GameAnalysisView:{ move_history, move_evaluations, commentary }- Subscribes to: GameAnalysisCompleted.
- Powers: Post-game analysis pages (/games/{id}/analysis).
 
- Subscribes to: 
- 
NotificationsView: A per-player list of notifications (challenges, messages, follows, etc.).- Subscribes to: ChallengeIssued,PlayerFollowedPlayer,MessageSent, etc.
- Powers: The site-wide notification system.
 
- Subscribes to: 
- 
AutomataFL_TV_View: The current game state of a single, high-profile match.- Subscribes to: Events for the currently featured game_id. A separate process monitorsGameStartedevents to select which game to feature.
- Powers: The main "TV" feature on the homepage.
 
- Subscribes to: Events for the currently featured