Let a scoreboard actually be dismissed

The pause between hands never ended. driveIdleSeats asks
awaitsConfirmation about the state, and the state does not change while
the players are reading it — so confirmAdvance restarted the loop, the
loop stopped on the same scoreboard, and the next hand was never dealt.
The screens hid their own dialog on the way out, so the table looked
released and then simply sat there.

The release is now something spendable rather than a question re-asked
about an unchanged state: confirmAdvance grants one, and the loop
consumes it to walk past the hold exactly once.

This is not new with cribbage — Golf's card, Wizard's bids against
tricks and the Crazy 8s round score all stop on the same hold and all
end there. Cribbage's show is what made it impossible to miss, because
a game of cribbage is nothing but hands one after another.

Covered by MatchControllerScoreboardTest, which plays a whole game of
one-move hands and insists every one of them stops to be read and then
goes on.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DCuRESmSZeJoJond43itw2
This commit is contained in:
Claude
2026-08-07 19:11:59 +00:00
parent 27af034095
commit 136f5bc4f1
2 changed files with 176 additions and 3 deletions
@@ -98,6 +98,18 @@ class MatchController<S : Any, M : Any>(
private val lock = Mutex()
private var sequence = 0
/**
* A scoreboard a player has read and released, waiting to be let past.
*
* The hold belongs to a particular scoreboard rather than to the phase, and
* releasing it has to be spent: [awaitsConfirmation] is asked about the
* state, which has not changed while the players were reading it, so
* without something consumable here the same state would stop the table
* again the instant [confirmAdvance] restarted the loop — and the next hand
* would never be dealt.
*/
private var released = false
private val peers = mutableMapOf<String, Connection>()
private var hostLink: Connection? = null
@@ -288,8 +300,15 @@ class MatchController<S : Any, M : Any>(
if (role == Role.HOST && awaitsConfirmation(current)) {
// Hold here until a player has read the scoreboard. Only the
// host waits; clients simply render what they were sent.
_awaitingConfirmation.value = true
return
val letThrough = lock.withLock {
val granted = released
released = false
granted
}
if (!letThrough) {
_awaitingConfirmation.value = true
return
}
}
delay(aiThinkingMillis() * 2)
val advanced = advanceIdle(current)
@@ -398,7 +417,10 @@ class MatchController<S : Any, M : Any>(
if (!_awaitingConfirmation.value) return
_awaitingConfirmation.value = false
if (role != Role.HOST) return
scope.launch { driveIdleSeats() }
scope.launch {
lock.withLock { released = true }
driveIdleSeats()
}
}
/** Lets a player go on looking at a table they have been told is over. */