Screens for Hearts and peg solitaire, and a seat for one

Both engines had been sitting behind ids with no screen. This gives them one
each and puts them in the catalogue.

Peg solitaire is the first game here played alone, and almost nothing had to
move for it. seatCountFor already fell through to minPlayers, so one seat came
free; CatalogTest's floor of two players is the only invariant that had to go,
and it went to one. The setup screen grows a solo path instead: a one-seat game
has nobody to play against and no seat anyone could join, so it offers "Play"
and not "Host a game for others to join", and the menu says "On your own"
rather than "1 players".

Playing again cannot go through advanceIdle. driveIdleSeats returns the moment
rules.isFinished is true, before it ever reaches the confirmation branch, and a
peg board with no jumps left genuinely is finished -- unlike a Wizard round,
which is only between deals. Rather than bend isFinished into meaning something
else, the screen rebuilds the controller behind a generation counter. The board
is deterministic, so the retry is the same puzzle rather than a new one.

One board layout serves both geometries. Each hole is placed in hole-widths
rather than pixels and a triangle offsets its rows by half a hole, which is the
same staggering that gives a triangular hole six neighbours instead of four.

The Hearts screen reads the phase because two different things happen at it:
choosing three cards to give away, which is a multiple selection nothing else
in the app does, and then playing one card at a time like every other trick
screen here. Passing is capped at three by the picker rather than by the error
the rules would otherwise raise.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Urn6eEBA6XzrnfoibL7faC
This commit is contained in:
Claude
2026-08-08 14:13:10 +00:00
parent 51aa0c3518
commit 434ea86605
7 changed files with 693 additions and 22 deletions
@@ -71,6 +71,8 @@ class CatalogTest {
GameCatalog.KAISER,
GameCatalog.CRAZY_EIGHTS,
GameCatalog.CRIBBAGE,
GameCatalog.HEARTS,
GameCatalog.PEG_SOLITAIRE,
GameCatalog.WIZARD,
GameCatalog.CHESS,
GameCatalog.TAYU,
@@ -87,7 +89,11 @@ class CatalogTest {
@Test
fun `player counts are sane`() {
GameCatalog.all.forEach { game ->
assertTrue("${game.id} min players", game.minPlayers >= 2)
// One, not two. Peg solitaire is played alone, and the engine took
// that without changes — one seat, always on the clock, nothing to
// redact. Everything downstream reads the seat count from here, so
// this is the only place that had to allow it.
assertTrue("${game.id} min players", game.minPlayers >= 1)
assertTrue("${game.id} max >= min", game.maxPlayers >= game.minPlayers)
}
}