Files
ProLibertateGames/README.md
T
Claude 532c3ea48a Add President and Golf
Two more games on the existing framework — no changes were needed to the
lobby, networking, settings or update layers, which is what the shared
GameRules/GameAi contract was for.

President
- Threes low, twos high. Lead any number of one rank; follow with the
  same count of a higher rank, or pass. When everyone has passed the
  pile comes down and the last player to play leads again.
- Toggles: player count, rounds, twos clear the pile, four of a kind
  bombs, and the between-round card exchange.
- Plays are offered as whole sets rather than by selecting cards. A set
  is the unit of the game, so choosing from the legal sets is both
  easier to operate and impossible to get wrong.

Golf
- 4, 6 or 9 card grids. Draw from stock or discard, then either swap the
  card in or — only if it came off the stock — throw it and turn one of
  your own over. A card taken from the discard pile has to be used.
- Matching columns cancel to nothing; on the 3x3 board rows do too, but
  not on the shallower grids where three cards are not a line.
- Face-down cards are hidden from everyone including their owner, which
  is the point of the game: viewFor masks them with a fixed placeholder
  so the real card never reaches the device that has not turned it over.

Both engines are pure Kotlin and unit tested — 71 tests now, covering
ranking, legal moves, pile and hole resolution, scoring, redaction, wire
round-trips, and full random and AI playthroughs. The Golf test also
checks the AI actually beats random play rather than merely being legal.

Also widened the setup screen's option chips to wrap: the reveal-count
row carries up to ten, and a chip off the edge of the screen is a
setting that cannot be changed — the same failure as the Euchre Pass
button.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qjfMiPUDshdKEUa6vxqzN
2026-08-06 03:25:09 +00:00

169 lines
7.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Pro Libertate Games
A collection of card and board games for Android, playable against the computer
or against other people over Wi-Fi (LAN) or Bluetooth.
## What works today
The app shell is complete and two games are playable end to end:
| | |
| --- | --- |
| **Playable** | Euchre, Sequence, President, Golf |
| **Listed, not yet implemented** | Kaiser, Wizard, Crazy 8s, Mastermind, Backgammon, Chess, Checkers |
Unimplemented games appear in the menu marked *Coming soon* **on dev builds
only** — a production release lists just the games that can actually be played,
so nobody installs it and taps into a dead end. The menu decides this from the
installed APK's own version name, so it follows the build rather than the
channel selected for future updates. Adding one means
writing a `GameRules` implementation, a `GameAi`, and a screen, then flipping
`available` in `GameCatalog` — the menu, lobby, networking, settings and OTA
layers are game-agnostic and need no changes.
## Playing with other people
Pick a game, choose **Host a game for others to join**, and the device advertises
itself on every radio it has. Other devices choose **Join a game nearby** and
pick the host from the list. Seats start out filled by the computer and are
handed to people as they join, so any mix of humans and AI works — whatever is
still marked *Computer* when the host starts stays AI.
- **Wi-Fi (LAN)** — both devices must be on the same network. Discovery uses
mDNS/NSD; play runs over a TCP socket.
- **Bluetooth** — devices must already be **paired** in Android's Bluetooth
settings. Play runs over Classic RFCOMM. Scanning for unpaired devices is not
implemented.
Both transports can be live at once, so some players can be on Wi-Fi and others
on Bluetooth at the same table.
### How a match is kept in step
The host is authoritative. Clients send *intents* ("I would like to play this
card") and the host re-validates every one against the rules before it takes
effect — a client cannot make an illegal move by sending a malformed message.
Each client is then sent a state **redacted for its own seat**, so opponents'
hands and the deck order are never transmitted to devices that should not see
them.
## Settings
- **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, from either of two
channels:
- **Production** — stable builds, published from `main`.
- **Dev** — preview builds, published from `dev` on every green CI run.
## Branding
The launcher icon is a single square of the Wallace tartan, and the app opens on
a tartan splash carrying the name in a Celtic uncial hand for two seconds before
fading into the menu.
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.
Two honest caveats:
- **The sett is an approximation.** `WallaceSett` reproduces what the Wallace
tartan reads as — a bold red and black check divided by a narrow gold
overstripe — but the registered threadcount could not be verified, so the
numbers were chosen to look right rather than to be authoritative. They are
plain data; replacing them with the registered counts rebrands the whole app
and needs no other change.
- **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.
## Screen sizes
There are no fixed orientations or hard-coded card sizes. Layouts measure the
space they are given: the menu grid reflows by width, the Sequence board is
always square and sized to fit whatever is left after the hand, and card widths
are a fraction of the smaller screen dimension. Rotation is handled inside the
activity, so a hand is not lost when the device turns.
## Building
```sh
./gradlew assembleDebug # APK at app/build/outputs/apk/debug/
./gradlew testDebugUnitTest # rules engines and AI
```
Requires JDK 17 and the Android SDK (compileSdk 34).
## CI
- `.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
`1.0.0-dev`.
- `.github/workflows/release.yml` — on push to `main`, builds the APK and
publishes it as a GitHub Release tagged `v1.0.<run-number>`. The in-app updater
reads that tag, which is why the APK's `versionCode` is the same run number.
### Update channels
Both pipelines publish, to two separate channels:
| Channel | Published by | Tag | Marked |
| --- | --- | --- | --- |
| Production | `release.yml`, push to `main` | `v1.0.<n>` | release |
| Dev | `ci.yml`, push to `dev` | `v1.0.<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.
`release.yml` additionally refuses to run off any branch but `main`, so a manual
dispatch aimed elsewhere does nothing rather than shipping unreviewed code.
**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. Versions are therefore only ever compared *within* a channel; the app
reads the `-dev` suffix in its own `versionName` to know which channel it is on,
and treats a change of channel as an explicit switch rather than an upgrade.
One consequence: switching channels can be a downgrade as far as Android is
concerned, in which case the install is refused until the current copy is
uninstalled. The settings screen says so when it applies.
The debug keystore is committed on purpose: every build is signed with the same
key, so an OTA update installs over the previous one instead of being rejected
for a signature mismatch.
## Rules
The exact rules implemented, and the options that vary them, are documented per
game:
- [Euchre](docs/RULES-euchre.md)
- [Sequence](docs/RULES-sequence.md) — **note the caveat about the board layout**
- [President](docs/RULES-president.md)
- [Golf](docs/RULES-golf.md)
## Layout
```
game/ rules engines and AI — pure Kotlin, no Android, unit-tested
cards/ deck, suits, ranks
engine/ GameRules / GameAi contracts, seats, table config
euchre/ Euchre model, rules, AI
sequence/ Sequence model, board, rules, AI
president/ President model, rules, AI
golf/ Golf model, scoring, rules, AI
net/ wire protocol, LAN and Bluetooth transports, lobby, match driver
settings/ DataStore-backed preferences
update/ GitHub Releases OTA updater
ui/ Compose screens
```
Keeping `game/` free of Android types is deliberate: the same code drives the
AI, the host's authoritative state and the JVM unit tests.