fix: replace DiceRoll sentinel value (-1) with nullable Int? (Closes #78) (#120)

This commit was merged in pull request #120.
This commit is contained in:
2026-04-04 21:46:24 +02:00
parent 240b994900
commit c94341df62
9 changed files with 26 additions and 24 deletions
@@ -29,7 +29,8 @@ fun DiceRollResultDialog(
) { ) {
val ones = diceRoll.result.count { it == 1 } val ones = diceRoll.result.count { it == 1 }
val isGlitch = ones > diceRoll.numberOfDice / 2 val isGlitch = ones > diceRoll.numberOfDice / 2
val isCriticalGlitch = isGlitch && diceRoll.numberOfSuccesses == 0 val successes = diceRoll.numberOfSuccesses ?: 0
val isCriticalGlitch = isGlitch && successes == 0
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
@@ -55,19 +56,19 @@ fun DiceRollResultDialog(
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium
) )
Text( Text(
text = "${diceRoll.numberOfSuccesses} successes", text = "$successes successes",
modifier = Modifier.testTag(TestTags.DICE_ROLL_SUCCESS_COUNT), modifier = Modifier.testTag(TestTags.DICE_ROLL_SUCCESS_COUNT),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = if (diceRoll.numberOfSuccesses > 0) color = if (successes > 0)
Color(0xFF4CAF50) else MaterialTheme.colorScheme.onSurface Color(0xFF4CAF50) else MaterialTheme.colorScheme.onSurface
) )
} }
// Threshold result for simple tests // Threshold result for simple tests
if (threshold != null) { if (threshold != null) {
val success = diceRoll.numberOfSuccesses >= threshold val success = successes >= threshold
val netHits = if (success) diceRoll.numberOfSuccesses - threshold else 0 val netHits = if (success) successes - threshold else 0
Surface( Surface(
color = if (success) Color(0xFF4CAF50).copy(alpha = 0.12f) color = if (success) Color(0xFF4CAF50).copy(alpha = 0.12f)
else MaterialTheme.colorScheme.errorContainer, else MaterialTheme.colorScheme.errorContainer,
@@ -62,11 +62,11 @@ fun ExtendedTestResultDialog(
// Per-round breakdown // Per-round breakdown
var runningTotal = 0 var runningTotal = 0
result.rolls.forEachIndexed { index, roll -> result.rolls.forEachIndexed { index, roll ->
runningTotal += roll.numberOfSuccesses runningTotal += roll.numberOfSuccesses ?: 0
RoundEntry( RoundEntry(
roundNumber = index + 1, roundNumber = index + 1,
poolSize = roll.numberOfDice, poolSize = roll.numberOfDice,
hits = roll.numberOfSuccesses, hits = roll.numberOfSuccesses ?: 0,
cumulativeHits = runningTotal, cumulativeHits = runningTotal,
results = roll.result, results = roll.result,
testTag = TestTags.extendedRoundEntry(index) testTag = TestTags.extendedRoundEntry(index)
@@ -141,7 +141,7 @@ fun HealingDialog(
style = MaterialTheme.typography.bodySmall style = MaterialTheme.typography.bodySmall
) )
Text( Text(
text = "Successes: ${result.diceRoll.numberOfSuccesses}", text = "Successes: ${result.diceRoll.numberOfSuccesses ?: 0}",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium fontWeight = FontWeight.Medium
) )
@@ -53,11 +53,11 @@ fun OpposedTestResultDialog(
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium
) )
Text( Text(
text = "${result.attackRoll.numberOfSuccesses} hits", text = "${result.attackRoll.numberOfSuccesses ?: 0} hits",
modifier = Modifier.testTag(TestTags.OPPOSED_ATTACK_HITS), modifier = Modifier.testTag(TestTags.OPPOSED_ATTACK_HITS),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = if (result.attackRoll.numberOfSuccesses > 0) color = if ((result.attackRoll.numberOfSuccesses ?: 0) > 0)
Color(0xFF4CAF50) else MaterialTheme.colorScheme.onSurface Color(0xFF4CAF50) else MaterialTheme.colorScheme.onSurface
) )
} }
@@ -80,11 +80,11 @@ fun OpposedTestResultDialog(
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium
) )
Text( Text(
text = "${result.defenseRoll.numberOfSuccesses} hits", text = "${result.defenseRoll.numberOfSuccesses ?: 0} hits",
modifier = Modifier.testTag(TestTags.OPPOSED_DEFENSE_HITS), modifier = Modifier.testTag(TestTags.OPPOSED_DEFENSE_HITS),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = if (result.defenseRoll.numberOfSuccesses > 0) color = if ((result.defenseRoll.numberOfSuccesses ?: 0) > 0)
Color(0xFF4CAF50) else MaterialTheme.colorScheme.onSurface Color(0xFF4CAF50) else MaterialTheme.colorScheme.onSurface
) )
} }
@@ -16,7 +16,7 @@ data class DiceRoll(
val numberOfSides: Int = 6, val numberOfSides: Int = 6,
val numberForSuccessOrHigher: Int = 5, val numberForSuccessOrHigher: Int = 5,
val result: List<Int> = listOf(), val result: List<Int> = listOf(),
val numberOfSuccesses: Int = -1 val numberOfSuccesses: Int? = null
) { ) {
fun roll(numberForSuccessOrHigher: Int = 5): DiceRoll { fun roll(numberForSuccessOrHigher: Int = 5): DiceRoll {
val newResult = rollXDice(sides = this.numberOfSides, numberOfDice = this.numberOfDice) val newResult = rollXDice(sides = this.numberOfSides, numberOfDice = this.numberOfDice)
@@ -33,8 +33,9 @@ data class DiceRoll(
*/ */
fun performSimpleTest(pool: Int, threshold: Int): SimpleTestResult { fun performSimpleTest(pool: Int, threshold: Int): SimpleTestResult {
val roll = DiceRoll(numberOfDice = pool).roll() val roll = DiceRoll(numberOfDice = pool).roll()
val success = roll.numberOfSuccesses >= threshold val successes = roll.numberOfSuccesses ?: 0
val netHits = if (success) roll.numberOfSuccesses - threshold else 0 val success = successes >= threshold
val netHits = if (success) successes - threshold else 0
return SimpleTestResult( return SimpleTestResult(
diceRoll = roll, diceRoll = roll,
threshold = threshold, threshold = threshold,
@@ -51,7 +52,7 @@ fun performSimpleTest(pool: Int, threshold: Int): SimpleTestResult {
fun performOpposedTest(attackPool: Int, defensePool: Int): OpposedTestResult { fun performOpposedTest(attackPool: Int, defensePool: Int): OpposedTestResult {
val attackRoll = DiceRoll(numberOfDice = attackPool).roll() val attackRoll = DiceRoll(numberOfDice = attackPool).roll()
val defenseRoll = DiceRoll(numberOfDice = defensePool).roll() val defenseRoll = DiceRoll(numberOfDice = defensePool).roll()
val netHits = attackRoll.numberOfSuccesses - defenseRoll.numberOfSuccesses val netHits = (attackRoll.numberOfSuccesses ?: 0) - (defenseRoll.numberOfSuccesses ?: 0)
return OpposedTestResult( return OpposedTestResult(
attackRoll = attackRoll, attackRoll = attackRoll,
defenseRoll = defenseRoll, defenseRoll = defenseRoll,
@@ -76,7 +77,7 @@ fun performExtendedTest(pool: Int, targetHits: Int, maxRolls: Int = 10): Extende
if (currentPool <= 0) break if (currentPool <= 0) break
val roll = DiceRoll(numberOfDice = currentPool).roll() val roll = DiceRoll(numberOfDice = currentPool).roll()
rolls.add(roll) rolls.add(roll)
cumulativeHits += roll.numberOfSuccesses cumulativeHits += roll.numberOfSuccesses ?: 0
if (cumulativeHits >= targetHits) { if (cumulativeHits >= targetHits) {
return ExtendedTestResult( return ExtendedTestResult(
rolls = rolls.toList(), rolls = rolls.toList(),
@@ -41,7 +41,7 @@ class DiceRollHistory(private val maxEntries: Int = 20) {
label = label, label = label,
poolSize = roll.numberOfDice, poolSize = roll.numberOfDice,
results = roll.result, results = roll.result,
successes = roll.numberOfSuccesses, successes = roll.numberOfSuccesses ?: 0,
sequenceNumber = nextSequence++, sequenceNumber = nextSequence++,
rollType = rollType rollType = rollType
) )
@@ -93,7 +93,7 @@ object HealingSystem {
damageTrack = damageTrack, damageTrack = damageTrack,
dicePool = dicePool, dicePool = dicePool,
diceRoll = roll, diceRoll = roll,
boxesHealed = roll.numberOfSuccesses boxesHealed = roll.numberOfSuccesses ?: 0
) )
} }
@@ -55,7 +55,7 @@ class DiceTest {
// successes from the stale empty result list. // successes from the stale empty result list.
val initial = DiceRoll(numberOfDice = 20) val initial = DiceRoll(numberOfDice = 20)
assertEquals(listOf<Int>(), initial.result, "Initial result should be empty") assertEquals(listOf<Int>(), initial.result, "Initial result should be empty")
assertEquals(-1, initial.numberOfSuccesses, "Initial numberOfSuccesses should be -1") assertEquals(null, initial.numberOfSuccesses, "Initial numberOfSuccesses should be null")
val rolled = initial.roll() val rolled = initial.roll()
assertEquals(20, rolled.result.size, "Rolled result should have 20 dice") assertEquals(20, rolled.result.size, "Rolled result should have 20 dice")
@@ -23,7 +23,7 @@ class RollTypeTest {
atLeastOneSuccess = true atLeastOneSuccess = true
assertTrue(result.netHits >= 0, "Net hits should be >= 0 on success") assertTrue(result.netHits >= 0, "Net hits should be >= 0 on success")
assertEquals( assertEquals(
result.diceRoll.numberOfSuccesses - result.threshold, result.diceRoll.numberOfSuccesses!! - result.threshold,
result.netHits, result.netHits,
"Net hits = successes - threshold" "Net hits = successes - threshold"
) )
@@ -46,7 +46,7 @@ class RollTypeTest {
val result = performSimpleTest(pool = 5, threshold = 0) val result = performSimpleTest(pool = 5, threshold = 0)
assertTrue(result.success, "Zero threshold should always succeed") assertTrue(result.success, "Zero threshold should always succeed")
assertEquals( assertEquals(
result.diceRoll.numberOfSuccesses, result.diceRoll.numberOfSuccesses!!,
result.netHits, result.netHits,
"Net hits should equal successes when threshold is 0" "Net hits should equal successes when threshold is 0"
) )
@@ -61,7 +61,7 @@ class RollTypeTest {
val result = performOpposedTest(attackPool = 8, defensePool = 6) val result = performOpposedTest(attackPool = 8, defensePool = 6)
assertEquals(8, result.attackRoll.result.size, "Attacker should roll 8 dice") assertEquals(8, result.attackRoll.result.size, "Attacker should roll 8 dice")
assertEquals(6, result.defenseRoll.result.size, "Defender should roll 6 dice") assertEquals(6, result.defenseRoll.result.size, "Defender should roll 6 dice")
val expectedNetHits = result.attackRoll.numberOfSuccesses - result.defenseRoll.numberOfSuccesses val expectedNetHits = result.attackRoll.numberOfSuccesses!! - result.defenseRoll.numberOfSuccesses!!
assertEquals(expectedNetHits, result.netHits, "Net hits = attack successes - defense successes") assertEquals(expectedNetHits, result.netHits, "Net hits = attack successes - defense successes")
assertEquals(expectedNetHits > 0, result.attackerWins, "Attacker wins if net hits > 0") assertEquals(expectedNetHits > 0, result.attackerWins, "Attacker wins if net hits > 0")
} }
@@ -86,7 +86,7 @@ class RollTypeTest {
repeat(20) { repeat(20) {
val result = performExtendedTest(pool = 10, targetHits = 1, maxRolls = 10) val result = performExtendedTest(pool = 10, targetHits = 1, maxRolls = 10)
assertTrue(result.rolls.isNotEmpty(), "Should have at least one round") assertTrue(result.rolls.isNotEmpty(), "Should have at least one round")
val manualSum = result.rolls.sumOf { it.numberOfSuccesses } val manualSum = result.rolls.sumOf { it.numberOfSuccesses ?: 0 }
assertEquals(manualSum, result.cumulativeHits, "Cumulative hits should be sum of per-round hits") assertEquals(manualSum, result.cumulativeHits, "Cumulative hits should be sum of per-round hits")
assertEquals(result.rolls.size, result.roundsUsed, "Rounds used should match number of rolls") assertEquals(result.rolls.size, result.roundsUsed, "Rounds used should match number of rolls")
} }