From fad4778ad8e5250c672657df7c7c6f46e37e6dd7 Mon Sep 17 00:00:00 2001 From: adrianoftyriel <12343778+adrianoftyriel@users.noreply.github.com> Date: Tue, 13 Oct 2026 22:03:27 +0000 Subject: [PATCH 1/6] Bootstrap Pilgrims of Katan: rebrand, Gitea CI and OTA Import of ProLibertateGames (upstream main, GPL-3.0) rebranded for Wallacenet and repointed at git.wallacenet.ca. - Identity: applicationId ca.wallacenet.pilgrimsofkatan (+ .dev for the dev channel), launcher names "Pilgrims of Katan" / "Pilgrims of Katan DEV", neutral hexagon branding replacing the Wallace tartan. Uncial Antiqua keeps its OFL licence, and the GPL-3.0 LICENSE plus upstream attribution stay. - OTA: Updater.kt now reads this repo's own Gitea releases (/api/v1/repos/devadmin/PilgrimsOfKatan/releases...) and BuildConfig. OTA_ENABLED is on; Gitea answers the same JSON shape the code already parsed. - Build numbers: versionCode = CI run number + plgVersionRunOffset (1000), with the tag and versionName derived from the same figure. The offset is permanent, because Gitea Actions restarts run numbers at 1 and a reset would otherwise make Android reject the next build as a downgrade. - CI: ci.yml publishes a dev prerelease from dev, release.yml a production release from main, both through scripts/publish-release.sh (Gitea REST API, runnable by hand until an Actions runner is registered). - Docs: docs/CI.md records the channel, numbering and runner decisions; docs/README.md seeds the per-game RULES-* conventions; CLAUDE.md carries the agent ground rules. Verified: ./gradlew assembleDebug and testDebugUnitTest green on JDK 17 / compileSdk 34, both variants stamped, installing side by side under the new identity. Co-Authored-By: Paperclip --- .github/workflows/ci.yml | 74 +++---- .github/workflows/release.yml | 71 +++--- CLAUDE.md | 169 +++++++------- README.md | 208 ++++++++---------- app/build.gradle.kts | 75 ++++--- app/src/main/AndroidManifest.xml | 6 +- .../org/prolibertate/games/MainActivity.kt | 4 +- .../prolibertate/games/net/LanTransport.kt | 2 +- .../org/prolibertate/games/net/Protocol.kt | 2 +- .../games/ui/LaunchUpdateCheck.kt | 2 +- .../org/prolibertate/games/ui/SplashScreen.kt | 44 ++-- .../prolibertate/games/ui/game/CardTable.kt | 4 +- .../games/ui/game/KlondikeScreen.kt | 4 +- .../games/ui/game/PegSolitaireScreen.kt | 10 +- .../games/ui/game/YahtzeeScreen.kt | 10 +- .../org/prolibertate/games/ui/theme/Tartan.kt | 187 ---------------- .../org/prolibertate/games/ui/theme/Theme.kt | 28 ++- .../prolibertate/games/update/UpdatePolicy.kt | 24 +- .../org/prolibertate/games/update/Updater.kt | 68 ++++-- .../res/drawable/ic_launcher_background.xml | 68 +----- .../res/drawable/ic_launcher_foreground.xml | 18 +- app/src/main/res/mipmap/ic_launcher.xml | 72 +----- app/src/main/res/mipmap/ic_launcher_round.xml | 72 +----- app/src/main/res/values/colors.xml | 16 +- app/src/main/res/values/strings.xml | 2 +- app/src/main/res/values/themes.xml | 2 +- .../games/update/UpdatePolicyTest.kt | 4 +- docs/CI.md | 136 ++++++++++++ docs/README.md | 45 ++++ gradle.properties | 8 + scripts/publish-release.sh | 96 ++++++++ settings.gradle.kts | 2 +- 32 files changed, 760 insertions(+), 773 deletions(-) delete mode 100644 app/src/main/java/org/prolibertate/games/ui/theme/Tartan.kt create mode 100644 docs/CI.md create mode 100644 docs/README.md create mode 100755 scripts/publish-release.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc2f564..40dcf0b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,15 +1,19 @@ name: CI # Pre-merge checks. Runs on the dev branch and on every pull request aimed at -# dev or main, so breakage is caught before it reaches main and triggers a -# release. +# dev or main, so breakage is caught before it reaches main. # -# On a push to dev it also publishes the build as a dev-channel prerelease, so -# the in-app updater can offer it to anyone who has switched to that channel. -# Prereleases are used deliberately: GitHub's "latest release" endpoint skips -# them, so a production-channel user never sees a dev build. Pull requests only -# build and check — they never publish, which keeps a PR from a fork from -# shipping anything. +# On a push to dev it also publishes the build as a dev-channel prerelease on +# this Gitea instance, so the in-app updater can offer it to anyone on the dev +# channel. Prereleases are used deliberately: the "latest release" endpoint +# skips them, so a production-channel user never sees a dev build. Pull +# requests only build and check — they never publish, which keeps a PR from a +# fork from shipping anything. +# +# These files live in .github/workflows/ on purpose: Gitea Actions reads that +# path as well as .gitea/workflows/, so one file serves both. Publishing goes +# through Gitea's REST API (the same routes the app's updater reads) rather +# than the `gh` CLI, which a runner image need not carry. on: push: branches: [ dev ] @@ -17,7 +21,7 @@ on: branches: [ main, dev ] workflow_dispatch: -# Needed to publish the dev-channel prerelease below. +# Needed by the job token to create the release and attach the APK. permissions: contents: write @@ -37,10 +41,7 @@ jobs: # .0-dev: they are never published and must not claim a channel # version. # - # This condition has to stay in step with the publish steps' own. Stamping - # a build the publish steps would ship, or shipping one this left - # unstamped, both end the same way — an APK claiming versionCode 1, which - # the updater can never see as an upgrade over anything. + # This condition has to stay in step with the publish steps' own. PLG_CHANNEL: ${{ github.ref == 'refs/heads/dev' && 'dev' || '' }} steps: - name: Checkout @@ -68,7 +69,7 @@ jobs: - name: Upload debug APK uses: actions/upload-artifact@v4 with: - name: ProLibertateGames-debug-apk + name: PilgrimsOfKatan-debug-apk path: app/build/outputs/apk/debug/app-debug.apk if-no-files-found: error retention-days: 14 @@ -78,7 +79,7 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: ProLibertateGames-reports + name: PilgrimsOfKatan-reports path: | app/build/reports/ if-no-files-found: ignore @@ -88,22 +89,27 @@ jobs: # branch itself, after it has built, tested and linted clean above. # # Gated on the branch alone rather than on the event, which is how - # release.yml has always gated main. A pull request is still excluded - # without needing to name it: a PR build runs against refs/pull/N/merge, - # never refs/heads/dev, so a fork still cannot ship anything. + # release.yml gates main. A pull request is still excluded without + # needing to name it: a PR build runs against refs/pull/N/merge, never + # refs/heads/dev, so a fork still cannot ship anything. - name: Stage dev APK id: stage if: github.ref == 'refs/heads/dev' run: | set -euo pipefail - # The series comes from gradle.properties, which is the same file the - # build read when it stamped the APK — so the tag and what is inside - # it cannot name different versions. + # The series and the offset come from gradle.properties, which is + # the same file the build read when it stamped the APK — so the tag + # and what is inside it cannot name different versions. The number + # is run number + offset, exactly the arithmetic build.gradle.kts + # did for versionCode. series="$(sed -n 's/^plgVersionSeries=//p' gradle.properties)" + offset="$(sed -n 's/^plgVersionRunOffset=//p' gradle.properties)" test -n "$series" - version="v${series}.${{ github.run_number }}-dev" + test -n "$offset" + stamp="$(( ${{ github.run_number }} + offset ))" + version="v${series}.${stamp}-dev" src="app/build/outputs/apk/debug/app-debug.apk" - dest="ProLibertateGames-${version}.apk" + dest="PilgrimsOfKatan-${version}.apk" cp "$src" "$dest" echo "version=$version" >> "$GITHUB_OUTPUT" echo "apk=$dest" >> "$GITHUB_OUTPUT" @@ -111,17 +117,11 @@ jobs: - name: Publish dev channel prerelease if: github.ref == 'refs/heads/dev' env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - gh release create "${{ steps.stage.outputs.version }}" \ - "${{ steps.stage.outputs.apk }}" \ - --repo "${{ github.repository }}" \ - --target "${{ github.sha }}" \ - --title "ProLibertateGames ${{ steps.stage.outputs.version }}" \ - --prerelease \ - --notes "Automated **dev channel** build from \`${{ github.sha }}\` on \`dev\`. - - Preview build: newer than production, and less tested. Switch to the - Dev update channel in Settings to be offered these. Marked as a - prerelease so production-channel installs never see it." + # The job's built-in token: contents: write is declared above. + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + RELEASE_TAG: ${{ steps.stage.outputs.version }} + RELEASE_APK: ${{ steps.stage.outputs.apk }} + RELEASE_KIND: prerelease + RELEASE_TARGET: ${{ github.sha }} + # One script, also runnable by hand — see docs/CI.md. + run: ./scripts/publish-release.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4ef3a4a..6715723 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,13 +1,17 @@ name: Build and Release APK -# Build an APK and publish it as a GitHub Release whenever commits land on main -# (for example when a pull request is merged). Can also be run manually. +# Production-channel build. Runs on a push to main, or by hand +# (workflow_dispatch), and publishes to this Gitea instance's releases — which +# is the channel the in-app updater reads. See docs/CI.md. +# +# Publishing goes through Gitea's REST API rather than the `gh` CLI, which a +# runner image need not carry. on: push: branches: [ main ] workflow_dispatch: -# Allow the built-in token to create releases and upload the APK. +# Needed by the job token to create the release and attach the APK. permissions: contents: write @@ -19,14 +23,13 @@ concurrency: jobs: build: name: Build APK and publish release - # workflow_dispatch can be aimed at any branch from the Actions UI. Releases - # are what the in-app updater installs, so only main may produce one — - # anything else is a no-op rather than shipping unreviewed code to everyone. + # workflow_dispatch can be aimed at any branch from the Actions UI. The + # production stamp belongs to main alone — anything else is a no-op rather + # than shipping unreviewed code to everybody. if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest env: - # Stamps the APK as a production-channel build, versioned with the run - # number that matches the tag published below. + # Stamps the APK as a production-channel build. PLG_CHANNEL: production steps: - name: Checkout @@ -42,52 +45,44 @@ jobs: - name: Set up Android SDK uses: android-actions/setup-android@v4 - - name: Build debug APK + - name: Build production APK run: ./gradlew assembleDebug --no-daemon --stacktrace - - name: Stage APK for release + - name: Stage APK id: stage run: | set -euo pipefail - # The series comes from gradle.properties, which is the same file the - # build read when it stamped the APK — so the tag and what is inside - # it cannot name different versions. + # The series and the offset come from gradle.properties, which is + # the same file the build read when it stamped the APK — so the tag + # and what is inside it cannot name different versions. The number + # is run number + offset, exactly the arithmetic build.gradle.kts + # did for versionCode. series="$(sed -n 's/^plgVersionSeries=//p' gradle.properties)" + offset="$(sed -n 's/^plgVersionRunOffset=//p' gradle.properties)" test -n "$series" - version="v${series}.${{ github.run_number }}" + test -n "$offset" + stamp="$(( ${{ github.run_number }} + offset ))" + version="v${series}.${stamp}" src="app/build/outputs/apk/debug/app-debug.apk" - dest="ProLibertateGames-${version}.apk" + dest="PilgrimsOfKatan-${version}.apk" cp "$src" "$dest" - # The same build again under a name that never changes. GitHub's - # /releases/latest/download/ redirect needs a constant file - # name, so this second copy is what makes a permanent download link - # possible — the versioned copy stays as the traceable one. - cp "$src" "ProLibertateGames.apk" echo "version=$version" >> "$GITHUB_OUTPUT" echo "apk=$dest" >> "$GITHUB_OUTPUT" - name: Upload APK as workflow artifact uses: actions/upload-artifact@v4 with: - name: ProLibertateGames-apk + name: PilgrimsOfKatan-apk path: ${{ steps.stage.outputs.apk }} if-no-files-found: error - - name: Create GitHub release + - name: Publish production release env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - gh release create "${{ steps.stage.outputs.version }}" \ - "${{ steps.stage.outputs.apk }}" \ - "ProLibertateGames.apk" \ - --repo "${{ github.repository }}" \ - --target "${{ github.sha }}" \ - --title "ProLibertateGames ${{ steps.stage.outputs.version }}" \ - --notes "Automated release built from \`${{ github.sha }}\` on push to \`main\`. - - Download **${{ steps.stage.outputs.apk }}** below and install it on an Android device (you may need to allow *Install unknown apps*). This is a debug-signed build. - - Both files below are the same build. \`ProLibertateGames.apk\` is a copy under a fixed name, so this link always fetches the newest release and can be bookmarked or shared: - - https://github.com/${{ github.repository }}/releases/latest/download/ProLibertateGames.apk" + # The job's built-in token: contents: write is declared above. + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + RELEASE_TAG: ${{ steps.stage.outputs.version }} + RELEASE_APK: ${{ steps.stage.outputs.apk }} + RELEASE_KIND: release + RELEASE_TARGET: ${{ github.sha }} + # One script, also runnable by hand — see docs/CI.md. + run: ./scripts/publish-release.sh diff --git a/CLAUDE.md b/CLAUDE.md index 858e8b5..d00e24e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,114 +1,93 @@ -# Working on this repo with Claude +# Working on this repo (agent notes) -## Nothing a session does triggers CI. Dispatch it explicitly. +Pilgrims of Katan — an Android collection of card and board games, playable +against AI or over Wi-Fi (LAN). Bootstrapped from +[adrianoftyriel/ProLibertateGames](https://github.com/adrianoftyriel/ProLibertateGames) +(GPL-3.0, kept as the `upstream` remote) for Wallacenet. Canonical repo: +. -GitHub creates **no workflow run** for anything this sandbox does — pushing a -branch, opening a pull request, or merging one. All of it goes through the same -integration, and GitHub suppresses event-triggered runs from it. The pull -request will say it was opened by the repo owner; that is only how the UI -attributes it, and it does not change the suppression. +## Ground rules -This was measured, not assumed: +- **Licences travel.** `LICENSE` is GPL-3.0 and must stay. The Uncial Antiqua + font ships with its OFL licence at `docs/licenses/UncialAntiqua-OFL.txt`; + never delete or edit either. Attribution to the upstream lives in the README. +- **No Catan trademarks or brand assets**, and none of the upstream's Wallace + clan branding (it was removed in the rebrand; see README → Branding). The + brand is: *Pilgrims of Katan*, `ca.wallacenet.pilgrimsofkatan` (+ `.dev`), + slate blue / dev green, hexagon emblem. +- **The existing catalogue is the regression safety net.** Keep every game + green; new games (Catan) arrive as separate tasks, not edits to old ones. + All 22 games are `available = true`. +- **Docs conventions**: every game gets `docs/RULES-.md` (index and gaps in + `docs/README.md`); build/CI/release decisions go in `docs/CI.md`. Update the + docs when behaviour changes — they are the check against the rules code. -| Action | Result | -| --- | --- | -| Human pushes a Claude-authored commit (`d165e5ef`, `469c900e`) | CI runs | -| Session pushes `dev` / `main` (`43394b8`, `742add4`) | no run | -| Session opens a PR against `dev` (#1) | no run | -| Session calls `workflow_dispatch` | **runs** | +## Build -`workflow_dispatch` works because it creates a run directly instead of relying -on an event. It is the only mechanism available from a session, so every build -has to be asked for by hand. +```sh +export JAVA_HOME=... # JDK 17 +export ANDROID_HOME=... # SDK with platform 34, build-tools 34 +./gradlew assembleDebug # dev-channel APK (default) +PLG_CHANNEL=production ./gradlew assembleDebug # production APK +./gradlew testDebugUnitTest # JVM unit tests (rules/AI/net/score) +./gradlew lintDebug # Android lint (CI runs it) +``` -## Verify before merging, without publishing +On the Wallacenet project workspace a ready toolchain sits at +`../.toolchain/` (`jdk17/`, `android-sdk/`) — export `JAVA_HOME` and +`ANDROID_HOME` to it rather than installing again. Both variants must build: +they install side by side (different applicationId, launcher name, colour). -`ci.yml` builds, tests and lints on any ref, but its staging and publishing -steps are gated on `github.ref == 'refs/heads/dev'`. Dispatching it on a -**feature branch** therefore gives the full check — build, unit tests, Android -lint — and publishes nothing. +## Channels and OTA -So the working loop is: +One flag — `isDevBuild` in `app/build.gradle.kts` — drives applicationId, +launcher name and brand colour together. Never set them individually. -1. Commit and push the feature branch. -2. Dispatch `ci.yml` on that branch. Wait for green. This is the real review - gate, and it is the only verification that exists (see below). -3. Open the PR through the API and merge it there, so the change is reviewable - and the merge commit is honest about what happened. Neither step builds - anything. -4. Dispatch the target branch's workflow to actually build and publish: - `ci.yml` on `dev`, `release.yml` on `main`. +In-app OTA points at this repo's own Gitea releases +(`/api/v1/repos/devadmin/PilgrimsOfKatan/releases…`) and is **on**: +`BuildConfig.OTA_ENABLED` is `true`. The channel is a decision already made and +recorded — `docs/CI.md` documents the endpoints, the build-number scheme, and +what publishing requires. Setting the flag to `false` is the kill switch. -Skipping step 4 leaves a branch whose head was never built and no release. +## CI -## Merging is a shipping action +`.github/workflows/` — Actions-compatible so the Gitea instance runs them. +`ci.yml`: build + test + lint + artifact, and a dev prerelease from `dev`. +`release.yml`: production release from `main`. Both publish through +`scripts/publish-release.sh` (Gitea REST API), which is also runnable by hand. -Both long-lived branches publish, so a merge is a release, not just an -integration: +**No Actions runner is registered on the instance yet**, so the workflows are +idle until one is — the local gradle commands above are the source of truth, +and CI green means those plus lint. Do not treat a workflow that has never run +as evidence. -- **`dev`** → `ci.yml` publishes a `v.N-dev` **prerelease**, offered by the - in-app updater to anyone on the Dev channel. -- **`main`** → `release.yml` publishes a **production** release, which the - updater installs for everyone on the Production channel. +## Code map -Confirm before shipping to `main`. Note that `release.yml` runs `assembleDebug` -only — no tests, no lint — so a `main` release is only as verified as the `dev` -build it replays. +- `game/` — rules engines and AI, **pure Kotlin, no Android types**, unit + tested. `Catalog.kt` is the registry (id, title, blurb, options, `available`). +- `net/` — wire protocol, LAN transport (mDNS + TCP), lobby, host-authoritative + match driver; `score/` the scorekeeper sheet, also pure. +- `ui/` — Compose screens; `ui/theme/Theme.kt` holds the brand palette + (`BrandField`/`BrandDeep`/`BrandAccent`) and table accents. +- `update/` — OTA policy (`UpdatePolicy.kt`, pure and tested) and the gated + fetch (`Updater.kt`). +- `settings/` — DataStore preferences. -## Gradle does not run in the sandbox +Host is authoritative over the wire; clients send intents, host re-validates +against the rules. Keep `game/` free of Android imports — the JVM tests depend +on it. -`./gradlew` cannot resolve the Android plugin here: the proxy blocks -`dl.google.com` with a 403. No local build, no local test run, no local lint. A -dispatched CI run is the only way to find out whether a change compiles. +## Git -## A permanent fix, if you want one +- Branches: `main` (production), `dev` (integration). Feature branches → PR + into `dev`. +- Commits by Paperclip agents end with + `Co-Authored-By: Paperclip `. +- Pull upstream fixes with `git pull upstream` (the GitHub source); do not + point `origin` anywhere but this repo's Gitea URL. -The suppression is a property of the sandbox's credential, so it cannot be -fixed from inside a session. Giving the environment a personal access token to -push with would make pushes and PRs behave normally for everyone. Until then, -the dispatch step above is the workaround. +## Legacy files -## Bash output is filtered before it reaches the model - -`.claude/settings.json` registers a `PreToolUse` hook on `Bash` that rewrites -each command through [rtk](https://github.com/rtk-ai/rtk): `git status` runs as -`rtk git status`, and rtk compresses the output. Write commands normally — the -rewrite is transparent and costs nothing to invoke. - -Measured on this repo rather than quoted from rtk's README: - -| Command | Raw | Filtered | Saved | -| --- | --- | --- | --- | -| `git diff HEAD~3` | 521 KB | 28 KB | 95% | -| `grep -rn "fun " app/src/main` | 117 KB | 19 KB | 84% | -| `find . -name "*.kt"` | 7.7 KB | 1.7 KB | 79% | -| `git log -20` | 20 KB | 5.3 KB | 74% | -| `git diff --stat` | 4.0 KB | 4.0 KB | 1% | -| `ls` of a single package | 50 B | 62 B | −24% | - -The saving is all in the large outputs, which is the point — a full diff of a -few commits is otherwise most of a context window. Already-compact output grows -slightly, because rtk adds a header; at those sizes it does not matter. `rtk -gain` reports what the current session has actually saved. - -Two limits are worth knowing. The `Read`, `Grep` and `Glob` tools are not -`Bash`, so they never reach the hook and are unaffected; a deliberate `rg` or -`find` in a shell *is* filtered. And filtering never truncates a file: `cat` -becomes `rtk read`, which was checked byte-for-byte against this repo's longest -Kotlin source (`GameSetupScreen.kt`, 946 lines) and came back identical. Commit -messages survive the rewrite intact, quotes and colons included. To bypass -filtering for one command, run `rtk proxy `. - -## rtk is reinstalled at the start of every session - -The container starts empty, so `.claude/hooks/session-start.sh` installs rtk -before the session does anything. It pins `RTK_VERSION`, because the installer -otherwise asks `api.github.com` for the latest tag and the proxy answers 403 for -every repository except this one — the lookup fails and takes the install with -it. Bump that variable by hand. - -The hook is written not to matter when it fails. Outside the sandbox it installs -nothing and just prints the command to run, since dropping a binary into -someone's `~/.local/bin` uninvited is not its business; if the download fails it -says so and exits clean. With rtk absent the rewrite hook no-ops, and the only -consequence is verbose output. +`.claude/` and `.rtk/` are the upstream author's assistant tooling, harmless +and left as inherited. This file supersedes the upstream's CLAUDE.md (which +documented their GitHub sandbox; that history is still in git). diff --git a/README.md b/README.md index c7d19c9..2f818c3 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,14 @@ -# Pro Libertate Games +# Pilgrims of Katan A collection of card and board games for Android, playable against the computer or against other people over Wi-Fi (LAN). +Derived from [ProLibertateGames](https://github.com/adrianoftyriel/ProLibertateGames) +by adrianoftyriel, used under the GNU GPL v3 — see [LICENSE](LICENSE). Game +rules documentation lives under [docs/](docs/README.md); build, CI and release +decisions live in [docs/CI.md](docs/CI.md). Catan itself arrives in later work; +this app ships the reference catalogue unchanged as the regression safety net. + ## What works today The app shell is complete and every game in the catalogue is playable end to end: @@ -111,55 +117,52 @@ it: every rule above is a pure function and unit-tested on the JVM. - **Sound** on/off. - **Animation speed**, 0.5×–2×. One multiplier drives card movement and how long the computer appears to think. -- **Updates** — check on launch, or check on demand. **There is no channel to - choose**: a build updates from its own channel and no other. A production - install is offered production builds from `main`; a dev install is offered dev - builds from `dev`. The settings screen states which one is running rather than - offering a switch. +- **Updates** — check on launch, or check on demand. The channel is this + project's own Gitea releases ([docs/CI.md](docs/CI.md)), and **there is no + channel to choose**: a build updates from its own channel and no other. The + settings screen states which one is running rather than offering a switch. + `BuildConfig.OTA_ENABLED` is the kill switch, not a preference. The check on launch **only ever speaks when there is a build to install**. It runs behind the splash and waits for it to clear, and an app that is already - current — or a phone that cannot reach GitHub — says nothing, because opening - the app was not a question. The button in Settings is a question, so it - answers every time, including "you're on the latest" and whatever went wrong. + current — or a phone that cannot reach the release server — says nothing, + because opening the app was not a question. The button in Settings is a + question, so it answers every time, including "you're on the latest" and + whatever went wrong. Wanting both means installing both — they are separate apps and each updates itself, see [Both channels on one device](#both-channels-on-one-device). ## Branding -The launcher icon is a single square of the Wallace tartan set on the bias, and -the app opens on the same cloth carrying the name in a Celtic uncial hand for two -seconds before fading into the menu. +Neutral, original, and deliberately *not* Catan's: no third-party marks or +trade dress ship in this app (rebrand scope: WALA-118). The launcher icon is a +hexagonal ring around a solid core — a game tile, not any game's logo — drawn +as vector drawables so it stays sharp at every density and needs no PNGs. The +app opens on the brand field fading to its deep shade, the name set in a Celtic +uncial hand for two seconds before it fades into the menu. -A dev build wears **Wallace Hunting** instead — the same threadcount with green -where the clan sett has red — and installs as *Pro Libertate Games DEV*. That is -what tells the two copies apart when both are on the phone; see -[Both channels on one device](#both-channels-on-one-device). - -The tartan is drawn, not an image: `ui/theme/Tartan.kt` lays the sett down as -warp and then as weft at half opacity, which is what produces the blended -squares instead of a flat grid. The icon is the same construction as a vector -drawable, so it stays sharp at every density and needs no PNGs. +A dev build wears the **dev green** field where production wears **slate +blue**, and installs as *Pilgrims of Katan DEV*. That is what tells the two +copies apart when both are on the phone; see +[Both channels on one device](#both-channels-on-one-device). The palette lives +in `ui/theme/Theme.kt` (`BrandField`, `BrandDeep`, `BrandAccent`) and the two +channel colours are generated per build by `resValue` in `app/build.gradle.kts` +— the icon vectors reference `@color/brand_field` rather than a literal, which +is how one set of vectors serves both colourways. Two honest caveats: -- **The sett is the published threadcount: `K/4 R32 K32 Y/4`.** The slashes are - pivots, so `WallaceSett` holds the half-sett and reflects it out to - `K4 R32 K32 Y4 K32 R32` — 136 threads, about half black, half red, with a - yellow line worth 3%. Two details define the pattern and are easy to get - backwards: the yellow overstripe runs down the *centre of a wide black band*, - and the narrow black guard sits *between two red blocks*. The icon is - generated from the same numbers, so the icon and the splash cannot drift. - Weave scale is derived from the sett rather than fixed, so changing the - threadcount rescales both instead of silently zooming in. The hunting - colourway is the same threadcount passed a different field colour, not a - second copy of the numbers, so it cannot fall out of step either. - **The typeface is bundled under a licence.** Uncial Antiqua, © 2011 Brian J. Bonislawsky DBA Astigmatic, Reserved Font Name "Uncial Antiqua", used under the SIL Open Font License 1.1. The full licence is at [docs/licenses/UncialAntiqua-OFL.txt](docs/licenses/UncialAntiqua-OFL.txt) and must stay with the font — the OFL requires it to be distributed alongside. +- **The upstream's clan tartan is gone; its code is not.** The Wallace tartan + was the upstream's brand and left with the rebrand. The GPL-licensed history + that produced this app remains in git, and the source stays at + [adrianoftyriel/ProLibertateGames](https://github.com/adrianoftyriel/ProLibertateGames) + as the `upstream` remote. ## Screen sizes @@ -183,92 +186,70 @@ smaller than a fingertip. Requires JDK 17 and the Android SDK (compileSdk 34). -A local build is a dev build: it installs as `org.prolibertate.games.dev` under -the name *Pro Libertate Games DEV* in the hunting sett, so it goes on beside an -installed release rather than over it. Set `PLG_CHANNEL=production` to build the -production package instead. +A local build is a dev build: it installs as +`ca.wallacenet.pilgrimsofkatan.dev` under the name *Pilgrims of Katan DEV* on +the dev green field, so it goes on beside a production copy rather than over it. +Set `PLG_CHANNEL=production` to build the production package +(`ca.wallacenet.pilgrimsofkatan`, slate blue). ## CI +Full decision record: [docs/CI.md](docs/CI.md). + - `.github/workflows/ci.yml` — builds, tests and lints on `dev` and on pull - requests into `dev` or `main`. Its APK artifact is a dev build, versioned - `.0-dev` — the series being whatever `plgVersionSeries` says in - `gradle.properties`, currently **1.3**. -- `.github/workflows/release.yml` — on push to `main`, builds the APK and - publishes it as a GitHub Release tagged `v.`. The in-app updater - reads that tag, which is why the APK's `versionCode` is the same run number. - -### Installing without the app - -The newest production build is always at this link, which never changes: - -**https://github.com/adrianoftyriel/ProLibertateGames/releases/latest/download/ProLibertateGames.apk** - -Every release carries the same APK twice — once named for its version, and once -as plain `ProLibertateGames.apk`, because GitHub's `/releases/latest/download/` -redirect needs a file name that stays put. The in-app updater deliberately picks -the versioned copy, so an installed build can always be traced back to its tag. - -There is no equivalent link for the dev channel: `/releases/latest` skips -prereleases by design, and that is exactly what keeps a production install from -being handed a dev build. A dev build is reached by the in-app updater of a dev -install — which looks at the prerelease list rather than that endpoint — or from -the releases page. + requests into `dev` or `main`, and publishes a dev prerelease from `dev`. +- `.github/workflows/release.yml` — builds and publishes the production release + on push to `main`, or by hand (`workflow_dispatch`). +- Both publish through [scripts/publish-release.sh](scripts/publish-release.sh) + against Gitea's REST API, which is also runnable by hand. **No Actions runner + is registered on the instance yet**, so until one is, the local build is the + guaranteed path (`./gradlew assembleDebug && ./gradlew testDebugUnitTest`) + and releases go out via the script. ### The version series -A version is **`.`**. The series is set by hand, once, in -`gradle.properties`: +A version is **`.`**. Both values are set by hand, once, +in `gradle.properties`: ```properties -plgVersionSeries=1.3 +plgVersionSeries=1.4 +plgVersionRunOffset=1000 ``` -Both workflows read that same property when they name a release tag, and the -build reads it when it stamps the APK, so the tag and what is inside it cannot -name different versions. Bumping a series is a one-line change to that file and -nothing else. +Both workflows read those same properties when they name a release tag, and the +build reads them when it stamps the APK, so the tag and what is inside it cannot +name different versions. Changing either is a one-line change to that file and +nothing else — but they are not equivalent: -The number after it is the CI run number and keeps climbing on its own, which is -what makes a series bump safe: `versionCode` is the run number *alone*, so -going from 1.0 to 1.1 never looks like a downgrade to Android and never -interrupts an OTA update. The series is a name for people; the run number is -what the machines compare. +- The **series** is a name for people. Bumping it changes nothing about + ordering. +- The **build number** is what the machines compare: `versionCode`, the tag's + trailing number, and the number in `versionName`, all the same figure — + `CI run number + plgVersionRunOffset`. Because the offset never changes and + the run number only climbs, going from 1.4 to 1.5 never looks like a downgrade + to Android and never interrupts an OTA update. + +The offset exists because Gitea Actions restarts run numbers at 1 for each +workflow: without a floor, the first release of a fresh instance would collide +with the `versionCode 1` every unstamped local build carries, and a counter +reset would make Android refuse the next build outright. Full reasoning in +[docs/CI.md](docs/CI.md). ### Update channels -Both pipelines publish, to two separate channels: +`ci.yml` stamps builds of `dev` as dev-channel, `release.yml` stamps `main` as +production, so an APK's identity is decided where it is built. Publishing goes +to this project's Gitea releases, and the rules it keeps: -| Channel | Published by | Tag | Marked | -| --- | --- | --- | --- | -| Production | `release.yml`, push to `main` | `v.` | release | -| Dev | `ci.yml`, any build of `dev` | `v.-dev` | prerelease | - -Dev builds are published as **prereleases** deliberately: GitHub's "latest -release" endpoint skips prereleases, so a production-channel install can never -be handed a dev build even by accident. Pull requests build and check but never -publish, so a PR from a fork cannot ship anything. - -**Both pipelines gate publishing on the branch rather than on what triggered -the build**, so a manual run from the Actions tab publishes exactly as a push -does — useful when a push cannot fire the workflow itself, which is the case -for anything pushed by an automation token. The safety property is unchanged, -because publishing still happens only after that same job has built, tested and -linted clean. A pull request is still excluded without having to be named: a PR -build runs against `refs/pull//merge`, never `refs/heads/dev`. - -In `ci.yml` the `PLG_CHANNEL` stamp is gated on the same condition as the -publish steps, and the two have to stay in step. Stamping a build that then -ships, or shipping one that was left unstamped, both end with an APK claiming -`versionCode` 1 — which the updater can never see as an upgrade over anything. - -`release.yml` refuses to run off any branch but `main`, so a manual dispatch -aimed elsewhere does nothing rather than shipping unreviewed code. +- production comes only from `main` (`release.yml` refuses any other branch); +- dev prereleases are marked so the "latest release" endpoint can never hand one + to a production install; +- pull requests build and check but never publish. **The two channels have independent version sequences** — a build's `versionCode` -is the run number of the workflow that produced it, and the two workflows count -separately. Dev is on 69 while production is on 40, so the numbers say nothing -across the divide. +is the build number of the workflow that produced it (run number + offset), and +the two workflows count separately, so the numbers say nothing across the +divide. That is why **an install only ever updates within its own channel**, and why there is no setting for it. The app reads the `-dev` suffix in its own @@ -280,18 +261,18 @@ unit-tested on the JVM like the rules engines are. ### Both channels on one device -A dev build installs under its own `applicationId`, `org.prolibertate.games.dev`, -so it is a separate app as far as Android is concerned and can sit beside the -production copy rather than replacing it. Each channel still updates itself +A dev build installs under its own `applicationId`, +`ca.wallacenet.pilgrimsofkatan.dev`, so it is a separate app as far as Android +is concerned and can sit beside the production copy rather than replacing it. Each channel still updates itself normally, because an update within a channel keeps the same package. Three things move together, all off `isDevBuild` in `app/build.gradle.kts`: | | Production | Dev | | --- | --- | --- | -| `applicationId` | `org.prolibertate.games` | `org.prolibertate.games.dev` | -| Launcher name | Pro Libertate Games | Pro Libertate Games DEV | -| Sett | Wallace (red) | Wallace Hunting (green) | +| `applicationId` | `ca.wallacenet.pilgrimsofkatan` | `ca.wallacenet.pilgrimsofkatan.dev` | +| Launcher name | Pilgrims of Katan | Pilgrims of Katan DEV | +| Brand field | slate blue | deep green | Anything that is not an explicit `PLG_CHANNEL=production` build is a dev build, so a local `assembleDebug` is a dev build too and installs alongside a release @@ -301,11 +282,11 @@ They have to move together. A dev package wearing the production name and colours is precisely the mix-up that having both installed is meant to prevent, which is why one flag drives all three rather than each being set by hand. The name and the colours reach the resources through `resValue`, so `app_name`, -`tartan_field`, `splash_background` and `ic_launcher_background` are generated -by the build and are deliberately *not* in `res/values/` — the icon vectors -reference `@color/tartan_field` rather than a literal, which is how one set of -vectors serves both colourways. `BuildConfig.DEV_BUILD` carries the same flag -into Kotlin, where `AppSett` picks the matching Compose sett for the splash. +`brand_field` and `splash_background` are generated by the build and are +deliberately *not* in `res/values/` — the icon vectors reference +`@color/brand_field` rather than a literal, which is how one set of vectors +serves both colourways. `BuildConfig.DEV_BUILD` carries the same flag into +Kotlin, where `BrandField` picks the matching Compose colour for the splash. Because they are separate apps, having both is a matter of installing both, and each then updates itself down its own line. Nothing in the app moves a phone @@ -319,7 +300,7 @@ for a signature mismatch. ## Rules The exact rules implemented, and the options that vary them, are documented per -game: +game (index: [docs/README.md](docs/README.md)): - [Euchre](docs/RULES-euchre.md) - [Sequence](docs/RULES-sequence.md) — **note the caveat about the board layout** @@ -367,7 +348,8 @@ game/ rules engines and AI — pure Kotlin, no Android, unit-tested net/ wire protocol, LAN transport, lobby, match driver score/ scorekeeper sheet — pure Kotlin, unit-tested — and its store settings/ DataStore-backed preferences -update/ GitHub Releases OTA updater, and the channel rule — pure, tested +update/ OTA updater against this repo's Gitea releases, and the + channel rule — pure, tested ui/ Compose screens ``` diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4a0628f..0ef53d6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -16,6 +16,20 @@ val buildChannel = System.getenv("PLG_CHANNEL").orEmpty() val runNumber = (System.getenv("GITHUB_RUN_NUMBER") ?: "").toIntOrNull() ?: 0 val stampVersion = buildChannel.isNotEmpty() && runNumber > 0 +// The fixed floor every stamped build number is measured from, in +// gradle.properties. Gitea Actions restarts its run numbers at 1 for each +// workflow, so an unstamped build (versionCode 1) and the first release of a +// fresh instance would otherwise collide — and once a phone has installed +// versionCode N, Android refuses anything smaller as a downgrade. Adding a +// permanent offset is what makes "the counter never resets" true by +// construction: it is read from one place, it never changes, and the tag, +// versionName and versionCode all get the same number (see docs/CI.md). +val runOffset = (project.findProperty("plgVersionRunOffset") as String?)?.toIntOrNull() ?: 0 + +// The stamped build number: what the release tag, the versionName and the +// versionCode are all named after. +val buildNumber = runNumber + runOffset + // The version series, from gradle.properties. Both workflows read the same // property when they name a release tag, so the tag and the APK inside it // cannot disagree about which series they belong to. @@ -26,35 +40,40 @@ val versionSeries = (project.findProperty("plgVersionSeries") as String?) ?: "1. // // This one flag drives everything that makes a dev build a separate app rather // than a replacement for the production one — its own applicationId, its own -// launcher name, and the hunting sett in place of the clan sett. They have to -// move together: a dev package wearing the production name and colours is -// exactly the confusion having both installed is meant to avoid. +// launcher name, and its own brand colour. They have to move together: a dev +// package wearing the production name and colours is exactly the confusion +// having both installed is meant to avoid. val isDevBuild = buildChannel != "production" android { + // Deliberately left at the upstream source package: R and BuildConfig stay + // in org.prolibertate.games for both channels, so no source file has to + // care which applicationId it ended up installed as. The *installed* + // identity is the applicationId below — that is what the rebrand changes + // (WALA-118), not the JVM package names. namespace = "org.prolibertate.games" compileSdk = 34 defaultConfig { // A dev build installs under its own package, so a dev copy and a // production copy can sit side by side on one device instead of one - // replacing the other. The namespace above is deliberately left alone: - // R and BuildConfig stay in org.prolibertate.games for both, so no - // source has to care which package it ended up installed as. - applicationId = if (isDevBuild) "org.prolibertate.games.dev" else "org.prolibertate.games" + // replacing the other. + applicationId = if (isDevBuild) "ca.wallacenet.pilgrimsofkatan.dev" else "ca.wallacenet.pilgrimsofkatan" minSdk = 24 targetSdk = 34 // The -dev suffix in versionName is what the installed app reads to // know its own channel, so it must survive into the shipped APK. It // tracks isDevBuild rather than the stamp alone, so an unstamped local // build cannot end up claiming a channel its applicationId contradicts. - // versionCode stays the run number and nothing else, so it keeps + // versionCode is the stamped build number and nothing else, so it keeps // climbing across a change of series: bumping 1.0 to 1.1 must not make - // an update look like a downgrade to Android. - versionCode = if (stampVersion) runNumber else 1 + // an update look like a downgrade to Android. An unstamped build is 1, + // below every release, which is the honest answer for a build nobody + // stamped. + versionCode = if (stampVersion) buildNumber else 1 versionName = when { - stampVersion && buildChannel == "production" -> "$versionSeries.$runNumber" - stampVersion -> "$versionSeries.$runNumber-dev" + stampVersion && buildChannel == "production" -> "$versionSeries.$buildNumber" + stampVersion -> "$versionSeries.$buildNumber-dev" isDevBuild -> "$versionSeries.0-dev" else -> "$versionSeries.0" } @@ -64,23 +83,27 @@ android { resValue( "string", "app_name", - if (isDevBuild) "Pro Libertate Games DEV" else "Pro Libertate Games", + if (isDevBuild) "Pilgrims of Katan DEV" else "Pilgrims of Katan", ) - // The one colour that separates the two setts: Wallace red for - // production, Wallace Hunting green for dev. The icon vectors reference - // this rather than a literal, and Tartan.kt picks the matching Compose - // colour off DEV_BUILD below — the two have to stay in step or the - // launcher icon and the splash it opens on would be different cloth. - resValue("color", "tartan_field", if (isDevBuild) "#1E7A3C" else "#E01B24") - // Shades of the same field, so a launch does not flash the wrong colour - // before the tartan is drawn: splash_background is held behind the - // window for that instant, ic_launcher_background is the icon's flat - // fallback. - resValue("color", "splash_background", if (isDevBuild) "#0D2E17" else "#4A0B10") - resValue("color", "ic_launcher_background", if (isDevBuild) "#145C2D" else "#B3121B") + // The one colour that separates the two installs: slate blue on + // production, deep green on dev. The launcher icon and the splash are + // both built from it, so the drawer shows at a glance which of the two + // copies is which — the two have to stay in step or the icon and the + // splash it opens on would be different brands. + resValue("color", "brand_field", if (isDevBuild) "#2A6B54" else "#2B4A78") + // A darker shade of the same hue, held behind the window for the + // instant before the splash draws, so a launch never flashes the wrong + // colour. + resValue("color", "splash_background", if (isDevBuild) "#153B2F" else "#182A44") buildConfigField("boolean", "DEV_BUILD", isDevBuild.toString()) + + // In-app OTA updates, pointed at this project's own Gitea releases + // (docs/CI.md). True in this build — the channel exists. Flipping it + // false is the kill switch: every check then reports that and fetches + // nothing, with the machinery and its tests still compiled. + buildConfigField("boolean", "OTA_ENABLED", "true") } signingConfigs { @@ -108,7 +131,7 @@ android { buildFeatures { compose = true - // For DEV_BUILD, which is how the Compose tartan picks its sett. + // For DEV_BUILD, which is how the Compose splash picks its brand colour. buildConfig = true } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bdb6768..819e9ad 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,9 @@ - + @@ -23,7 +25,7 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:theme="@style/Theme.ProLibertateGames"> + android:theme="@style/Theme.PilgrimsOfKatan"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index 0e5a48e..d082dbf 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -1,15 +1,23 @@ + + android:fillColor="@color/brand_accent" + android:fillType="evenOdd" + android:pathData="M84,54 L69,79.98 L39,79.98 L24,54 L39,28.02 L69,28.02 Z M72,54 L63,69.59 L45,69.59 L36,54 L45,38.41 L63,38.41 Z M63,54 L58.5,61.79 L49.5,61.79 L45,54 L49.5,46.21 L58.5,46.21 Z" /> diff --git a/app/src/main/res/mipmap/ic_launcher.xml b/app/src/main/res/mipmap/ic_launcher.xml index 5dc5e35..69be1ce 100644 --- a/app/src/main/res/mipmap/ic_launcher.xml +++ b/app/src/main/res/mipmap/ic_launcher.xml @@ -1,70 +1,22 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + diff --git a/app/src/main/res/mipmap/ic_launcher_round.xml b/app/src/main/res/mipmap/ic_launcher_round.xml index 5dc5e35..69be1ce 100644 --- a/app/src/main/res/mipmap/ic_launcher_round.xml +++ b/app/src/main/res/mipmap/ic_launcher_round.xml @@ -1,70 +1,22 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 338becd..73a134c 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,16 +1,16 @@ #FF000000 #FFFFFFFF - - #0C0C0C - #F2C518 + + #EFD9A4 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 41e3314..a16a35a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,7 +1,7 @@ diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 492b38a..fb26528 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -2,7 +2,7 @@ -