From 37c74e2adb9877d877c3cfd7a497076446b3c2b6 Mon Sep 17 00:00:00 2001 From: alexchao26 Date: Sun, 27 Dec 2020 20:09:01 -0500 Subject: [PATCH] 2015-day22: memoized battle simulator, lots of states to sift through --- 2015/day22/main.go | 134 +++++++++++++++++++++++++--------------- 2015/day22/main_test.go | 10 +-- 2 files changed, 89 insertions(+), 55 deletions(-) diff --git a/2015/day22/main.go b/2015/day22/main.go index cff5424..474a4b6 100644 --- a/2015/day22/main.go +++ b/2015/day22/main.go @@ -17,19 +17,25 @@ func main() { flag.Parse() fmt.Println("Running part", part) - ans := part1(util.ReadFile("./input.txt"), 50, 500, part) + ans := wizardSimulator(util.ReadFile("./input.txt"), 50, 500, part) fmt.Println("Output:", ans) } -func part1(input string, myHP, myMana, part int) int { - bossHP, bossDamage := parseInput(input) - _, _, _, _ = bossHP, bossDamage, myHP, myMana - return backtrackBattleSim(myHP, myMana, bossHP, bossDamage, [5]int{}, true, 0, map[string]int{}, part) +func wizardSimulator(input string, myHP, myMana, part int) int { + lines := strings.Split(input, "\n") + bossHP := cast.ToInt(strings.Split(lines[0], ": ")[1]) + bossDamage := cast.ToInt(strings.Split(lines[1], ": ")[1]) + + initState := newBattleState(myHP, myMana, bossHP, bossDamage, [5]int{}, true, 0) + + return simBattle(initState, map[string]int{}, part) } +// Spell struct is used to generalize all spell types by leveraging zero values. +// The zero value for ints is 0 (which can be added with no effect) type spell struct { - name string - index int + name string // redundant, for debugging + index int // for indexing in an array (which is easily passed by value) cost int effectLength int instantDamage int @@ -77,91 +83,119 @@ var spellsMap = map[string]spell{ }, } -func hashState(myHP, myMana, bossHP int, effectDurations [5]int, isMyTurn bool) string { - return fmt.Sprintf("%d_%d_%d_%v_%v", myHP, myMana, bossHP, effectDurations, isMyTurn) +type battleState struct { + myHP int + myMana int + bossHP int + bossDamage int + effectDurations [5]int + isMyTurn bool + depth int // recursive branch depth, for debugging } -func backtrackBattleSim(myHP, myMana, bossHP, bossDamage int, effectDurations [5]int, isMyTurn bool, depth int, memo map[string]int, part int) (minMana int) { - // fmt.Printf("\nDEPTH: %d %v\nMe %d Mana: %d; Boss %d Dmg: %d\n DURATIONS: %v\n", depth, isMyTurn, myHP, myMana, bossHP, bossDamage, effectDurations) +func newBattleState(myHP, myMana, bossHP, bossDamage int, effectDurations [5]int, isMyTurn bool, depth int) battleState { + return battleState{ + myHP: myHP, + myMana: myMana, + bossHP: bossHP, + bossDamage: bossDamage, + effectDurations: effectDurations, + isMyTurn: isMyTurn, + depth: depth, + } +} - hash := hashState(myHP, myMana, bossHP, effectDurations, isMyTurn) +func (s battleState) hashKey() string { + return fmt.Sprintf("%d_%d_%d_%v_%v", s.myHP, s.myMana, s.bossHP, s.effectDurations, s.isMyTurn) +} + +func simBattle(state battleState, memo map[string]int, part int) (minMana int) { + // check cache + hash := state.hashKey() if val, ok := memo[hash]; ok { return val } - if part == 2 && isMyTurn { - myHP-- + if part == 2 && state.isMyTurn { + state.myHP-- } - if myHP <= 0 { - // fmt.Println("BOSS WINS") + + // check myHP after a potential part 2 HP loss, if player dies, then return + // a huge number which will essentially be ignored by a mathy.MinInt comparison + if state.myHP <= 0 { return math.MaxInt32 } // apply any active spell effects var myArmor int for _, sp := range spellsMap { - if effectDurations[sp.index] > 0 { - effectDurations[sp.index]-- + if state.effectDurations[sp.index] > 0 { + state.effectDurations[sp.index]-- // many of values will be zero for any given spell - bossHP -= sp.effectDamage - myHP += sp.heal + state.bossHP -= sp.effectDamage + state.myHP += sp.heal myArmor += sp.armorBuff - myMana += sp.manaRecharge + state.myMana += sp.manaRecharge } } - // fmt.Printf(" Post Effects: Me %d Mana: %d Def: %d; Boss %d\n", myHP, myMana, myArmor, bossHP) - - if bossHP <= 0 { - // fmt.Println("PLAYER WINS") + // check bossHP after effects take place, it could die form poison + if state.bossHP <= 0 { return 0 } + // get minMana from the current state, to a player win minMana = math.MaxInt32 - if isMyTurn { + if state.isMyTurn { // iterate through spells, create a recursive call for each spell that // can be called (i.e. its effectDuration index is zero) var spellCasted bool - for _, spName := range []string{"Recharge", "Poison", "Shield", "Drain", "Magic Missile"} { - sp := spellsMap[spName] - if effectDurations[sp.index] == 0 { - if myMana >= sp.cost { + for _, sp := range spellsMap { + if state.effectDurations[sp.index] == 0 { + if state.myMana >= sp.cost { spellCasted = true // make new durations array & add effect duration for this spell - newDurations := effectDurations + newDurations := state.effectDurations newDurations[sp.index] += sp.effectLength - // fmt.Printf(" dp %d, casting %s\n", depth, sp.name) - castResult := sp.cost + backtrackBattleSim(myHP+sp.instantHeal, myMana-sp.cost, bossHP-sp.instantDamage, bossDamage, newDurations, false, depth+1, memo, part) - // fmt.Printf(" result from casting %s @ depth %d: %d\n", sp.name, depth, castResult) + + nextState := newBattleState(state.myHP+sp.instantHeal, + state.myMana-sp.cost, + state.bossHP-sp.instantDamage, + state.bossDamage, + newDurations, + false, + state.depth+1, + ) + + castResult := sp.cost + simBattle(nextState, memo, part) minMana = mathy.MinInt(minMana, castResult) } - // } else { - // fmt.Println(" CANNOT cast", sp.name) } } - // if cannot cast spell, lose + // if cannot cast spell, player loses if !spellCasted { - // fmt.Println(" CANNOT CAST SPELL, LOST") return math.MaxInt32 } } else { - // boss attacks, minimum 1 damage - attackDamage := mathy.MaxInt(1, bossDamage-myArmor) - // recurse - bossAttackResult := backtrackBattleSim(myHP-attackDamage, myMana, bossHP, bossDamage, effectDurations, true, depth+1, memo, part) + // boss's turn, boss attacks w/ a minimum damage of 1 + attackDamage := mathy.MaxInt(1, state.bossDamage-myArmor) + + // recurse w/ next state + nextState := newBattleState(state.myHP-attackDamage, + state.myMana, + state.bossHP, + state.bossDamage, + state.effectDurations, + true, + state.depth+1, + ) + bossAttackResult := simBattle(nextState, memo, part) minMana = mathy.MinInt(minMana, bossAttackResult) } - // fmt.Println(" from", depth, minMana) + // add to memoized to prevent unnecessary recursive branches, then return memo[hash] = minMana return minMana } - -func parseInput(input string) (bossHP, bossDamage int) { - lines := strings.Split(input, "\n") - bossHP = cast.ToInt(strings.Split(lines[0], ": ")[1]) - bossDamage = cast.ToInt(strings.Split(lines[1], ": ")[1]) - return bossHP, bossDamage -} diff --git a/2015/day22/main_test.go b/2015/day22/main_test.go index 413f7a0..c32f219 100644 --- a/2015/day22/main_test.go +++ b/2015/day22/main_test.go @@ -6,7 +6,7 @@ import ( "github.com/alexchao26/advent-of-code-go/util" ) -func Test_part1(t *testing.T) { +func Test_wizardSimulator(t *testing.T) { type args struct { input string myHP int @@ -23,13 +23,13 @@ func Test_part1(t *testing.T) { args: args{"Hit Points: 13\nDamage: 8", 10, 250, 1}, want: spellsMap["Poison"].cost + spellsMap["Magic Missile"].cost, }, - {"actual", args{util.ReadFile("input.txt"), 50, 500, 1}, 953}, - {"actual", args{util.ReadFile("input.txt"), 50, 500, 2}, 1289}, + {"part1 actual", args{util.ReadFile("input.txt"), 50, 500, 1}, 953}, + {"part2 actual", args{util.ReadFile("input.txt"), 50, 500, 2}, 1289}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := part1(tt.args.input, tt.args.myHP, tt.args.myMana, tt.args.part); got != tt.want { - t.Errorf("part1() = %v, want %v", got, tt.want) + if got := wizardSimulator(tt.args.input, tt.args.myHP, tt.args.myMana, tt.args.part); got != tt.want { + t.Errorf("wizardSimulator() = %v, want %v", got, tt.want) } }) }