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
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
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
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
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
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
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
- 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
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