Features
Offline-first gameplay
Every game runs without a network connection. Sudoku and Killer Sudoku generation, solitaire engines, Mahjong deals, Minesweeper and computer chess are all local. Saves, settings, statistics and the local leaderboard are stored on the device.
The app uses the network only for:
- Update checks against the release feed.
- The optional account server the user configures.
- Competitive leaderboard reads and score uploads when signed in.
- Online chess when signed in.
There are no ads, analytics SDKs or advertising identifiers.
Accounts
Accounts are optional. The app can be used indefinitely without one.
| Feature | Details |
|---|---|
| Registration | Username and password, optional e-mail |
| Sign-in | Username or e-mail |
| Identity | Server-generated UUID; renaming does not change it |
| Rename | Available from Account settings |
| Optional confirmation; never blocks play | |
| Password reset | Requires an assigned and confirmed e-mail |
| Sessions | JWT; password reset invalidates every existing session |
| Save backups | Optional per-account upload and restore of saved games |
| Live sync | Unfinished games follow the player between devices |
| Deletion | Password confirmation required |
The verification label refreshes automatically when Account settings opens or the app resumes, so confirming an address in a browser updates the app without a manual refresh.
The account server is configurable from Account → Account server. The public default is https://account.games.emilyh.de. A self-hosted server can also be baked in at build time:
flutter build apk --release \
--dart-define=UNIVERSAL_GAMING_API_URL=https://accounts.example.com
Unencrypted http:// servers show a warning but are not blocked, which is useful for local development.
Competitive leaderboard
The leaderboard opens in Competitive mode by default.
- Viewing is public.
- Submitting requires a free account.
- The server keeps one best score per user, game and mode.
- Ranking is by seconds, then moves, then submission time.
- Ten solo games are ranked; chess is excluded.
- If the server is unreachable or disabled, the app falls back to the on-device board and explains why.
- The Local switch keeps all results on the device. Local mode never uploads.
- Local results are saved first, so a network failure never loses a score.
Score uploads contain the username, game, mode, time, moves and optional detail line. See the backend API wiki.
Saved games
- Sudoku and Killer Sudoku keep one saved game per difficulty, so an Easy game and an Expert game can be paused independently. The difficulty screen shows a Continue card for every saved slot.
- Card games, Mahjong, Minesweeper and chess save automatically. Chess stores its move log after every move, so a crash or a kill never loses a running game.
- The hub marks any game with an unfinished session with a green Continue pill, and it updates live when another device syncs a game in.
Save backups
Signed-in players can store their saved games on the account server and restore them on another device.
- Open Account → Save backups while signed in.
- Back up now uploads every saved game for the account (Sudoku and Killer Sudoku, one slot per difficulty).
- Restore downloads the account's saves and replaces the saved games on this device. A confirmation dialog explains that unfinished games in progress are overwritten.
- New saves are uploaded automatically a few seconds after a game is saved, as long as the server is reachable. The last backup time is shown in the card.
- Saves are always written locally first, so a failed or offline backup never loses progress.
- The server stores each save as an opaque JSON document per game (max 32 kB). Unknown game ids from a newer build are ignored, so an older build can restore from a newer backup.
- Servers without the feature show This server does not support save backups and the app keeps using local saves. Operators can turn it off with
SAVE_BACKUP_ENABLED=false. - Backups need backend 1.5.0 or newer; everything else works with 1.4.0.
The endpoints are GET /api/v1/saves, PUT /api/v1/saves (bulk), PUT /api/v1/saves/:gameId and DELETE /api/v1/saves/:gameId. See the backend API wiki.
Live sync
Unfinished games follow a signed-in player from one device to another while they play. No button, no waiting for a manual backup.
- A game is uploaded about a second after it changes. Every device polls a cheap revision cursor (
GET /api/v1/sessions?since=<revision>) every five seconds, so a move on the phone is on the desktop almost immediately. Only the games that changed are downloaded. - A game that is open is never replaced under the player's fingers. If another device changes the game on screen, a banner says Updated on another device with a Reload action, and the player's own progress wins the conflict.
- A game that is not open is adopted silently, so the continue badges on the hub are already current.
- A finished game is reported as finished and cleared on the other devices, so no stale Continue entry is left behind.
- Each installation has a random device id and the server records which device wrote last, so a device ignores the echo of its own writes. A game played offline is uploaded rather than overwritten when the device comes back.
- Account → Live sync shows the state, the last sync time and a Sync now button, and switches the feature off for that device.
- Everything is local-first: a failed request only means the next poll retries. Servers without the feature show This server does not support live sync and the app stays local-only. Operators can turn it off with
SESSION_SYNC_ENABLED=false. - Live sync needs backend 1.6.0 or newer. Save backups still need only 1.5.0.
The endpoints are GET /api/v1/sessions, GET|PUT|DELETE /api/v1/sessions/:gameId, with a 32 kB payload cap and a 409 conflict answer for a stale revision from another device.
Update checks and changelog
On startup the app checks the release feed. The changelog button in the hub shows:
- Current and latest versions.
- Release notes.
- A direct link to the release page.
- An update badge when a newer stable release exists.
Native builds call the Gitea releases API directly. Web builds prefer the configured backend's /api/v1/app/releases proxy to avoid browser CORS restrictions, then fall back to a same-origin latest.json when available.
Discord Rich Presence
Desktop builds can publish the current game to Discord:
- Game name.
- Difficulty or mode.
- Move count or progress.
- Live elapsed play time.
- Paused state with frozen time.
It is best-effort: if Discord is not running, the app retries later and never blocks gameplay. The feature is off when the build has no Discord application ID.
Enable it
- Create an application in the Discord Developer Portal.
- Name it Universal Gaming; Discord uses that name as the activity title.
- Build desktop targets with the Application ID:
flutter build macos --release \
--dart-define=DISCORD_CLIENT_ID=your-application-id
The same flag works for Windows and Linux. Users can turn the feature off in Settings → Integrations → Discord Rich Presence.
Discord Rich Presence is desktop-only. Web, Android and iOS builds compile without the desktop IPC dependency. The macOS build disables the App Sandbox so the app can reach Discord's Unix-domain IPC socket; it is not intended for the Mac App Store.
Responsive layout
- Phone portrait and landscape.
- Tablets and iPad, including all four orientations.
- Desktop windows and browser windows.
- Landscape phones use a compact mode that hides non-essential chrome and enlarges the playfield.
- Sudoku, card and tile boards scale and centre on large screens.
Languages
The interface ships in English, German, Spanish, French and Italian.
- On first start the app follows the system language and falls back to English when the device speaks none of the five.
- Settings → Language switches at any time; System language hands control back to the device. The choice is stored with the other settings, so it survives restarts and works offline.
- Strings live in
lib/l10n/app_{en,de,es,fr,it}.arb, generated from one table intools/build_l10n.py(python3 tools/build_l10n.pythenflutter gen-l10n). Because there is a single table, no language can drift or lose a key. - Widgets read strings through
context.l10n, which falls back to English when no localization delegate is installed — plain widget tests keep working. - The long-form content lives in two tables that also fall back to English:
lib/l10n/game_help_text.dart(the "How to play" sheet of every game) andlib/games/chess/chess_lesson_l10n.dart(the chess learning path). Tests enforce that every game and every lesson step is present in all four translations.
Themes and accessibility
- Light and dark themes, following the system by default.
- Manual theme override in Settings.
- Haptic feedback can be disabled.
- Mistake, peer and matching-number highlighting can be toggled independently.
- Large touch targets and keyboard controls.
- Every game has an in-app rules sheet.
Password-manager support
The sign-in fields use standard username, e-mail and password autofill hints and trigger the platform save prompt.
- Android uses Digital Asset Links and App Links.
- iOS carries
applinksandwebcredentialsassociated-domain entitlements. - Web uses the Credential Management API with a Fill password action and automatic browser lookup.
- Android opens
https://games.emilyh.delinks in the app. - iOS Universal Links and web credentials require an Apple Developer Team ID at build time and a signed app.
Deep links
https://games.emilyh.de opens the app when the platform association is configured. The /account path routes to the Account screen and passes a reset query token for password reset.
The web build uses path-based URLs (usePathUrlStrategy), so the host must provide an SPA fallback to index.html.
Web footer
The web build shows a footer with:
- A link to the source repository.
- The current version.
- A warning when the hosted build is older than the latest release.
Self-hosting
The web build is static and can be served by any HTTP server. Ready-made Nginx and Caddy configurations are in deploy/, and container images are published to the Gitea registry. See Deployment and WEB_HOSTING.md.
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.