5 Architecture
Emily edited this page 2026-11-07 19:37:32 +01:00

Architecture

Overview

Universal Gaming is a Flutter application with no third-party state-management framework. It uses ChangeNotifier, ListenableBuilder and plain StatefulWidget state. Game rules are isolated in engine/controller classes under lib/games/ so they can be unit tested without a UI.

lib/
  config/                 Build-time links and configuration
  core/                   Sudoku difficulty, puzzle, solver and variant
  games/
    cards/                Card model, shared rules and six solitaire engines
    chess/                Chess rules engine and computer opponent
    killer/               Killer cages, generator and solver
    mahjong/              Tiles, layered layout and solvable deals
    minesweeper/          Board, mines and game state
  models/                 Settings, accounts, game state, stats, presence
  screens/                Hub, onboarding, account, game screens
  services/               Persistence, accounts, leaderboard, backups, updates, Discord
  theme/                  Material 3 light and dark themes
  utils/                  Time formatting helpers
  widgets/                Shared chrome, boards, controls, dialogs, sheets

Startup

main():

  1. Configures the URL strategy (usePathUrlStrategy on web).
  2. Initializes SharedPreferences.
  3. Creates StorageService, AppSettings and AccountService.
  4. Starts the Discord presence service in the background.
  5. Creates UpdateService with the account server URL provider.
  6. Hooks StorageService.onSavedGameChanged so a saved game queues a debounced backup for signed-in users.
  7. Runs UniversalGamingApp.

UniversalGamingApp:

  • Listens to AppSettings for theme changes.
  • Shows OnboardingScreen until onboarding is complete.
  • Otherwise shows GameHubScreen.
  • Handles deep links in onGenerateRoute: /account opens AccountScreen with an optional reset token.
  • Starts a background update check on first frame when enabled.

State management

Concern Owner
Theme, gameplay toggles, Discord opt-in AppSettings (ChangeNotifier)
Account session, server URL, auth state AccountService (ChangeNotifier)
Update state and changelog UpdateService (ChangeNotifier)
Competitive/local leaderboard state LeaderboardService (ChangeNotifier)
Save backup state SaveBackupService (ChangeNotifier, reachable as AccountService.backups)
Current Sudoku game GameController (ChangeNotifier)
Card, Mahjong, Minesweeper, chess state Per-game controllers/engines
Screen-local UI state StatefulWidget

Screens use ListenableBuilder around the parts that need to rebuild, rather than rebuilding the whole app.

Sudoku and Killer Sudoku

  • SudokuPuzzle holds givens and the solution.
  • KillerPuzzle extends SudokuPuzzle with cages and cage lookup maps.
  • Generators create a full solution, remove clues, and verify uniqueness with the solver.
  • Killer generation also verifies cage coverage, non-repeating digits, sums and uniqueness.
  • GameController owns values, notes, selection, history, elapsed time, mistakes, hints and pause state.
  • Saved games are JSON and include the variant-specific state.
  • Classic and Killer use separate storage keys, statistics and leaderboard game IDs.

Card games

All solitaire games share PlayingCard, card widgets and common rules. Each game has its own controller:

  • KlondikeController — draw-one/draw-three Klondike.
  • FreeCellController — free cells and multi-card moves.
  • SpiderController — 1/2/4 suits and completed runs.
  • PyramidController — pairs totalling 13.
  • TriPeaksController — wrapping rank chains and series tracking.

Controllers expose legal moves, undo state, win/loss state, moves and JSON serialization.

Mahjong

MahjongController builds a layered layout, tracks removed tiles and guarantees a solution order when dealing. MahjongTileWidget renders the 3D-style tiles.

Chess

  • ChessGame implements the rules and legal move generation.
  • ChessAi uses alpha-beta search for the computer opponent.
  • ChessScreen supports computer, local and online modes.
  • Online matches are stored as UCI move lists and replayed through the same engine.
  • The server independently validates moves with chess.js; the client is never trusted.

Services

Service Responsibility
StorageService JSON-backed shared_preferences for saves, settings, stats, leaderboard mode and account session
AccountService Registration, login, profile, e-mail verification, password reset, server URL and connection test
LeaderboardService Competitive API reads/writes, local fallback and upload hook
UpdateService Release feed, version comparison and changelog
ChessOnlineService Challenge, match, move and resign API calls
SaveBackupService Uploads and restores saved games for the signed-in account; debounced auto-backup, silent failure handling
DiscordPresenceService Best-effort desktop Discord IPC; throttled updates and silent failures
PuzzleService Sudoku and Killer generation entry points

Storage keys

Local state is versioned in key names:

  • universal_gaming.sudoku.saved_game.v1 (legacy single slot, migrated on read)
  • universal_gaming.killer.saved_game.v1 (legacy single slot, migrated on read)
  • universal_gaming.saved.v2.sudoku.<difficulty> — one slot per difficulty
  • universal_gaming.saved.v2.killer.<difficulty>
  • universal_gaming.session.<gameId> — unfinished sessions for the card games, Mahjong, Minesweeper and chess
  • universal_gaming.settings.v1
  • universal_gaming.sudoku.stats.v1
  • universal_gaming.killer.stats.v1
  • universal_gaming.leaderboard.v1
  • universal_gaming.leaderboard.mode.v1
  • universal_gaming.onboarding.v1
  • universal_gaming.account.token.v1
  • universal_gaming.account.user.v1
  • universal_gaming.account.server_url.v1
  • universal_gaming.saves.backup.v1
  • universal_gaming.chess.lessons.v1 - finished chess lessons

The account token and user are local session data. The authoritative account remains on the server.

Saved games are exported and imported per slot (sudoku-easy, killer-expert, klondike, chess, ...) so a backup can be merged into local storage without touching other keys. Legacy single-slot saves are moved to the difficulty they were started on the first time they are read.

Chess

  • lib/games/chess/chess_engine.dart holds the rules, the UCI move log and the replay-based move history.
  • lib/games/chess/chess_lessons.dart is the learning path as data: a step is a position (pieces by square name, side to move, castling rights), a prompt, accepted moves in UCI form and an explanation. Mate puzzles accept any mate.
  • lib/widgets/chess_board.dart draws the board for both the game and the lessons; lib/widgets/chess_glyph.dart paints the figures.

Chess figures

The twelve figures ship with the app instead of coming from the system fonts: assets/fonts/ChessFigures.ttf is a 6 KB subset of Noto Sans Symbols 2 (OFL 1.1, see assets/fonts/OFL.txt) containing only U+2654-U+265F.

Relying on system fonts went wrong twice. Some platforms resolve the hollow set (used for white pieces) and the filled set (used for black pieces) to different fonts, so white figures were drawn larger and lower and got shaved flat by the edge of their square; and U+265F defaults to emoji presentation in some browsers, which turned white pawns black. One bundled font removes both problems.

tools/build_chess_font.py rebuilds the subset from the upstream font. It also shifts the outlines so the ink is centred on the baseline and sets symmetric ascent/descent (500/-500), because text engines place the baseline at ascent / (ascent + descent) of the line box and do not agree on which metrics table to read - with symmetric metrics the figures land centred in their square everywhere. Rebuild it with:

python3 -m venv /tmp/fontenv && /tmp/fontenv/bin/pip install fonttools
/tmp/fontenv/bin/python tools/build_chess_font.py [NotoSansSymbols2-Regular.ttf]

The board never clips a figure: Clip.none on the glyph stacks and on the board squares, visible text overflow, and the drop shadow comes from TextStyle.shadows so it follows the outline instead of painting a dark rectangle behind the piece.

Networking

  • All API calls use package:http with timeouts.
  • The account server URL is normalized and stored locally.
  • http:// servers are allowed with a visible warning.
  • Competitive leaderboard failures fall back to local data instead of blocking gameplay.
  • Update checks never block startup.
  • Discord IPC is isolated behind conditional imports so web and mobile builds do not compile dart:io code.

Responsive design

Shared chrome lives in GamePage and GameBottomBar. Landscape phones use a compact mode when orientation == landscape && height < 520, reducing header/bottom-bar sizes and hiding secondary text. Individual game screens calculate board, card or tile sizes from the available width and height, with raised clamps for larger displays.

Privacy boundary

The app does not send gameplay state anywhere by default. Data leaves the device only when:

  • The user signs in and submits a competitive score.
  • The user plays online chess.
  • The update checker requests the public release feed.
  • The user enables Discord Rich Presence on desktop.
  • The user backs up saved games (manual Back up now, or the automatic backup after a save).

The local leaderboard mode never uploads.