Commit Graph
18 Commits
Author SHA1 Message Date
Claude 97386aa942 Lay the scorekeeper out as a score sheet
The same sheet, drawn as the paper it replaces: a column per player with
their name at the head, a row per round running down the page, and the
running total ruled off at the foot.

The bottom row is always the one being filled in. Finishing it writes it
down and opens a fresh empty row above the totals, so the place to type
is always in the same spot. The head and the totals sit outside the
scrolling rounds and share one horizontal scroll state with them, which
is what keeps a column and the name over it from ever coming apart —
every row is built by the same composable and is therefore exactly the
same width, which is what lets them share it.

Reordering moves to where a spreadsheet puts it: drag a name sideways
and the whole column travels with it. Tapping a name renames it or
removes that player, tapping a round number down the left edge opens
that row for correction, and a + column at the right adds a player. The
mode toggle and the round dialog are gone with them.

Points still go in as typed text until the row is finished, so a
half-typed minus is not a number yet. The ± button flips the last number
typed: not every keyboard offers a minus on its number pad, and taking
points away has to work on all of them.

adjustResize is now stated in the manifest rather than left to the
system to resolve. This screen types into the bottom row with the totals
and the finish button below it, and they have to stay above the keyboard
rather than being panned off the screen.

The model is untouched: ScoreSheet and its tests did not need to change
for any of this, which is the point of it having no Android in it.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019Pr6B8xDDtJJcb6WJMw7Ud
2026-08-07 03:53:00 +00:00
Claude b532749093 Add a scorekeeper for games the app does not deal
A pencil, reachable from the main menu: say how many are playing, name
them, and write down what each scored every round. The tally sits at the
foot of the screen because that is the one number anybody looks up.

Points are recorded against a player's id rather than their position,
which is what lets the players be dragged into a different order without
their scores moving with them. Ids are never reused, so somebody sitting
down where somebody else got up starts at nought instead of inheriting a
stranger's score. Removing a player does take their points with them: a
score nobody can see is a score nobody can correct.

The drag leaves the sheet alone until the finger lifts. Rows move on
screen as the drag passes them, but the positions the gesture is working
from cannot shift underneath it, and the handlers read the sheet through
rememberUpdatedState so a drag cannot be applied to a stale copy and
throw away a name typed since.

Rounds are not written in ink — tapping one reopens it for correction or
strikes it out — and the sheet is saved as it is edited, so a phone call
does not lose the game. It lives in its own DataStore rather than with
the settings: a game in progress is not a preference.

score/ScoreSheet.kt has no Android in it, for the same reason game/ does
not, and every rule above is unit-tested on the JVM.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019Pr6B8xDDtJJcb6WJMw7Ud
2026-08-06 23:33:11 +00:00
Claude 7939395c3c Install dev and production side by side, in the hunting sett
A dev build now installs under its own applicationId, org.prolibertate.games.dev,
so it is a separate app to Android and sits beside the production copy instead
of replacing it. Each channel still updates itself normally, because an update
within a channel keeps the same package.

Three things move together off one isDevBuild flag, because a dev package
wearing the production name and colours is exactly the mix-up that having both
installed is meant to prevent:

  - applicationId gets a .dev suffix;
  - the launcher label becomes "Pro Libertate Games DEV";
  - the cloth becomes Wallace Hunting, the same threadcount with green where
    the clan sett has red.

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 rather than declared in res/values. The icon vectors reference
@color/tartan_field instead of a literal, which is how one set of vectors serves
both colourways without a second copy of the paths. BuildConfig.DEV_BUILD
carries the same flag into Kotlin, where AppSett picks the Compose sett for the
splash, and WallaceHuntingSett is derived from the same threadcount builder as
WallaceSett so the two cannot drift.

versionName now tracks isDevBuild rather than the CI stamp alone, so an
unstamped local build cannot claim a channel its applicationId contradicts.

Separate packages also mean a channel switch can no longer be refused as a
downgrade; the settings copy and the README said it could, and now say what
actually happens.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JDuAayWXLRoUNTsfE79Mg1
2026-08-06 22:07:32 +00:00
Claude 43394b8d45 Remove Bluetooth from multiplayer
Multiplayer now runs over Wi-Fi (LAN) only.

- Delete BluetoothTransport and drop it from LobbyController's transport
  list, leaving LanTransport as the sole transport.
- Drop TransportKind.BLUETOOTH and the now-unused RFCOMM_UUID constant.
- Remove the BLUETOOTH/BLUETOOTH_ADMIN/BLUETOOTH_CONNECT permissions and
  the android.hardware.bluetooth feature from the manifest, along with
  the runtime permission request in MainActivity and the
  requestBluetoothPermissions hook on AppEnv.
- Update lobby copy so an empty transport list and an empty host list
  talk about Wi-Fi rather than pairing, and refresh the comments and
  README sections that described two transports.

The transport abstraction itself is kept: everything after the handshake
was already transport independent, so adding another one later needs no
changes above LanTransport.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YEsqYAMJKUJoM7UAavm169
2026-08-06 21:38:35 +00:00
Claude e17272b909 Publish the dev channel from any build of dev, not only a push
The dev prerelease steps were gated on the event as well as the branch, so a
manual run from the Actions tab built, tested and linted dev and then skipped
the publish. That matters because a push does not always fire the workflow:
anything pushed by an automation token is suppressed by GitHub's own recursion
guard, which leaves the dev channel stuck on an older build with no way to move
it from the Actions tab.

Gated on the branch alone now, which is how release.yml has always gated main —
the inconsistency was between the two pipelines, not a deliberate rule. Nothing
about the safety property changes: publishing still only happens after the same
job has built, tested and linted clean, and a pull request is still excluded
without being named, because a PR build runs against refs/pull/N/merge and
never refs/heads/dev.

PLG_CHANNEL moves to the same condition, which is the part that would bite if
it were missed. It decides whether the APK is stamped with its run number, and
the publish steps decide whether that APK ships. Leave them out of step and the
dev channel gets an APK claiming versionCode 1 — an "update" the installed app
can never see as newer than what it already has.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015pyhC1KyFkQk6hUnnX2Tof
2026-08-06 19:04:25 +00:00
Claude cdd515afc6 Add Ta Yü
Rivers laid out from the centre of an 18x18 board on 1x3 tiles, each with
three mouths that must line up with whatever they touch. One side runs water
north and south, the other east and west, and a side's score is one edge
multiplied by the other — so reaching only one of your two edges is worth
nothing, which is the whole tension of the game.

Two things fell out of the geometry rather than needing to be tabulated. The
tile set is generated: three mouths chosen from the eight places one can sit
on a 1x3 tile gives 56 arrangements, turning a tile end for end pairs them
off with none fixed, so there are exactly 28 distinct tiles and the box's 84
is three of each. And because every tile joins the network by a mouth while
all three of its own mouths interconnect, the board is always one connected
river system — so scoring never traces connectivity, and a mouth at the edge
is an exit by construction. Both properties are asserted over whole games.

The placement check is one comparison doing two jobs: where a new tile abuts
an old one, both sides must agree. That refuses a river dead-ending against
stone and, read the other way, refuses a blank flank that would wall off a
river already down.

18x18 comes out under 20dp a cell on a phone, too fine to tap, so laying a
tile is a tap to line it up and a button to commit. Tapping the same cell
again cycles the other legal ways the tile could cover it, which is also how
it gets turned — a rotate control would have landed on illegal positions. A
tap on a cell the tile cannot cover is ignored rather than treated as a
deselection, so a fingertip one cell wide of the mark does not throw away a
placement.

Two AI settings, not three. A blocking-heavy variant was written and dropped
because it played the full setting dead even over 30 games; shipping it as a
step up would have been a claim that could not be backed. What ships is held
to measured numbers: full strength won 20 of 20 against random placement from
either axis, gentle beats random about three games in four and loses to full
strength about four in five, and random against random comes out even, which
is what makes the rest of those numbers mean anything.

The reconstruction's uncertainties stay called out in the rules doc rather
than being smoothed over now that the code exists: the 18x18 board reading,
the marked exits placed at 3, 8 and 13 by choice, and the three-player flood
variant left out because nothing found says how the flood scores.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015pyhC1KyFkQk6hUnnX2Tof
2026-08-06 18:09:10 +00:00
Claude 9f71e0844b Specify Ta Yü ahead of building it, and list it as coming
Ta Yü has been out of print since the 2009 Goliath reissue, so the rulebook
had to be reconstructed from secondary sources — and this machine's network
policy blocks everything but search summaries, so BGG's rules files were out
of reach too.

The tile set fell out of the geometry rather than needing a component list.
A 1x3 tile has eight places a river mouth can sit; three mouths chosen from
eight gives 56 arrangements, and turning a tile end for end pairs them off
with no arrangement fixed, leaving exactly 28 distinct tiles. The reissue
has 84 tiles and the 1999 original had 112 — 3x28 and 4x28. Twelve of the 28
touch three flanks of the rectangle, which is what the ringed centre stud on
the reissued tiles marks. Three independent numbers agreeing is why the
geometry is worth trusting.

What could not be pinned down is called out in the doc rather than smoothed
over: the board is taken as 18x18 because that is the reading under which
"the four centre indentations" is true, the marked exits are placed at 3, 8
and 13 by choice, and the three-player flood variant is left out entirely
because nothing found says how the flood scores, and inventing that would be
shipping a different game. Same posture as the Sequence board layout.

The catalogue entry is added with available = false, so it shows as coming
soon on dev builds and stays out of production until the engine lands.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015pyhC1KyFkQk6hUnnX2Tof
2026-08-06 17:18:47 +00:00
Claude 331a620f83 Publish a fixed-name APK so the latest build has a permanent link
GitHub's /releases/latest/download/<name> redirect needs a file name
that stays put, and the release asset is named after its version. Each
production release now carries the same build twice: the versioned copy,
and a plain ProLibertateGames.apk, which makes this link permanent:

  .../releases/latest/download/ProLibertateGames.apk

Only the production channel gets one. /releases/latest skips
prereleases, which is exactly what stops a production install being
handed a dev build, so there is no equivalent dev link to offer.

The updater now picks its asset by name rather than taking the first
.apk it sees. Both copies are byte-identical, so nothing would have
broken either way, but relying on upload order for which file gets
installed is not something to leave lying around.
2026-08-06 15:33:44 +00:00
Claude d6e237ce4a Add chess
The full game: castling with every condition, en passant including the
case where taking would expose your own king, promotion to any of the
four, pins, checkmate, stalemate, insufficient material, the fifty-move
rule and threefold repetition.

Move generation is checked against the published perft counts rather
than against hand-written cases — the opening array to depth four
(197,281 positions) plus four standard test positions chosen precisely
because they break generators. Those counts are exact, so matching them
leaves very little room for a rule to be subtly wrong.

The opponent is a negamax search with alpha-beta, iterative deepening
and a capture-only quiescence tail, at three strengths. The quiescence
tail is the part that stops it hanging pieces: a fixed-depth search that
stops mid-exchange scores the position as though the recapture never
comes.

Two supporting changes:

The AI now thinks off the caller's thread. A card AI answers instantly,
but a chess search runs for a second or more, and the match controller's
scope belongs to the screen — searching there would freeze the board it
is drawn on.

Offline setup can seat the local player somewhere other than seat 0,
which is what lets you play Black. Seat 0 is White by definition, so
choosing a colour has to choose a seat.

FEN is read and written, so a table can start from a set position.

203 engine tests pass locally.
2026-08-06 13:34:31 +00:00
Claude fb70396d09 Add Kaiser, Crazy 8s and Wizard
Three more card games, engine to screen.

Kaiser: 32-card prairie trick-taking with bidding, the five of hearts
worth five and the three of spades costing three. Bidding is capped at
ten because ten is all a hand can be worth, and a losing score gives the
game a floor — without one a table that keeps bidding and going down
never reaches a result.

Crazy 8s: eights are wild and name the suit in force, which is why the
table states that suit in words rather than leaving it to be read off
the pile. Blocked rounds are scored rather than hanging.

Wizard: the 60-card deck, one more card dealt each round until the pack
runs out, bid your tricks exactly. The shared card model gains WIZARD
and JESTER ranks, so every ordinary deck now builds from Rank.standard
rather than walking the whole enum.

Also fixes a lobby bug this would have hit: hosting seated the
catalogue minimum rather than the table the host had just set up, so a
six-handed game would have been rejected by its own rules engine.

164 engine tests pass locally.
2026-08-06 10:21:23 +00:00
Claude f5a2626196 Use the published Wallace threadcount, and scale the weave to the sett
Two separate faults. The pattern was wrong, and the scale was broken.

Pattern: the sett is published as K/4 R32 K32 Y/4, the slashes marking
pivots, which reflects out to K4 R32 K32 Y4 K32 R32 — 136 threads, about
half black and half red with a yellow line worth 3%. I had the structure
inverted: the yellow overstripe belongs down the centre of a wide black
band, and the narrow black guard belongs between two red blocks. Mine put
yellow between narrow blacks and the wide black elsewhere, which is why it
read as a generic check rather than as Wallace.

Scale: thread width was a fixed fraction of the screen while the sett grew
from 84 threads to 216, so a repeat became 933dp on a 360dp phone and only
0.39 of one was ever visible. The cloth had zoomed into a few enormous
blocks and stopped being a weave at all — that is the part that got worse,
not the colours. Thread width is now derived from the sett, so a couple of
full repeats are on screen whatever the threadcount says, and changing the
count rescales instead of silently magnifying.

The icon is generated from the same numbers at one repeat across the
adaptive safe zone.

Threadcount cross-checked against two independent sources rather than
guessed; recorded in Tartan.kt and the README so the next change starts
from the published figures.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qjfMiPUDshdKEUa6vxqzN
2026-08-06 04:14:08 +00:00
Claude 510fd40d86 Rework the Wallace tartan from a reference, and set it on the bias
The old sett was three flat stripes in roughly equal measure, which read
as a generic red-and-black check rather than as Wallace. Rebuilt against
a reference swatch.

- Setts are now written as a half-sett and reflected about their pivots,
  the way a threadcount is actually published, so the data stays in the
  form it is quoted in instead of a flat list kept symmetric by hand.
- Wallace is gold 6, black 6, red 36, black 24, red 36, black 6: a red
  ground at about two thirds, black bands alternating wide and narrow,
  and one thin gold overstripe per repeat. The reflection puts black on
  both sides of the gold line, which is what the reference shows.
- Colours taken from the swatch: a brighter scarlet and a stronger gold
  than before.
- Both the cloth and the icon are turned 45 degrees. The renderer draws
  well past the canvas edges so the corners stay covered once rotated.

The icon vector is generated from the same numbers rather than written
out by hand, so the icon and the splash cannot drift apart — changing
the sett rebrands both.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qjfMiPUDshdKEUa6vxqzN
2026-08-06 04:04:51 +00:00
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
Claude dbe76c34c4 Brand the app with the Wallace tartan and a Celtic splash
- The launcher icon is a single square of the tartan, drawn as a vector
  so it stays sharp at any density and ships no PNGs. The adaptive
  icon's foreground is deliberately empty: the cloth is the icon.
- Tartan.kt draws the sett as warp, then as weft at half opacity. That
  overlap is what gives the blended squares and reads as woven cloth
  rather than a printed grid, and it is reused by both the icon and the
  splash.
- The app opens on the tartan under "Pro Libertate Games" in Uncial
  Antiqua, holds two seconds, then fades. It is an overlay rather than a
  destination, so the menu is already composed behind it and the fade
  reveals a live screen. The window background is set to a dark red so
  launch does not flash white before Compose draws.

Two things to be aware of:

- WallaceSett is an approximation. It reads as Wallace — bold red and
  black check with a narrow gold overstripe — but the registered
  threadcount could not be verified here, so the counts were chosen to
  look right. They are plain data and can be swapped for the registered
  ones without touching anything else.
- Uncial Antiqua is bundled under the SIL Open Font License 1.1, with
  the full licence at docs/licenses/UncialAntiqua-OFL.txt. The OFL
  requires that to be distributed with the font, so it must not be
  separated from it.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qjfMiPUDshdKEUa6vxqzN
2026-08-06 02:52:55 +00:00
Claude ce83f7c2c4 Hide unfinished games from production builds
A release listed all eleven games with nine of them marked "Coming
soon", so most of the menu was a dead end for anyone installing from the
production channel. Production now lists only what can be played; dev
builds still show everything, which is where seeing the roadmap is
useful.

The menu reads the installed APK's own version name, so this follows the
build rather than the update channel selected for the future — a dev
build shows the full list even if its owner has switched back to
production for updates.

CatalogTest pins both views, including that filtering never empties a
category and strands its heading.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qjfMiPUDshdKEUa6vxqzN
2026-08-06 02:45:56 +00:00
Claude 1bcd2a951e Add dev/production update channels, clearer Euchre table, bolder Sequence highlight
Update channels
- ci.yml now publishes each green push to dev as a prerelease tagged
  v1.0.<n>-dev, so the dev channel has something to install. Workflow
  artifacts could not serve this: downloading one needs authentication,
  which the app does not have.
- Prereleases specifically, because GitHub's "latest release" endpoint
  skips them — a production install can never be handed a dev build.
  Pull requests build and check but never publish.
- Settings gains a channel picker. The two channels are numbered by
  separate workflow run counters, so versions are only compared within a
  channel; the app reads the -dev suffix in its own versionName to know
  where it stands, and treats a channel change as an explicit switch. The
  screen warns that such a switch can be a downgrade Android will refuse.
- PLG_RELEASE becomes PLG_CHANNEL, naming the channel rather than a flag.

Euchre
- Cards in a trick now sit on the side of the table belonging to whoever
  played them, rotated to face them, labelled You / Left / Partner /
  Right. Previously they were a flat row tagged with seat numbers, which
  made it hard to tell who had played what.
- Bidding says what it will actually do. As dealer the button reads "Pick
  it up" rather than "Order up", since the turn card comes to your hand,
  and a line of explanatory text spells out the consequence either way.

Sequence
- Playable squares get a much heavier outline, scaled off cell size and
  in a fixed amber that reads against a white card, a green corner and a
  chip alike. The old 2dp theme-coloured border was hard to see.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qjfMiPUDshdKEUa6vxqzN
2026-08-06 00:18:59 +00:00
Claude 80ff178437 Pin OTA releases to main and stop dev builds outranking them
Two ways the updater could have served or blocked the wrong build:

- release.yml also triggers on workflow_dispatch, which can be aimed at
  any branch from the Actions UI. That would have built dev code and
  published it as a real Release, which every install would then offer to
  download. The job now refuses to run off anything but main.

- versionCode came from GITHUB_RUN_NUMBER, but that counter is per
  workflow, so the dev pipeline had its own independent sequence. A debug
  APK installed from a CI artifact already carried a higher versionCode
  than the first several releases would, and the updater's check is
  latest <= installed, so those users would have been told they were up
  to date indefinitely. Only release.yml now sets PLG_RELEASE, and only
  that stamps a real version; everything else stays at 1.0.0-dev.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qjfMiPUDshdKEUa6vxqzN
2026-08-05 23:47:20 +00:00
Claude 487c68419e Add app shell with Euchre and Sequence, LAN/Bluetooth play
First vertical slice: the whole shell plus two games playable end to end,
so every layer is exercised before the other nine games are built on it.

Games
- Euchre: bowers, ordering up, dealer discard, going alone, stick the
  dealer, and full scoring. Rule variants (deck size, points to win,
  stick the dealer, going alone) are toggles in game setup.
- Sequence: two-eyed and one-eyed jacks, free corners, locked sequences,
  the shared-chip limit, and dead-card exchange. Teams, players per team
  and the sequence target are toggles.
- The board layout in SequenceBoard is generated, not the commercial
  board — it holds every structural property the rules need, but does not
  match a physical board. Documented in docs/RULES-sequence.md.

Multiplayer
- One transport abstraction over newline-delimited JSON, implemented
  twice: LAN via NSD discovery and TCP, Bluetooth via Classic RFCOMM.
  A lobby advertises on both at once, so a table can mix them.
- Host-authoritative: clients send move intents, the host re-validates
  every one against the rules, and each client receives a state redacted
  for its own seat so hidden information never crosses the wire.
- Seats start as AI and are handed to people as they join, which is what
  makes any mix of humans and AI work without a separate code path.

Shell
- Main menu lists all eleven games; the nine without engines are marked
  coming soon and are driven entirely by GameCatalog.
- Settings: sound, animation speed (one multiplier drives card movement
  and AI thinking time), and OTA updates from the latest GitHub release.
- Layouts measure available space rather than assuming an orientation.

The game package is free of Android types, so the rules engines and AI
run under plain JUnit; 43 tests cover ranking, following suit, scoring,
sequence detection, redaction, wire round-trips and full random and AI
playthroughs.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qjfMiPUDshdKEUa6vxqzN
2026-08-05 22:37:05 +00:00