2018-day15: lots of code for elf vs goblin battles

This commit is contained in:
alexchao26
2020-12-12 17:23:24 -05:00
parent 0df13683fb
commit 9619c379ee
2 changed files with 196 additions and 142 deletions
+151 -122
View File
@@ -3,6 +3,7 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"sort"
"strings" "strings"
"github.com/alexchao26/advent-of-code-go/util" "github.com/alexchao26/advent-of-code-go/util"
@@ -18,28 +19,40 @@ func main() {
ans := part1(util.ReadFile("./input.txt")) ans := part1(util.ReadFile("./input.txt"))
fmt.Println("Output:", ans) fmt.Println("Output:", ans)
} else { } else {
// ans := part2(util.ReadFile("./input.txt")) ans := part2(util.ReadFile("./input.txt"))
// fmt.Println("Output:", ans) fmt.Println("Output:", ans)
} }
} }
func part1(input string) int { func part1(input string) int {
g := newGame(input) g := newGame(input)
fmt.Println("game: ", g) return g.runFullGame()
}
var gameover bool func part2(input string) int {
for !gameover { var outcome int
gameover = g.runTurn() for elfPower := 4; ; elfPower++ {
fmt.Println("AFTER IN PART 1", g) g := newGame(input)
elvesBefore := g.countElves()
// update all elves to the new attack power
for _, c := range g.coordsToChars {
if c.charType == "E" {
c.attackPower = elfPower
}
}
// run the game until it ends... not optimized: could abort when an elf dies
// but it's good enough
outcome = g.runFullGame()
// check if all elves are still alive
if elvesBefore == g.countElves() {
break
}
} }
var totalHp int return outcome
for _, c := range g.coordsToChars {
totalHp += c.hp
}
// NOT 182715, too low
return g.rounds * totalHp
} }
type game struct { type game struct {
@@ -50,23 +63,34 @@ type game struct {
func (g game) String() string { func (g game) String() string {
ans := fmt.Sprintf("Rounds: %d\n", g.rounds) ans := fmt.Sprintf("Rounds: %d\n", g.rounds)
for _, row := range g.grid { for rowNum, row := range g.grid {
ans += fmt.Sprintf("\n%02d: ", rowNum)
for _, v := range row { for _, v := range row {
ans += v ans += v
} }
ans += "\n"
} }
ans += "\nAlive characters:"
for coord, char := range g.coordsToChars { for coord, char := range g.coordsToChars {
ans += fmt.Sprintf("%v: Char: %v\n", coord, char) ans += fmt.Sprintf("\n%v: Char: %v", coord, char)
} }
return ans return ans
} }
func (g *game) countElves() int {
var elves int
for _, c := range g.coordsToChars {
if c.charType == "E" {
elves++
}
}
return elves
}
type character struct { type character struct {
coord [2]int coord [2]int
hp int hp int
charType string // "E" or "G" charType string // "E" or "G"
attackPower int
} }
func (c character) String() string { func (c character) String() string {
@@ -86,9 +110,10 @@ func newGame(input string) *game {
case "E", "G": case "E", "G":
coord := [2]int{row, col} coord := [2]int{row, col}
coordsToChars[coord] = &character{ coordsToChars[coord] = &character{
coord: coord, coord: coord,
hp: 200, hp: 200,
charType: val, charType: val,
attackPower: 3, // default to 3
} }
} }
} }
@@ -101,23 +126,34 @@ func newGame(input string) *game {
} }
} }
func (g *game) runTurn() (gameover bool) { func (g *game) runFullGame() int {
turnOrder := g.getTurnOrder() var gameover bool
for !gameover {
gameover = g.runTurn()
}
for _, charCoords := range turnOrder { var totalHp int
// ensure this character is still alive for _, c := range g.coordsToChars {
currentChar, ok := g.coordsToChars[charCoords] totalHp += c.hp
if !ok { }
fmt.Println("character is dead, skipping turn; ", charCoords)
return g.rounds * totalHp
}
func (g *game) runTurn() (gameover bool) {
charsInOrder := g.getTurnOrder()
for _, char := range charsInOrder {
// if char is already dead, just continue on
if char.hp <= 0 {
continue continue
} }
fmt.Println("turn's coordinates:", charCoords)
// check if there are enemies still // check if there are enemies in entire game
enemyType := getEnemyType(g.grid[charCoords[0]][charCoords[1]]) enemyType := getEnemyType(char.charType)
var enemiesFound bool var enemiesFound bool
for _, char := range g.coordsToChars { for _, c := range g.coordsToChars {
if char.charType == enemyType { if c.charType == enemyType {
enemiesFound = true enemiesFound = true
break break
} }
@@ -127,40 +163,35 @@ func (g *game) runTurn() (gameover bool) {
} }
// check if the character has a unit next to ir right now // check if the character has a unit next to ir right now
enemy := g.pickTarget(charCoords) enemy := g.pickTarget(char.coord)
if enemy != nil { if enemy != nil {
// attack & move on // attack & move on
g.attack(g.coordsToChars[charCoords], enemy) g.attack(g.coordsToChars[char.coord], enemy)
fmt.Println(" immediately attacking:", enemy)
} else { } else {
// try to move, then try to pick an enemy again // else try to move, then try to pick an enemy again
inRangeCoordsMap := g.calcInRangeCoordsMap(g.grid[charCoords[0]][charCoords[1]]) inRangeCoordsMap := g.getInRangeOfEnemies(char.charType)
// fmt.Println("for starting", startingCoord, "\n IN RANGE MAP", inRangeCoordsMap) // if no in range coords, that does not mean all enemies are dead
// if no enemies are in the map, they're all dead // it just means there is no open floor around enemies
if len(inRangeCoordsMap) == 0 { if len(inRangeCoordsMap) == 0 {
fmt.Println("--nowhere to move, continuing...")
continue continue
} }
// get next move
nextCoord, willMove := g.determineNextMove(charCoords, inRangeCoordsMap) nextCoord, willMove := g.determineNextMove(char.coord, inRangeCoordsMap)
if willMove { if willMove {
fmt.Println("moving to", nextCoord) // update grid for this character
// update grid and coordinates for this character g.grid[nextCoord[0]][nextCoord[1]] = char.charType
g.grid[nextCoord[0]][nextCoord[1]] = currentChar.charType g.grid[char.coord[0]][char.coord[1]] = "."
g.grid[charCoords[0]][charCoords[1]] = "."
g.coordsToChars[nextCoord] = g.coordsToChars[charCoords]
currentChar.coord = nextCoord
delete(g.coordsToChars, charCoords)
fmt.Println(" searching for enemy after moving") // coords of this char have changed, update variables
delete(g.coordsToChars, char.coord) // delete old entry using char's outdated coords
g.coordsToChars[nextCoord] = char // add new entry
char.coord = nextCoord // update char's coords too
// pick a target and attack it
enemy := g.pickTarget(nextCoord) enemy := g.pickTarget(nextCoord)
if enemy != nil { if enemy != nil {
// attack & move on
g.attack(g.coordsToChars[nextCoord], enemy) g.attack(g.coordsToChars[nextCoord], enemy)
fmt.Println(" after attack:", enemy)
} }
} else {
fmt.Println("NO IN RANGE TARGETS TO MOVE TO")
} }
} }
} }
@@ -170,20 +201,18 @@ func (g *game) runTurn() (gameover bool) {
} }
// returns a slice of coordinates where there are characters, in turn order // returns a slice of coordinates where there are characters, in turn order
func (g *game) getTurnOrder() [][2]int { func (g *game) getTurnOrder() []*character {
var charCoords [][2]int var charsInOrder []*character
for i, row := range g.grid { for i, row := range g.grid {
for j, tile := range row { for j, tile := range row {
if tile == "E" || tile == "G" { if tile == "E" || tile == "G" {
charCoords = append(charCoords, [2]int{i, j}) charsInOrder = append(charsInOrder, g.coordsToChars[[2]int{i, j}])
} }
} }
} }
return charCoords return charsInOrder
} }
// order diffs in such a way that the shortest paths found will be in reading
// list order
var diffs = [][2]int{ var diffs = [][2]int{
{-1, 0}, // up {-1, 0}, // up
{0, -1}, // left {0, -1}, // left
@@ -215,9 +244,8 @@ func (g *game) pickTarget(currentCoords [2]int) *character {
} }
func (g *game) attack(attacker, target *character) { func (g *game) attack(attacker, target *character) {
target.hp -= 3 target.hp -= attacker.attackPower
if target.hp <= 0 { if target.hp <= 0 {
fmt.Println(" KILLED:", target)
// remove target from map and update grid // remove target from map and update grid
targetCoords := target.coord targetCoords := target.coord
delete(g.coordsToChars, target.coord) delete(g.coordsToChars, target.coord)
@@ -233,50 +261,70 @@ type bfsNode struct {
func (g *game) determineNextMove(startingCoord [2]int, inRangeCoordsMap map[[2]int]bool) (nextCoord [2]int, willMove bool) { func (g *game) determineNextMove(startingCoord [2]int, inRangeCoordsMap map[[2]int]bool) (nextCoord [2]int, willMove bool) {
queue := []bfsNode{ queue := []bfsNode{
{coord: startingCoord, dist: 0, initialMove: [2]int{}}, {coord: startingCoord, dist: 0, initialMove: [2]int{}}, // some zero values are redundant, but readable
} }
visitedCoords := map[[2]int]bool{[2]int{0, 0}: true} visitedCoords := map[[2]int]bool{[2]int{0, 0}: true}
for len(queue) > 0 { // store the closest in range nodes to tie break
// get front of queue var closestInRange []bfsNode
front := queue[0]
queue = queue[1:]
// if front is in range of an enemy, return the initial move // run while the closet in range slice is still empty and queue is not empty
if inRangeCoordsMap[front.coord] { for checkDist := 0; len(closestInRange) == 0 && len(queue) > 0; checkDist++ {
return front.initialMove, true // process front of queue while its distance is equal to the check distance
} for len(queue) > 0 && queue[0].dist == checkDist {
front := queue[0]
queue = queue[1:]
// if it has not been visited before, then check its four directions // if front is in range of an enemy, add to closest in range slice
if !visitedCoords[front.coord] { if inRangeCoordsMap[front.coord] {
for _, d := range diffs { closestInRange = append(closestInRange, front)
nextCoord := [2]int{d[0] + front.coord[0], d[1] + front.coord[1]} }
// only proceed if next coordinate is walkable
if g.grid[nextCoord[0]][nextCoord[1]] == "." { // if it has not been visited before, then check its four directions
// add next coord to queue if !visitedCoords[front.coord] {
node := bfsNode{ for _, d := range diffs {
coord: nextCoord, nextCoord := [2]int{d[0] + front.coord[0], d[1] + front.coord[1]}
dist: front.dist + 1, // only proceed if next coordinate is walkable
initialMove: front.initialMove, if g.grid[nextCoord[0]][nextCoord[1]] == "." {
// add next coord to queue
node := bfsNode{
coord: nextCoord,
dist: front.dist + 1,
initialMove: front.initialMove,
}
if front.dist == 0 {
node.initialMove = nextCoord
}
queue = append(queue, node)
} }
if front.dist == 0 {
node.initialMove = nextCoord
}
queue = append(queue, node)
} }
} }
visitedCoords[front.coord] = true
} }
visitedCoords[front.coord] = true
} }
fmt.Println("WILL NOT MOVE FROM ", startingCoord, "\nON GAME\n", g) if len(closestInRange) == 0 {
return [2]int{}, false return [2]int{}, false
}
// sort destination nodes via reading order of coords, break ties on initialMove
sort.Slice(closestInRange, func(i, j int) bool {
nodeI, nodeJ := closestInRange[i], closestInRange[j]
if nodeI.coord != nodeJ.coord {
return readingOrderSortFunc(nodeI.coord, nodeJ.coord)
}
return readingOrderSortFunc(nodeI.initialMove, nodeJ.initialMove)
})
// return the initial move of the winning bfs node, will be used to move
// the character
return closestInRange[0].initialMove, true
} }
// returns a slice of coordinates that are next to enemies and tile is floor // returns a slice of coordinates that are next to enemies and tile is floor
// to be run when a character wants to figure out where to move // to be run when a character wants to figure out where to move
// if the returned map is empty (len 0), that indicates no one should move // if the returned map is empty (len 0), that indicates no one should move
func (g *game) calcInRangeCoordsMap(attackingType string) map[[2]int]bool { func (g *game) getInRangeOfEnemies(attackingType string) map[[2]int]bool {
enemyType := getEnemyType(attackingType) enemyType := getEnemyType(attackingType)
inRangeCoords := map[[2]int]bool{} inRangeCoords := map[[2]int]bool{}
@@ -304,31 +352,12 @@ func getEnemyType(attacker string) string {
return "G" return "G"
} }
// Path finding... // should i go before j in a slice where we're sorting by reading order
// Ties broken in READING order func readingOrderSortFunc(i, j [2]int) (iBeforeJ bool) {
// top to bottom, left to right // compare via first indices if not equal
// if i[0] != j[0] {
// # wall return i[0] < j[0]
// . open }
// G goblin // otherwise tie break via second indices
// E Elf return i[1] < j[1]
// }
// Rounds:
// each unit takes a turn & completes all its actions until the next unit goes
// IN order of reading position (record at start of round)
// 1. IF not in range of energy, tries to move towrds one
// 1.1. identify all possible targets, if no targets, end combat
// 1.2. get open squares (.) in range of all targets
// 1.2.1 if no open squares found, end turn
// 1.3. determine closest open square, tie break via reading order
// 1.4. takes single step towards chosen target, along SHORTEST path, ties broken via reading order
// 2. IF in range, attack
// 2.1. if no units next to it, move on
// 2.2. select neighbor with fewest hitpoints, tie break via reading order
// 2.3. do damage equal to attack power (starts w/ 200HP & 3 attack power)
// 2.4. if unit dies, make it a (.)
// Part 1:
// - find number of FULL rounds (i.e. do not include last one)
// - find sum of remaining hit points on board
// multiply them together
+45 -20
View File
@@ -1,6 +1,10 @@
package main package main
import "testing" import (
"testing"
"github.com/alexchao26/advent-of-code-go/util"
)
var exampleInput1 = `####### var exampleInput1 = `#######
#.G...# #.G...#
@@ -9,6 +13,8 @@ var exampleInput1 = `#######
#..G#E# #..G#E#
#.....# #.....#
#######` #######`
// this example is in part 1 but not part 2 -shrug-
var exampleInput2 = `####### var exampleInput2 = `#######
#G..#E# #G..#E#
#E#E.E# #E#E.E#
@@ -58,7 +64,7 @@ var tests1 = []struct {
{"example4", exampleInput4, 27755}, {"example4", exampleInput4, 27755},
{"example5", exampleInput5, 28944}, {"example5", exampleInput5, 28944},
{"example6", exampleInput6, 18740}, {"example6", exampleInput6, 18740},
// {"actual", ACTUAL_ANSWER, util.ReadFile("input.txt")}, {"actual", util.ReadFile("input.txt"), 183300},
} }
func TestPart1(t *testing.T) { func TestPart1(t *testing.T) {
@@ -72,22 +78,41 @@ func TestPart1(t *testing.T) {
} }
} }
// var tests2 = []struct { var reddit1 = `#######
// name string #######
// want int #.E..G#
// input string #.#####
// // add extra args if needed #G#####
// }{ #######
// // {"actual", ACTUAL_ANSWER, util.ReadFile("input.txt")}, #######`
// }
// func TestPart2(t *testing.T) { // used a sample test from a reddit thread where I was failing this specific issue
// for _, test := range tests2 { // https://www.reddit.com/r/adventofcode/comments/a6r6kg/2018_day_15_part_1_what_am_i_missing/ebxjjuo?utm_source=share&utm_medium=web2x&context=3
// t.Run(test.name, func(*testing.T) { func TestMovement(t *testing.T) {
// got := part2(test.input) t.Log("Expect Elf's first move to go RIGHT")
// if got != test.want { part1(reddit1)
// t.Errorf("got %v, want %v", got, test.want) }
// }
// }) var tests2 = []struct {
// } name string
// } input string
want int
}{
{"example1", exampleInput1, 4988},
{"example3", exampleInput3, 31284},
{"example4", exampleInput4, 3478},
{"example5", exampleInput5, 6474},
{"example6", exampleInput6, 1140},
{"actual", util.ReadFile("input.txt"), 40625},
}
func TestPart2(t *testing.T) {
for _, test := range tests2 {
t.Run(test.name, func(*testing.T) {
got := part2(test.input)
if got != test.want {
t.Errorf("got %v, want %v", got, test.want)
}
})
}
}