Keeping dice is a set, not a list
Holding dice did nothing unless they happened to be tapped left to right. The screen appends each die as it is tapped, so holding the third and then the first produced [2, 0]. legalMoves only ever generated ascending subsets, and RollDice is a data class over a List, so [2, 0] was a different move from [0, 2] and simply was not among the legal ones. MatchController drops a move it cannot find there, silently -- hence a highlighted die, a Throw that did nothing, and no error to go on. A keep is a set of positions. Holding the third die and then the first is the same act as holding the first and then the third, and a set says so: the two are now equal, and the order they were built up in cannot matter. That kills the whole class of bug rather than sorting at the one call site that happened to show it. Positions rather than faces still stands -- two fives are two different dice. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Urn6eEBA6XzrnfoibL7faC
This commit is contained in:
@@ -28,9 +28,8 @@ object YahtzeeAi : GameAi<YahtzeeState, YahtzeeMove> {
|
||||
if (rolls.isEmpty()) return best ?: writes.first()
|
||||
if (bestScore >= WORTH_KEEPING) return best!!
|
||||
|
||||
val keep = whatToKeep(state.dice)
|
||||
return rolls.firstOrNull { it.keep.sorted() == keep.sorted() }
|
||||
?: rolls.first()
|
||||
val keep = whatToKeep(state.dice).toSet()
|
||||
return rolls.firstOrNull { it.keep == keep } ?: rolls.first()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -132,10 +132,16 @@ sealed interface YahtzeeMove
|
||||
* Positions rather than faces: two fives are two different dice, and a player
|
||||
* who keeps one of them and rerolls the other is doing something a set of faces
|
||||
* could not express.
|
||||
*
|
||||
* A set rather than a list, because holding the third die and then the first is
|
||||
* the same act as holding the first and then the third. As a list those were two
|
||||
* different moves, and only the ascending one was ever offered as legal — so
|
||||
* keeping dice in any other order was silently dropped and the throw did
|
||||
* nothing.
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("roll")
|
||||
data class RollDice(val keep: List<Int> = emptyList()) : YahtzeeMove
|
||||
data class RollDice(val keep: Set<Int> = emptySet()) : YahtzeeMove
|
||||
|
||||
@Serializable
|
||||
@SerialName("score")
|
||||
|
||||
@@ -53,7 +53,7 @@ object YahtzeeRules : GameRules<YahtzeeState, YahtzeeMove> {
|
||||
// The first throw of a turn is all five dice: there is nothing on
|
||||
// the table yet to keep.
|
||||
moves += if (!state.hasRolled) {
|
||||
listOf(RollDice(emptyList()))
|
||||
listOf(RollDice(emptySet()))
|
||||
} else {
|
||||
keepSubsets().map { RollDice(it) }
|
||||
}
|
||||
@@ -67,9 +67,9 @@ object YahtzeeRules : GameRules<YahtzeeState, YahtzeeMove> {
|
||||
}
|
||||
|
||||
/** Every set of dice positions a player might hold back — thirty-two of them. */
|
||||
private fun keepSubsets(): List<List<Int>> =
|
||||
private fun keepSubsets(): List<Set<Int>> =
|
||||
(0 until (1 shl DICE_COUNT)).map { mask ->
|
||||
(0 until DICE_COUNT).filter { mask and (1 shl it) != 0 }
|
||||
(0 until DICE_COUNT).filter { mask and (1 shl it) != 0 }.toSet()
|
||||
}
|
||||
|
||||
override fun applyMove(state: YahtzeeState, seat: Int, move: YahtzeeMove): YahtzeeState {
|
||||
@@ -83,7 +83,6 @@ object YahtzeeRules : GameRules<YahtzeeState, YahtzeeMove> {
|
||||
|
||||
private fun applyRoll(state: YahtzeeState, move: RollDice): YahtzeeState {
|
||||
require(state.rollsUsed < ROLLS_PER_TURN) { "No rolls left this turn" }
|
||||
require(move.keep.distinct().size == move.keep.size) { "Cannot keep a die twice" }
|
||||
require(move.keep.all { it in 0 until DICE_COUNT }) { "No such die" }
|
||||
require(state.hasRolled || move.keep.isEmpty()) { "Nothing to keep before the first roll" }
|
||||
return state.copy(
|
||||
@@ -92,7 +91,7 @@ object YahtzeeRules : GameRules<YahtzeeState, YahtzeeMove> {
|
||||
)
|
||||
}
|
||||
|
||||
private fun throwDice(state: YahtzeeState, keep: List<Int>): List<Int> {
|
||||
private fun throwDice(state: YahtzeeState, keep: Set<Int>): List<Int> {
|
||||
val random = Random(
|
||||
state.seed +
|
||||
state.round * 10_000L +
|
||||
|
||||
@@ -113,7 +113,7 @@ fun YahtzeeScreen(
|
||||
if (yours && canRoll) {
|
||||
Button(
|
||||
onClick = {
|
||||
controller.submit(RollDice(kept.toList()))
|
||||
controller.submit(RollDice(kept.toSet()))
|
||||
// What was kept applied to the throw just made; the next
|
||||
// one starts from whatever is on the table now.
|
||||
kept.clear()
|
||||
|
||||
Reference in New Issue
Block a user