Play onto the pile rather than instead of it

President drew only the set on top. Everything played before it was in
the state and simply not on the screen, so beating a pair looked like
the previous pair being taken away and replaced rather than covered.
The pile is the point of the game — what is under the top set is the
history of the round — and it was invisible.

The whole pile is now drawn in one loop keyed by position in it. The top
set is laid out square and side by side, because that is the set that
has to be beaten and the only one that has to be readable; everything
under it is pushed out towards the rim of the ground the top set covers,
so a corner shows. Buried dead centre is buried, and then drawing it
achieves nothing.

Keying by position is what makes the transition work. A superseded set
is the same composable it always was, so when its target moves from a
row slot to the rim it animates from wherever it actually is — no
bookkeeping about where it used to sit, and no card vanishing from the
row to reappear under the heap a frame later. The throw stays relative
to wherever a card belongs, so the two compose: a card lands in its
slot, and is shoved aside from it later without the landing having to
know.

The heap scatter is also now its own pair of bounds, looser than the
table's: eight degrees and fifteen per cent of a card width against four
and a half and five. This was not cosmetic. At the tight bounds a
discard pile of five cards was five cards hidden to within a twentieth
of a card width of each other, which reads as one card — Crazy 8s and
Golf were technically stacking and visibly not. A card keeps whichever
scatter it was given for as long as it lies there, since a card that
shuffled sideways the moment something landed on it would be a card
moving on its own.

Cribbage's series is the same shape of thing and was already keeping its
cards, so it gains the landing to match; its hand gains the tilt the
other hands have.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0132V9gE6DF9itz2z3Hn7BK9
This commit is contained in:
Claude
2026-08-13 22:00:58 +00:00
parent 7c839a9242
commit a7427b0cb5
4 changed files with 261 additions and 42 deletions
@@ -98,6 +98,60 @@ class CardPhysicsTest {
assertNotEquals(cardRest(cardSeed(card, 0)), cardRest(cardSeed(card, 1)))
}
/**
* A heap is untidier than a card laid in front of a player, and has to be:
* at the tight bounds five cards on a discard pile are hidden to within a
* twentieth of a card width of each other and the pile looks like one card.
*/
@Test
fun `a heap lies looser than a table but still within bounds`() {
deck.forEach { card ->
val seed = cardSeed(card)
val heap = heapRest(seed)
val table = cardRest(seed)
assertTrue(abs(heap.tiltDegrees) <= HEAP_TILT_DEGREES)
assertTrue(abs(heap.driftX) <= HEAP_DRIFT_FRACTION)
assertTrue(abs(heap.driftY) <= HEAP_DRIFT_FRACTION)
assertTrue(abs(heap.tiltDegrees) >= abs(table.tiltDegrees))
assertTrue(abs(heap.driftX) >= abs(table.driftX))
}
}
/**
* The two have to agree about everything except how far the card strayed,
* or a card would jump when it was drawn one way rather than the other.
*/
@Test
fun `a heap and a table agree about the throw`() {
deck.take(6).forEach { card ->
val seed = cardSeed(card)
assertEquals(cardRest(seed).spinDegrees, heapRest(seed).spinDegrees, 0f)
assertEquals(cardRest(seed).lateness, heapRest(seed).lateness, 0f)
}
}
/**
* A buried card has to end up near the rim of whatever is covering it. One
* left in the middle is one nobody can see, and then drawing the pile as a
* pile achieves nothing.
*/
@Test
fun `a buried card ends up out where a corner of it shows`() {
deck.forEach { card ->
val (x, y) = buriedOffset(cardSeed(card), halfWidth = 100f, halfHeight = 40f)
val howFarOut = kotlin.math.hypot(x / 100f, y / 40f)
assertTrue("$card sat at $howFarOut of the way out", howFarOut >= 0.719f)
assertTrue(howFarOut <= 1.001f)
}
}
/** Two cards buried under the same set must not end up in the same place. */
@Test
fun `buried cards do not stack on each other`() {
val places = (0 until 6).map { buriedOffset(cardSeed(deck[it], it), 100f, 40f) }
assertEquals(places.size, places.toSet().size)
}
/** Every seat's card arrives from that seat, and from a card-width or two out. */
@Test
fun `a card comes in from its own player's side`() {