Skip to content

Instantly share code, notes, and snippets.

@emberian
Created October 20, 2025 22:01
Show Gist options
  • Save emberian/07e61a3142955ca115105a063c5f548f to your computer and use it in GitHub Desktop.
Save emberian/07e61a3142955ca115105a063c5f548f to your computer and use it in GitHub Desktop.

Automatafl Backend Specification

1. Overview

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 Game or Player). 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.

2. Core Models & Enums

Core Identifiers

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

Game Logic Primitives

struct Coord { x: u8, y: u8 }
struct Move { who: Pid, from: Coord, to: Coord }

Core Enums

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 }

Core State Objects

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> }

3. Commands

Commands represent intents to change the system state and are named in the imperative tense.

Player & Authentication Commands

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 }

Social & Communication Commands

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 }

Game Creation & Challenge Commands

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 }

In-Game Action Commands

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 }

Matchmaking Commands

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 }

Tournament Commands

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 }

Team Commands

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 }

Study & Analysis Commands

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 }

Moderation & Admin Commands

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 }

4. Events

Events are immutable facts named in the past tense, forming the single source of truth.

Player & Authentication Events

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 }

Social & Communication Events

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 }

Game & Challenge Events

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>> }

Matchmaking Events

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> }

Tournament Events

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> }

Team Events

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 }

Analysis & Moderation Events

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 }

5. Aggregates

Aggregates process commands, enforce business rules, and produce events.

Player Aggregate

  • State: Player struct.
  • Handles: RegisterPlayer, UpdatePlayerProfile, UpdatePlayerSettings.
  • Business Rules:
    • On RegisterPlayer:
      1. Validate displayname for uniqueness, length (1-50), and character set.
      2. Validate password strength (e.g., >= 12 chars).
      3. Hash password.
      4. emit PlayerRegistered with default ELO ratings and zeroed stats.
    • On UpdatePlayerProfile:
      1. Validate bio length (<= 500 chars).
      2. Validate avatar_url format (must be a secure HTTPS URL).
      3. emit PlayerProfileUpdated.
  • Applies Events: Updates its state from PlayerRegistered, PlayerProfileUpdated, PlayerSettingsUpdated, PlayerStatsUpdated, PlayerEloUpdated.

Game Aggregate

  • State: Game struct.
  • Handles: CreateCustomGame, JoinGame, CreateMatchmadeGame, SubmitMove, CompleteRound, ResignGame, OfferDraw, AcceptDraw, ClaimVictoryOnTime.
  • Business Rules:
    • On CreateCustomGame:
      1. emit GameCreated.
      2. emit PlayerJoinedGame for the creator as Pid(0).
    • On CreateMatchmadeGame:
      1. emit GameCreated.
      2. For each player provided, emit PlayerJoinedGame.
      3. emit GameStarted.
      4. emit MatchFound.
    • On JoinGame:
      1. Guard: lifecycle must be Waiting.
      2. Guard: Player must not already be in the game.
      3. Guard: Game must not be full.
      4. Assign the lowest available Pid.
      5. emit PlayerJoinedGame.
      6. If the game is now full, emit GameStarted.
    • On SubmitMove:
      1. Guard: lifecycle must be InProgress.
      2. Translate player_id to in-game Pid.
      3. Use core_logic_state.propose_move(move).
      4. If the move is valid, emit MoveSubmitted.
      5. If the move is invalid, emit MoveInvalidated.
    • On CompleteRound:
      1. Guard: lifecycle must be InProgress.
      2. Guard: All players must have submitted moves.
      3. Use core_logic_state.try_complete_round().
      4. If successful, emit RoundCompleted. If a winner is determined, emit GameEnded and trigger ELO/stats updates.
      5. If conflicts occur, emit ConflictsOccurred.
    • On ResignGame: emit GameEnded with reason: Resignation.
    • On ClaimVictoryOnTime: Check opponent's clock. If expired, emit GameEnded with reason: Timeout.
    • On AcceptDraw: If a draw was offered by the other player, emit GameEnded with outcome: Draw.

Tournament Aggregate

  • State: Tournament struct.
  • Handles: CreateTournament, JoinTournament, LeaveTournament, StartTournament.
  • Business Rules:
    • On JoinTournament:
      1. Guard: status must be Scheduled.
      2. Guard: Player must not already be in the tournament.
      3. emit PlayerJoinedTournament.
    • On StartTournament:
      1. Guard: status must be Scheduled.
      2. emit TournamentStarted.
      3. Generate first-round pairings based on ELO.
      4. emit PairingsGenerated.

6. Processes & Sagas

Background processes that coordinate logic by reacting to events and issuing new commands.

Matchmaking Process

  • Trigger: Runs on a periodic timer (e.g., every 5 seconds).
  • Logic:
    1. Query: Fetch players from the MatchmakingQueueView.
    2. Group: Group players by variant and time_control.
    3. 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.
    4. Command: If a match is found, issue a CreateMatchmadeGame command.

Challenge Saga

  • Listens to: ChallengeIssued.
  • Handles: AcceptChallenge, DeclineChallenge, CancelChallenge.
  • Logic:
    • On AcceptChallenge: emit ChallengeAccepted, then issue a CreateCustomGame command using the configuration from the original ChallengeIssued event.

Tournament Pairing & Scoring Saga

  • Listens to: GameEnded.
  • Logic:
    1. If the game belongs to an active tournament, update tournament standings and emit TournamentStandingUpdated.
    2. Check if all games in the current round are finished.
    3. If so, generate new pairings based on current standings (e.g., Swiss rules).
    4. emit PairingsGenerated and issue CreateCustomGame commands for the new games.

Session Cleanup Process

  • Trigger: Runs on a periodic timer (e.g., every hour).
  • Logic:
    1. Query: Find all sessions in the AuthenticationView where expires_at < now().
    2. Command: For each expired session, issue a LogOutPlayer command.

Game Analysis Process

  • Listens to: GameAnalysisRequested.
  • Logic:
    1. A background worker loads the game's move history from its event stream.
    2. It runs a game engine analysis on the moves.
    3. emit GameAnalysisCompleted with the evaluation data.

7. Projections (Read Models)

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*, GameEnded events.
    • Powers: Main player profile pages (/@/{displayname}).
  • AuthenticationView: Map<displayname, {player_id, password_hash}>, Map<session_id, Session>

    • Subscribes to: PlayerRegistered, PlayerLoggedIn, PlayerLoggedOut.
    • Powers: Login/logout logic and session validation.
  • LobbyView: { open_challenges: Vec<...>, waiting_custom_games: Vec<...> }

    • Subscribes to: ChallengeIssued, GameCreated.
    • Powers: The main game lobby dashboard.
  • 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.
  • GameListView: Vec<{ id, lifecycle, players, variant, time_control, created_at }>

    • Subscribes to: GameCreated, PlayerJoinedGame, GameStarted, GameEnded.
    • Powers: Browsing public and active games (/games).
  • TournamentPageView: { name, status, standings, current_pairings, rules, chat }

    • Subscribes to: All events for a single tournament_id.
    • Powers: Tournament home pages (/tournament/{id}).
  • TeamPageView: { name, leader, description, members, recent_activity }

    • Subscribes to: All Team* events for a single team_id.
    • Powers: Team profile pages (/team/{id}).
  • LeaderboardView: { elo: Vec<{player_id, displayname, elo}>, wins: Vec<...> }

    • Subscribes to: PlayerRegistered, PlayerStatsUpdated, PlayerEloUpdated.
    • Powers: Leaderboard pages (/leaderboard/*).
  • GameAnalysisView: { move_history, move_evaluations, commentary }

    • Subscribes to: GameAnalysisCompleted.
    • Powers: Post-game analysis pages (/games/{id}/analysis).
  • NotificationsView: A per-player list of notifications (challenges, messages, follows, etc.).

    • Subscribes to: ChallengeIssued, PlayerFollowedPlayer, MessageSent, etc.
    • Powers: The site-wide notification system.
  • 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 monitors GameStarted events to select which game to feature.
    • Powers: The main "TV" feature on the homepage.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment