diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b740728..c230701 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -2,7 +2,7 @@ version: 2 # Keeps the pinned GitHub Actions current. Without this they silently drift: # major tags like `actions/checkout@v7` float across patches but never across -# majors, so a workflow keeps working while falling years behind — which is +# majors, so a workflow keeps working while falling years behind, which is # exactly how this repo ended up on `pnpm/action-setup@v4` after the workspace # moved to pnpm 11 (v6 is where pnpm 11 support landed). # diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index caca57c..7989a27 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -11,12 +11,12 @@ name: Checks # Releases are tag-triggered rather than main-triggered, so without it a merge # to `main` would go unverified until someone cut a tag. # -# `workflow_call` re-executes these jobs rather than reusing an earlier result — +# `workflow_call` re-executes these jobs rather than reusing an earlier result, # a tag can point at any commit, so the run gating a publish has to be against # the tagged tree. # # Validates the Flutter/Dart engine package in isolation so `main` stays green -# for the apps that depend on it. No secrets needed — the engine reads no +# for the apps that depend on it. No secrets needed; the engine reads no # env/Firebase config itself (apps inject runtime config via EngineConfig). # The server it talks to is validated in the eigen-server repo's own CI. on: @@ -47,7 +47,7 @@ jobs: # markdown LINK, which requires a matching reference definition at the # bottom of the file. Without one `## [Unreleased]` parses as ordinary # text: `cider release` cannot find it, leaves the entries where they sit, - # and appends an EMPTY version section instead — while exiting 0. + # and appends an EMPTY version section instead, while exiting 0. # # That is exactly how the first release pull request came out, and nothing # anywhere said so. The next reviewer would have shipped an empty section @@ -55,7 +55,7 @@ jobs: # unpublished. # # `cider log` writes both halves, so this can only break when CHANGELOG.md - # is edited by hand — precisely when nobody is thinking about cider's + # is edited by hand, precisely when nobody is thinking about cider's # parser. Runs before the toolchain because it needs nothing but grep. - name: Changelog headings are link references run: | @@ -73,7 +73,7 @@ jobs: done < <(grep -o '^## \[[^]]*\]' CHANGELOG.md | sed 's/^## \[//; s/\]$//') [ "$missing" -eq 0 ] || { - echo "::error::Add entries with 'cider log \"…\"' rather than editing CHANGELOG.md directly — it maintains these definitions for you." + echo "::error::Add entries with 'cider log \"…\"' rather than editing CHANGELOG.md directly; it maintains these definitions for you." exit 1 } @@ -87,8 +87,8 @@ jobs: run: flutter pub get # Hand-written source only. `dart format` has no exclude flag, and - # `build_runner` output does not always survive it — so listing the - # files keeps the check on hand-written sources only — the committed + # `build_runner` output does not always survive it, so listing the + # files keeps the check on hand-written sources only: the committed # generated code is already formatted by its builders and is verified # instead by the build_runner + git-diff drift guard below. - name: Check formatting @@ -116,8 +116,8 @@ jobs: # The RPS example is its own package with its own resolution, so the job # above does not reach it. It is also the only place the framework is - # exercised the way a real game uses it — through the barrel, against a - # hand-built GameContentContext — and its twin fixtures are the Dart half of + # exercised the way a real game uses it (through the barrel, against a + # hand-built GameContentContext) and its twin fixtures are the Dart half of # a contract the server's CI checks from the other side. example: name: Example (RPS / ${{ matrix.target }}) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3ceaf6f..015d904 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,18 +7,18 @@ name: Release # # Tag-triggered rather than push-triggered because that is what pub.dev's # automated publishing requires: it trusts a GitHub OIDC token only when the -# token's ref is a TAG matching the pattern configured on the package — set at +# token's ref is a TAG matching the pattern configured on the package, set at # Admin → Automated publishing to repository `eigeninteractive/eigen-flutter`, # tag pattern `v{{version}}`. No pub credential is stored in GitHub secrets at -# all — the OIDC identity IS the authentication. +# all: the OIDC identity IS the authentication. # -# One package. `eigen_api` — the generated REST client — is published by the +# One package. `eigen_api`, the generated REST client, is published by the # ENGINE repo (eigen-server/clients/dart), because that is where the wire # contract lives. This package consumes it as an ordinary versioned dependency, # so the two release independently. # # Generated code (`*.g.dart`, `*.freezed.dart`) is committed, so the tarball -# compiles for consumers and publishing needs no `.pubignore` — `dart pub +# compiles for consumers and publishing needs no `.pubignore`; `dart pub # publish` falls back to `.gitignore`, which excludes build output and # credentials while keeping the generated sources. `checks.yml` proves that # committed output is current, so nothing is regenerated below. @@ -38,8 +38,8 @@ permissions: jobs: # The same gate every pull request clears, re-run against the tagged commit. # A tag can point at any commit, so this is a fresh execution rather than a - # lookup of whatever the branch run concluded. Sharing the definition — not - # re-typing format/analyze/test here — is the point: a check added to + # lookup of whatever the branch run concluded. Sharing the definition, not + # re-typing format/analyze/test here, is the point: a check added to # checks.yml automatically guards releases too. gate: name: Checks @@ -79,9 +79,9 @@ jobs: exit 0 # Makes the pipeline idempotent. `dart pub publish` fails on a version - # that already exists, which turns every harmless re-run — a retried job, + # that already exists, which turns every harmless re-run (a retried job, # a re-pushed tag, a version published by hand before this automation - # existed — into a red release someone has to explain. + # existed) into a red release someone has to explain. # # Checking first turns those into a clean skip, and keeps the state that # matters (what is on pub.dev) authoritative over the state that does not @@ -92,7 +92,7 @@ jobs: v=$(grep -m1 '^version:' pubspec.yaml | awk '{print $2}') if curl -sf https://pub.dev/api/packages/eigen_flutter \ | jq -e --arg v "$v" '.versions[] | select(.version == $v)' >/dev/null; then - echo "::notice::eigen_flutter $v is already on pub.dev — skipping publish." + echo "::notice::eigen_flutter $v is already on pub.dev. Skipping publish." echo "skip=true" >> "$GITHUB_OUTPUT" else echo "skip=false" >> "$GITHUB_OUTPUT" @@ -103,7 +103,7 @@ jobs: # and nothing here did. # # `dart-lang/setup-dart` does this exchange for you, which is why the - # pub.dev instructions never mention it — but this job installs the SDK + # pub.dev instructions never mention it, but this job installs the SDK # through `subosito/flutter-action`, because publishing a Flutter package # needs the Flutter SDK, and that action has no equivalent step. # @@ -123,7 +123,7 @@ jobs: -H "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" | jq -r '.value') [ -n "$PUB_TOKEN" ] && [ "$PUB_TOKEN" != "null" ] || { - echo "::error::could not mint an OIDC token for pub.dev — publishing would fall back to an interactive prompt and hang" + echo "::error::could not mint an OIDC token for pub.dev; publishing would fall back to an interactive prompt and hang" exit 1 } @@ -133,7 +133,7 @@ jobs: dart pub token add https://pub.dev --env-var PUB_TOKEN # Surfaces score-affecting problems (missing docs, unresolved links, - # platform support) before they are permanent — pub.dev versions cannot be + # platform support) before they are permanent; pub.dev versions cannot be # unpublished, only retracted. - name: Dry run if: steps.published.outputs.skip == 'false' @@ -143,8 +143,8 @@ jobs: if: steps.published.outputs.skip == 'false' run: dart pub publish --force - # eigen-web publishes the compatibility table — which engine line each - # `eigen_flutter` release speaks — and pub.dev is the authority for it. + # eigen-web publishes the compatibility table, which engine line each + # `eigen_flutter` release speaks, and pub.dev is the authority for it. # Nothing over there can notice a publish on its own, so tell it. # # NOT gated on `skip`: a re-run after a failed dispatch is exactly when @@ -155,15 +155,15 @@ jobs: # The version travels in the payload because pub.dev serves a publish from # its API a moment after the call returns. The receiving workflow waits for # this exact version rather than reading whatever is there when it happens - # to start — otherwise it would regenerate from the state just before this + # to start, or else it would regenerate from the state just before this # release and silently produce no change at all. # # Failing here fails the release, matching eigen-server: a published client # with an unnotified docs site is a state worth stopping on. The recovery # is the `Sync compatibility table` workflow's manual trigger. # - # GITHUB_TOKEN cannot do this — it is scoped to the repository that minted - # it, same organization or not — so this needs the App. Its credentials + # GITHUB_TOKEN cannot do this; it is scoped to the repository that minted + # it, same organization or not, so this needs the App. Its credentials # must exist HERE (`RELEASE_APP_CLIENT_ID` variable, # `RELEASE_APP_PRIVATE_KEY` secret) and it must be installed on eigen-web. - uses: actions/create-github-app-token@v3 diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml index e7e6415..0ca9196 100644 --- a/.github/workflows/tag.yml +++ b/.github/workflows/tag.yml @@ -6,7 +6,7 @@ name: Tag # publishing trusts a GitHub OIDC token only when the token's ref is a TAG # matching the pattern configured on the package. `release.yml` therefore # cannot run on a branch push, and something has to create the tag. Doing it -# here rather than by hand removes the one release step with no safety net — +# here rather than by hand removes the one release step with no safety net: # a mistyped or forgotten tag. # # Deliberately keyed on the FILE rather than on the pull request that changed @@ -48,7 +48,7 @@ jobs: # think nothing had ever been released. fetch-depth: 0 - # pubspec.yaml also changes for ordinary dependency edits — an `eigen_api` + # pubspec.yaml also changes for ordinary dependency edits, e.g. an `eigen_api` # constraint bump, say. Those leave `version:` alone, so this must decide # "has this version already been released?" and exit cleanly when it has. # Being idempotent is what lets the trigger stay this simple. @@ -57,7 +57,7 @@ jobs: # theoretical: 0.1.0 was published by hand before this automation existed, # so it had no tag. "No tag" therefore meant "unreleased" only by # accident, and the first dependency edit afterwards duly tagged an - # unrelated commit as v0.1.0 — a tag whose tree was never what shipped. + # unrelated commit as v0.1.0: a tag whose tree was never what shipped. # # release.yml already makes the same argument for its own publish step: # what is on pub.dev is the state that matters, and which refs happen to @@ -71,9 +71,9 @@ jobs: [ -n "$v" ] || { echo "::error file=pubspec.yaml::no version: line"; exit 1; } # Cheap and local first. Once a version is tagged, nothing else here - # needs to run — including on every unrelated pubspec.yaml edit. + # needs to run, including on every unrelated pubspec.yaml edit. if git rev-parse -q --verify "refs/tags/v$v" >/dev/null; then - echo "v$v is already tagged — nothing to release." + echo "v$v is already tagged. Nothing to release." exit 0 fi @@ -81,12 +81,12 @@ jobs: # release is re-runnable: a transient pub.dev failure costs one rerun, # whereas a wrong tag has to be noticed before it can be undone. published=$(curl -sf --retry 3 https://pub.dev/api/packages/eigen_flutter) || { - echo "::error::could not reach pub.dev to check whether $v is already released — refusing to tag blind" + echo "::error::could not reach pub.dev to check whether $v is already released; refusing to tag blind" exit 1 } if jq -e --arg v "$v" '.versions[] | select(.version == $v)' >/dev/null <<<"$published"; then - echo "::notice::eigen_flutter $v is already on pub.dev — nothing to release." + echo "::notice::eigen_flutter $v is already on pub.dev. Nothing to release." exit 0 fi diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml index 566d8ca..f9aed67 100644 --- a/.github/workflows/version.yml +++ b/.github/workflows/version.yml @@ -4,7 +4,7 @@ name: Version # or `gh workflow run version.yml -f bump=minor`. # # This is the only manual decision in a release: which bump. Everything after it -# is automatic — merging the pull request this opens tags the commit +# is automatic: merging the pull request this opens tags the commit # (`tag.yml`), and the tag publishes to pub.dev (`release.yml`). # # `cider` owns the arithmetic and the changelog. Contributors have already @@ -99,18 +99,18 @@ jobs: # report success and create nothing: the branch updated, and there was # no pull request to merge. That is the exact recovery path taken when # a release pull request came out wrong, so it failed at the one moment - # it was needed — and reported "Refreshed" while doing it. + # it was needed, and reported "Refreshed" while doing it. state=$(gh pr view "release/v$V" --json state --jq .state 2>/dev/null || echo NONE) if [ "$state" = "OPEN" ]; then echo "Refreshed the existing pull request for v$V." exit 0 fi - [ "$state" = "NONE" ] || echo "The previous pull request for v$V is $state — opening a new one." + [ "$state" = "NONE" ] || echo "The previous pull request for v$V is $state. Opening a new one." body="Raises \`eigen_flutter\` to **v$V** (\`$BUMP\`) and dates the changelog section. Merging this tags \`v$V\`, which publishes to pub.dev. Published versions - cannot be unpublished — only retracted — so read the changelog diff before + cannot be unpublished, only retracted, so read the changelog diff before merging." gh pr create --base main --title "Release v$V" --body "$body" diff --git a/.gitignore b/.gitignore index 5698b52..e25e5a8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,8 +6,8 @@ build/ pubspec.lock # Generated code (`*.g.dart`, `*.freezed.dart`) is deliberately NOT ignored: it -# is committed, so the published package compiles for consumers — who never run -# build_runner on a dependency — and so `git diff` after `build_runner build` +# is committed, so the published package compiles for consumers (who never run +# build_runner on a dependency) and so `git diff` after `build_runner build` # is the drift guard that proves it is current. This is why there is no # `.pubignore`: publishing falls back to this file, which correctly excludes # build output and the credentials below while keeping the generated sources. diff --git a/AGENTS.md b/AGENTS.md index 172d625..d0233cf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,8 +10,8 @@ and is the source of truth. Retrieve it rather than relying on memory: | Any page as Markdown | append `.md` to its URL | | HTTP contract (OpenAPI 3.1) | `https://eigeninteractive.com/openapi.json` | -The docs are task-first, and each page covers **both halves** of a task — the -TypeScript rules and the Dart client — because they are one change. +The docs are task-first, and each page covers **both halves** of a task, the +TypeScript rules and the Dart client, because they are one change. Start points: `/docs/getting-started/your-first-game.md` (a whole game, both languages), `/docs/build-a-game/the-contract.md` (what you write and what the diff --git a/CHANGELOG.md b/CHANGELOG.md index 97a37af..4064d2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -Entries are maintained with [`cider`](https://pub.dev/packages/cider) — add them +Entries are maintained with [`cider`](https://pub.dev/packages/cider); add them as you work (`cider log added "…"`), and `cider release` moves everything under `## [Unreleased]` into a dated section at release time. @@ -14,22 +14,26 @@ Pre-1.0, breaking changes land in a **MINOR** bump: `^0.1.0` resolves to [Versions and compatibility](https://eigeninteractive.com/docs/reference/compatibility) for how this package, the engine and the generated `eigen_api` client pair up. +## [Unreleased] +### Changed +- Em dashes are gone from every line this package writes, including the strings a player reads: 'The game updated. Try again.', the notification-permission rows in settings, and the in-progress replay notice. Comments, docs, the example game, the workflows and the changelog itself went with them. + ## [0.3.5] - 2026-08-07 ### Changed -- `configure_firebase` checks that a Google account is signed in to the Firebase CLI before it starts, and names `firebase login` when none is — those are the credentials both CLIs share, and previously the one preflight failure FlutterFire was left to discover for itself. The check fails open: only an answer that positively reports no accounts stops the run. -- A successful run names the Firebase project and the Android and Web app IDs it configured against. FlutterFire matches an existing Android app on the package name and an existing Web app on its display name, and reuses either without comment — so adopting the apps a project already had looked identical to registering new ones, and now does not. -- The Web SDK configuration is downloaded quietly. It is an intermediate — read, checked, rewritten as `web/firebase-config.js`, then deleted — so the Firebase CLI's own success line announced a temporary file that no longer existed by the time anyone read it. A failing download still prints everything it said. +- `configure_firebase` checks that a Google account is signed in to the Firebase CLI before it starts, and names `firebase login` when none is. Those are the credentials both CLIs share, and previously the one preflight failure FlutterFire was left to discover for itself. The check fails open: only an answer that positively reports no accounts stops the run. +- A successful run names the Firebase project and the Android and Web app IDs it configured against. FlutterFire matches an existing Android app on the package name and an existing Web app on its display name, and reuses either without comment, so adopting the apps a project already had looked identical to registering new ones, and now does not. +- The Web SDK configuration is downloaded quietly. It is an intermediate (read, checked, rewritten as `web/firebase-config.js`, then deleted) so the Firebase CLI's own success line announced a temporary file that no longer existed by the time anyone read it. A failing download still prints everything it said. ## [0.3.4] - 2026-08-07 ### Changed -- The credit line defaults to `Built with EigenInteractive`, matching the game's own website, and renders as prose with only the brand name linked — accent-coloured, no underline. A `Branding.madeByCredit` that never names the engine stays plain text. The settings and about screens now share one `MadeByCredit` widget instead of the same footer written twice. +- The credit line defaults to `Built with EigenInteractive`, matching the game's own website, and renders as prose with only the brand name linked, accent-coloured and without an underline. A `Branding.madeByCredit` that never names the engine stays plain text. The settings and about screens now share one `MadeByCredit` widget instead of the same footer written twice. ### Fixed -- `configure_firebase` no longer fails with `FirebaseProjectRequiredException` on a first run: `--yes` is only passed once the Firebase project is settled, so an unconfigured run gets FlutterFire's project picker — where a project can also be created — instead of a hard stop. The project can be named with `--project `, and is otherwise read back from the `firebase.json` a previous run wrote or a `.firebaserc`, so re-runs stay non-interactive. `--account ` is accepted for a machine signed in to several Google accounts, `--help` prints the usage, and a bare `--` is ignored so `run firebase:configure -- --project x` works under both npm and pnpm. Every other option is refused with a usage error rather than passed to FlutterFire: platforms in particular are fixed at Android and Web, which are the platforms the app has and where the messaging service worker's configuration comes from. +- `configure_firebase` no longer fails with `FirebaseProjectRequiredException` on a first run: `--yes` is only passed once the Firebase project is settled, so an unconfigured run gets FlutterFire's project picker, where a project can also be created, instead of a hard stop. The project can be named with `--project `, and is otherwise read back from the `firebase.json` a previous run wrote or a `.firebaserc`, so re-runs stay non-interactive. `--account ` is accepted for a machine signed in to several Google accounts, `--help` prints the usage, and a bare `--` is ignored so `run firebase:configure -- --project x` works under both npm and pnpm. Every other option is refused with a usage error rather than passed to FlutterFire: platforms in particular are fixed at Android and Web, which are the platforms the app has and where the messaging service worker's configuration comes from. ## [0.3.3] - 2026-08-07 ### Changed -- Bundle Inter and Space Grotesk as single variable files instead of nine static Inter weights, and pair them across the Material 3 text roles: Space Grotesk on display and headline, Inter on everything read at length. `Branding.displayFontFamily` overrides the display face and `Branding.seedColor` now defaults to the EigenInteractive teal, so a game looks deliberate before anyone has configured it and is one line to rebrand. Font payload drops from 2.7 MB to under 1 MB while gaining a family, because `FontWeight` has driven the `wght` axis since Flutter 3.41 and this package requires 3.44 — `tool/download_fonts.sh`, which fetched static weights from gstatic by content hash, is gone. +- Bundle Inter and Space Grotesk as single variable files instead of nine static Inter weights, and pair them across the Material 3 text roles: Space Grotesk on display and headline, Inter on everything read at length. `Branding.displayFontFamily` overrides the display face and `Branding.seedColor` now defaults to the EigenInteractive teal, so a game looks deliberate before anyone has configured it and is one line to rebrand. Font payload drops from 2.7 MB to under 1 MB while gaining a family, because `FontWeight` has driven the `wght` axis since Flutter 3.41 and this package requires 3.44. `tool/download_fonts.sh`, which fetched static weights from gstatic by content hash, is gone. ## [0.3.2] - 2026-08-07 ### Changed @@ -47,8 +51,8 @@ The manifest merger treats two `` entries with the same An app that declares `com.google.firebase.messaging.default_notification_channel_id` or `default_notification_icon` with a value other than `your_turn` / -`@drawable/ic_notification` must now either drop its copy — this package -supplies both — or keep it and add +`@drawable/ic_notification` must now either drop its copy, since this package +supplies both, or keep it and add `tools:replace="android:value"` (or `android:resource`) to that element. Declaring the same values as this package needs no change. @@ -58,14 +62,14 @@ Declaring the same values as this package needs no change. resources at all, so the icon only resolved in apps that happened to create that drawable themselves. The plugin now ships it, together with the Firebase default-channel and default-notification-icon meta-data. An app overrides the -silhouette by declaring the same resource name — Android resource merging gives +silhouette by declaring the same resource name; Android resource merging gives the application module precedence over a library. ## [0.2.0] - 2026-08-03 ### Changed - `eigen_api` moved to `^0.2.0`, following the engine to its 0.2.x release line. -No Dart API changed — the 0.2.0 spec is byte-identical to 0.1.0's apart from -the version stamp, and 0.2.0 of the engine was a TypeScript-side cleanup — but +No Dart API changed. The 0.2.0 spec is byte-identical to 0.1.0's apart from +the version stamp, and 0.2.0 of the engine was a TypeScript-side cleanup, but a consumer cannot depend on this package and `eigen_api 0.2.0` at the same time until this ships, so it is a breaking change to the constraint rather than a patch. @@ -98,7 +102,7 @@ player permission prompt remains an explicit opt-in and delivery remains best-effort. - Avatar upload and display against the worker-served avatar URL, via `cached_network_image`. -- Rock–Paper–Scissors under `example/` — a complete game, and the worked answer +- Rock–Paper–Scissors under `example/`: a complete game, and the worked answer to "how do I test a game screen". Its `fixtures/v1/rps.json` is the Dart half of a twin-fixture contract the engine repo checks from the other side. - **Inter** bundled as a package font (all 9 weights under `fonts:`), so @@ -128,7 +132,7 @@ engine's own version and consumed here as an ordinary versioned dependency. There is no vendored OpenAPI spec and no local codegen for it. - Riverpod toolchain moved to the 3.3.2 line (`flutter_riverpod ^3.3.2`, `riverpod_annotation ^4.0.3`, `riverpod_generator ^4.0.4`, `riverpod_lint ^3.1.4`, `riverpod_sqflite ^0.4.3`) so the engine resolves the same riverpod -core consuming apps do — generated code must be built against the core the app +core consuming apps do; generated code must be built against the core the app compiles against, and riverpod 3.3.x changed `Notifier.runBuild`'s signature. ### Removed @@ -137,9 +141,10 @@ compiles against, and riverpod 3.3.x changed `Notifier.runBuild`'s signature. CLI that copied it into consuming apps, and the `update-ratings` / `refresh-fcm-token` edge functions. Ratings, notifications and every other server-side concern now live in the engine. -- `google_fonts`, which fetched Inter at runtime — replaced by the bundled +- `google_fonts`, which fetched Inter at runtime, replaced by the bundled package font above. +[Unreleased]: https://github.com/eigeninteractive/eigen-flutter/compare/v0.3.5...HEAD [0.3.5]: https://github.com/eigeninteractive/eigen-flutter/compare/v0.3.4...v0.3.5 [0.3.4]: https://github.com/eigeninteractive/eigen-flutter/compare/v0.3.3...v0.3.4 [0.3.3]: https://github.com/eigeninteractive/eigen-flutter/compare/v0.3.2...v0.3.3 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f72c6a7..acd0cfc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -133,7 +133,7 @@ the contributor's intent. **Use the command; do not hand-write the section.** Every `## [x]` heading needs a matching `[x]: ` reference definition at the bottom of the file, because -that is the only thing that makes the heading parse as a link — and cider finds +that is the only thing that makes the heading parse as a link, and cider finds its sections by looking for links, not by matching text. A heading without one is invisible to `cider release`, which then leaves your entry in place, appends an empty version section, and exits 0. `cider log` writes both halves; typing the @@ -142,7 +142,7 @@ silent until a release is already open. Reformatting is expected and fine. cider re-serialises the whole file, so it will unindent your wrapped list continuations and drop blank lines after headings. It -renders identically — leave it alone rather than restoring the wrapping, which +renders identically. Leave it alone rather than restoring the wrapping, which only guarantees the next release diff is noisy again. ## Generated code diff --git a/MAINTAINERS.md b/MAINTAINERS.md index e66b969..dc7fe71 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -7,7 +7,7 @@ For local setup, code generation, testing, changelog entries, and pull-request expectations, start with [CONTRIBUTING.md](CONTRIBUTING.md). Temporary compatibility code that cannot yet be removed because of upstream -packages is tracked in [`eigen-server/docs/blockers.md`][blockers] — one +packages is tracked in [`eigen-server/docs/blockers.md`][blockers]: one cross-repository list, because the Flutter and engine workarounds get reviewed at the same moments and two lists meant neither was re-checked. @@ -40,7 +40,7 @@ fails only at publish time. It is the pub.dev side of the contract `.github/workflows/release.yml` relies on: pub.dev trusts a GitHub OIDC token only when the token's ref is a tag matching that pattern, which is why the workflow is tag-triggered rather than push-triggered. No long-lived pub.dev -credential exists in repository secrets — the OIDC identity is the +credential exists in repository secrets; the OIDC identity is the authentication, and the workflow needs `permissions.id-token: write` to obtain it. @@ -58,7 +58,7 @@ copy means one place to rotate the key. ## Releasing -Releasing is one decision — which bump — and one review. There are no local +Releasing is one decision, which bump, and one review. There are no local commands, and nothing to remember to run afterwards. 1. **Dispatch the bump.** Actions → **Version** → Run workflow, or: @@ -101,14 +101,14 @@ is never generated from commit messages. **Read the changelog diff on the release pull request.** It should be two lines: `## [Unreleased]` becoming `## [x.y.z] - `, and its link definition becoming the version's. Anything larger means cider re-serialised sections it -could not parse — `checks.yml` fails the pull request in that case, but knowing +could not parse. `checks.yml` fails the pull request in that case, but knowing the shape of a healthy diff is what makes an unhealthy one obvious. **0.1.0 has no git tag**, so its link points at pub.dev (`/packages/eigen_flutter/versions/0.1.0`) rather than a release tag. It was published by hand before this automation existed, and the tag later created for it described a tree that never shipped, so it was deleted rather than corrected -— no commit provably matches the published tarball. +and no commit provably matches the published tarball. One consequence, once only: cider will generate `[0.2.0]: …/compare/v0.1.0...v0.2.0`, and that comparison cannot resolve. Change that line @@ -123,7 +123,7 @@ need this repository's builders to use `eigen_flutter`. pub.dev's automated publishing trusts a GitHub OIDC token only when its ref is a **tag** matching the configured pattern, so publication cannot run on a branch push and something must create the tag. `tag.yml` keys on `pubspec.yaml` -changing on `main` rather than on the release pull request merging — the +changing on `main` rather than on the release pull request merging: the version in the file is the fact, and how it got there does not matter, so a hand-edited bump releases identically and there is no second path to maintain. @@ -141,7 +141,7 @@ at all. That second check exists because the first release predates this automation. `eigen_flutter 0.1.0` was published by hand and never tagged, so "no tag" meant -"unreleased" only by accident — and the first `pubspec.yaml` edit afterwards, an +"unreleased" only by accident, and the first `pubspec.yaml` edit afterwards, an `eigen_api` constraint bump that left `version:` alone, duly tagged an unrelated commit as `v0.1.0`. Nothing was published, because the skip above held, but the tag described a tree that never shipped and had to be deleted. A tag that cannot @@ -153,7 +153,7 @@ inherits the error silently. **pub.dev builds and hosts Dartdoc for every published version.** Nothing here runs `dart doc` for publication, and no generated HTML is committed or copied into `eigen-web`. This is deliberate, and the opposite of how the TypeScript -side works — eigen-web vendors generated references from `eigen-server` because +side works: eigen-web vendors generated references from `eigen-server` because npm hosts nothing comparable. Dart needs no such machinery: - pub.dev's build is **versioned**. `/documentation/eigen_flutter/0.1.0/` stays @@ -191,14 +191,14 @@ After publication: Do **not** expect re-running the failed run to pick the fix up. A tag-triggered run uses the workflow definition **at the tag**, not the one on - `main`, so re-running replays the same broken file — and if the fault is in + `main`, so re-running replays the same broken file, and if the fault is in `release.yml` itself, it will keep replaying it. Deleting and re-tagging is the only recovery that reloads the workflow. (This is safe here precisely because the version has not been published; never move a tag whose version reached pub.dev.) - **The version was published:** never reuse it. Correct the problem, add a changelog entry, and dispatch a new `patch` or `breaking` release. - Re-triggering the published version is safe but does nothing — `release.yml` + Re-triggering the published version is safe but does nothing; `release.yml` detects it on pub.dev and skips. - **A harmful version shipped:** use pub.dev retraction, communicate the affected range, and publish the replacement. Published versions cannot be diff --git a/README.md b/README.md index 77ae1e7..2636fc7 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ The Flutter half of [EigenInteractive](https://eigeninteractive.com): a server-authoritative engine for turn-based multiplayer games. -`eigen_flutter` supplies the complete app shell—authentication, lobbies, +`eigen_flutter` supplies the complete app shell: authentication, lobbies, reconnection, timing, ratings, history, replay, social features, notifications, deep links, and update UX. A game supplies a small `GameModule` containing its client-side rules and presentation. diff --git a/android/src/main/res/drawable/ic_notification.xml b/android/src/main/res/drawable/ic_notification.xml index 223dcaf..54df454 100644 --- a/android/src/main/res/drawable/ic_notification.xml +++ b/android/src/main/res/drawable/ic_notification.xml @@ -3,7 +3,7 @@ from its manifest's default_notification_icon meta-data. It ships here so those references always resolve. An app that wants its - own silhouette declares res/drawable/ic_notification.xml itself — Android + own silhouette declares res/drawable/ic_notification.xml itself; Android resource merging gives the application module precedence over a library, so nothing needs replacing or deleting here. diff --git a/bin/configure_firebase.dart b/bin/configure_firebase.dart index 3409fbf..c2a5397 100644 --- a/bin/configure_firebase.dart +++ b/bin/configure_firebase.dart @@ -39,7 +39,7 @@ directly for anything this does not cover.'''; /// /// A deliberately narrow interface rather than a passthrough. This command /// exists to configure a generated app the one way it is supported, and the -/// options that would change that — `--platforms` above all — leave the rest of +/// options that would change that, `--platforms` above all, leave the rest of /// the script unable to finish: it needs the Web app FlutterFire records, and /// fails several steps later if it is not there. const _options = {'project', 'account'}; @@ -75,7 +75,7 @@ Future main(List rawArguments) async { File(path.join(appRoot.path, '.firebaserc')).existsSync(); if (!settled) { stdout.writeln( - 'No Firebase project chosen yet — FlutterFire will ask which to use, ' + 'No Firebase project chosen yet. FlutterFire will ask which to use, ' 'and can create one.\nPass --project to skip that.\n', ); } @@ -115,8 +115,8 @@ Future main(List rawArguments) async { path.join(temporaryDirectory.path, 'firebase-config.json'), ); // Quiet, unlike the FlutterFire call above, which is quiet only when it - // has nothing to ask. This one cannot prompt — the project and app are - // already decided — and its own success line names the temporary file it + // has nothing to ask. This one cannot prompt (the project and app are + // already decided) and its own success line names the temporary file it // wrote, which is deleted a few lines below and was never the output // anyone was waiting for. The captured stream is replayed on failure. await _runQuiet('firebase', [ @@ -164,7 +164,7 @@ Future main(List rawArguments) async { // Named, not just announced. FlutterFire matches an existing Android app // on the package name and an existing Web app on its display name, and - // reuses either silently — so a run against a project that already had + // reuses either silently, so a run against a project that already had // them looks exactly like a run that registered them. These three lines // are what distinguishes the two, and what a second run can be compared // against. @@ -174,7 +174,7 @@ Future main(List rawArguments) async { 'service worker.', ) ..writeln(' project $projectId') - ..writeln(' android ${configurations?['android'] ?? '—'}') + ..writeln(' android ${configurations?['android'] ?? 'none'}') ..writeln(' web $webAppId'); } on _UsageError catch (error) { stderr.writeln('configure_firebase: ${error.reason}\n\n$_usage'); @@ -261,7 +261,7 @@ Map? _selectedOutput(Directory appRoot) { return dartOutputs?['lib/firebase_options.dart'] as Map?; } -/// The project a previous run configured. Absent, the run asks — which is what +/// The project a previous run configured. Absent, the run asks, which is what /// it would have done anyway. String? _configuredProject(Directory appRoot) => _selectedOutput(appRoot)?['projectId'] as String?; diff --git a/build.yaml b/build.yaml index 1c139af..78ed63f 100644 --- a/build.yaml +++ b/build.yaml @@ -12,6 +12,6 @@ targets: # No `field_rename`: the engine's wire vocabulary is camelCase, so a # framework model's Dart field names are already the wire keys. The # engine wire types themselves come from the generated `eigen_api` - # package (which sets its own keys from the OpenAPI schema) — this + # package (which sets its own keys from the OpenAPI schema); this # config only governs any hand-written model in this package. explicit_to_json: true diff --git a/example/README.md b/example/README.md index 5c25f0c..bf1531c 100644 --- a/example/README.md +++ b/example/README.md @@ -8,8 +8,8 @@ and the two halves are checked against each other by the shared fixtures in `fixtures/`. RPS is small but not easy: both players commit at the same time and neither may -see the other's throw. That makes it the *hardest* case for a client — hidden -information, simultaneous turns, and nothing worth predicting — so what it does +see the other's throw. That makes it the *hardest* case for a client (hidden +information, simultaneous turns, and nothing worth predicting) so what it does and does not do is worth reading before you write your own. ## What's here @@ -18,13 +18,13 @@ and does not do is worth reading before you write your own. |---|---| | `lib/main.dart` | The whole app: which game, what it's called, where the server is | | `lib/src/rps_module.dart` | The version registry, the create dialog, the rules page | -| `lib/src/v1/models.dart` | Generated payload types and handwritten helpers — the Dart mirror of the TypeScript schemas | +| `lib/src/v1/models.dart` | Generated payload types and handwritten helpers, the Dart mirror of the TypeScript schemas | | `lib/src/v1/rules.dart` | The `GameRules` unit: legality, optimism, and what to render | | `lib/src/v1/board.dart` | The board | | `fixtures/v1/rps.json` | Behaviour recorded once, run by both languages | -Everything else a player touches — sign-in, home, lobby, friends, profile, -settings, history, replay, push, deep links — is `eigen_flutter`, and no code +Everything else a player touches (sign-in, home, lobby, friends, profile, +settings, history, replay, push, deep links) is `eigen_flutter`, and no code here is involved in any of it. ## The division of labour @@ -35,7 +35,7 @@ and the payload schemas. This side inherits generated payload parsing, then owns the legality check behind a greyed-out button, the optimism contract, and the widgets. -Where the two overlap — legality, `ratingPool`, `botSeatable` — they are +Where the two overlap (legality, `ratingPool`, `botSeatable`) they are transcriptions of each other, and drifting apart is a bug the fixtures catch. ## Two things RPS teaches that a simpler game would not @@ -44,7 +44,7 @@ transcriptions of each other, and drifting apart is a bug the fixtures catch. payload per seat, and for RPS it emits two different *shapes*: live play carries `yourMove` and simply omits the opponent's commit, while replay carries `commits` for both seats because the match is over. The opponent's throw is not -hidden by the UI — it is not in the bytes. `RpsV1Observation.fromJson` handles +hidden by the UI; it is not in the bytes. `RpsV1Observation.fromJson` handles both shapes, and that is the entire cost of hidden information on the client. **`previewAction` returns null, and that is the right answer.** The engine also @@ -52,7 +52,7 @@ masks the opponent's *pending* status, so after you throw you cannot tell whether you are waiting or whether you just resolved the round. Predicting either would be wrong half the time. The board still feels instant, because it holds the tapped move in widget state and resolves it against the -`ActionSubmitResult` the submit returns — optimism about your own action, which +`ActionSubmitResult` the submit returns: optimism about your own action, which you can always know, rather than about the resulting position, which here you cannot. @@ -102,7 +102,7 @@ flutter build appbundle --release \ > **`dependency_overrides` in `pubspec.yaml`** is temporary. `eigen_flutter` > depends on `eigen_api` by version, but the override that points it at the -> local checkout is honoured only for the *root* package — and here the root is +> local checkout is honoured only for the *root* package, and here the root is > this example, so it has to be repeated. Delete the block once `eigen_api` is > published; an app depending on `eigen_flutter` never needs it. @@ -116,4 +116,4 @@ checkout layout. Note that a case's `pending` array is written from the server's point of view. The client normally sees a masked projection of it, which for RPS is at most -your own seat — no case turns on the difference, but your game's might. +your own seat. No case turns on the difference, but your game's might. diff --git a/example/lib/main.dart b/example/lib/main.dart index a2515e2..5e3593d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,7 +1,7 @@ /// The entire app. /// -/// Every screen a player sees — sign-in, home, the lobby, friends, profile, -/// settings, history, replay, push permission prompts — comes from +/// Every screen a player sees (sign-in, home, the lobby, friends, profile, +/// settings, history, replay, push permission prompts) comes from /// `eigen_flutter`. What an app supplies is this file: which game to play, /// what it is called, what colour it is, and where its server lives. /// @@ -28,7 +28,7 @@ const _appHost = String.fromEnvironment('APP_HOST'); Future main() async { await runEngineApp( - // The game. One value, one line — this is the seam the whole framework is + // The game. One value, one line: the seam the whole framework is // built around. module: const RpsModule(), config: AppConfig( @@ -54,7 +54,7 @@ Future main() async { /// FCM delivers background messages on a separate isolate, so this must be a /// top-level function marked as an entry point and must re-initialise Firebase -/// itself — it cannot close over anything from [main]. +/// itself; it cannot close over anything from [main]. /// /// The engine's notifications carry their own display payload, so there is /// nothing to do here beyond making the isolate valid. diff --git a/example/lib/rps.dart b/example/lib/rps.dart index a837803..5889524 100644 --- a/example/lib/rps.dart +++ b/example/lib/rps.dart @@ -1,4 +1,4 @@ -/// Rock–Paper–Scissors — the reference game implementation. +/// Rock–Paper–Scissors: the reference game implementation. /// /// A game package exports its [RpsModule] and its payload types. The module is /// what `main.dart` hands to `runEngineApp`; the payload types are exported diff --git a/example/lib/src/rps_module.dart b/example/lib/src/rps_module.dart index 403399b..8530e97 100644 --- a/example/lib/src/rps_module.dart +++ b/example/lib/src/rps_module.dart @@ -16,7 +16,7 @@ class RpsModule extends GameModule { const RpsModule(); /// One entry today. A breaking rules change adds `2: RpsRulesV2()` and keeps - /// this one until every v1 match has drained — infra picks the unit from the + /// this one until every v1 match has drained; infra picks the unit from the /// match's own `schemaVersion`, so both generations play side by side. @override Map get versions => const {1: RpsRulesV1()}; @@ -74,7 +74,7 @@ class RpsModule extends GameModule { /// The only setting RPS has. Whatever this reports is sent as the game's /// `config` at creation and arrives back as [RpsV1Config] for the match's whole -/// life — the server validates it against the TS `configSchema`, so an +/// life: the server validates it against the TS `configSchema`, so an /// out-of-range value is rejected there, not trusted from here. class _TargetWinsPicker extends StatefulWidget { const _TargetWinsPicker({required this.onChanged}); diff --git a/example/lib/src/v1/board.dart b/example/lib/src/v1/board.dart index c35ef91..6ba95e6 100644 --- a/example/lib/src/v1/board.dart +++ b/example/lib/src/v1/board.dart @@ -1,10 +1,10 @@ -/// The in-game screen for RPS v1 — everything [RpsRulesV1.buildContent] +/// The in-game screen for RPS v1: everything [RpsRulesV1.buildContent] /// renders. /// /// The framework has already done the work that is not game-specific by the /// time this builds: auth, the socket, gap recovery, the countdown, the /// pending/finished chrome, and the JSON parsing. What is left is this file, -/// and it is the shape most games end up with — read the observation, draw it, +/// and it is the shape most games end up with: read the observation, draw it, /// submit an action, reconcile. library; @@ -81,7 +81,7 @@ class _RpsBoardState extends State { // `committed` needs no handling: the confirming frame is guaranteed to be // the next one this seat receives, and `didUpdateWidget` clears the guess // when it lands. The other two mean no frame is coming for this tap, so - // the buttons have to come back — infra has already shown the player why. + // the buttons have to come back; infra has already shown the player why. if (result != ActionSubmitResult.committed) { setState(() => _submitting = null); } diff --git a/example/lib/src/v1/rules.dart b/example/lib/src/v1/rules.dart index f227dfa..e4c3a4f 100644 --- a/example/lib/src/v1/rules.dart +++ b/example/lib/src/v1/rules.dart @@ -1,4 +1,4 @@ -/// Schema version 1 of Rock–Paper–Scissors on the client — the Dart twin of +/// Schema version 1 of Rock–Paper–Scissors on the client: the Dart twin of /// the same-named TypeScript unit in /// `eigen-server/examples/rps/src/module/v1.ts`. /// @@ -6,8 +6,8 @@ /// everything that decides anything: `initialState`, `applyAction`, /// `applyLifecycle`, `computeObservation`, and the Zod `schemas`. This unit /// owns the codec, the legality check that greys out an illegal tap, the -/// optimism contract, and the rendering. Where the two overlap — legality, the -/// two predicates — they are transcriptions, and the shared fixtures under +/// optimism contract, and the rendering. Where the two overlap (legality, the +/// two predicates) they are transcriptions, and the shared fixtures under /// `example/fixtures/v1/` are what keeps them transcriptions. /// /// A version unit never branches on version. When the rules change @@ -30,8 +30,8 @@ class RpsRulesV1 extends RpsV1RulesBase { /// /// Two conditions, and they are the same two the server checks: the seat's /// main turn has to be active, and it must not have already thrown this - /// round. `pending` here is this seat's *masked* projection — see - /// [previewAction] — so in live play it holds at most your own seat, which + /// round. `pending` here is this seat's *masked* projection (see + /// [previewAction]) so in live play it holds at most your own seat, which /// is all this check needs. @override bool isValidAction({ @@ -42,7 +42,7 @@ class RpsRulesV1 extends RpsV1RulesBase { required RpsV1Config config, }) => pending.contains(playerIndex) && !obs.committedBy(playerIndex); - /// Always null — RPS cannot predict its own next observation, and saying so + /// Always null. RPS cannot predict its own next observation, and saying so /// is the correct answer rather than a gap. /// /// The reason is the game itself. `computeObservation` masks the opponent's @@ -51,7 +51,7 @@ class RpsRulesV1 extends RpsV1RulesBase { /// yet (your next frame just echoes `yourMove`), or they threw first and /// your throw resolves the round (your next frame is a full reveal with a /// new score). Predicting either one would be wrong half the time, and a - /// prediction that is wrong half the time is worse than no prediction — it + /// prediction that is wrong half the time is worse than no prediction: it /// shows a reveal that never happened. /// /// That masking is also what makes simultaneous play work at all: because @@ -61,7 +61,7 @@ class RpsRulesV1 extends RpsV1RulesBase { /// /// The UI still feels immediate. `board.dart` holds the tapped move in local /// widget state and resolves it against the - /// [GameContentContext.onAction] future — [ActionSubmitResult.committed] + /// [GameContentContext.onAction] future. [ActionSubmitResult.committed] /// means the very next frame confirms it. That is optimism about *your own /// submission*, which you can always know; [previewAction] is optimism about /// *the resulting position*, which here you cannot. diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 08120a7..d47b4d6 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,5 +1,5 @@ name: rps_example -description: "Rock-Paper-Scissors — the reference game implementation for the EigenInteractive Flutter client." +description: "Rock-Paper-Scissors: the reference game implementation for the EigenInteractive Flutter client." publish_to: none version: 1.0.0 diff --git a/example/test/board_test.dart b/example/test/board_test.dart index 8e0c234..4eb7b81 100644 --- a/example/test/board_test.dart +++ b/example/test/board_test.dart @@ -1,6 +1,6 @@ /// Widget tests for the board. /// -/// Testing a game screen means building a [GameContentContext] by hand — there +/// Testing a game screen means building a [GameContentContext] by hand; there /// is no server, no socket and no auth involved, because `buildContent` /// receives a plain value object. [_context] below is the whole harness, and /// it is the piece worth copying into your own game. @@ -52,7 +52,7 @@ void main() { await tester.tap(find.bySemanticsLabel('paper')); await tester.pump(); - // The tap is reflected before any frame comes back — the optimism this + // The tap is reflected before any frame comes back: the optimism this // game has, standing in for a `previewAction` it cannot implement. expect(submitted, {'move': 'paper'}); expect(find.text('Waiting for your opponent…'), findsOneWidget); diff --git a/example/test/models_test.dart b/example/test/models_test.dart index 262e508..d97ee20 100644 --- a/example/test/models_test.dart +++ b/example/test/models_test.dart @@ -1,4 +1,4 @@ -/// Codec tests — the part of a game the shared fixtures cannot fully cover. +/// Codec tests: the part of a game the shared fixtures cannot fully cover. /// /// The fixtures exercise live play, because that is what the server's /// `applyAction` produces. The *replay* observation shape has no fixture: it diff --git a/fonts/README.md b/fonts/README.md index 8ab309f..f267c5c 100644 --- a/fonts/README.md +++ b/fonts/README.md @@ -14,7 +14,7 @@ font, including inside an application binary. ## Updating -Download the family from Google Fonts — [Inter], [Space Grotesk] — take the +Download the family from Google Fonts ([Inter], [Space Grotesk]) and take the `*-VariableFont_*.ttf` from the top level of the archive rather than anything under `static/`, rename it as above, and copy `OFL.txt` alongside. @@ -27,7 +27,7 @@ The italic file is deliberately not bundled: nothing in the shell sets ## Why variable, and why no `weight:` entries -`FontWeight` drives the `wght` axis directly — that landed in Flutter 3.41, and +`FontWeight` drives the `wght` axis directly; that landed in Flutter 3.41, and this package requires 3.44, so the workaround it replaced is behind us. One file per family covers every weight, and intermediate weights become available for free. @@ -40,7 +40,7 @@ content hash. That approach pinned an Inter with no `opsz` axis, could not be verified against anything, and cost 2.7 MB; both variable faces together are under 1 MB. -Nothing sets `opsz`. It rests at its default of 14 — Inter's text optical size, +Nothing sets `opsz`. It rests at its default of 14, Inter's text optical size, which is the correct one for the roles Inter has here. [Inter]: https://fonts.google.com/specimen/Inter diff --git a/lib/app_runner.dart b/lib/app_runner.dart index eac6bb8..84fa036 100644 --- a/lib/app_runner.dart +++ b/lib/app_runner.dart @@ -27,7 +27,7 @@ import 'package:eigen_flutter/features/game/providers/game_providers.dart'; /// [firebaseOptions] are the app's generated `DefaultFirebaseOptions`; they /// live in the app package because `firebase_options.dart` is app-specific. /// [onBackgroundMessage] must be a top-level (or static) function annotated -/// `@pragma('vm:entry-point')` — FCM runs it in a separate isolate, so it +/// `@pragma('vm:entry-point')`, since FCM runs it in a separate isolate, so it /// cannot close over [firebaseOptions] and must re-init Firebase itself. Future runEngineApp({ required GameModule module, @@ -71,7 +71,7 @@ Future runEngineApp({ runApp( ProviderScope( // Narrows Riverpod's over-eager default retry to transport failures only - // — a server-reported error is never re-run. See [engineProviderRetry]. + // A server-reported error is never re-run. See [engineProviderRetry]. retry: engineProviderRetry, overrides: [ currentGameModuleProvider.overrideWithValue(module), diff --git a/lib/core/api/auth_interceptor.dart b/lib/core/api/auth_interceptor.dart index 1d55b59..7bbe76a 100644 --- a/lib/core/api/auth_interceptor.dart +++ b/lib/core/api/auth_interceptor.dart @@ -8,7 +8,7 @@ import 'package:firebase_auth/firebase_auth.dart'; /// close to expiry, so this stays cheap while never sending a stale token. /// /// A signed-out caller sends no header at all and the server answers 401. That -/// is deliberate — the engine has no anonymous surface, and a missing token is +/// is deliberate: the engine has no anonymous surface, and a missing token is /// the same failure as a bad one. /// /// There is no refresh-and-retry on 401. The transport `RetryInterceptor` @@ -16,7 +16,7 @@ import 'package:firebase_auth/firebase_auth.dart'; /// response, and on a request of any method) is never retried; and because /// `getIdToken()` already refreshes ahead of expiry, a 401 here means the /// session is genuinely gone, which the auth layer handles by signing the user -/// out — not something a retry would fix. +/// out, not something a retry would fix. class AuthInterceptor extends Interceptor { const AuthInterceptor(this._auth); diff --git a/lib/core/api/engine_api_providers.dart b/lib/core/api/engine_api_providers.dart index 961c888..15d35b7 100644 --- a/lib/core/api/engine_api_providers.dart +++ b/lib/core/api/engine_api_providers.dart @@ -11,7 +11,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'engine_api_providers.g.dart'; -/// The app-wide HTTP client for the engine — the data layer's single backend +/// The app-wide HTTP client for the engine: the data layer's single backend /// handle. /// /// Only repositories and data services may watch this or the API providers @@ -22,7 +22,7 @@ part 'engine_api_providers.g.dart'; /// `/api/engine` prefix. /// /// The generated `EigenApi` facade is deliberately not used, but not because it -/// can't take this Dio — it can (`EigenApi(dio: ..., interceptors: const [])` +/// can't take this Dio. It can (`EigenApi(dio: ..., interceptors: const [])` /// installs none of its own). The reason is the split below: each repository /// depends on the one narrow `*Api` it needs, so a fake in a test is that one /// resource, not the whole surface. The facade would hand every repository all @@ -40,11 +40,11 @@ Dio engineDio(Ref ref) { dio.interceptors.add(AuthInterceptor(FirebaseAuth.instance)); dio.interceptors.add(ref.watch(serverClockProvider).interceptor); // Transport-level retry, idempotent reads only. Retries a GET whose failure - // carried no response — a dropped connection or a timeout, where the outcome - // is unknown — twice with short backoff. A write is never retried (a + // carried no response (a dropped connection or a timeout, where the outcome + // is unknown) twice with short backoff. A write is never retried (a // timed-out POST may have landed on the server), and any failure that carried // a response is the server's decision, left untouched for `engineCall` to map - // (a 429 included — its Retry-After is respected, not auto-retried). The + // (a 429 included; its Retry-After is respected, not auto-retried). The // retry replays the whole interceptor chain, so `AuthInterceptor` re-attaches // a fresh token each attempt; added last so it is the outermost handler. dio.interceptors.add( @@ -69,7 +69,7 @@ Dio engineDio(Ref ref) { @Riverpod(keepAlive: true) ServerClock serverClock(Ref ref) => ServerClock(); -/// Games, the lobby, and the frame history — the whole play surface. +/// Games, the lobby, and the frame history: the whole play surface. @Riverpod(keepAlive: true) GamesApi gamesApi(Ref ref) => GamesApi(ref.watch(engineDioProvider)); diff --git a/lib/core/api/engine_api_providers.g.dart b/lib/core/api/engine_api_providers.g.dart index bead695..4241a8a 100644 --- a/lib/core/api/engine_api_providers.g.dart +++ b/lib/core/api/engine_api_providers.g.dart @@ -8,7 +8,7 @@ part of 'engine_api_providers.dart'; // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint, type=warning -/// The app-wide HTTP client for the engine — the data layer's single backend +/// The app-wide HTTP client for the engine: the data layer's single backend /// handle. /// /// Only repositories and data services may watch this or the API providers @@ -19,7 +19,7 @@ part of 'engine_api_providers.dart'; /// `/api/engine` prefix. /// /// The generated `EigenApi` facade is deliberately not used, but not because it -/// can't take this Dio — it can (`EigenApi(dio: ..., interceptors: const [])` +/// can't take this Dio. It can (`EigenApi(dio: ..., interceptors: const [])` /// installs none of its own). The reason is the split below: each repository /// depends on the one narrow `*Api` it needs, so a fake in a test is that one /// resource, not the whole surface. The facade would hand every repository all @@ -28,7 +28,7 @@ part of 'engine_api_providers.dart'; @ProviderFor(engineDio) final engineDioProvider = EngineDioProvider._(); -/// The app-wide HTTP client for the engine — the data layer's single backend +/// The app-wide HTTP client for the engine: the data layer's single backend /// handle. /// /// Only repositories and data services may watch this or the API providers @@ -39,7 +39,7 @@ final engineDioProvider = EngineDioProvider._(); /// `/api/engine` prefix. /// /// The generated `EigenApi` facade is deliberately not used, but not because it -/// can't take this Dio — it can (`EigenApi(dio: ..., interceptors: const [])` +/// can't take this Dio. It can (`EigenApi(dio: ..., interceptors: const [])` /// installs none of its own). The reason is the split below: each repository /// depends on the one narrow `*Api` it needs, so a fake in a test is that one /// resource, not the whole surface. The facade would hand every repository all @@ -47,7 +47,7 @@ final engineDioProvider = EngineDioProvider._(); final class EngineDioProvider extends $FunctionalProvider with $Provider { - /// The app-wide HTTP client for the engine — the data layer's single backend + /// The app-wide HTTP client for the engine: the data layer's single backend /// handle. /// /// Only repositories and data services may watch this or the API providers @@ -58,7 +58,7 @@ final class EngineDioProvider extends $FunctionalProvider /// `/api/engine` prefix. /// /// The generated `EigenApi` facade is deliberately not used, but not because it - /// can't take this Dio — it can (`EigenApi(dio: ..., interceptors: const [])` + /// can't take this Dio. It can (`EigenApi(dio: ..., interceptors: const [])` /// installs none of its own). The reason is the split below: each repository /// depends on the one narrow `*Api` it needs, so a fake in a test is that one /// resource, not the whole surface. The facade would hand every repository all @@ -153,17 +153,17 @@ final class ServerClockProvider String _$serverClockHash() => r'3305c9476050c05d8e503d96005e86648cb7520f'; -/// Games, the lobby, and the frame history — the whole play surface. +/// Games, the lobby, and the frame history: the whole play surface. @ProviderFor(gamesApi) final gamesApiProvider = GamesApiProvider._(); -/// Games, the lobby, and the frame history — the whole play surface. +/// Games, the lobby, and the frame history: the whole play surface. final class GamesApiProvider extends $FunctionalProvider with $Provider { - /// Games, the lobby, and the frame history — the whole play surface. + /// Games, the lobby, and the frame history: the whole play surface. GamesApiProvider._() : super( from: null, diff --git a/lib/core/api/engine_call.dart b/lib/core/api/engine_call.dart index f7150a9..753f605 100644 --- a/lib/core/api/engine_call.dart +++ b/lib/core/api/engine_call.dart @@ -11,7 +11,7 @@ import 'package:json_annotation/json_annotation.dart'; /// Only failures that carried a response are converted: those are the server /// answering `{ error, code? }`, and the stable `code` survives onto /// [EngineException.code] for callers and `humanize` to dispatch on. A failure -/// with no response — connection refused, DNS, timeout, a cancelled request — +/// with no response (connection refused, DNS, timeout, a cancelled request) /// propagates untouched, preserving the distinction between "the server said /// no" and "the outcome is unknown". That difference matters for a /// state-changing command: a rejected move did not happen, whereas a timed-out @@ -37,8 +37,8 @@ Future engineCall(Future Function() run) async { /// /// The successor to the `final response = await engineCall(...); return /// response.data?.x ?? ...` shape that recurred at every read site: it runs -/// through [engineCall] — so a server-reported failure still surfaces as an -/// [EngineException] and a transport failure still propagates untouched — then +/// through [engineCall], so a server-reported failure still surfaces as an +/// [EngineException] and a transport failure still propagates untouched, then /// returns the non-null payload. /// /// ```dart @@ -48,7 +48,7 @@ Future engineCall(Future Function() run) async { /// Throws [EngineException] if a success response carries no body, which is a /// contract violation for an endpoint declared to return one (never the normal /// path: the generated models make list fields non-null, so a present body -/// needs no `?? const []`). A call that expects no body — a 204 write — uses +/// needs no `?? const []`). A call that expects no body, such as a 204 write, uses /// [engineCall] directly instead. Future engineData(Future> Function() run) async { final response = await engineCall(run); diff --git a/lib/core/api/game_socket.dart b/lib/core/api/game_socket.dart index 5f89147..36b038e 100644 --- a/lib/core/api/game_socket.dart +++ b/lib/core/api/game_socket.dart @@ -35,8 +35,8 @@ Uri buildGameSocketUri({ /// One message from a game's socket, or the signal that the socket (re)opened. /// -/// The server never reads from this socket — every client-to-server command -/// rides HTTP — so this is a one-way feed of the message kinds the protocol +/// The server never reads from this socket, since every client-to-server +/// command rides HTTP, so this is a one-way feed of the message kinds the protocol /// defines, plus the connection signal the ordering pipeline needs. sealed class GameSocketEvent { const GameSocketEvent(); @@ -54,7 +54,7 @@ final class GameSocketConnected extends GameSocketEvent { /// A pre-game roster snapshot: who is seated and what the game's status is. /// -/// Unversioned and idempotent — the server pushes one on every roster change +/// Unversioned and idempotent: the server pushes one on every roster change /// and one on socket open while the game is still in the waiting room, so a /// reconnect simply gets the current one. final class GameSocketRoster extends GameSocketEvent { @@ -77,7 +77,7 @@ final class GameSocketSync extends GameSocketEvent { /// One versioned frame for the receiving seat. /// -/// Only ever this socket's own seat's view — the server resolves each frame's +/// Only ever this socket's own seat's view; the server resolves each frame's /// owner against the roster before sending, so another player's hidden /// information never crosses the wire. final class GameSocketFrame extends GameSocketEvent { @@ -116,7 +116,7 @@ class GameSocket { /// The stream never completes on its own and never surfaces a connection /// failure as an error: a dropped socket is an expected condition on mobile, /// and the recovery for it is the reconnect this already performs. Errors - /// that are *not* recoverable that way — a malformed message — are logged and + /// that are *not* recoverable that way, such as a malformed message, are logged and /// skipped rather than tearing down a working connection. Stream connect(String gameId) async* { var backoff = _initialBackoff; diff --git a/lib/core/api/retry_policy.dart b/lib/core/api/retry_policy.dart index e03aae2..75276c5 100644 --- a/lib/core/api/retry_policy.dart +++ b/lib/core/api/retry_policy.dart @@ -3,15 +3,15 @@ import 'dart:async'; import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -/// The two retry policies the engine client installs — one at the transport -/// layer, one at the provider layer — kept together because they draw the same +/// The two retry policies the engine client installs, one at the transport +/// layer and one at the provider layer, kept together because they draw the same /// line: a retry is only ever safe for a failure whose outcome is unknown (a /// transport blip), never for one the server decided. /// Whether a failed request should be retried at the transport layer, wired into /// the `RetryInterceptor` on the engine Dio. /// -/// True only for an idempotent GET whose failure carried no response — a dropped +/// True only for an idempotent GET whose failure carried no response: a dropped /// connection or a timeout, where the request may not have reached the server. /// A write is never retried (a timed-out POST may already have landed), and a /// failure that carried a response is the server's answer, not a blip, so it is @@ -30,7 +30,7 @@ FutureOr retryTransientGet(DioException error, int attempt) { /// The provider-retry policy installed on the root [ProviderScope]. /// -/// Riverpod's default retries *every* non-[Error] failure up to ten times — +/// Riverpod's default retries *every* non-[Error] failure up to ten times, /// including an [EngineException], which is the server deliberately saying no (a /// full lobby, an unknown game, a failed validation). Re-running those is /// pointless and needlessly hammers the server, so this narrows automatic @@ -40,7 +40,7 @@ FutureOr retryTransientGet(DioException error, int attempt) { /// [EngineException] and is never retried here. /// /// The coarse, whole-build net above [retryTransientGet]. It runs only for read -/// providers — mutations happen in notifier methods, not in `build`, so they are +/// providers; mutations happen in notifier methods, not in `build`, so they are /// never governed by this. Two tries with short exponential backoff; a read that /// exhausts them surfaces to the UI, which can still refresh manually. Duration? engineProviderRetry(int retryCount, Object error) { diff --git a/lib/core/api/server_clock.dart b/lib/core/api/server_clock.dart index 08b8ebd..d979f63 100644 --- a/lib/core/api/server_clock.dart +++ b/lib/core/api/server_clock.dart @@ -3,7 +3,7 @@ import 'package:dio/dio.dart'; /// Server time, as best the client can tell. /// /// Every deadline on the wire is an absolute server timestamp, so rendering a -/// countdown means subtracting *server* now — not device now. A device whose +/// countdown means subtracting *server* now, not device now. A device whose /// clock is minutes off would otherwise show a turn timer that disagrees with /// when the turn actually expires, and the server is the authority: its alarm /// fires on exactly the deadline it sent. @@ -29,7 +29,7 @@ class ServerClock { /// /// Clamped at zero: an expired deadline reads as "no time left" rather than /// as a negative duration a countdown would have to special-case. The server - /// grants a grace period beyond this, which is deliberately invisible here — + /// grants a grace period beyond this, which is deliberately invisible here: /// the client shows the true deadline and lets the server be lenient. Duration remainingUntil(int deadline) { final left = DateTime.fromMillisecondsSinceEpoch( @@ -42,7 +42,7 @@ class ServerClock { /// A server timestamp expressed on the *device* clock. /// /// Countdown widgets tick by repeatedly subtracting `DateTime.now()`, which - /// is device time — so converting once here keeps that arithmetic correct on + /// is device time, so converting once here keeps that arithmetic correct on /// every tick, where handing them a raw server timestamp would bake the skew /// into every frame. DateTime deviceTimeFor(int serverEpochMs) => @@ -93,7 +93,7 @@ class _ServerClockInterceptor extends Interceptor { /// `dart:io`'s `HttpDate` is not available on web, and this runs on both. abstract final class HttpDate { static DateTime? tryParse(String value) { - // `Sun, 06 Nov 1994 08:49:37 GMT` — the only format CF emits, and the only + // `Sun, 06 Nov 1994 08:49:37 GMT`: the only format CF emits, and the only // one HTTP/1.1 requires a client to generate. final match = RegExp( r'^\w{3}, (\d{2}) (\w{3}) (\d{4}) (\d{2}):(\d{2}):(\d{2}) GMT$', diff --git a/lib/core/config/app_config.dart b/lib/core/config/app_config.dart index fdcbbd9..5a9b6ac 100644 --- a/lib/core/config/app_config.dart +++ b/lib/core/config/app_config.dart @@ -10,8 +10,8 @@ part 'app_config.g.dart'; /// by [runEngineApp]. It groups the app's configurable concerns by /// responsibility: /// -/// - [branding] — user-facing identity (name, theme seed). -/// - [engine] — runtime backend/integration values the framework needs. +/// - [branding]: user-facing identity (name, theme seed). +/// - [engine]: runtime backend/integration values the framework needs. /// /// Keeping each concern as its own value object is what stops this from /// decaying into a junk drawer of unrelated flags. A consuming app reads its @@ -44,7 +44,7 @@ class EngineConfig { }); /// Origin of the EigenInteractive server, with no trailing slash and no - /// path — for example `https://api.example.com`. + /// path, for example `https://api.example.com`. /// /// Only the origin: every generated route already carries the `/api/engine` /// prefix, and the game socket is built from this same origin with the scheme @@ -66,7 +66,7 @@ class EngineConfig { /// customer's own domain; null disables the features built on it. /// /// One host serves everything: the app's deep links (`/join/:code`, - /// `/game/:id`), and — when the worker has `site` configured — the legal + /// `/game/:id`), and, when the worker has `site` configured, the legal /// pages and landing page. The App Links intent-filter is scoped to the /// deep-link prefixes, so legal URLs on this same host open in the browser /// rather than being intercepted. @@ -152,7 +152,7 @@ class Branding { /// Material 3 seed color; the full light/dark [ColorScheme] derives from it. /// /// Defaults to the EigenInteractive teal, so a game looks deliberate before - /// anyone has thought about colour. Replace it with one line — and note that + /// anyone has thought about colour. Replace it with one line, and note that /// Material 3 treats a seed as a *hue*, not a colour: it pulls whatever you /// give it to tone 40 and rebuilds the ramp, so neighbouring greens all /// arrive at much the same scheme, and a near-black seed comes back @@ -171,7 +171,7 @@ class Branding { /// ends on; override per app if needed. /// /// Whichever part of the line reads `EigenInteractive` is rendered as a link - /// to it, and nothing else is — so a replacement that never names the engine + /// to it, and nothing else is, so a replacement that never names the engine /// is plain text. final String madeByCredit; } diff --git a/lib/core/errors/engine_exception.dart b/lib/core/errors/engine_exception.dart index dc99bab..faf18af 100644 --- a/lib/core/errors/engine_exception.dart +++ b/lib/core/errors/engine_exception.dart @@ -1,6 +1,6 @@ import 'package:eigen_api/eigen_api.dart'; -/// A failure the engine server itself reported — a non-2xx response carrying +/// A failure the engine server itself reported: a non-2xx response carrying /// the `{ error, code? }` envelope. /// /// Distinct from a transport failure (no response at all), which propagates as @@ -16,7 +16,7 @@ class EngineException implements Exception { /// null; a coded failure is phrased by `humanize` instead. final String message; - /// The stable machine code, or null for failures that carry none — request + /// The stable machine code, or null for failures that carry none: request /// validation and unexpected 500s. /// /// Values introduced by a newer server decode to diff --git a/lib/core/errors/error_messages.dart b/lib/core/errors/error_messages.dart index c188309..62119d1 100644 --- a/lib/core/errors/error_messages.dart +++ b/lib/core/errors/error_messages.dart @@ -7,7 +7,7 @@ import 'package:eigen_flutter/core/errors/engine_exception.dart'; /// /// Copy is chosen by the server's stable [ErrorCode], never by matching message /// text, so rewording a server message can never change what the user sees. -/// Uncoded failures get the generic message rather than the server's own text — +/// Uncoded failures get the generic message rather than the server's own text, /// those are validation details and unexpected 500s, whose wording is /// diagnostic and sometimes internal. String humanize(Object e) => switch (e) { @@ -26,12 +26,12 @@ String humanize(Object e) => switch (e) { /// decodes to [ErrorCode.unknownDefaultOpenApi] and uses generic copy until the /// app updates. String messageForCode(ErrorCode code) => switch (code) { - // Kernel rejections — the move reached the game and it refused. + // Kernel rejections: the move reached the game and it refused. ErrorCode.notActive => 'This game has already ended.', ErrorCode.notReady => 'This game needs more players before it can start.', ErrorCode.expired => 'Time ran out for this turn.', ErrorCode.notPending => "It's not your turn.", - ErrorCode.stateUpdated => 'The game updated — try again.', + ErrorCode.stateUpdated => 'The game updated. Try again.', ErrorCode.invalidPayload => "That move isn't valid.", ErrorCode.illegalMove => "That move isn't allowed.", // Lobby rejections. @@ -42,7 +42,7 @@ String messageForCode(ErrorCode code) => switch (code) { ErrorCode.notParticipant => "You're not in this game.", ErrorCode.notCreator => 'Only the host can do that.', ErrorCode.creatorCannotLeave => - 'You created this game — cancel it instead of leaving.', + 'You created this game; cancel it instead of leaving.', // Raised before the command reached the game. ErrorCode.schemaUnsupported => 'Update your app to play this game.', ErrorCode.usernameInvalid => diff --git a/lib/core/game/game_creation_spec.dart b/lib/core/game/game_creation_spec.dart index c22e979..3a6e4de 100644 --- a/lib/core/game/game_creation_spec.dart +++ b/lib/core/game/game_creation_spec.dart @@ -26,7 +26,7 @@ class UntimedConfig extends TimingModeConfig { /// Each turn gets a fresh fixed window of [minSeconds]–[maxSeconds]. /// -/// [minSeconds] is clamped to [kMinTurnSeconds] (30 s) — the infra hard +/// [minSeconds] is clamped to [kMinTurnSeconds] (30 s), the infra hard /// limit. The dialog renders a slider between [minSeconds] and [maxSeconds], /// with optional [presets] shown as quick-pick chips. class PerActionConfig extends TimingModeConfig { @@ -104,7 +104,7 @@ class GameCreationSpec { /// optional presets for each mode. Insertion order is preserved (Dart /// [LinkedHashMap] guarantee), so the first entry is selected by default. /// - /// Multiple entries of the same subtype are allowed — for example, a game + /// Multiple entries of the same subtype are allowed: for example, a game /// could offer both "Blitz" ([PerActionConfig] 30–300 s) and "Daily" /// ([PerActionConfig] 3600–86400 s) as distinct named options. final Map timingConfigs; diff --git a/lib/core/game/game_frame.dart b/lib/core/game/game_frame.dart index d5c777c..dda5930 100644 --- a/lib/core/game/game_frame.dart +++ b/lib/core/game/game_frame.dart @@ -15,7 +15,7 @@ import 'package:eigen_flutter/core/game/timing_context.dart'; /// Frames arrive as an ordered, gap-recovered stream (observations are /// append-only server-side, and a reconnect replays whatever versions the /// socket missed), so a game may animate the transition between consecutive frames -/// and trust that it sees every one — falling back to a snap only after a +/// and trust that it sees every one, falling back to a snap only after a /// cold (re)load, where no predecessor was rendered. class GameFrame { const GameFrame({ diff --git a/lib/core/game/game_module.dart b/lib/core/game/game_module.dart index 3cab9f8..ac33063 100644 --- a/lib/core/game/game_module.dart +++ b/lib/core/game/game_module.dart @@ -15,17 +15,17 @@ import 'package:eigen_api/eigen_api.dart' show GameAccess, GameStatus, Outcome; /// rendering can ignore the result entirely. enum ActionSubmitResult { /// The server committed the action. Its confirming frame is the *next* - /// frame this seat receives — the optimistic lock guarantees no other + /// frame this seat receives; the optimistic lock guarantees no other /// frame can land in between. committed, /// The action definitively did not commit: the server rejected it, or it /// was never sent (another submit was already in flight). Infra has - /// already surfaced any error to the player; revert optimistic rendering — + /// already surfaced any error to the player; revert optimistic rendering, /// no frame will arrive for this action. rejected, - /// The submission failed in transit and the outcome is unknown — the + /// The submission failed in transit and the outcome is unknown: the /// server may still have committed it. Revert optimistic rendering; if the /// action did commit, its frame arrives over the game socket and re-applies /// the move. @@ -36,7 +36,7 @@ enum ActionSubmitResult { /// /// Passing a single context (instead of a long parameter list) means adding a /// new piece of infra data later does not change the [GameRules.buildContent] -/// signature — and therefore does not force every game to update. Redundant +/// signature, and therefore does not force every game to update. Redundant /// values ([mySeat], [timing]) are exposed as getters that delegate to /// the authoritative source so there is only ever one of each. /// @@ -82,7 +82,7 @@ class GameContentContext { /// [ActionSubmitResult] for what each value guarantees about the frame /// stream); it never throws, and infra has already surfaced any error to /// the player before it resolves. Games that render purely from server - /// frames may ignore the result — fire-and-forget remains the simplest + /// frames may ignore the result; fire-and-forget remains the simplest /// correct usage. final Future Function(Map actionJson) onAction; @@ -97,7 +97,7 @@ class GameContentContext { /// through frame by frame), false during live play. /// /// [frame] is a historical snapshot and [onAction] is inert, so a game - /// never needs this to stay correct — it disables input off the pending set + /// never needs this to stay correct; it disables input off the pending set /// as usual. Use it only for replay-specific presentation, e.g. surfacing /// move-by-move narration or suppressing "your turn" prompts. When [mySeat] /// is a [Viewer] the current user did not play in the game (a public replay @@ -105,7 +105,7 @@ class GameContentContext { final bool isReplay; /// The current user's place in the game: [Seated] at an index, or a [Viewer] - /// (no seat — only when replaying a public game they did not play in). + /// (no seat, only when replaying a public game they did not play in). MySeat get mySeat => playersContext.mySeat; /// Timing metadata for the current turn (mirrors [GameFrame.timing]). @@ -114,8 +114,8 @@ class GameContentContext { /// The chosen game settings, passed to [GameRules.ratingPool]. /// -/// Field-for-field twin of the TS `RatingPoolArgs` interface — same names, -/// same types — so the Dart and TS `ratingPool` implementations read +/// Field-for-field twin of the TS `RatingPoolArgs` interface, with the same +/// names and types, so the Dart and TS `ratingPool` implementations read /// identically and stay trivially diffable. class RatingPoolArgs { const RatingPoolArgs({ @@ -141,7 +141,7 @@ class RatingPoolArgs { /// /// Field-for-field twin of the TS `BotSeatableArgs` interface. [gameConfig] /// is the game's creation config; [botConfig] is the bot's declared -/// capabilities (`bots.config`) — game-owned but unversioned, so it stays an +/// capabilities (`bots.config`): game-owned but unversioned, so it stays an /// opaque map. class BotSeatableArgs { const BotSeatableArgs({required this.gameConfig, required this.botConfig}); @@ -150,7 +150,7 @@ class BotSeatableArgs { final Map botConfig; } -/// The client-side surface of one `schemaVersion` of the game — the Dart +/// The client-side surface of one `schemaVersion` of the game: the Dart /// twin of the same-named TS `GameRules` unit. /// /// A version unit is self-contained: it parses and renders exactly one @@ -165,15 +165,15 @@ class BotSeatableArgs { /// owns the client half, member for member: /// /// - the generated payload parsing and serialization ([parseConfig] / -/// [parseObservation] / [parseAction] / [serializeAction]) — emitted from +/// [parseObservation] / [parseAction] / [serializeAction]), emitted from /// the TS schemas; -/// - [isValidAction] — the legality half of the TS `applyAction`, transcribed; -/// - [previewAction] — the game's own optimistic projection of `applyAction` +/// - [isValidAction]: the legality half of the TS `applyAction`, transcribed; +/// - [previewAction]: the game's own optimistic projection of `applyAction` /// (a standardized contract; infra never calls it); /// - rendering ([buildContent]); /// - display-only twins of the two predicates ([ratingPool] / [botSeatable]). /// -/// Keep the twins in sync with the TS unit for the same version — the server +/// Keep the twins in sync with the TS unit for the same version; the server /// recomputes everything authoritative, so drift only degrades UX, never /// stored data. /// @@ -195,10 +195,10 @@ abstract class GameRules { /// Parses a raw observation JSON map into this version's observation type. /// - /// Called once per network event — never on frame rebuild. + /// Called once per network event, never on frame rebuild. TObs parseObservation(Map json); - /// Parses a raw action JSON map into this version's action type — the input + /// Parses a raw action JSON map into this version's action type: the input /// mirror of [serializeAction] (the TS twin's `schemas.action` covers both /// directions). Infra uses it to re-type a logged action (e.g. for replay /// cues). @@ -208,7 +208,7 @@ abstract class GameRules { /// /// Infra holds rules units erased and cannot call a concrete `toJson`, so /// the unit owns this serialization step. The returned map is the action - /// `data` the TS `applyAction` hook consumes — identical whether the move + /// `data` the TS `applyAction` hook consumes, identical whether the move /// came from a human tap, a local bot, or a server bot, because every /// producer routes through this one seam. Map serializeAction(TAction action); @@ -216,7 +216,7 @@ abstract class GameRules { /// Validates local legality of an action for client-side UX feedback. /// /// The authoritative check runs server-side in the TS `applyAction` hook; - /// this is for disabling illegal taps and similar — essentially the + /// this is for disabling illegal taps and similar: essentially the /// legality half of that hook, transcribed. The parameter names /// deliberately match the TS `ApplyActionArgs` fields (`pending`, `data`, /// `playerIndex`, `config`) so the two read side by side. All parameters @@ -224,14 +224,14 @@ abstract class GameRules { /// styles; simple games can ignore whatever they don't need. /// /// - [obs]: the current typed game payload (board, hand, fog, ...). - /// - [pending]: 0-based indices whose "main turn" is active right now — + /// - [pending]: 0-based indices whose "main turn" is active right now, /// this seat's projection of `game_states.pending_players`, from its /// observation row. Games with interrupt actions (e.g. Exploding /// Kittens's Nope) use this to distinguish a main-turn action from an /// interrupt (anyone holding the card may play). /// - [data]: the candidate action payload. /// - [playerIndex]: the 0-based index of the player attempting [data]. - /// For games where piece ownership matters (Chess — only your color), + /// For games where piece ownership matters (Chess, only your color), /// this identifies the actor; sequential games that don't care can /// ignore it. /// - [config]: this game's parsed config. @@ -245,7 +245,7 @@ abstract class GameRules { /// Predicts this seat's next observation for [data], or returns null when /// the outcome depends on hidden information (a combat resolution, a - /// reveal, a draw from a deck) — that move is then simply server-driven. + /// reveal, a draw from a deck); that move is then simply server-driven. /// /// **Infra never calls this.** It is required anyway so every game states /// its optimism contract explicitly in one standard place, instead of each @@ -253,7 +253,7 @@ abstract class GameRules { /// wants optimistic rendering calls it from its own widgets, pairing the /// predicted observation with the [GameContentContext.onAction] result /// (`false` → revert; `true` → the next frame is the confirming one). A - /// game that wants every move server-driven returns null unconditionally — + /// game that wants every move server-driven returns null unconditionally, /// always correct. /// /// Keeping the signature standardized (parameters mirror [isValidAction]) @@ -284,7 +284,7 @@ abstract class GameRules { /// The rating pool a game with these settings would fall into, or `null` if /// it is unrated (casual). Drives the create dialog: the Rated toggle is - /// shown only when this returns non-null. **Display only** — the server + /// shown only when this returns non-null. **Display only**; the server /// recomputes the authoritative pool (the TS `GameRules.ratingPool` twin) at /// creation and a guest is always forced unrated, so a wrong value here only /// affects the UI, never the stored rating. @@ -292,7 +292,7 @@ abstract class GameRules { /// Whether a bot whose declared capabilities are [BotSeatableArgs.botConfig] /// can play a game with [BotSeatableArgs.gameConfig]. Used to filter the bot - /// pickers locally (no network call). **UX only** — the server enforces the + /// pickers locally (no network call). **UX only**; the server enforces the /// same rule (the TS `GameRules.botSeatable` twin) before seating. bool botSeatable(BotSeatableArgs args); } @@ -300,20 +300,20 @@ abstract class GameRules { /// Contract every game implementor provides. /// /// **Extend** (don't implement) this in the game package's `game_module.dart` -/// (e.g. `games/tic_tac_toe/lib/game_module.dart`) — that is the single file to +/// (e.g. `games/tic_tac_toe/lib/game_module.dart`), the single file to /// edit when swapping games, and extending inherits the default /// [playersForConfig]. Register the implementation via /// `currentGameModuleProvider.overrideWithValue(...)` in the app's `main.dart`. /// -/// The module is a thin container — the same-named twin of the TS +/// The module is a thin container: the same-named twin of the TS /// `GameModule`: the version registry ([versions], one [GameRules] unit per /// `schemaVersion`) plus the creation/about UI, which is version-independent /// because creation always targets [latestSchemaVersion]. All version -/// dispatch is owned by infra — game code never branches on version. +/// dispatch is owned by infra, and game code never branches on version. abstract class GameModule { const GameModule(); - /// The [GameRules] units keyed by `schemaVersion` — exactly the versions + /// The [GameRules] units keyed by `schemaVersion`: exactly the versions /// this build ships, mirroring the keys of the TS `GameModule.versions`. /// /// Sparse on purpose: loading a game requires its version's entry @@ -325,14 +325,14 @@ abstract class GameModule { /// entry until those games drain (write) / stop being replayable (read). Map get versions; - /// The version new games are created at — the highest key of [versions] — + /// The version new games are created at, the highest key of [versions], /// and the value sent as `p_client_schema_version` on join/create. int get latestSchemaVersion => versions.keys.reduce((a, b) => a > b ? a : b); /// The rules unit new games use ([versions] at [latestSchemaVersion]). GameRules get latestRules => versions[latestSchemaVersion]!; - /// Whether this build can load a game created at [version] — i.e. [versions] + /// Whether this build can load a game created at [version], i.e. [versions] /// ships an entry for it. Sparse like the TS side: an old version dropped /// after draining is unsupported even if lower than [latestSchemaVersion]. bool supportsSchema(int version) => versions.containsKey(version); @@ -363,7 +363,7 @@ abstract class GameModule { /// Return null if the game has no config beyond timing and player count. /// /// [onChanged] is called whenever the player adjusts a setting. The dialog - /// stores the latest value in a plain field (not state — it is never + /// stores the latest value in a plain field (not state, since it is never /// displayed in the UI) and sends it with the create-game request at submit /// time. Widget? buildCreationConfig({ @@ -379,7 +379,7 @@ abstract class GameModule { } /// Thrown when a game's `games.schema_version` has no entry in -/// [GameModule.versions] — it was created by a newer app version (or one this +/// [GameModule.versions]. It was created by a newer app version (or one this /// build has retired) and can't be loaded until the user updates. class UnsupportedGameSchemaException implements Exception { const UnsupportedGameSchemaException({ @@ -396,5 +396,5 @@ class UnsupportedGameSchemaException implements Exception { @override String toString() => 'UnsupportedGameSchemaException: no rules for game schema $gameSchema ' - '(latest supported: $supportedSchema) — the app must be updated.'; + '(latest supported: $supportedSchema). The app must be updated.'; } diff --git a/lib/core/game/my_seat.dart b/lib/core/game/my_seat.dart index b36a6e3..9f2b470 100644 --- a/lib/core/game/my_seat.dart +++ b/lib/core/game/my_seat.dart @@ -17,7 +17,7 @@ sealed class MySeat { /// The seat index when [Seated], or null for a [Viewer]. /// - /// A convenience for reads that treat "no seat" as "never me" — e.g. + /// A convenience for reads that treat "no seat" as "never me", e.g. /// `pendingPlayers.contains(mySeat.indexOrNull)` is `false` for a viewer. /// Prefer an exhaustive `switch` when the viewer case needs distinct /// handling. @@ -35,7 +35,7 @@ final class Seated extends MySeat { final int index; } -/// The current user has no seat — a non-participant viewing a replay. +/// The current user has no seat: a non-participant viewing a replay. final class Viewer extends MySeat { const Viewer(); } diff --git a/lib/core/game/players_context.dart b/lib/core/game/players_context.dart index d60c906..4af73f5 100644 --- a/lib/core/game/players_context.dart +++ b/lib/core/game/players_context.dart @@ -21,7 +21,7 @@ class PlayersContext { /// Returns the [GamePlayer] for [playerIndex]. /// - /// Always returns data — the provider guarantees completeness. + /// Always returns data; the provider guarantees completeness. GamePlayer operator [](int playerIndex) => players[playerIndex]!; /// The current user's [GamePlayer], or null when a [Viewer] (no seat). diff --git a/lib/core/game/timing_constants.dart b/lib/core/game/timing_constants.dart index ed187a4..082451a 100644 --- a/lib/core/game/timing_constants.dart +++ b/lib/core/game/timing_constants.dart @@ -5,7 +5,7 @@ import 'dart:math' as math; /// The server is the sole authority on deadlines: a game's Durable Object holds /// an alarm at the current turn's deadline and expires the turn itself, so the /// client never nudges it and never needs to model the server's grace window. -/// These constants only shape what the client *displays* — they can never cause +/// These constants only shape what the client *displays*; they can never cause /// a wrong rejection or a wrong timeout. /// Target headroom subtracted from a player's *displayed* countdown so an diff --git a/lib/core/navigation/widgets/shell_scaffold.dart b/lib/core/navigation/widgets/shell_scaffold.dart index 39c555f..f20e584 100644 --- a/lib/core/navigation/widgets/shell_scaffold.dart +++ b/lib/core/navigation/widgets/shell_scaffold.dart @@ -50,7 +50,7 @@ class ShellScaffold extends ConsumerWidget { }); final isOffline = ref.watch(isOfflineProvider); final isGuest = ref.watch(isAnonymousProvider); - // Offer solo play only when a playable combination exists — an untimed mode + // Offer solo play only when a playable combination exists; an untimed mode // with a usable local bot, or a timed mode with a usable server bot (so the // name is "solo", not "local bots": both classes can fill the seats). See // [soloPlayAvailableProvider]. Most deployments with no bots get an empty @@ -93,7 +93,7 @@ class ShellScaffold extends ConsumerWidget { label: Text('History'), ), // Social is registered-only. For guests the destination stays visible - // but disabled (greyed, non-tappable) — the same visible-but-disabled + // but disabled (greyed, non-tappable): the same visible-but-disabled // treatment rated games get in the lobby. enabled:false also keeps the // tap from firing onDestinationSelected, so branch indices stay 1:1. NavigationDrawerDestination( diff --git a/lib/core/notifications/firebase_notification_service.dart b/lib/core/notifications/firebase_notification_service.dart index a59334f..0e34c99 100644 --- a/lib/core/notifications/firebase_notification_service.dart +++ b/lib/core/notifications/firebase_notification_service.dart @@ -41,7 +41,7 @@ const _socialChannel = AndroidNotificationChannel( importance: Importance.low, ); -/// Catch-all for a category this build does not recognise — a newer server +/// Catch-all for a category this build does not recognise, e.g. a newer server /// than the installed app. Nothing is dropped; it surfaces here instead. const _generalChannel = AndroidNotificationChannel( 'general', @@ -60,7 +60,7 @@ enum _NotificationCategory { friendRequest, friendAccepted; - /// Parses the `category` field from the FCM data payload — the exact set the + /// Parses the `category` field from the FCM data payload: the exact set the /// engine sends (see the server's `push.ts`). Returns null for an unknown or /// missing value (a newer server than this build); the caller falls back to a /// generic notification rather than dropping it. @@ -297,7 +297,7 @@ class FirebaseNotificationService { } /// Removes this install's notification registration on sign-out so the server - /// stops targeting it immediately. Errors are logged but never thrown — + /// stops targeting it immediately. Errors are logged but never thrown, /// sign-out must succeed regardless of cleanup status. /// /// Deletes only the DB row; it deliberately leaves the FCM registration and @@ -480,14 +480,14 @@ class FirebaseNotificationService { final notification = message.notification; if (notification == null) return; // An unrecognised or missing category (a newer server than this build) - // resolves to null and still shows — on the general channel — rather than + // resolves to null and still shows, on the general channel, rather than // being dropped. final category = _NotificationCategory.fromString( message.data['category'] as String?, ); if (category == null) { developer.log( - 'Unknown notification category: ${message.data['category']} — ' + 'Unknown notification category: ${message.data['category']}: ' 'showing on the general channel', name: 'notifications', ); @@ -542,7 +542,7 @@ class FirebaseNotificationService { }; /// Game-scoped notifications (a turn, a ready, a finish) key off the deepLink - /// — which carries the gameId — so a later update for the same game replaces + /// which carries the gameId, so a later update for the same game replaces /// the earlier one. Everything else (invites, social, unknown) keys off /// messageId so events from different people/games stack independently. int _notificationId(RemoteMessage message, _NotificationCategory? category) { diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart index 08fef54..775aa74 100644 --- a/lib/core/theme/app_theme.dart +++ b/lib/core/theme/app_theme.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; /// Minimal app theme configuration using Material 3. /// /// Colour is derived from a seed supplied by `Branding` (see -/// `core/config/app_config.dart`) — call [light]/[dark] with that seed. +/// `core/config/app_config.dart`); call [light]/[dark] with that seed. /// Typography pairs two faces bundled by this package (see `fonts:` in the /// engine `pubspec.yaml`), so both render offline from the first frame and /// consuming apps need no font wiring. Built themes are cached per seed and @@ -33,12 +33,12 @@ abstract final class AppTheme { /// Grotesk is drawn for size: tight sidebearings and a single-storey `g` read /// as character in a headline and as noise in a paragraph. Inter is drawn for /// UI at small sizes and has the taller x-height to prove it. So the roles a - /// player *reads* — body, label, and the title role that labels list rows — + /// player *reads* (body, label, and the title role that labels list rows) /// stay on Inter, and only the roles they glance at take the display face. /// /// Applying the families here rather than through [ThemeData.fontFamily] - /// keeps every other property of the Material 3 type scale — sizes, weights, - /// tracking, line heights — exactly as the framework defines it, which is + /// keeps every other property of the Material 3 type scale (sizes, weights, + /// tracking, line heights) exactly as the framework defines it, which is /// what makes this survive the move to Material Expressive. static TextTheme textTheme(TextTheme base, String display) => base.copyWith( displayLarge: base.displayLarge?.copyWith(fontFamily: display), diff --git a/lib/eigen_flutter.dart b/lib/eigen_flutter.dart index c67978c..b809011 100644 --- a/lib/eigen_flutter.dart +++ b/lib/eigen_flutter.dart @@ -1,7 +1,7 @@ // Dartdoc selects the two supported package entry points by library name. // ignore_for_file: unnecessary_library_name -/// EigenInteractive — a whitelabel turn-based multiplayer game engine. +/// EigenInteractive: a whitelabel turn-based multiplayer game engine. /// /// A game app depends on this package, implements a [GameModule], and boots /// with [runEngineApp]: @@ -31,7 +31,7 @@ library eigen_flutter; /// The wire types a game renders from. /// /// Re-exported deliberately: they are generated, but they *are* this engine's -/// domain vocabulary — there are no hand-written mirrors to hide them behind, +/// domain vocabulary; there are no hand-written mirrors to hide them behind, /// and inventing some would be pure transcription. A game app must be able to /// name a [GameStatus] or an [OutcomeResultEnum] without depending on /// `eigen_api` itself, which is a build artifact that `tool/generate_api.sh` @@ -64,7 +64,7 @@ export 'package:eigen_api/eigen_api.dart' export 'app_runner.dart' show runEngineApp, MyApp; /// Server time. Exported because [TimingContext.clock] is typed as it, so -/// without this a game could read the field but never name it — which is what +/// without this a game could read the field but never name it, which is what /// building a [GameContentContext] in a widget test requires. export 'core/api/server_clock.dart' show ServerClock; export 'core/config/app_config.dart' diff --git a/lib/features/auth/data/models/auth_user.dart b/lib/features/auth/data/models/auth_user.dart index 5ee27d5..57bf327 100644 --- a/lib/features/auth/data/models/auth_user.dart +++ b/lib/features/auth/data/models/auth_user.dart @@ -2,7 +2,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'auth_user.freezed.dart'; -/// The signed-in user as the app sees it — the backend-agnostic twin of the +/// The signed-in user as the app sees it: the backend-agnostic twin of the /// auth provider's session user. /// /// Carries only the fields the app actually consumes. Identity details @@ -16,7 +16,7 @@ abstract class AuthUser with _$AuthUser { /// Auth lifecycle events surfaced by [AuthService.authStateChanges]. /// /// Derived by diffing consecutive sessions rather than reported by the provider, -/// so these are exactly the transitions the app acts on — no provider-specific +/// so these are exactly the transitions the app acts on, with no provider-specific /// events it has no behaviour for. enum AuthEvent { /// A session began, was restored on launch, or switched to another account. @@ -27,7 +27,7 @@ enum AuthEvent { /// The session ended. signedOut, - /// Same user, changed session — a token refresh, or the guest upgrade that + /// Same user, changed session: a token refresh, or the guest upgrade that /// keeps the uid and flips [AuthUser.isAnonymous]. State that depends on /// guest-ness must re-read on this. userUpdated, diff --git a/lib/features/auth/providers/auth_providers.dart b/lib/features/auth/providers/auth_providers.dart index 271a5ce..5c98203 100644 --- a/lib/features/auth/providers/auth_providers.dart +++ b/lib/features/auth/providers/auth_providers.dart @@ -43,7 +43,7 @@ AuthGateway authService(Ref ref) { /// /// Derived from the auth state stream. Because the value is a [String], /// Riverpod's `==` check means dependents only rebuild when the id actually -/// changes — token refreshes re-emit the same id and propagate no further. +/// changes; token refreshes re-emit the same id and propagate no further. @riverpod String? currentUserId(Ref ref) => ref.watch(authStateChangesProvider).value?.user?.id; @@ -68,7 +68,7 @@ Stream authStateChanges(Ref ref) { /// Whether the current session is an anonymous (guest) session. /// /// Watches the auth state stream (not [currentUserId]) so it re-evaluates on -/// `userUpdated` events — the id is unchanged when a guest upgrades, but the +/// `userUpdated` events: the id is unchanged when a guest upgrades, but the /// `isAnonymous` claim flips to false. UI gates (social, rated games, upgrade /// nudge) watch this; `==` on the bool keeps unrelated token refreshes inert. @riverpod diff --git a/lib/features/auth/providers/auth_providers.g.dart b/lib/features/auth/providers/auth_providers.g.dart index 0e69d09..78bd912 100644 --- a/lib/features/auth/providers/auth_providers.g.dart +++ b/lib/features/auth/providers/auth_providers.g.dart @@ -58,7 +58,7 @@ String _$authServiceHash() => r'15d3682203a7cc48a19f41b4fcf3edc5740b4943'; /// /// Derived from the auth state stream. Because the value is a [String], /// Riverpod's `==` check means dependents only rebuild when the id actually -/// changes — token refreshes re-emit the same id and propagate no further. +/// changes; token refreshes re-emit the same id and propagate no further. @ProviderFor(currentUserId) final currentUserIdProvider = CurrentUserIdProvider._(); @@ -67,7 +67,7 @@ final currentUserIdProvider = CurrentUserIdProvider._(); /// /// Derived from the auth state stream. Because the value is a [String], /// Riverpod's `==` check means dependents only rebuild when the id actually -/// changes — token refreshes re-emit the same id and propagate no further. +/// changes; token refreshes re-emit the same id and propagate no further. final class CurrentUserIdProvider extends $FunctionalProvider @@ -76,7 +76,7 @@ final class CurrentUserIdProvider /// /// Derived from the auth state stream. Because the value is a [String], /// Riverpod's `==` check means dependents only rebuild when the id actually - /// changes — token refreshes re-emit the same id and propagate no further. + /// changes; token refreshes re-emit the same id and propagate no further. CurrentUserIdProvider._() : super( from: null, @@ -214,7 +214,7 @@ String _$authStateChangesHash() => r'ee83d39078d728a4e5a0431d43cb29b07bb412f4'; /// Whether the current session is an anonymous (guest) session. /// /// Watches the auth state stream (not [currentUserId]) so it re-evaluates on -/// `userUpdated` events — the id is unchanged when a guest upgrades, but the +/// `userUpdated` events: the id is unchanged when a guest upgrades, but the /// `isAnonymous` claim flips to false. UI gates (social, rated games, upgrade /// nudge) watch this; `==` on the bool keeps unrelated token refreshes inert. @@ -224,7 +224,7 @@ final isAnonymousProvider = IsAnonymousProvider._(); /// Whether the current session is an anonymous (guest) session. /// /// Watches the auth state stream (not [currentUserId]) so it re-evaluates on -/// `userUpdated` events — the id is unchanged when a guest upgrades, but the +/// `userUpdated` events: the id is unchanged when a guest upgrades, but the /// `isAnonymous` claim flips to false. UI gates (social, rated games, upgrade /// nudge) watch this; `==` on the bool keeps unrelated token refreshes inert. @@ -233,7 +233,7 @@ final class IsAnonymousProvider extends $FunctionalProvider /// Whether the current session is an anonymous (guest) session. /// /// Watches the auth state stream (not [currentUserId]) so it re-evaluates on - /// `userUpdated` events — the id is unchanged when a guest upgrades, but the + /// `userUpdated` events: the id is unchanged when a guest upgrades, but the /// `isAnonymous` claim flips to false. UI gates (social, rated games, upgrade /// nudge) watch this; `==` on the bool keeps unrelated token refreshes inert. IsAnonymousProvider._() diff --git a/lib/features/game/presentation/extensions/game_ui.dart b/lib/features/game/presentation/extensions/game_ui.dart index 3f6350c..3bf0473 100644 --- a/lib/features/game/presentation/extensions/game_ui.dart +++ b/lib/features/game/presentation/extensions/game_ui.dart @@ -1,7 +1,7 @@ import 'package:eigen_api/eigen_api.dart'; import 'package:flutter/material.dart'; -/// UI helpers for [GameStatus] — color and icon mappings. +/// UI helpers for [GameStatus]: color and icon mappings. extension GameStatusUI on GameStatus { /// Returns the color associated with this status from [colorScheme]. Color color(ColorScheme colorScheme) => switch (this) { @@ -24,7 +24,7 @@ extension GameStatusUI on GameStatus { }; } -/// UI helpers for [OutcomeResultEnum] — icon, color, and label mappings. +/// UI helpers for [OutcomeResultEnum]: icon, color, and label mappings. /// /// Defined on the nullable type so the null case (aborted game, no outcome /// row written) can be handled uniformly alongside real results. diff --git a/lib/features/game/presentation/screens/game_screen.dart b/lib/features/game/presentation/screens/game_screen.dart index 4847df8..b410f81 100644 --- a/lib/features/game/presentation/screens/game_screen.dart +++ b/lib/features/game/presentation/screens/game_screen.dart @@ -174,7 +174,7 @@ class _GameScreenState extends ConsumerState { AsyncValue next, ) { if (!mounted) return; - // Use AsyncData pattern — next.value returns previous data even during + // Use AsyncData pattern: next.value returns previous data even during // AsyncLoading/AsyncError in Riverpod 3.x, which would cause premature // action-pending reset during pull-to-refresh or app resume. switch (next) { @@ -191,7 +191,7 @@ class _GameScreenState extends ConsumerState { if (_pendingAction == _PendingAction.submittingAction) { setState(() => _pendingAction = null); } - // One snackbar per error episode, terminal or not — Riverpod's retry + // One snackbar per error episode, terminal or not, since Riverpod's retry // cycle would otherwise re-show it on every failed attempt. The flag // resets on the next successful observation. if (_errorSnackBarShown) return; @@ -230,7 +230,7 @@ class _GameScreenState extends ConsumerState { void _onConnectivityChange(bool? wasOffline, bool isOffline) { if (wasOffline != true || isOffline) return; - // Network restored — re-subscribe immediately for the fast + // Network restored, so re-subscribe immediately for the fast // offline→online transition. _invalidateStreams(); } @@ -289,7 +289,7 @@ class _GameScreenState extends ConsumerState { child: SafeArea( child: switch (gameAsync) { // value is non-null for AsyncData and for AsyncError with - // stale data — keep showing the game while the banner + // stale data: keep showing the game while the banner // communicates the reconnecting state. _ when gameAsync.value != null => RefreshIndicator( onRefresh: _onRefresh, @@ -358,7 +358,7 @@ class _GameScreenState extends ConsumerState { } /// Submits an action and reports the outcome to the game's content widget - /// (via [GameContentContext.onAction]). Error display stays here — the game + /// (via [GameContentContext.onAction]). Error display stays here; the game /// only uses the [ActionSubmitResult] to manage optimistic rendering. /// /// An [EngineException] is a definitive server verdict → [rejected]; any @@ -476,7 +476,7 @@ class _GameScreenState extends ConsumerState { /// All branches return a [CustomScrollView] with [AlwaysScrollableScrollPhysics] /// so the parent [RefreshIndicator] can detect a pull in every game state. /// -/// A pure routing widget — provider subscriptions are owned by the leaf +/// A pure routing widget; provider subscriptions are owned by the leaf /// widgets ([_PreGameContent], [_ActiveGameContent]) so observation updates /// only rebuild the subtree that needs them. class _GameBody extends StatelessWidget { diff --git a/lib/features/game/presentation/screens/game_screen_active.dart b/lib/features/game/presentation/screens/game_screen_active.dart index bc4c959..a5343ff 100644 --- a/lib/features/game/presentation/screens/game_screen_active.dart +++ b/lib/features/game/presentation/screens/game_screen_active.dart @@ -49,7 +49,7 @@ class _ActiveGameContent extends ConsumerWidget { final gamePlayers = gamePlayersAsync.value!; // A non-participant has no observation rows and no per-seat frame stream, - // so the live board can never render for them — it would spin forever. + // so the live board can never render for them; it would spin forever. // Offer the replay (finished) or a wait message (active) instead. final mySeat = gamePlayers.mySeat; if (mySeat is! Seated) { @@ -64,7 +64,7 @@ class _ActiveGameContent extends ConsumerWidget { // Outcomes are fetched once when the game finishes (invalidated by the // gameRosterProvider listener in _GameScreenState). Empty list for active - // games — no outcome rows exist yet. + // games, where no outcome rows exist yet. final outcomes = ref .watch(gameOutcomesProvider(gameId: game.id)) @@ -193,7 +193,7 @@ class _NonParticipantContent extends StatelessWidget { Text( isFinished ? 'This game has finished.' - : 'Game in progress — the replay will be available when it ' + : 'Game in progress. The replay will be available when it ' 'finishes.', style: textTheme.titleMedium, textAlign: TextAlign.center, diff --git a/lib/features/game/presentation/screens/game_screen_pre_game.dart b/lib/features/game/presentation/screens/game_screen_pre_game.dart index 283d197..a24dde4 100644 --- a/lib/features/game/presentation/screens/game_screen_pre_game.dart +++ b/lib/features/game/presentation/screens/game_screen_pre_game.dart @@ -2,7 +2,7 @@ part of 'game_screen.dart'; /// Waiting room shown for [GameStatus.waiting] and [GameStatus.ready]. /// -/// Does not require an observation — game_states and observations are only +/// Does not require an observation; game_states and observations are only /// created at [GameStatus.active] transition. class _PreGameContent extends ConsumerWidget { const _PreGameContent({ @@ -235,7 +235,7 @@ class _AddBotDialog extends ConsumerStatefulWidget { /// The game's raw config payload, as it comes off the wire. final Object config; - /// The game's `schemaVersion` — seating gates run against *this* game's + /// The game's `schemaVersion`; seating gates run against *this* game's /// rules unit, not the latest. final int schemaVersion; @@ -376,7 +376,7 @@ class _ParticipantList extends StatelessWidget { radius: 20, isBot: isBot, // A deleted seat's info is a synthetic placeholder whose id - // resolves to no player — never open the profile sheet on it. + // resolves to no player; never open the profile sheet on it. onTap: gp.isDeleted ? null : () => PlayerProfileSheet.show( diff --git a/lib/features/game/presentation/screens/game_screen_states.dart b/lib/features/game/presentation/screens/game_screen_states.dart index e05b025..ce9075f 100644 --- a/lib/features/game/presentation/screens/game_screen_states.dart +++ b/lib/features/game/presentation/screens/game_screen_states.dart @@ -1,6 +1,6 @@ part of 'game_screen.dart'; -/// Shown for aborted games — cancelled by the host before starting, or +/// Shown for aborted games: cancelled by the host before starting, or /// closed by the idle-cleanup job (which also aborts long-abandoned /// untimed active games), so the copy stays neutral about timing. class _AbortedContent extends StatelessWidget { @@ -120,7 +120,7 @@ class _UpdateRequiredContent extends StatelessWidget { /// state (waiting, ready, or active). /// /// Disconnected means device-level offline, the observation stream is in -/// [AsyncError], or the game stream itself is in [AsyncError] — covering +/// [AsyncError], or the game stream itself is in [AsyncError], covering /// transient socket blips where [isOfflineProvider] stays false. Uses /// [AsyncValue.value] to read stale status during error states. Isolated as a /// [ConsumerWidget] leaf so changes don't rebuild the entire game tree. diff --git a/lib/features/game/presentation/screens/history_screen.dart b/lib/features/game/presentation/screens/history_screen.dart index 03c81fe..fb1cc86 100644 --- a/lib/features/game/presentation/screens/history_screen.dart +++ b/lib/features/game/presentation/screens/history_screen.dart @@ -27,7 +27,7 @@ RatingDelta? _myRatingChange(GameSummary game, String? myUserId) { return game.ratings?.where((r) => r.identity.userId == myUserId).firstOrNull; } -/// The caller's own result in a finished game, or null when there is none — +/// The caller's own result in a finished game, or null when there is none: /// an aborted game writes no outcomes. OutcomeResultEnum? _myResult(GameSummary game, String? myUserId) { final seat = game.participants diff --git a/lib/features/game/presentation/screens/lobby_screen.dart b/lib/features/game/presentation/screens/lobby_screen.dart index ceb274e..ac0bbdd 100644 --- a/lib/features/game/presentation/screens/lobby_screen.dart +++ b/lib/features/game/presentation/screens/lobby_screen.dart @@ -29,7 +29,7 @@ class _LobbyScreenState extends ConsumerState with SingleTickerProviderStateMixin { // Guests cannot have friends. The Friends tab stays visible but disabled // (greyed, with a locked sign-in panel as its content) so guests still see - // the feature exists — and app_friends_games is never called for them. + // the feature exists, and app_friends_games is never called for them. // Decided once at init: a guest→permanent conversion is a full auth-state // change that re-navigates into a fresh lobby. late final bool _isAnonymous; @@ -89,7 +89,7 @@ class _LobbyScreenState extends ConsumerState } /// Locked content shown to guests in place of the friends lobby. The Friends -/// tab is visible but non-functional until they create an account — mirroring +/// tab is visible but non-functional until they create an account, mirroring /// the disabled "Sign up to play rated" treatment on rated game cards. class _FriendsLockedView extends StatelessWidget { const _FriendsLockedView(); diff --git a/lib/features/game/presentation/screens/replay_screen.dart b/lib/features/game/presentation/screens/replay_screen.dart index 3259828..f5a4263 100644 --- a/lib/features/game/presentation/screens/replay_screen.dart +++ b/lib/features/game/presentation/screens/replay_screen.dart @@ -18,7 +18,7 @@ import 'package:eigen_api/eigen_api.dart'; /// scrubbing or stepping back snaps. /// /// Serves both a participant reviewing their own game and a non-participant -/// replaying a public one — the difference is only which projection the +/// replaying a public one; the difference is only which projection the /// `game/replay` route returned, which this screen does not need to know. class ReplayScreen extends ConsumerWidget { const ReplayScreen({super.key, required this.gameId}); diff --git a/lib/features/game/presentation/widgets/budget_clock.dart b/lib/features/game/presentation/widgets/budget_clock.dart index ae8dec5..5386512 100644 --- a/lib/features/game/presentation/widgets/budget_clock.dart +++ b/lib/features/game/presentation/widgets/budget_clock.dart @@ -14,7 +14,7 @@ import 'package:eigen_flutter/core/api/engine_api_providers.dart'; /// Any bank below 60 seconds turns [ColorScheme.error]. /// /// The clock stays truthful (unlike the per-action countdown, it is not pulled -/// earlier by a soft margin — that would make a chess-style clock snap back up +/// earlier by a soft margin; that would make a chess-style clock snap back up /// on submit). Instead, the local player's own cell shows a "Submit!" cue once /// their remaining bank drops into the final-headroom zone /// ([softDeadlineMarginFor]), nudging them to commit before latency carries the @@ -41,7 +41,7 @@ class BudgetClock extends ConsumerWidget { /// Indices of currently active (draining) players. final List pendingPlayers; - /// This client's player index — their cell is labelled "You". + /// This client's player index; their cell is labelled "You". final int myPlayerIndex; @override @@ -106,7 +106,7 @@ class _ClockCell extends StatelessWidget { final bool isActive; /// True for the local player's active cell once it drops into the final - /// headroom before the deadline — shows a "Submit!" nudge. + /// headroom before the deadline, shows a "Submit!" nudge. final bool isSubmitZone; final ColorScheme colorScheme; diff --git a/lib/features/game/presentation/widgets/new_game_dialog.dart b/lib/features/game/presentation/widgets/new_game_dialog.dart index ad40b17..392ef6f 100644 --- a/lib/features/game/presentation/widgets/new_game_dialog.dart +++ b/lib/features/game/presentation/widgets/new_game_dialog.dart @@ -15,7 +15,7 @@ import 'package:eigen_api/eigen_api.dart'; /// /// Reads [GameModule.creationSpec] to render only the controls valid for the /// current game type. Timing options come from [GameCreationSpec.timingConfigs] -/// — each map key is a [SegmentedButton] label; each value declares the +/// Each map key is a [SegmentedButton] label; each value declares the /// valid range and optional quick-pick presets. class NewGameDialog extends ConsumerStatefulWidget { const NewGameDialog({super.key}); @@ -32,7 +32,7 @@ class _NewGameDialogState extends ConsumerState { // there is no null window before the first interaction. late ResolvedTiming _timing; - // Plain fields — never displayed, only consumed at submit. + // Plain fields: never displayed, only consumed at submit. Map _gameConfig = {}; late int _minPlayers; late int _maxPlayers; @@ -218,7 +218,7 @@ class _NewGameDialogState extends ConsumerState { setState(() => _isLoading = true); try { // `rated` is a concrete assertion validated by the server (rejected on - // mismatch, not coerced), so compute the eligibility-gated value here — + // mismatch, not coerced), so compute the eligibility-gated value here, // the same Dart twin of GameRules.ratingPool used to show the toggle in // build. final pool = _module.latestRules.ratingPool( diff --git a/lib/features/game/presentation/widgets/play_vs_bot_dialog.dart b/lib/features/game/presentation/widgets/play_vs_bot_dialog.dart index a93bce7..f2624e8 100644 --- a/lib/features/game/presentation/widgets/play_vs_bot_dialog.dart +++ b/lib/features/game/presentation/widgets/play_vs_bot_dialog.dart @@ -9,16 +9,16 @@ import 'package:eigen_flutter/features/game/presentation/widgets/timing_selector import 'package:eigen_flutter/features/game/providers/game_providers.dart'; /// Solo-game picker (the "New Solo Game" FAB): choose an opponent for each bot -/// seat and start a solo game (you + bots) immediately — no waiting room. +/// seat and start a solo game (you + bots) immediately, with no waiting room. /// -/// Fully generic — nothing branches on the game's identity. The opponent-seat +/// Fully generic: nothing branches on the game's identity. The opponent-seat /// count is `playersForConfig(config) - 1`: the game's own `buildCreationConfig` /// carries the player count for variable-count games, and a generic count /// selector appears only when the game exposes a true range (`min < max`). /// /// Every seat defaults to the first available bot; the user overrides only the /// seats they care about. **Timing selects the bot class**: an *untimed* game -/// offers **local** bots (driven by the present human's client — no deadline +/// offers **local** bots (driven by the present human's client, so no deadline /// backstop needed); a *timed* game offers **server** bots (their endpoint may be /// unreachable, so a deadline is required) and never to guests. The server /// enforces this partition; switching timing re-derives the usable list, and any @@ -41,7 +41,7 @@ class _PlayVsBotDialogState extends ConsumerState { late int _totalPlayers; // Resolved timing from the shared TimingSelector; seeded with its default. late ResolvedTiming _timing; - // The mode the picker opens in — computed from the (warm) bot catalog so it has + // The mode the picker opens in, computed from the (warm) bot catalog so it has // opponents. Held to pass the same key to TimingSelector and its seed. late final String? _initialTimingKey; // Sparse per-seat overrides: only seats the user explicitly changed. Every @@ -118,7 +118,7 @@ class _PlayVsBotDialogState extends ConsumerState { if (b.schemaVersion > module.latestSchemaVersion) return false; // Config gate: the game's own botSeatable rule decides which bots support the // chosen config (the Dart twin of the server's GameRules.botSeatable, which - // enforces it at seating). Local UX only — no network round-trip per config. + // enforces it at seating). Local UX only, with no network round-trip per config. if (!rules.botSeatable( BotSeatableArgs( gameConfig: _config, @@ -187,7 +187,7 @@ class _PlayVsBotDialogState extends ConsumerState { onChanged: (timing) => setState(() => _timing = timing), ), const SizedBox(height: 16), - // Opponent selectors — filtered locally by module.botSeatable, so + // Opponent selectors, filtered locally by module.botSeatable, so // they update instantly as the config changes (no refetch). if (usable.isEmpty) const Text('No AI opponents are available for this game yet.') diff --git a/lib/features/game/presentation/widgets/timer_builders.dart b/lib/features/game/presentation/widgets/timer_builders.dart index c3e882f..db8d6ce 100644 --- a/lib/features/game/presentation/widgets/timer_builders.dart +++ b/lib/features/game/presentation/widgets/timer_builders.dart @@ -10,12 +10,12 @@ import 'package:flutter/material.dart'; /// computation layer beneath any styled countdown widget. /// /// Set [isPaused] to freeze the displayed value without cancelling the -/// underlying timer — the frozen duration resumes from the correct wall-clock +/// underlying timer; the frozen duration resumes from the correct wall-clock /// value as soon as [isPaused] becomes false again. /// /// [softMargin] shifts the *displayed* zero point earlier than [deadline] so /// the player is nudged to submit before the true server deadline (absorbing -/// network latency). It is display-only — the server stays authoritative and +/// network latency). It is display-only; the server stays authoritative and /// the expiry trigger uses the true deadline. Defaults to no margin. class TurnTimerBuilder extends StatefulWidget { const TurnTimerBuilder({ @@ -111,7 +111,7 @@ class _TurnTimerBuilderState extends State { /// receive their static [playerTimes] value. Use this as the computation layer /// beneath any styled clock cell. /// -/// Only one bank drains at a time — budget mode permits a single pending seat, +/// Only one bank drains at a time: budget mode permits a single pending seat, /// so the turn deadline and the acting seat's remaining bank are the same /// quantity. That is why the deadline alone is enough here, with no separate /// turn-start to track and drift against. @@ -159,7 +159,7 @@ class _PlayerTimerBuilderState extends State { void initState() { super.initState(); _timer = Timer.periodic(const Duration(seconds: 1), (_) { - // Only rebuild when active and not paused — inactive players return a + // Only rebuild when active and not paused; inactive players return a // static value so rebuilding every second is wasteful. Parent rebuilds // (new observation) still update inactive cells immediately via the // widget getters. diff --git a/lib/features/game/presentation/widgets/timing_selector.dart b/lib/features/game/presentation/widgets/timing_selector.dart index adc2f95..9f206b6 100644 --- a/lib/features/game/presentation/widgets/timing_selector.dart +++ b/lib/features/game/presentation/widgets/timing_selector.dart @@ -30,11 +30,11 @@ class TimingSelector extends StatefulWidget { /// The mode to select initially (a key of [configs]). `null` or an unknown key /// falls back to the first declared mode. Lets a caller that knows more than - /// this widget — e.g. the solo picker, which inspects the bot catalog — open it + /// this widget (e.g. the solo picker, which inspects the bot catalog) open it /// in a mode that actually has opponents, without coupling this widget to bots. final String? initialKey; - /// The resolved timing for the initial selection — matching what the widget + /// The resolved timing for the initial selection, matching what the widget /// would emit before any interaction, so a parent can seed its state without a /// null window. Pass the same [initialKey] used on the widget. static ResolvedTiming initial( @@ -86,7 +86,7 @@ class _TimingSelectorState extends State { final selected = configs[_key]!; // Nothing to choose when the game offers only untimed play (the default - // spec) — don't render a lone "Timing" header with no control under it. + // spec); don't render a lone "Timing" header with no control under it. if (configs.length == 1 && selected is UntimedConfig) { return const SizedBox.shrink(); } @@ -290,7 +290,7 @@ class _BudgetPanel extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Preset pairs — each chip sets both sliders. + // Preset pairs: each chip sets both sliders. if (config.presets.isNotEmpty) ...[ Wrap( spacing: 8, @@ -338,7 +338,7 @@ class _BudgetPanel extends StatelessWidget { ), ), - // Increment slider — only when the range is non-trivial. + // Increment slider, only when the range is non-trivial. if (hasIncrementRange) ...[ const SizedBox(height: 8), Text('Increment', style: textTheme.labelMedium), @@ -359,7 +359,7 @@ class _BudgetPanel extends StatelessWidget { ), ), ] else if (config.minIncrementSeconds > 0) ...[ - // Fixed increment — show as a label, no slider needed. + // Fixed increment: show as a label, no slider needed. const SizedBox(height: 4), Center( child: Text( diff --git a/lib/features/game/presentation/widgets/turn_countdown.dart b/lib/features/game/presentation/widgets/turn_countdown.dart index d25ba96..ac7a091 100644 --- a/lib/features/game/presentation/widgets/turn_countdown.dart +++ b/lib/features/game/presentation/widgets/turn_countdown.dart @@ -16,17 +16,17 @@ import 'package:eigen_flutter/features/game/presentation/widgets/timer_builders. /// player knows the timer is not draining locally (though the server clock /// continues). /// -/// Provide [style] to override the default [TextTheme.bodySmall] — useful +/// Provide [style] to override the default [TextTheme.bodySmall], useful /// when the countdown should be larger (e.g. inside the game screen). /// /// [deadline] is an absolute server timestamp in epoch milliseconds, converted -/// here against [ServerClock] — a device with a skewed clock would otherwise +/// here against [ServerClock]; a device with a skewed clock would otherwise /// show a countdown that disagrees with when the turn actually expires. /// /// Provide [windowMillis] (how long the turn was when it began) to enable the /// soft-deadline margin: the countdown reaches zero slightly before the true /// deadline so an on-time submit survives network latency. The margin is capped -/// to a fraction of the window so a short one — a three-second reaction window — +/// to a fraction of the window so a short one, a three-second reaction window, /// is not swallowed whole. Omit it (e.g. on at-a-glance home cards) for a /// truthful, unmargined countdown. /// diff --git a/lib/features/game/providers/replay_controller.dart b/lib/features/game/providers/replay_controller.dart index f5c5a89..a90b607 100644 --- a/lib/features/game/providers/replay_controller.dart +++ b/lib/features/game/providers/replay_controller.dart @@ -58,7 +58,7 @@ class ReplayCursor extends _$ReplayCursor { /// /// Memoized per `(gameId, index)`: [GameRules.parseObservation] runs once per /// frame no matter how often the user steps back and forth across it. Timing is -/// always empty — a replay has no live clocks. Returns null until the frames +/// always empty, since a replay has no live clocks. Returns null until the frames /// and the version unit have both loaded, or for an out-of-range index. @riverpod GameFrame? replayFrameAt( diff --git a/lib/features/game/providers/replay_controller.g.dart b/lib/features/game/providers/replay_controller.g.dart index f0afb7c..04cfbb1 100644 --- a/lib/features/game/providers/replay_controller.g.dart +++ b/lib/features/game/providers/replay_controller.g.dart @@ -304,7 +304,7 @@ abstract class _$ReplayCursor extends $Notifier { /// /// Memoized per `(gameId, index)`: [GameRules.parseObservation] runs once per /// frame no matter how often the user steps back and forth across it. Timing is -/// always empty — a replay has no live clocks. Returns null until the frames +/// always empty, since a replay has no live clocks. Returns null until the frames /// and the version unit have both loaded, or for an out-of-range index. @ProviderFor(replayFrameAt) @@ -314,7 +314,7 @@ final replayFrameAtProvider = ReplayFrameAtFamily._(); /// /// Memoized per `(gameId, index)`: [GameRules.parseObservation] runs once per /// frame no matter how often the user steps back and forth across it. Timing is -/// always empty — a replay has no live clocks. Returns null until the frames +/// always empty, since a replay has no live clocks. Returns null until the frames /// and the version unit have both loaded, or for an out-of-range index. final class ReplayFrameAtProvider @@ -324,7 +324,7 @@ final class ReplayFrameAtProvider /// /// Memoized per `(gameId, index)`: [GameRules.parseObservation] runs once per /// frame no matter how often the user steps back and forth across it. Timing is - /// always empty — a replay has no live clocks. Returns null until the frames + /// always empty, since a replay has no live clocks. Returns null until the frames /// and the version unit have both loaded, or for an out-of-range index. ReplayFrameAtProvider._({ required ReplayFrameAtFamily super.from, @@ -383,7 +383,7 @@ String _$replayFrameAtHash() => r'2db71a9d8886942ff93570311cd6c915991702d0'; /// /// Memoized per `(gameId, index)`: [GameRules.parseObservation] runs once per /// frame no matter how often the user steps back and forth across it. Timing is -/// always empty — a replay has no live clocks. Returns null until the frames +/// always empty, since a replay has no live clocks. Returns null until the frames /// and the version unit have both loaded, or for an out-of-range index. final class ReplayFrameAtFamily extends $Family @@ -401,7 +401,7 @@ final class ReplayFrameAtFamily extends $Family /// /// Memoized per `(gameId, index)`: [GameRules.parseObservation] runs once per /// frame no matter how often the user steps back and forth across it. Timing is - /// always empty — a replay has no live clocks. Returns null until the frames + /// always empty, since a replay has no live clocks. Returns null until the frames /// and the version unit have both loaded, or for an out-of-range index. ReplayFrameAtProvider call({required String gameId, required int index}) => diff --git a/lib/features/home/presentation/screens/home_screen.dart b/lib/features/home/presentation/screens/home_screen.dart index 322dd5b..943082a 100644 --- a/lib/features/home/presentation/screens/home_screen.dart +++ b/lib/features/home/presentation/screens/home_screen.dart @@ -123,7 +123,7 @@ class _JoinCodeDialogState extends State<_JoinCodeDialog> { void _join() { final code = _controller.text.trim().toUpperCase(); if (code.length != 6) return; - // Capture the router before popping — this State's context is being + // Capture the router before popping; this State's context is being // removed from the tree once the dialog closes. final router = GoRouter.of(context); Navigator.pop(context); @@ -236,7 +236,7 @@ class _UsernameShimmer extends StatelessWidget { /// /// Uses [CustomScrollView] + [SliverFillRemaining] so the parent /// [RefreshIndicator] can detect a pull gesture even when content is -/// shorter than the viewport — without hardcoding any heights. +/// shorter than the viewport, without hardcoding any heights. class _EmptyState extends StatelessWidget { const _EmptyState({required this.onBrowseLobby, required this.onJoinViaCode}); diff --git a/lib/features/profile/data/profile_repository.dart b/lib/features/profile/data/profile_repository.dart index 473eced..8f88286 100644 --- a/lib/features/profile/data/profile_repository.dart +++ b/lib/features/profile/data/profile_repository.dart @@ -15,7 +15,7 @@ class ProfileRepository { /// public [Player] projection omits. Future getProfile() => engineData(() => _api.getProfile()); - /// Changes the caller's username — the unique, charset-constrained handle. + /// Changes the caller's username: the unique, charset-constrained handle. /// /// Throws an [EngineException] with [ErrorCode.usernameTaken] or /// [ErrorCode.usernameInvalid]; both are field-level form errors rather than @@ -29,7 +29,7 @@ class ProfileRepository { return body.username; } - /// Changes the caller's display name — the free-form label shown beside their + /// Changes the caller's display name: the free-form label shown beside their /// moves. Not unique: two players may share one, which is what the username /// disambiguates. Future updateDisplayName(String displayName) async { @@ -44,7 +44,7 @@ class ProfileRepository { /// Deletes the caller's account and all of its data. /// /// Irreversible. The server forfeits or cancels their live games, deletes the - /// identity provider account, then purges the database — in that order, so a + /// identity provider account, then purges the database, in that order, so a /// failure leaves the account intact and the call retriable. Future deleteAccount() => engineCall(() => _api.deleteAccount()); } diff --git a/lib/features/rating/data/rating_repository.dart b/lib/features/rating/data/rating_repository.dart index 5e8a5a6..c16437a 100644 --- a/lib/features/rating/data/rating_repository.dart +++ b/lib/features/rating/data/rating_repository.dart @@ -25,7 +25,7 @@ class RatingRepository { /// The caller's own ratings. /// - /// Distinct from [getPlayerRatings] only in that it needs no id — the token + /// Distinct from [getPlayerRatings] only in that it needs no id; the token /// identifies the caller. Future> getMyRatings() async { final body = await engineData(() => _me.getMyRatings()); diff --git a/lib/features/rating/presentation/widgets/player_ratings.dart b/lib/features/rating/presentation/widgets/player_ratings.dart index bcaaa71..2b0c9a4 100644 --- a/lib/features/rating/presentation/widgets/player_ratings.dart +++ b/lib/features/rating/presentation/widgets/player_ratings.dart @@ -7,10 +7,10 @@ import 'package:eigen_flutter/features/rating/providers/rating_providers.dart'; /// How [Ratings] renders each pool. enum RatingsLayout { - /// Elevated cards, two to a row — for the roomy profile screen. + /// Elevated cards, two to a row, for the roomy profile screen. cards, - /// Dense `pool … rating` rows — for the compact profile sheet. + /// Dense `pool … rating` rows, for the compact profile sheet. rows, } diff --git a/lib/features/rating/providers/rating_providers.dart b/lib/features/rating/providers/rating_providers.dart index cd7b2e2..ebbff2d 100644 --- a/lib/features/rating/providers/rating_providers.dart +++ b/lib/features/rating/providers/rating_providers.dart @@ -23,7 +23,7 @@ Future> playerRatings(Ref ref, String id) => /// to the profile page always fetches fresh data. @riverpod Future> myRatings(Ref ref) { - // Null during the brief sign-out window before navigation completes — + // Null during the brief sign-out window before navigation completes, // return empty rather than crashing the disposing profile screen. final userId = ref.watch(currentUserIdProvider); if (userId == null) return Future.value(const []); diff --git a/lib/features/settings/presentation/screens/settings_screen.dart b/lib/features/settings/presentation/screens/settings_screen.dart index 0e573e7..98c553e 100644 --- a/lib/features/settings/presentation/screens/settings_screen.dart +++ b/lib/features/settings/presentation/screens/settings_screen.dart @@ -226,8 +226,8 @@ class _DeniedTile extends StatelessWidget { title: const Text('Notifications'), subtitle: Text( kIsWeb - ? 'Blocked — enable notifications in this site’s browser settings' - : 'Disabled — tap to open Settings', + ? 'Blocked: enable notifications in this site’s browser settings' + : 'Disabled: tap to open Settings', ), trailing: kIsWeb ? null : const Icon(Icons.open_in_new, size: 18), onTap: kIsWeb @@ -524,7 +524,7 @@ class _UpgradeAccountCard extends ConsumerWidget { } Future _upgrade(BuildContext context, WidgetRef ref) async { - // Capture before the await — on success this card is removed from the tree + // Capture before the await, since on success this card is removed from the tree // (isAnonymous flips false), but the messenger ancestor survives. final messenger = ScaffoldMessenger.of(context); final colorScheme = Theme.of(context).colorScheme; @@ -537,7 +537,7 @@ class _UpgradeAccountCard extends ConsumerWidget { messenger.showSnackBar( const SnackBar( content: Text( - 'Account created — your games, ratings, and friends are saved.', + 'Account created. Your games, ratings, and friends are saved.', ), ), ); diff --git a/lib/features/social/presentation/widgets/friend_actions.dart b/lib/features/social/presentation/widgets/friend_actions.dart index bf046c0..d3b9be0 100644 --- a/lib/features/social/presentation/widgets/friend_actions.dart +++ b/lib/features/social/presentation/widgets/friend_actions.dart @@ -11,14 +11,14 @@ import 'package:eigen_flutter/features/social/providers/social_providers.dart'; /// /// Returns [SizedBox.shrink] when [playerId] is the current user. /// -/// Self-gates when the viewer is an anonymous guest — the server rejects all +/// Self-gates when the viewer is an anonymous guest, since the server rejects all /// friend writes from guests, so instead of action buttons a guest sees a /// sign-in hint that routes to the account-upgrade flow in settings (or /// nothing, when [compact]). Gating here rather than in each parent keeps /// every embedding correct by construction. /// /// Each button owns its mutation state machine, so this widget only needs -/// to route on [FriendStatus] — no mutation watching or coordination needed. +/// to route on [FriendStatus], with no mutation watching or coordination needed. /// /// [compact] true is suited for search-result list tile trailing (single small /// button). [compact] false (default) is suited for a profile sheet diff --git a/lib/features/social/presentation/widgets/friend_buttons.dart b/lib/features/social/presentation/widgets/friend_buttons.dart index 323a71c..5fd3838 100644 --- a/lib/features/social/presentation/widgets/friend_buttons.dart +++ b/lib/features/social/presentation/widgets/friend_buttons.dart @@ -216,7 +216,7 @@ class RemoveFriendButton extends ConsumerStatefulWidget { class _RemoveFriendButtonState extends ConsumerState { // Guards against the dialog opening twice on rapid double-tap. - // Not UI state — no setState needed. + // Not UI state, so no setState needed. bool _confirming = false; Future _remove() async { diff --git a/lib/features/social/presentation/widgets/player_profile_sheet.dart b/lib/features/social/presentation/widgets/player_profile_sheet.dart index 4123214..5a44301 100644 --- a/lib/features/social/presentation/widgets/player_profile_sheet.dart +++ b/lib/features/social/presentation/widgets/player_profile_sheet.dart @@ -14,8 +14,8 @@ import 'package:eigen_flutter/shared/widgets/player_tags.dart'; /// Modal bottom sheet showing a player's public profile. /// -/// Displays identity, ratings across all pools, and — for registered human -/// players — friendship status with actions to add, accept, decline, or +/// Displays identity, ratings across all pools, and, for registered human +/// players, friendship status with actions to add, accept, decline, or /// remove. Bots and anonymous guests show identity and ratings only: bots /// have no social features, and guests cannot be friended (server-enforced), /// so the sheet tags them and omits the section. @@ -333,7 +333,7 @@ class _RecentGameRow extends StatelessWidget { ), trailing: Icon(Icons.chevron_right, color: colorScheme.onSurfaceVariant), onTap: () { - // Capture the router before closing the sheet — the row's context is + // Capture the router before closing the sheet; the row's context is // torn down by the pop. final router = GoRouter.of(context); Navigator.of(context).pop(); diff --git a/lib/shared/data/device_installation_repository.dart b/lib/shared/data/device_installation_repository.dart index 1ff7e17..cce6d2f 100644 --- a/lib/shared/data/device_installation_repository.dart +++ b/lib/shared/data/device_installation_repository.dart @@ -2,7 +2,7 @@ import 'package:eigen_api/eigen_api.dart'; import 'package:eigen_flutter/core/api/engine_call.dart'; import 'package:flutter/foundation.dart'; -/// The current user's push registrations — one per Firebase installation id. +/// The current user's push registrations: one per Firebase installation id. /// /// Pushes are targeted at the FID, so the server needs the FID for every /// install the user is signed in on. The notification service owns *when* to @@ -14,7 +14,7 @@ class DeviceInstallationRepository { /// Registers (or refreshes) this install for the signed-in user. /// - /// Idempotent — re-registering the same FID is the normal case on every + /// Idempotent: re-registering the same FID is the normal case on every /// launch. The platform is a property of the running build rather than of the /// call, so it is resolved here and callers never name it. Future upsert({required String fid}) => engineCall( @@ -41,7 +41,7 @@ class DeviceInstallationRepository { /// Drops this install's registration so the server stops targeting it. /// - /// Idempotent — unregistering an FID the server never knew about succeeds. + /// Idempotent: unregistering an FID the server never knew about succeeds. Future delete({required String fid}) => engineCall(() => _api.unregisterDevice(fid: fid)); } diff --git a/lib/shared/data/player_batch_loader.dart b/lib/shared/data/player_batch_loader.dart index 870ada2..45c491a 100644 --- a/lib/shared/data/player_batch_loader.dart +++ b/lib/shared/data/player_batch_loader.dart @@ -7,7 +7,7 @@ import 'package:eigen_flutter/shared/data/player_repository.dart'; /// /// The batch endpoint `players?ids=` is the decided alternative to /// denormalising identity onto game rows, but the app reads identity one id at -/// a time through a per-id cache ([PlayerInfoCache]) — a lobby of N games, a +/// a time through a per-id cache ([PlayerInfoCache]): a lobby of N games, a /// roster of N seats. Left alone that turns one screen into N requests to a /// *batch* endpoint, the exact N+1 the endpoint exists to avoid. /// @@ -18,7 +18,7 @@ import 'package:eigen_flutter/shared/data/player_repository.dart'; /// /// The window is a few milliseconds rather than zero on purpose. A zero-delay /// timer would coalesce a build only because [PlayerInfoCache.build] happens -/// to call [load] synchronously, before its first `await` — an invisible +/// to call [load] synchronously, before its first `await`, is an invisible /// coupling that a later refactor (an `await` slipped in ahead of the load) /// would break silently, degrading back to one request per id with no error. /// A small real window makes coalescing robust regardless of how the callers @@ -37,7 +37,7 @@ class PlayerBatchLoader { /// Fetches the given ids in one request. Ids that match nothing are simply /// absent from the result (a purged account is a normal outcome, not an - /// error) — the loader maps that absence to [PlayerNotFoundException] for + /// error); the loader maps that absence to [PlayerNotFoundException] for /// the specific waiter, exactly as a single lookup would. final Future> Function(List ids) _fetch; @@ -53,7 +53,7 @@ class PlayerBatchLoader { /// Resolves one player, batched with every other [load] in the same window. /// /// Throws [PlayerNotFoundException] if the id matches no player, or the - /// underlying transport error if the batch request fails — the same + /// underlying transport error if the batch request fails: the same /// outcomes a direct single lookup produces, so the cache above is unaware /// it was batched. Future load(String id) { diff --git a/lib/shared/data/player_repository.dart b/lib/shared/data/player_repository.dart index 6d74815..7d8d34f 100644 --- a/lib/shared/data/player_repository.dart +++ b/lib/shared/data/player_repository.dart @@ -16,7 +16,7 @@ class PlayerNotFoundException implements Exception { String toString() => 'No player found for id: $playerId'; } -/// Fetches public player identities — humans and bots alike. +/// Fetches public player identities, humans and bots alike. /// /// The batch endpoint is the decided alternative to denormalising identity onto /// game rows: a caller collects the ids it needs and resolves them in one @@ -24,10 +24,10 @@ class PlayerNotFoundException implements Exception { /// /// This is the batch itself; single-id resolution is [PlayerBatchLoader]'s job, /// which coalesces per-id cache misses back into one call here. Nothing calls a -/// per-id method on this repository — that would be the N+1 the endpoint exists +/// per-id method on this repository; that would be the N+1 the endpoint exists /// to avoid. /// -/// Everything returned is public-safe — no email, no account state. +/// Everything returned is public-safe: no email, no account state. class PlayerRepository { PlayerRepository(this._api); @@ -36,7 +36,7 @@ class PlayerRepository { /// Public identities for a batch of ids. /// /// Ids that match nothing are simply absent, so the result may be shorter - /// than [ids] — a purged account is a normal outcome here, not an error. An + /// than [ids]; a purged account is a normal outcome here, not an error. An /// empty [ids] resolves without a request. Future> getPlayers(List ids) async { if (ids.isEmpty) return const []; diff --git a/lib/shared/providers/player_providers.dart b/lib/shared/providers/player_providers.dart index 3fff13f..9647164 100644 --- a/lib/shared/providers/player_providers.dart +++ b/lib/shared/providers/player_providers.dart @@ -32,12 +32,12 @@ PlayerBatchLoader playerBatchLoader(Ref ref) { /// Globally cached public player identity by ID. /// -/// Works for both human users and bots — the batch endpoint covers both. +/// Works for both human users and bots; the batch endpoint covers both. /// `keepAlive: true` keeps the result in memory for the session lifetime. /// Native apps also restore it from the local API cache before the network /// response arrives. Web fetches fresh data after a browser reload. /// -/// Player identity is public data — the cache is never cleared on sign-out. +/// Player identity is public data, so the cache is never cleared on sign-out. /// Bump [StorageOptions.destroyKey] if [Player]'s JSON schema changes. @Riverpod(keepAlive: true) @JsonPersist() diff --git a/lib/shared/providers/player_providers.g.dart b/lib/shared/providers/player_providers.g.dart index 83f266c..b0b9da0 100644 --- a/lib/shared/providers/player_providers.g.dart +++ b/lib/shared/providers/player_providers.g.dart @@ -128,12 +128,12 @@ String _$playerBatchLoaderHash() => r'b304409a3f6b309cde08d35a10fd8138c53ce989'; /// Globally cached public player identity by ID. /// -/// Works for both human users and bots — the batch endpoint covers both. +/// Works for both human users and bots; the batch endpoint covers both. /// `keepAlive: true` keeps the result in memory for the session lifetime. /// Native apps also restore it from the local API cache before the network /// response arrives. Web fetches fresh data after a browser reload. /// -/// Player identity is public data — the cache is never cleared on sign-out. +/// Player identity is public data, so the cache is never cleared on sign-out. /// Bump [StorageOptions.destroyKey] if [Player]'s JSON schema changes. @ProviderFor(PlayerInfoCache) @@ -142,24 +142,24 @@ final playerInfoCacheProvider = PlayerInfoCacheFamily._(); /// Globally cached public player identity by ID. /// -/// Works for both human users and bots — the batch endpoint covers both. +/// Works for both human users and bots; the batch endpoint covers both. /// `keepAlive: true` keeps the result in memory for the session lifetime. /// Native apps also restore it from the local API cache before the network /// response arrives. Web fetches fresh data after a browser reload. /// -/// Player identity is public data — the cache is never cleared on sign-out. +/// Player identity is public data, so the cache is never cleared on sign-out. /// Bump [StorageOptions.destroyKey] if [Player]'s JSON schema changes. @JsonPersist() final class PlayerInfoCacheProvider extends $AsyncNotifierProvider { /// Globally cached public player identity by ID. /// - /// Works for both human users and bots — the batch endpoint covers both. + /// Works for both human users and bots; the batch endpoint covers both. /// `keepAlive: true` keeps the result in memory for the session lifetime. /// Native apps also restore it from the local API cache before the network /// response arrives. Web fetches fresh data after a browser reload. /// - /// Player identity is public data — the cache is never cleared on sign-out. + /// Player identity is public data, so the cache is never cleared on sign-out. /// Bump [StorageOptions.destroyKey] if [Player]'s JSON schema changes. PlayerInfoCacheProvider._({ required PlayerInfoCacheFamily super.from, @@ -201,12 +201,12 @@ String _$playerInfoCacheHash() => r'780b668d15edf866ab1ca930436510fe003197c8'; /// Globally cached public player identity by ID. /// -/// Works for both human users and bots — the batch endpoint covers both. +/// Works for both human users and bots; the batch endpoint covers both. /// `keepAlive: true` keeps the result in memory for the session lifetime. /// Native apps also restore it from the local API cache before the network /// response arrives. Web fetches fresh data after a browser reload. /// -/// Player identity is public data — the cache is never cleared on sign-out. +/// Player identity is public data, so the cache is never cleared on sign-out. /// Bump [StorageOptions.destroyKey] if [Player]'s JSON schema changes. @JsonPersist() @@ -230,12 +230,12 @@ final class PlayerInfoCacheFamily extends $Family /// Globally cached public player identity by ID. /// - /// Works for both human users and bots — the batch endpoint covers both. + /// Works for both human users and bots; the batch endpoint covers both. /// `keepAlive: true` keeps the result in memory for the session lifetime. /// Native apps also restore it from the local API cache before the network /// response arrives. Web fetches fresh data after a browser reload. /// - /// Player identity is public data — the cache is never cleared on sign-out. + /// Player identity is public data, so the cache is never cleared on sign-out. /// Bump [StorageOptions.destroyKey] if [Player]'s JSON schema changes. @JsonPersist() @@ -248,12 +248,12 @@ final class PlayerInfoCacheFamily extends $Family /// Globally cached public player identity by ID. /// -/// Works for both human users and bots — the batch endpoint covers both. +/// Works for both human users and bots; the batch endpoint covers both. /// `keepAlive: true` keeps the result in memory for the session lifetime. /// Native apps also restore it from the local API cache before the network /// response arrives. Web fetches fresh data after a browser reload. /// -/// Player identity is public data — the cache is never cleared on sign-out. +/// Player identity is public data, so the cache is never cleared on sign-out. /// Bump [StorageOptions.destroyKey] if [Player]'s JSON schema changes. @JsonPersist() diff --git a/lib/shared/widgets/made_by_credit.dart b/lib/shared/widgets/made_by_credit.dart index 0a73f4b..943a400 100644 --- a/lib/shared/widgets/made_by_credit.dart +++ b/lib/shared/widgets/made_by_credit.dart @@ -14,7 +14,7 @@ final _creditUrl = Uri.parse('https://eigeninteractive.com'); /// The credit line at the foot of the settings and about screens. /// /// Only the brand name inside the line is tappable, marked by colour rather -/// than an underline — the sentence around it is prose and does not point +/// than an underline, because the sentence around it is prose and does not point /// anywhere. A custom [Branding.madeByCredit] that never mentions the engine /// renders as plain text, so an app that replaced the line does not silently /// link its own words to us. diff --git a/lib/shared/widgets/player_avatar.dart b/lib/shared/widgets/player_avatar.dart index 0d088ee..df1737c 100644 --- a/lib/shared/widgets/player_avatar.dart +++ b/lib/shared/widgets/player_avatar.dart @@ -24,7 +24,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; /// overlaid in the bottom-right corner, and the no-photo fallback uses a robot /// glyph instead of the person glyph. This is the single place bots are made /// visually distinct from humans, so every surface that renders a [PlayerAvatar] -/// gets the marker for free — pass [isBot] wherever the participant type is known. +/// gets the marker for free; pass [isBot] wherever the participant type is known. class PlayerAvatar extends ConsumerWidget { const PlayerAvatar({ super.key, diff --git a/lib/shared/widgets/player_tags.dart b/lib/shared/widgets/player_tags.dart index 455941f..8985526 100644 --- a/lib/shared/widgets/player_tags.dart +++ b/lib/shared/widgets/player_tags.dart @@ -1,8 +1,8 @@ /// Inline pills labelling a player's kind (bot, guest). /// /// The single inline label for each kind, used wherever there's room for -/// text (player lists, profile header). For label-less contexts — e.g. small -/// overlapping avatar stacks — use the corner badge on [PlayerAvatar] instead. +/// text (player lists, profile header). For label-less contexts, e.g. small +/// overlapping avatar stacks, use the corner badge on [PlayerAvatar] instead. library; import 'package:flutter/material.dart'; @@ -68,7 +68,7 @@ class BotTag extends StatelessWidget { /// Compact pill that labels a player as an anonymous guest. /// -/// Guests are throwaway accounts — they cannot be friended, so UI showing +/// Guests are throwaway accounts; they cannot be friended, so UI showing /// this tag typically also hides social affordances. class GuestTag extends StatelessWidget { const GuestTag({super.key}); diff --git a/lib/testing/twin_fixtures.dart b/lib/testing/twin_fixtures.dart index fe75bd4..36f0412 100644 --- a/lib/testing/twin_fixtures.dart +++ b/lib/testing/twin_fixtures.dart @@ -1,7 +1,7 @@ // Dartdoc selects the two supported package entry points by library name. // ignore_for_file: unnecessary_library_name -/// Twin-drift fixture runner — the Dart half of the shared JSON fixtures +/// Twin-drift fixture runner: the Dart half of the shared JSON fixtures /// that keep a version unit's TS and Dart [GameRules] twins in sync. /// /// One fixture file per concern lives under `fixtures/v/*.json` and is @@ -24,7 +24,7 @@ /// /// This side validates fields it never itself reads (`expected.state`, /// `participantCount`, ...) as well. Those belong to the TS runner, but a game -/// package may ship only a Dart twin — and then this is the only thing +/// package may ship only a Dart twin, and then this is the only thing /// standing between a typo and a silently skipped assertion. /// /// Framework-free on purpose (no `flutter_test` import), so it can live in @@ -86,8 +86,8 @@ sealed class TwinFixtureCase { final String name; } -/// Exercises the action codec, [GameRules.isValidAction] and — when the game -/// implements optimism — [GameRules.previewAction]. +/// Exercises the action codec, [GameRules.isValidAction] and, when the game +/// implements optimism, [GameRules.previewAction]. final class ActionCase extends TwinFixtureCase { const ActionCase({ required super.name, @@ -104,7 +104,7 @@ final class ActionCase extends TwinFixtureCase { final Map config; /// The TS runner's `applyAction` input. Read here only as the fallback for - /// [obs] — a perfect-information game omits `obs` because the two coincide. + /// [obs]; a perfect-information game omits `obs` because the two coincide. final Map state; /// The acting seat's observation payload: the fixture's `obs`, or `state` @@ -162,7 +162,7 @@ final class BotSeatableCase extends TwinFixtureCase { /// sorted by path for stable test ordering. /// /// Throws [FormatException] on malformed JSON, and on any fixture that does -/// not match the documented format — a broken fixture should fail loudly and +/// not match the documented format; a broken fixture should fail loudly and /// immediately, not silently shrink the suite or fail later as a phantom /// rules divergence. List loadTwinFixtureSuites(String rootPath) { @@ -199,7 +199,7 @@ TwinFixtureSuite parseTwinFixtureSuite(String path, dynamic json) { TwinFixtureCase _parseCase(String indexed, dynamic raw) { final map = _object(indexed, raw); - // Prefer the case's own name in the location once it is readable — a + // Prefer the case's own name in the location once it is readable: a // fixture author finds `cases[3] (seat 0 wins)` faster than an index. final name = map['name']; final where = name is String ? '$indexed ($name)' : indexed; @@ -327,7 +327,7 @@ T? _optional(String where, dynamic v, T Function(String, dynamic) read) => /// failure descriptions (empty ⇒ the case passes). /// /// A parse throw (config/observation/action `fromJson`) is reported as a -/// failure, not rethrown — a codec that cannot read the recorded payload is +/// failure, not rethrown: a codec that cannot read the recorded payload is /// itself twin drift. List runTwinFixtureCase( GameRules rules, @@ -353,12 +353,12 @@ List _runActionCase( if (failures.isNotEmpty) return failures; // The codec must round-trip the fixture action: what parseAction reads, - // serializeAction must write back — otherwise a submitted move would not + // serializeAction must write back, or else a submitted move would not // match what the TS side validated this fixture against. final roundTrip = rules.serializeAction(action); if (!_deepEquals(roundTrip, c.action)) { failures.add( - 'action codec does not round-trip the fixture action — ' + 'action codec does not round-trip the fixture action: ' 'serializeAction produced ${jsonEncode(roundTrip)}', ); } @@ -382,7 +382,7 @@ List _runActionCase( return failures; } -/// Compares [GameRules.previewAction] against `expected.observation` — but +/// Compares [GameRules.previewAction] against `expected.observation`, but /// only when the game implements optimism (a null preview means "this move is /// server-driven", which is always a correct answer, never drift). void _checkPreview( diff --git a/pubspec.yaml b/pubspec.yaml index 0b2d195..8495aed 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: eigen_flutter -description: "EigenInteractive Flutter client — the whitelabel app half for turn-based multiplayer games on the EigenInteractive server." +description: "EigenInteractive Flutter client: the whitelabel app half for turn-based multiplayer games on the EigenInteractive server." version: 0.3.5 homepage: https://eigeninteractive.com repository: https://github.com/eigeninteractive/eigen-flutter @@ -57,7 +57,7 @@ dependencies: package_info_plus: ^10.1.0 qr_flutter: ^4.1.0 # The generated REST client. It is produced and published by the ENGINE repo - # (eigen-server/clients/dart), because that is where the wire contract lives — + # (eigen-server/clients/dart), because that is where the wire contract lives: # so its version is the engine's, and this constraint is the compatibility # statement: `^0.1.0` means "any engine that has not broken the wire". # diff --git a/test/core/api/engine_call_test.dart b/test/core/api/engine_call_test.dart index bd71ce4..e887af7 100644 --- a/test/core/api/engine_call_test.dart +++ b/test/core/api/engine_call_test.dart @@ -19,7 +19,7 @@ DioException _serverSaidNo(int status, Object? body) { ); } -/// A [DioException] with no response — the wire never answered. +/// A [DioException] with no response: the wire never answered. DioException _noAnswer(DioExceptionType type) => DioException( requestOptions: RequestOptions(path: '/api/engine/games'), type: type, diff --git a/test/core/api/retry_policy_test.dart b/test/core/api/retry_policy_test.dart index 10fc801..3320e8f 100644 --- a/test/core/api/retry_policy_test.dart +++ b/test/core/api/retry_policy_test.dart @@ -31,7 +31,7 @@ void main() { }); test('never retries a write, even on a transport failure', () { - // A timed-out POST may have landed — the outcome is unknown, so it must + // A timed-out POST may have landed and the outcome is unknown, so it must // not be replayed. for (final method in ['POST', 'PUT', 'DELETE', 'PATCH']) { check( @@ -44,7 +44,7 @@ void main() { }); test('never retries a failure that carried a response', () { - // The server answered — a decision, not a blip — even for a 5xx or a 429. + // The server answered: a decision, not a blip, even for a 5xx or a 429. check(retryTransientGet(_withResponse('GET', 500), 1)).equals(false); check(retryTransientGet(_withResponse('GET', 429), 1)).equals(false); check(retryTransientGet(_withResponse('GET', 404), 1)).equals(false); diff --git a/test/core/api/server_clock_test.dart b/test/core/api/server_clock_test.dart index 3f7724d..0c4bd2f 100644 --- a/test/core/api/server_clock_test.dart +++ b/test/core/api/server_clock_test.dart @@ -46,7 +46,7 @@ void main() { // own `DateTime.now()`. That puts it in (298s, 300s]. // // `inSeconds >= 299` accounted for the dropped fraction but not the - // elapsed term, so it failed whenever the two summed past a second — rare + // elapsed term, so it failed whenever the two summed past a second: rare // enough to survive months, and it duly took out a release run. Asserted // in milliseconds because `inSeconds` truncates, hiding the very // sub-second margin this is about. diff --git a/test/core/architecture/api_isolation_test.dart b/test/core/architecture/api_isolation_test.dart index f603c4b..9eda9c9 100644 --- a/test/core/architecture/api_isolation_test.dart +++ b/test/core/architecture/api_isolation_test.dart @@ -38,7 +38,7 @@ final _serverAccess = [ /// notices breaking. void main() { test('the public barrel exports types, never the API classes', () { - // A game app depends on `eigen_flutter`, not on `eigen_api` — so the barrel + // A game app depends on `eigen_flutter`, not on `eigen_api`, so the barrel // must re-export the wire *vocabulary* a game renders from, without handing // apps the ability to call the server directly. A wholesale // `export 'package:eigen_api/eigen_api.dart';` would do exactly that, which diff --git a/test/core/config/configure_firebase_cli_test.dart b/test/core/config/configure_firebase_cli_test.dart index d33e01f..7124af1 100644 --- a/test/core/config/configure_firebase_cli_test.dart +++ b/test/core/config/configure_firebase_cli_test.dart @@ -96,7 +96,7 @@ echo "App configuration is written in \$output" contains('Firebase configured for Android, Flutter Web'), ); // Which project and which apps, because FlutterFire reuses an app the - // project already had without saying so — the two runs are otherwise + // project already had without saying so; the two runs are otherwise // indistinguishable. expect(result.stdout, contains('project example-project')); expect(result.stdout, contains('web 1:123:web:abc')); @@ -157,7 +157,7 @@ echo "App configuration is written in \$output" group('the arguments handed to FlutterFire', () { /// Runs the script against a `flutterfire` that records its arguments and /// stops there, and returns what it was called with. `firebase.json` is - /// never written, so the script fails straight after — which is not what is + /// never written, so the script fails straight after, which is not what is /// under test here. Future> flutterFireArguments( List arguments, { @@ -245,7 +245,7 @@ echo "App configuration is written in \$output" }); test('ignores the separator pnpm forwards but npm swallows', () async { - // So one documented form — `run firebase:configure -- --project x` — + // So one documented form, `run firebase:configure -- --project x`, // works under either, rather than FlutterFire being handed a bare `--` // that means nothing to it. final argv = await flutterFireArguments(const [ @@ -260,7 +260,7 @@ echo "App configuration is written in \$output" test('reuses the project a previous run recorded', () async { // FlutterFire writes the project into firebase.json, which this script - // already reads its Web app ID out of — so re-running to pick up a new + // already reads its Web app ID out of, so re-running to pick up a new // configuration does not ask the same question again. final argv = await flutterFireArguments(const [], configured: true); @@ -338,7 +338,7 @@ echo "App configuration is written in \$output" test('shows what the quiet command said when it fails', () async { // Captured is not swallowed. The download runs with its output held back - // because it has nothing to say on success — a failure is the other case. + // because it has nothing to say on success; a failure is the other case. final packageRoot = Directory.current; final appRoot = Directory.systemTemp.createTempSync('eigen-firebase-fail-'); addTearDown(() => appRoot.deleteSync(recursive: true)); @@ -401,7 +401,7 @@ exit 3 ); addTearDown(() => appRoot.deleteSync(recursive: true)); - // `firebase` present, `flutterfire` absent — the common case, because + // `firebase` present, `flutterfire` absent: the common case, because // FlutterFire is not installed alongside Flutter and lands somewhere that // is not on PATH. `dart` itself has to stay findable to run the script. final fakeBin = Directory(path.join(appRoot.path, 'bin'))..createSync(); diff --git a/test/core/errors/error_messages_test.dart b/test/core/errors/error_messages_test.dart index c92bce8..511e5a9 100644 --- a/test/core/errors/error_messages_test.dart +++ b/test/core/errors/error_messages_test.dart @@ -20,7 +20,7 @@ void main() { code: ErrorCode.stateUpdated, ), ), - ).equals('The game updated — try again.'); + ).equals('The game updated. Try again.'); check( humanize( const EngineException('slow down', code: ErrorCode.rateLimited), @@ -29,7 +29,7 @@ void main() { }); test('dispatches on code, not message text', () { - // The server copy can change freely — only the code decides. + // The server copy can change freely; only the code decides. check( humanize( const EngineException( diff --git a/test/core/game/timing_constants_test.dart b/test/core/game/timing_constants_test.dart index 95c9845..399e11f 100644 --- a/test/core/game/timing_constants_test.dart +++ b/test/core/game/timing_constants_test.dart @@ -12,7 +12,7 @@ void main() { }); test('caps the margin to a fraction of a short window', () { - // 2s window: 25% cap (0.5s) is below the 1s target, so the cap wins — + // 2s window: 25% cap (0.5s) is below the 1s target, so the cap wins, // a short Nope/hook-override window is not swallowed. check( softDeadlineMarginFor(const Duration(seconds: 2)), diff --git a/test/core/theme/variable_weight_test.dart b/test/core/theme/variable_weight_test.dart index abe0809..9ea086a 100644 --- a/test/core/theme/variable_weight_test.dart +++ b/test/core/theme/variable_weight_test.dart @@ -9,7 +9,7 @@ import 'package:flutter_test/flutter_test.dart'; /// The bundled faces are single variable files with no per-weight entries, so /// every weight is the `wght` axis moving rather than a different file being /// chosen. That only works because `FontWeight` drives the axis, which landed -/// in Flutter 3.41 — before it, one file meant one weight and every +/// in Flutter 3.41. Before it, one file meant one weight and every /// `FontWeight` rendered identically. /// /// Measuring is the only way to tell the two apart from a test: a variable font diff --git a/test/engine/example_twin_fixtures_test.dart b/test/engine/example_twin_fixtures_test.dart index b086b1a..1a5640d 100644 --- a/test/engine/example_twin_fixtures_test.dart +++ b/test/engine/example_twin_fixtures_test.dart @@ -9,7 +9,7 @@ import '../helpers/example_rules.dart'; /// `test/fixtures/game/` run here against the Dart [ExampleRules] twin. A real /// game runs the same files against its authoritative TS unit via /// `@eigen/testkit`; this package has no Worker, so only the Dart half runs -/// here — enough to keep [loadTwinFixtureSuites] and [runTwinFixtureCase] +/// here, enough to keep [loadTwinFixtureSuites] and [runTwinFixtureCase] /// honest. Downstream apps copy this wiring for their own game. void main() { const versions = >{ diff --git a/test/engine/twin_fixtures_runner_test.dart b/test/engine/twin_fixtures_runner_test.dart index 8f199bb..e9a0055 100644 --- a/test/engine/twin_fixtures_runner_test.dart +++ b/test/engine/twin_fixtures_runner_test.dart @@ -13,8 +13,8 @@ import '../helpers/fakes.dart'; /// /// Two halves, matching the two stages the library separates: parsing (a /// malformed fixture must fail at load, naming the offending field) and -/// evaluation (every checked surface — codec round-trip, legality, preview, -/// predicates — must both pass on agreement and produce a failure message on +/// evaluation (every checked surface, codec round-trip, legality, preview and +/// predicates, must both pass on agreement and produce a failure message on /// divergence). The evaluation tests build typed cases directly, so they /// exercise the runner without going through the parser. void main() { diff --git a/test/features/game/providers/replay_controller_test.dart b/test/features/game/providers/replay_controller_test.dart index 7446266..767f317 100644 --- a/test/features/game/providers/replay_controller_test.dart +++ b/test/features/game/providers/replay_controller_test.dart @@ -35,7 +35,7 @@ void main() { cursor.next(); cursor.next(); check(cursor.state).equals(2); - cursor.next(); // already at the last frame — no-op + cursor.next(); // already at the last frame, so a no-op check(cursor.state).equals(2); }); @@ -50,7 +50,7 @@ void main() { check(cursor.state).equals(1); cursor.previous(); check(cursor.state).equals(0); - cursor.previous(); // already at the first frame — no-op + cursor.previous(); // already at the first frame, so a no-op check(cursor.state).equals(0); }); diff --git a/test/helpers/container.dart b/test/helpers/container.dart index cb1cb36..a4ac520 100644 --- a/test/helpers/container.dart +++ b/test/helpers/container.dart @@ -8,7 +8,7 @@ import 'package:flutter_riverpod/misc.dart' show Override; /// /// Most engine providers need no override. Only graphs reaching /// `appConfigProvider` or `currentGameModuleProvider` (which throw by default) -/// require one — prefer overriding the *immediate* dependency a test reads +/// require one; prefer overriding the *immediate* dependency a test reads /// (e.g. `friendshipsProvider`) rather than the whole api/auth chain. ProviderContainer makeContainer({List overrides = const []}) => ProviderContainer.test(overrides: overrides); diff --git a/test/helpers/example_rules.dart b/test/helpers/example_rules.dart index 387590b..7d08586 100644 --- a/test/helpers/example_rules.dart +++ b/test/helpers/example_rules.dart @@ -1,7 +1,7 @@ /// A minimal Dart [GameRules] unit: the trivial one-move game where player 0 /// acts once and wins. /// -/// Exists so the engine can dogfood its own twin-fixture pipeline end to end — +/// Exists so the engine can dogfood its own twin-fixture pipeline end to end, /// this unit runs `test/fixtures/game/v1/` through /// `lib/testing/twin_fixtures.dart`, exercising the same path a real game's /// Dart twin takes. diff --git a/test/shared/api_contract_test.dart b/test/shared/api_contract_test.dart index 0345bda..d333428 100644 --- a/test/shared/api_contract_test.dart +++ b/test/shared/api_contract_test.dart @@ -130,7 +130,7 @@ void main() { group('RatingDelta.identity', () { // Flattened to a nullable pair matching `Seat`, so there is no union type - // to destructure — exactly one of the two ids is set. + // to destructure: exactly one of the two ids is set. test('carries a user identity', () { final identity = RatingIdentity.fromJson({ 'userId': 'user-1', diff --git a/test/shared/data/player_batch_loader_test.dart b/test/shared/data/player_batch_loader_test.dart index 892d5f4..cdbec20 100644 --- a/test/shared/data/player_batch_loader_test.dart +++ b/test/shared/data/player_batch_loader_test.dart @@ -112,7 +112,7 @@ void main() { window: const Duration(milliseconds: 20), ); - // ignore: unawaited_futures — the waiter intentionally never resolves + // ignore: unawaited_futures, since the waiter intentionally never resolves // once the flush is cancelled; we only assert the request never fires. loader.load('a'); loader.dispose();