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():
- Configures the URL strategy (
usePathUrlStrategyon web). - Initializes
SharedPreferences. - Creates
StorageService,AppSettingsandAccountService. - Starts the Discord presence service in the background.
- Creates
UpdateServicewith the account server URL provider. - Hooks
StorageService.onSavedGameChangedso a saved game queues a debounced backup for signed-in users. - Runs
UniversalGamingApp.
UniversalGamingApp:
- Listens to
AppSettingsfor theme changes. - Shows
OnboardingScreenuntil onboarding is complete. - Otherwise shows
GameHubScreen. - Handles deep links in
onGenerateRoute:/accountopensAccountScreenwith an optionalresettoken. - 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
SudokuPuzzleholds givens and the solution.KillerPuzzleextendsSudokuPuzzlewith 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.
GameControllerowns 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
ChessGameimplements the rules and legal move generation.ChessAiuses alpha-beta search for the computer opponent.ChessScreensupports 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 difficultyuniversal_gaming.saved.v2.killer.<difficulty>universal_gaming.session.<gameId>— unfinished sessions for the card games, Mahjong, Minesweeper and chessuniversal_gaming.settings.v1universal_gaming.sudoku.stats.v1universal_gaming.killer.stats.v1universal_gaming.leaderboard.v1universal_gaming.leaderboard.mode.v1universal_gaming.onboarding.v1universal_gaming.account.token.v1universal_gaming.account.user.v1universal_gaming.account.server_url.v1universal_gaming.saves.backup.v1universal_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.dartholds the rules, the UCI move log and the replay-based move history.lib/games/chess/chess_lessons.dartis 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.dartdraws the board for both the game and the lessons;lib/widgets/chess_glyph.dartpaints 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:httpwith 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:iocode.
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.
Universal Gaming
Repositories
Universal Gaming is a private project by Emily. The app is offline-first; accounts, online chess and the competitive leaderboard are optional.