Commit Graph
15 Commits
Author SHA1 Message Date
Claude 284f9f3f95 Add Checkers, Backgammon, Mastermind, and Pirates and Bulgars
The four games that were listed but not written. Nothing in the
catalogue is marked "coming soon" any more.

Checkers is English draughts on the thirty-two dark squares, numbered
the way the game is written down. Captures are compulsory and a multiple
jump is one move object, so a player cannot stop halfway through one —
true by construction rather than by a rule that has to be enforced
somewhere. The move generator matches the published perft counts at one
to four ply (7, 49, 302, 1469), which fold in compulsory capture,
crowning and the men's forward-only rule all at once.

Backgammon's hard rule is not how a checker moves but that a player must
use as much of the roll as they can, and that cannot be checked one move
at a time: a move can be illegal precisely because playing it strands
the other die. So the generator works backwards from whole turns —
memoised depth-first over the remaining dice — and offers only the first
steps of the longest plays. Recomputing it after each checker is what
makes the rule true by construction. Every die comes from the table's
seed and a count of rolls made, which is in the state rather than beside
it so a game is reproducible from what crosses the wire. There is no
doubling cube; the rules document says so plainly.

Mastermind is played as a duel — both players are set a code and both
are breaking one — because the pencil-and-paper game gives the codemaker
nothing to do. It ends at the foot of a round, never mid-round, or going
first would be worth half a guess. It is the one game here with anything
to hide, so viewFor does real work: the host sends each device only its
own code, and a device that was never given a code refuses to score a
guess against it rather than quietly answering "nothing".

Pirates and Bulgars is Sackson's theming of Asalto, and the rules
document is explicit about which parts are attested and which had to be
chosen — the book could not be obtained. Two hunters who take by
jumping against twenty-four who cannot take at all, on a thirty-three
point cross whose drawn lines come from the same adjacency table the
moves come from, so the board and the rules cannot disagree.

130 unit tests across the four engines, all run locally as well as in
CI: game/ has no Android in it, which is what makes that possible.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019Pr6B8xDDtJJcb6WJMw7Ud
2026-08-07 07:58:12 +00:00
Claude 94b51347c3 Add Nine Men's Morris
Nine pieces each on twenty-four points: place them, then move them, and
every line of three takes an enemy piece off the board. Two pieces left,
or no legal move, and the game is over. See docs/RULES-morris.md.

The board is numbered ring * 8 + index, so its geometry falls out
arithmetically rather than being typed in as a table of twenty-four
rows: neighbours around a square are the index either side, the ladders
between squares run through the odd indices alone, and a mill is three
consecutive points centred on an odd one. A table would say the same
thing and could be wrong in one row.

A turn is one move object — the piece placed or stepped, and the enemy
piece taken when that closes a mill. Generating the removal as part of
the move rather than as a turn of its own means there is no moment where
a player is halfway through a turn, so a device that drops off the
network between closing a mill and choosing what to take cannot leave
the table stuck, and the search counts one ply for what is one decision.

Three rules about mills are easy to get backwards and all three are
tested: the piece that arrives is what closes a mill, so stepping out
takes nothing and stepping back in pays again; a slide along the line
being completed is not a mill, because the third piece it needs is the
one that just moved; and a piece in a mill is spared unless every enemy
piece is in one, without which a player could never be reduced and the
game would not end.

The AI is negamax with alpha-beta and iterative deepening over the same
rules engine, with a node budget so the placing phase does not stall a
phone. It has no quiescence tail and does not need one: there are no
exchanges in Morris. Its evaluation knows that being down to three is
dangerous even with flying on — without that term the search reads three
pieces going anywhere as freedom and starts steering towards losing
pieces to get there, which is a real game it would throw away.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019Pr6B8xDDtJJcb6WJMw7Ud
2026-08-07 05:52:26 +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 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 0a4dca87f5 Merge dev: Ta Yü
Rivers laid out from the centre of an 18x18 board on 1x3 tiles, two or four
players, against a computer opponent held to measured numbers.

Clean merge: the Ta Yü work touches only its own package, its screen, and the
three wiring points every game goes through, so it does not overlap the guest
resync work already on dev. PlayerSlot.deviceId arrived there in the meantime
but the GameRules contract itself is unchanged, so the new engine needed no
adjustment for it.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015pyhC1KyFkQk6hUnnX2Tof
2026-08-06 18:16:10 +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 d165e5efff Show every player the table from the moment the game starts
A guest saw nothing until the next state push, which on most tables means
until its own turn came round: no board, no cards, no scoreboard. The
cause is a gap in who is listening. A guest's link is opened in the lobby
and read there; the table only subscribes to it once the host's start
message has been through a state change, a recomposition and a
LaunchedEffect. The host publishes the opening position immediately, into
a link whose table has not attached yet, and incoming is a SharedFlow with
replay = 0 — so that first push went to the lobby's collector and nowhere
else. Not a rare race but the normal order of events, which is why it
looked like a rule rather than a bug.

The guest now asks. A new Resync message is sent from inside
onSubscription, so the request cannot leave before the collector is
attached and the reply cannot arrive before there is anything to receive
it. The host answers one peer with that seat's redacted view. A request
that arrives before the deal needs no answer, since a peer that has asked
is listening and the opening publish will reach it.

Second reason a guest could not see its hand, on any table with more than
one guest: guests worked out their own seat by matching PlayerSlot.peerId,
which is the address the host's transport invented for the incoming link —
an IP on Wi-Fi, a MAC over Bluetooth. A guest has never seen that value,
so no guest ever matched it and every one fell through to "the first seat
held by somebody remote". With one guest that is right by luck; with two,
both claimed the same seat and the other drew a hand belonging to a seat
it was not sitting in, which redacted is no hand at all.

The host now records the id a guest introduced itself by, as
PlayerSlot.deviceId, kept separate from peerId because the two answer
different questions: peerId is how the host recognises a link, deviceId is
how a guest recognises itself. The lookup moved out of LobbyScreen into
seatForDevice, where it can be tested, and a dropped player stops claiming
its old seat.

Neither change needs a protocol bump. Resync is ignored by a host that
does not know it, deviceId is an unknown key to a guest that does not read
it, and the old fallback is kept for exactly that case.

Tests reproduce the real start-up order — host publishes, then the guest
subscribes a frame later — rather than the order that happens to work, and
cover a board, a dealt hand, and a guest that is on the clock at the deal.
Both regressions were confirmed to fail before the fix: the guest had no
state at all, and two guests both came back as seat 1.

216 JVM tests pass.
2026-08-06 17:14:33 +00:00
Claude 0927e735a2 Fix the hang that made a networked game impossible to leave
Leaving a multiplayer game froze the app for good. Closing a link closed
the BufferedReader first, and BufferedReader.close() wants the very lock
readLine() holds for the whole of a blocking read — a read that only
closing the socket underneath it can ever end, which is the step the
close never reached. Both callers run on the thread drawing the screen
(AppRoot.pop and PlayScreen's onDispose), so the main thread waited on
itself and the app stopped responding. runCatching was no help: nothing
was thrown, it simply never came back.

StreamConnection.close() now closes only the raw streams, which is what
actually ends the read; the reader coroutine then closes its own wrapper
on its own thread, where it owns the lock. Leaving takes single-digit
milliseconds instead of never returning.

It bit chess because chess is the one game here worth leaving in the
middle of. A card hand is over in minutes; a game of chess between two
people who have had enough of it has no natural end, and there was
nothing to end it with, so:

  - Chess has an End game action, with a confirmation, so a table can be
    walked away from without hunting for a way out.

  - A link that drops on its own now reports itself on the same channel
    as everything else, so the device still at the table is told the
    other player has left rather than waiting on a move that is not
    coming. Bye was in the protocol and never sent; nothing needed to
    change on the wire for this.

  - Leaving a table no longer lands back in the lobby that started it. A
    lobby starts advertising the moment it is drawn, so "leave the game"
    used to put the player straight into another one.

Two crashes of the same shape on the same path, both in coroutines
launched in the screen's own scope where nothing catches anything:

  - The host's AI applied a move chosen before it took the lock, checking
    only that the seat had not changed. applyMove throws on a move that
    is no longer legal. Harmless while an AI answered instantly, which
    stopped being true when the chess search moved off-thread and started
    taking seconds. It is now checked against the position as it stands,
    and a stale choice is made again rather than forced.

  - A table the rules reject, or a state a client cannot decode, took the
    app down. Both are now reported.

Covered by MatchControllerNetworkTest, which plays a game over a real
socket pair and leaves it from either end. The leaving test runs on a
daemon thread and asserts it returns, so a regression fails the build
rather than hanging it — verified by reinstating the old close(), which
hangs the suite until it is killed.
2026-08-06 14:44:01 +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 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 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 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 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