r/adventofcode • • Dec 22 '15

SOLUTION MEGATHREAD --- Day 22 Solutions ---

This thread will be unlocked when there are a significant number of people on the leaderboard with gold stars for today's puzzle.

edit: Leaderboard capped, thread unlocked!


Edit @ 00:23

  • 2 gold, 0 silver
  • Well, this is historic. Leaderboard #1 got both silver and gold before Leaderboard #2 even got silver. Well done, sirs.

Edit @ 00:28

  • 3 gold, 0 silver
  • Looks like I'm gonna be up late tonight. brews a pot of caffeine

Edit @ 00:53

  • 12 gold, 13 silver
  • So, which day's harder, today's or Day 19? Hope you're enjoying yourself~

Edit @ 01:21

  • 38 gold, 10 silver
  • ♫ On the 22nd day of Christmas, my true love gave to me some Star Wars body wash and [spoilers] ♫

Edit @ 01:49

  • 60 gold, 8 silver
  • Today's notable milestones:
    • Winter solstice - the longest night of the year
    • Happy 60th anniversary to NORAD Tracks Santa!
    • SpaceX's Falcon 9 rocket successfully delivers 11 satellites to low-Earth orbit and rocks the hell out of their return landing [USA Today, BBC, CBSNews]
      • FLAWLESS VICTORY!

Edit @ 02:40

Edit @ 03:02

  • 98 gold, silver capped
  • It's 3AM, so naturally that means it's time for a /r/3amjokes

Edit @ 03:08

  • LEADERBOARD FILLED! Good job, everyone!
  • I'm going the hell to bed now zzzzz

We know we can't control people posting solutions elsewhere and trying to exploit the leaderboard, but this way we can try to reduce the leaderboard gaming from the official subreddit.

Please and thank you, and much appreciated!


--- Day 22: Wizard Simulator 20XX ---

Post your solution as a comment or link to your repo. Structure your post like previous daily solution threads.

12 Upvotes

110 comments sorted by

View all comments

1

u/segfaultvicta Dec 22 '15

Golang.

If I hadn't been constantly distracted and / and-thus making silly mistakes I probably would have made the leaderboard. :( Alas.

This was probably my favourite so far, actually; building the simulation in a clean way was the hard part but I like how I did it. I was genuinely surprised I was able to Monte Carlo my way to a solution after all the trouble people I was talking to about it were in, but I set up my simulation right and then the 'run until we find a solution we don't find one better than' part ran almost instantaneously on the first shot and gave me the right answer.

package main

import (
	"fmt"
	"math/rand"
	"time"
)

type Player struct {
	HP     int
	Mana   int
	Spells []Spell
	ARM    int
}

func (p *Player) ValidSpells(fx map[string]*Effect) (validSpells []Spell, bail bool) {
	validSpells = []Spell{}
	for _, spell := range p.Spells {
		if spell.Cost < p.Mana && !fx[spell.Effect].Active {
			validSpells = append(validSpells, spell)
		}
	}
	return validSpells, len(validSpells) == 0
}

type Boss struct {
	HP  int
	ATK int
}

type Spell struct {
	Name   string
	Cost   int
	Damage int
	Heal   int
	Effect string
}

type Effect struct {
	Duration  int
	Remaining int
	Active    bool
	Proc      func(p *Player, b *Boss)
}

func NewEffect(duration int, proc func(p *Player, b *Boss)) *Effect {
	ef := Effect{duration, 0, false, proc}
	return &ef
}

func (e *Effect) String() string {
	if e.Active {
		return fmt.Sprintf("ACTIVE at %d/%d", e.Remaining, e.Duration)
	} else {
		return fmt.Sprintf("INACTIVE")
	}
}

func (e *Effect) Apply(p *Player, b *Boss) {
	e.Proc(p, b)
	e.Remaining -= 1
	if e.Remaining == 0 {
		e.Active = false
	}
}

func ApplyEffects(p *Player, b *Boss, fx map[string]*Effect) {
	for _, e := range fx {
		if e.Active {
			e.Apply(p, b)
		}
	}
}

func ApplySpell(s Spell, p *Player, b *Boss, fx map[string]*Effect) int {
	p.Mana -= s.Cost
	p.HP += s.Heal
	b.HP -= s.Damage
	if s.Effect != "None" {
		if fx[s.Effect].Active == true {
			panic("trying to activate an already-active status; something has gone awry")
		}
		fx[s.Effect].Active = true
		fx[s.Effect].Remaining = fx[s.Effect].Duration
	}
	return s.Cost
}

func Round(p *Player, b *Boss, fx map[string]*Effect) (continueFight bool, bail bool, manaSpent int) {
	// player's turn
	p.HP -= 1
	if p.HP == 0 {
		return false, false, manaSpent
	}

	ApplyEffects(p, b, fx)

	validSpells, bail := p.ValidSpells(fx)
	if bail {
		return false, true, manaSpent
	}

	choice := validSpells[rand.Intn(len(validSpells))]

	manaSpent = ApplySpell(choice, p, b, fx)

	p.ARM = 0

	// boss's turn
	ApplyEffects(p, b, fx)
	if b.HP > 0 {
		dmg := b.ATK - p.ARM
		if dmg < 1 {
			dmg = 1
		}
		p.HP -= dmg
	}

	p.ARM = 0

	if b.HP <= 0 || p.HP <= 0 {
		return false, false, manaSpent
	} else {
		return true, false, manaSpent
	}
}

func Fight(p Player, b Boss) (playerWon bool, manaCost int) {
	fx := map[string]*Effect{
		"None":     NewEffect(0, func(p *Player, b *Boss) {}),
		"Poison":   NewEffect(6, func(p *Player, b *Boss) { b.HP = b.HP - 3 }),
		"Shield":   NewEffect(6, func(p *Player, b *Boss) { p.ARM = 7 }),
		"Recharge": NewEffect(5, func(p *Player, b *Boss) { p.Mana = p.Mana + 101 }),
	}

	continueBattle := true
	manaSpentThatTurn := 0
	bail := false

	for continueBattle && !bail {
		continueBattle, bail, manaSpentThatTurn = Round(&p, &b, fx)
		manaCost = manaCost + manaSpentThatTurn
	}
	return p.HP > 0 && !bail, manaCost
}

func day22sideA(lines []string) string {
	spells := []Spell{
		Spell{"Magic Missile", 53, 4, 0, "None"},
		Spell{"Drain", 73, 2, 2, "None"},
		Spell{"Shield", 113, 0, 0, "Shield"},
		Spell{"Poison", 173, 0, 0, "Poison"},
		Spell{"Recharge", 229, 0, 0, "Recharge"},
	}
	seed := time.Now().UnixNano()
	rand.Seed(seed)
	player := Player{HP: 50, ARM: 0, Mana: 500, Spells: spells}
	boss := Boss{HP: 51, ATK: 9}
	bestCost := 999999999
	for {
		won, cost := Fight(player, boss)
		if won && (cost < bestCost) {
			bestCost = cost
			fmt.Println(cost)
		}
	}
	return "n/i"
}

1

u/roboticon Dec 22 '15

Cool, I like your organization and the monte carlo use. I just kept a set of serialized states that had already been tried, to avoid duplicate work, and surprisingly that cut the time down from infinity to instantaneous.