mirror of
https://github.com/Threnklyn/advent-of-code-go.git
synced 2026-05-18 19:13:27 +02:00
2015-day22: memoized battle simulator, lots of states to sift through
This commit is contained in:
+84
-50
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user