Bootstrap Pilgrims of Katan: rebrand, Gitea CI and OTA
Build and Release APK / Build APK and publish release (push) Canceled after 0s

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 <[email protected]>
This commit is contained in:
adrianoftyriel
2026-10-13 22:03:27 +00:00
co-authored by Paperclip
parent d27c2db894
commit fad4778ad8
32 changed files with 760 additions and 773 deletions
+95 -113
View File
@@ -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
`<series>.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<series>.<run-number>`. 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 **`<series>.<run number>`**. The series is set by hand, once, in
`gradle.properties`:
A version is **`<series>.<build number>`**. 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<series>.<n>` | release |
| Dev | `ci.yml`, any build of `dev` | `v<series>.<n>-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/<n>/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
```