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 asPid(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 issueCreateCustomGamecommands 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