// // Bunch_O__DiceTests.swift // Bunch O' DiceTests // // Created by Rory Hinnen on 22/2/20. // import Testing @testable import Bunch_O__Dice // MARK: - Dice Total Calculation @Suite("Dice Total Calculation") struct DiceTotalTests { // One die showing each face — a clean baseline for cutoff tests. func oneEach() -> Dice { var d = Dice() d.one = 1; d.two = 1; d.three = 1; d.four = 1; d.five = 1; d.six = 1 return d } @Test("No modifier counts faces at or above cutoff") func totalNoModifier() { let d = oneEach() #expect(d.total(cutoff: 6, modifier: .none) == 1) #expect(d.total(cutoff: 5, modifier: .none) == 2) #expect(d.total(cutoff: 4, modifier: .none) == 3) #expect(d.total(cutoff: 3, modifier: .none) == 4) #expect(d.total(cutoff: 2, modifier: .none) == 5) } @Test("Negative modifier effectively raises cutoff by 1") func totalNegativeModifier() { let d = oneEach() #expect(d.total(cutoff: 5, modifier: .negative) == 1) // effectively 6+ #expect(d.total(cutoff: 4, modifier: .negative) == 2) // effectively 5+ #expect(d.total(cutoff: 3, modifier: .negative) == 3) // effectively 4+ #expect(d.total(cutoff: 2, modifier: .negative) == 4) // effectively 3+ #expect(d.total(cutoff: 1, modifier: .negative) == 5) // effectively 2+ } @Test("Positive modifier effectively lowers cutoff by 1") func totalPositiveModifier() { let d = oneEach() #expect(d.total(cutoff: 6, modifier: .positive) == 2) // effectively 5+ #expect(d.total(cutoff: 5, modifier: .positive) == 3) // effectively 4+ #expect(d.total(cutoff: 4, modifier: .positive) == 4) // effectively 3+ #expect(d.total(cutoff: 3, modifier: .positive) == 5) // effectively 2+ #expect(d.total(cutoff: 2, modifier: .positive) == 5) // already at 2+, no further shift } } // MARK: - Armour Save Calculation @Suite("Armour Save Calculation") struct ArmourSaveTests { func oneEach() -> Dice { var d = Dice() d.one = 1; d.two = 1; d.three = 1; d.four = 1; d.five = 1; d.six = 1 return d } @Test("Counts dice that fail the save") func normalSaves() { let d = oneEach() #expect(d.armour(die: 6, invulnerable: false, save: 6, armourPenetration: 0) == 5) // 1-5 fail #expect(d.armour(die: 6, invulnerable: false, save: 5, armourPenetration: 0) == 4) // 1-4 fail #expect(d.armour(die: 6, invulnerable: false, save: 4, armourPenetration: 0) == 3) // 1-3 fail #expect(d.armour(die: 6, invulnerable: false, save: 3, armourPenetration: 0) == 2) // 1-2 fail #expect(d.armour(die: 6, invulnerable: false, save: 2, armourPenetration: 0) == 1) // only 1 fails } @Test("AP reduces effective save value") func armourPenetration() { let d = oneEach() // 3+ save with AP 1 → effective 4+: 1-3 fail #expect(d.armour(die: 6, invulnerable: false, save: 3, armourPenetration: 1) == 3) // 3+ save with AP 2 → effective 5+: 1-4 fail #expect(d.armour(die: 6, invulnerable: false, save: 3, armourPenetration: 2) == 4) } @Test("Invulnerable save ignores AP") func invulnerableSave() { let d = oneEach() // 4++ invulnerable with heavy AP should match a plain 4+ with no AP let withInvuln = d.armour(die: 6, invulnerable: true, save: 4, armourPenetration: 5) let noAP = d.armour(die: 6, invulnerable: false, save: 4, armourPenetration: 0) #expect(withInvuln == noAP) } } // MARK: - Roll Modifiers @Suite("Roll Modifiers") struct RollModifierTests { @Test("add() clamps at positive") func addModifier() { #expect(Dice.RollModifiers.negative.add() == .none) #expect(Dice.RollModifiers.none.add() == .positive) #expect(Dice.RollModifiers.positive.add() == .positive) } @Test("remove() clamps at negative") func removeModifier() { #expect(Dice.RollModifiers.positive.remove() == .none) #expect(Dice.RollModifiers.none.remove() == .negative) #expect(Dice.RollModifiers.negative.remove() == .negative) } } // MARK: - Wound Cutoff @Suite("Wound Cutoff") struct WoundCutoffTests { @Test("S double or more T: wound on 2+") func doubleStrength() { let roll = Roll() roll.strength = 10; roll.toughness = 5 #expect(roll.woundCutoff == 2) roll.strength = 6; roll.toughness = 3 #expect(roll.woundCutoff == 2) } @Test("S greater than T but less than double: wound on 3+") func strengthHigher() { let roll = Roll() roll.strength = 7; roll.toughness = 5 #expect(roll.woundCutoff == 3) roll.strength = 4; roll.toughness = 3 #expect(roll.woundCutoff == 3) } @Test("S equals T: wound on 4+") func equal() { let roll = Roll() roll.strength = 5; roll.toughness = 5 #expect(roll.woundCutoff == 4) roll.strength = 3; roll.toughness = 3 #expect(roll.woundCutoff == 4) } @Test("T greater than S but less than double: wound on 5+") func toughnessHigher() { let roll = Roll() roll.strength = 3; roll.toughness = 5 #expect(roll.woundCutoff == 5) roll.strength = 4; roll.toughness = 5 #expect(roll.woundCutoff == 5) } @Test("T double or more S: wound on 6+") func doubleToughness() { let roll = Roll() roll.strength = 2; roll.toughness = 5 #expect(roll.woundCutoff == 6) roll.strength = 1; roll.toughness = 3 #expect(roll.woundCutoff == 6) } } // MARK: - Reroll Logic @Suite("Reroll Logic") struct RerollTests { @Test("rerollBelow: no-op when no dice fall below cutoff") func rerollBelowNoop() { var d = Dice() d.four = 2; d.five = 3; d.six = 1 d.rerollBelow(cutoff: 4) #expect(d.one == 0) #expect(d.two == 0) #expect(d.three == 0) #expect(d.four == 2) #expect(d.five == 3) #expect(d.six == 1) } @Test("rerollBelow: preserves total dice count") func rerollBelowPreservesCount() { var d = Dice() d.one = 3; d.two = 2; d.three = 1; d.four = 1; d.five = 1; d.six = 1 d.rerollBelow(cutoff: 4) let total = d.one + d.two + d.three + d.four + d.five + d.six #expect(total == 9) } @Test("rerollBelow: cutoff 1 is a no-op (empty range)") func rerollBelowCutoffOne() { var d = Dice() d.one = 3; d.six = 2 d.rerollBelow(cutoff: 1) #expect(d.one == 3) #expect(d.six == 2) } @Test("rerollBelow: dice above cutoff are untouched") func rerollBelowPreservesHighFaces() { var d = Dice() d.one = 2; d.two = 2; d.three = 2; d.six = 4 d.rerollBelow(cutoff: 4) // The 4 sixes were not in the reroll range and should be unchanged #expect(d.six == 4) } @Test("applyReroll: none mode leaves hasRerolled false") func applyRerollNoneMode() { let roll = Roll() roll.state = .hit roll.hitRerollMode = .none roll.applyReroll() #expect(roll.hitHasRerolled == false) } @Test("applyReroll: sets hasRerolled flag after use") func applyRerollSetsFlag() { let roll = Roll() roll.state = .hit roll.hitRerollMode = .ones roll.dice.one = 3; roll.dice.six = 2 roll.total = 5 roll.applyReroll() #expect(roll.hitHasRerolled == true) } @Test("applyReroll: guard blocks second call") func applyRerollRespectsFlagAlreadySet() { let roll = Roll() roll.state = .hit roll.hitRerollMode = .all roll.hitHasRerolled = true // simulate already used roll.dice.one = 5 // known state roll.applyReroll() // should be blocked by guard #expect(roll.dice.one == 5) #expect(roll.dice.two == 0) #expect(roll.dice.three == 0) #expect(roll.dice.four == 0) #expect(roll.dice.five == 0) #expect(roll.dice.six == 0) } @Test("applyReroll: only sets flag for the active phase") func applyRerollPerPhaseIsolation() { let roll = Roll() roll.state = .wound roll.woundRerollMode = .ones roll.applyReroll() #expect(roll.woundHasRerolled == true) #expect(roll.hitHasRerolled == false) #expect(roll.saveHasRerolled == false) } @Test("applyReroll: failed mode preserves total dice count") func applyRerollFailedModeCount() { let roll = Roll() roll.state = .hit roll.hitCutoff = .four roll.hitRerollMode = .failed roll.dice.one = 1; roll.dice.two = 1; roll.dice.three = 1; roll.dice.six = 2 roll.total = 5 roll.applyReroll() let total = roll.dice.one + roll.dice.two + roll.dice.three + roll.dice.four + roll.dice.five + roll.dice.six #expect(total == 5) #expect(roll.dice.six == 2) // dice above cutoff are untouched } @Test("resetPool: clears hitHasRerolled flag") func resetPoolClearsFlag() { let roll = Roll() roll.total = 5 roll.hitHasRerolled = true roll.resetPool() #expect(roll.hitHasRerolled == false) } @Test("resetPool: rolls dice equal to total count") func resetPoolRollsCorrectCount() { let roll = Roll() roll.total = 10 roll.resetPool() let total = roll.dice.one + roll.dice.two + roll.dice.three + roll.dice.four + roll.dice.five + roll.dice.six #expect(total == 10) } } // MARK: - Blowup Logic @Suite("Blowup Logic") struct BlowupTests { @Test("blowup six: total increases by the original six count") func blowupSixIncreasesCount() { var d = Dice() d.one = 1; d.two = 1; d.six = 3 let originalTotal = 5 let originalSix = 3 d.blowup(dice: .six) let newTotal = d.one + d.two + d.three + d.four + d.five + d.six #expect(newTotal == originalTotal + originalSix) } @Test("blowup five: total increases by the original five and six count") func blowupFiveIncreasesCount() { var d = Dice() d.one = 1; d.five = 2; d.six = 3 let originalTotal = 6 let originalFiveSix = 5 d.blowup(dice: .five) let newTotal = d.one + d.two + d.three + d.four + d.five + d.six #expect(newTotal == originalTotal + originalFiveSix) } @Test("blowup six: no-op when no sixes in pool") func blowupSixNoop() { var d = Dice() d.one = 3; d.two = 2 d.blowup(dice: .six) let total = d.one + d.two + d.three + d.four + d.five + d.six #expect(total == 5) } @Test("applyBlowup: sets blowupUsed flag") func applyBlowupSetsFlag() { let roll = Roll() roll.dice.six = 3 roll.applyBlowup(face: .six) #expect(roll.blowupUsed == true) } @Test("applyBlowup: guard blocks second call") func applyBlowupGuard() { let roll = Roll() roll.dice.six = 2 roll.blowupUsed = true // already used let totalBefore = roll.dice.one + roll.dice.two + roll.dice.three + roll.dice.four + roll.dice.five + roll.dice.six roll.applyBlowup(face: .six) let totalAfter = roll.dice.one + roll.dice.two + roll.dice.three + roll.dice.four + roll.dice.five + roll.dice.six #expect(totalAfter == totalBefore) } @Test("applyBlowup: using any face disables both") func blowupAnyFaceDisablesBoth() { let roll = Roll() roll.dice.six = 2; roll.dice.five = 2 roll.applyBlowup(face: .six) #expect(roll.blowupUsed == true) // Five blowup should now be blocked let totalBefore = roll.dice.one + roll.dice.two + roll.dice.three + roll.dice.four + roll.dice.five + roll.dice.six roll.applyBlowup(face: .five) let totalAfter = roll.dice.one + roll.dice.two + roll.dice.three + roll.dice.four + roll.dice.five + roll.dice.six #expect(totalAfter == totalBefore) } }