Commit Graph
20 Commits
Author SHA1 Message Date
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 d30ea8656f Fix section ordering in the Golf rules
The opening-cards section had been dropped into the middle of the table
setup list, stranding the discard pile and deck-count bullets after it.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qjfMiPUDshdKEUa6vxqzN
2026-08-06 04:00:16 +00:00
Claude d989907d7e Golf: 8-card grids, chosen openers, final putt, end-of-hole score card
- An 8-card variant, laid out as two rows of four. The existing column
  scoring covers it without change.

- Opening cards are now chosen rather than dealt face up. Nothing starts
  revealed; each player in turn turns over their allowance, and the hole
  begins once everyone has picked. This needed a new SETUP phase in the
  engine, since it is a real decision and has to be validated like any
  other move rather than being decided at deal time.

- "Line up the final putt", off by default: throw a drawn card away
  without turning your last face-down card over, so you are not forced to
  close the hole on a card you did not choose. Only offered on the last
  face-down card. Because a whole table could otherwise decline forever,
  two full laps of nothing but thrown cards calls the hole and scores it
  where it stands.

- The end of a hole now shows a score card — every player's strokes for
  the hole and their running total, lowest first — and the next hole is
  not dealt until it is dismissed. MatchController gained a general
  awaitsConfirmation hook for this, so any game wanting a scoreboard
  between rounds can stop the table the same way. Only the host waits;
  clients render what they are sent.

Nine more Golf tests, covering the 8-card layout, the opening choice,
when the putt option is and is not offered, and that a table refusing to
putt out still finishes.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qjfMiPUDshdKEUa6vxqzN
2026-08-06 03:59:53 +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 b49bfc0fe6 Fix splash screen constraint scoping
BoxWithConstraints exposes maxWidth and maxHeight on its own scope, but
inside Column the implicit receiver becomes ColumnScope and they are no
longer reachable, so the two font-size expressions failed to compile.

Read the constraints once into locals at the top of the BoxWithConstraints
body, which is what the Euchre and Sequence screens already do.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qjfMiPUDshdKEUa6vxqzN
2026-08-06 02:59:32 +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 60ddcdf67f Caption Sequence jacks as WILD or REMOVE
A jack's power is carried by its artwork on a real deck — drawn in
profile it shows one eye and removes a chip, drawn face-on it shows two
and is wild. These cards render as rank and suit only, so J♦ and J♠ were
indistinguishable in hand despite doing opposite things.

Jacks in hand now carry the word on their face, and the hint under the
hand says what tapping will do rather than the generic "tap a highlighted
square", which was wrong for a one-eyed jack anyway — those target an
opponent's chip, not a highlighted empty square.

The caption is a general PlayingCardView option, so any later game whose
cards carry meaning beyond rank and suit can use it.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qjfMiPUDshdKEUa6vxqzN
2026-08-06 02:39:51 +00:00
Claude 39a312b157 Stop the AI ordering up every deal; hold and sweep finished tricks
The real cause of being forced to pick up the turn card was the AI, not
the Pass button. Measured over 2000 deals, the computer ordered up on
100% of them and the dealer was dragged into picking up on 97.2% —
bidding never once reached the second round, so a human dealer never got
a say. Average hand scored about 11 against a bid threshold of 9, so
essentially every hand bid.

- Voids no longer count towards a hand unless there is trump left to ruff
  with, which was inflating precisely the weak hands that should pass.
- Thresholds recalibrated against the actual score distribution.
- EuchreBiddingRateTest pins the result so this cannot drift back: now
  32.8% ordered up in round one, dealer forced to pick up on 12.8%, and
  round two reached on 67%.

Finished tricks are now held and swept, for this and any later
trick-taking game:

- The engine kept a trick only until it resolved, so the fourth card was
  never visible — it vanished the instant it landed. Completed tricks are
  now retained in state until the next card is played.
- The table holds them for a second, names the winner, then slides all
  four towards that player's edge and fades them out. The hold scales
  with the animation speed setting.
- MatchController gains a general holdBeforeNextMove hook so the AI waits
  out the animation rather than playing over it.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qjfMiPUDshdKEUa6vxqzN
2026-08-06 01:40:43 +00:00
Claude 76c136718d Let the player name field be cleared
Clearing the name put "Player" straight back before a real one could be
typed. Three things combined to cause it:

- setPlayerName coerced a blank value to "Player" on write, so deleting
  the last character immediately persisted the default.
- The settings flow then emitted that default, and the text field's
  remember was keyed on the stored name, so it re-seeded itself from the
  value that had just been written.
- The default also lived in the stored value, so a fresh install had
  "Player" pre-filled and had to be cleared to begin with.

The stored name is now whatever the user typed, blank included. The
default moved to Settings.displayName, which is what the lobby and seat
labels use, so it applies where a name is shown rather than to what is
being edited. The field keeps its own edit state once touched, and shows
"Player" as placeholder text with a note explaining the fallback.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qjfMiPUDshdKEUa6vxqzN
2026-08-06 01:30:36 +00:00
Claude 0e387ad6d4 Stop the Euchre Pass button being pushed off screen
Passing was always legal in the first round of bidding, but the buttons
were laid out in a horizontally scrolling row with Pass appended last.
On a phone "Pick it up" and "Pick it up, alone" filled the width, so Pass
sat off the right edge and bidding looked compulsory. The second round
was worse, with up to six suit buttons ahead of it.

The bid options now wrap instead of scrolling, and Pass is lifted out of
that group into its own full-width button underneath, where it cannot be
clipped or scrolled away.

Stick the dealer is the one case where there genuinely is no Pass, so it
now explains itself rather than looking like the same missing button.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qjfMiPUDshdKEUa6vxqzN
2026-08-06 01:26:13 +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 85ec10836b Request only the Bluetooth permission the app actually uses
Lint flagged ACCESS_FINE_LOCATION declared without ACCESS_COARSE_LOCATION,
which Android 12 requires to be requested together.

Rather than add the missing one, drop location entirely: the Bluetooth
transport only talks to already-paired devices, enumerating bonded
devices and opening an RFCOMM socket. Neither is a scan, so no location
access is needed. BLUETOOTH_SCAN and BLUETOOTH_ADVERTISE go for the same
reason — nothing was using them.

Discovery of unpaired devices would bring all of these back; the manifest
notes what that would require.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qjfMiPUDshdKEUa6vxqzN
2026-08-05 22:56:27 +00:00
Claude 4ad0f52331 Fix serialization import and quieten CI build logs
- Transport.kt used the reified Json.encodeToString extension without
  importing kotlinx.serialization.encodeToString, so it resolved to the
  two-argument member overload and failed to compile.
- Opt in to ExperimentalMaterial3Api for the clickable game tile.
- Drop --stacktrace from the CI build. A one-line Kotlin error was being
  buried under 150 lines of Gradle internals, which made the actual
  message hard to find in the log.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qjfMiPUDshdKEUa6vxqzN
2026-08-05 22:50:15 +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
Claude 4312616efa Add CI and release pipelines for Android APK builds
Mirrors the Holio setup:

- release.yml: on push to main (e.g. a merged PR), builds the debug APK
  and publishes it as a GitHub Release tagged v1.0.<run-number>.
- ci.yml: pre-merge checks on the dev branch and on PRs targeting dev or
  main — same build plus unit tests and lint, uploaded as artifacts only.

Both expect a Gradle Android project at the repo root with an :app module,
matching Holio's layout.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qjfMiPUDshdKEUa6vxqzN
2026-08-05 21:54:13 +00:00
adrianoftyriel a5258c8ee6 Initial commit 2026-08-05 15:50:56 -06:00