diff --git a/2018/day15/cave.go b/2018/day15/cave.go new file mode 100644 index 0000000..a2a1b79 --- /dev/null +++ b/2018/day15/cave.go @@ -0,0 +1,153 @@ +package main + +import ( + "fmt" + "sort" + "strings" +) + +type Cave struct { + Units SortableUnits + Map Map +} + +const ( + KindSpace = 1 << iota + KindElf + KindGoblin + KindWall + KindHighlight +) + +var KindRunes = map[int]rune{ + KindSpace: '.', + KindElf: 'E', + KindGoblin: 'G', + KindWall: '#', + KindHighlight: '@', +} + +var RuneKinds = map[rune]int{ + '.': KindSpace, + 'E': KindElf, + 'G': KindGoblin, + '#': KindWall, +} + +func IsUnit(bit int) bool { + return (KindElf|KindGoblin)&bit != 0 +} + +func NewCave(input []string, elfPower int) *Cave { + c := &Cave{} + c.ParseMap(input, elfPower) + return c +} + +func (c *Cave) ParseMap(input []string, elfPower int) { + m := make(Map) + + for y, row := range input { + for x, col := range row { + kind, ok := RuneKinds[col] + if !ok { + kind = KindWall + } + tile := &Tile{Kind: kind} + if IsUnit(kind) { + c.Units = append(c.Units, NewUnit(tile, kind, elfPower)) + } + m.SetTile(tile, x, y) + } + } + c.Map = m +} + +func (c Cave) PrintMap(highlight *Tile) { + for y := 0; y < len(c.Map); y++ { + var units []string + for x := 0; x < len(c.Map[y]); x++ { + t := c.Map.Tile(x, y) + if t == highlight { + fmt.Print(string(KindRunes[KindHighlight])) + } else { + fmt.Print(string(KindRunes[t.Kind])) + } + if t.Unit != nil { + units = append(units, fmt.Sprintf("%c(%d)", KindRunes[t.Unit.Kind], t.Unit.Hitpoints)) + } + } + if len(units) > 0 { + fmt.Print(" ", strings.Join(units, ", ")) + } + fmt.Println() + } +} + +func (c Cave) PrintDistance(t *Tile) { + distances, _ := c.Map.FindWalkableTiles(t) + for y := 0; y < len(c.Map); y++ { + for x := 0; x < len(c.Map[y]); x++ { + curT := c.Map.Tile(x, y) + if d, ok := distances[curT]; ok && curT != t { + fmt.Print(d) + } else { + fmt.Print(string(KindRunes[curT.Kind])) + } + } + fmt.Println() + } +} + +func (c Cave) Status() (int, bool) { + var elves, goblins bool + var hp int + + for _, u := range c.Units { + if u.Hitpoints <= 0 { + continue + } + if u.Kind == KindElf { + elves = true + } else { + goblins = true + } + hp = hp + u.Hitpoints + } + return hp, elves && goblins +} + +func (c *Cave) RemoveTheDead() { + var newUnits SortableUnits + for _, unit := range c.Units { + if unit.Hitpoints > 0 { + newUnits = append(newUnits, unit) + } + } + c.Units = newUnits +} + +func (c *Cave) RemoveUnit(u *Unit) { + u.Tile.Kind = KindSpace + u.Tile.Unit = nil + u.Tile = nil +} + +// Tick returns false if combat ended during the round, and whether or not an elf has died this round +func (c *Cave) Tick(stopOnElfDeath bool) (bool, bool) { + c.RemoveTheDead() + sort.Sort(c.Units) + for _, unit := range c.Units { + if unit.Hitpoints <= 0 { + continue + } + if !unit.Targets(c) { + return false, false + } + unit.Move(c) + if unit.Attack(c) && stopOnElfDeath { + return false, true + } + } + return true, false +} diff --git a/2018/day15/day15.go b/2018/day15/day15.go index eec6510..35459da 100644 --- a/2018/day15/day15.go +++ b/2018/day15/day15.go @@ -2,400 +2,60 @@ package main import ( "bufio" - "errors" "fmt" - "math" "os" - "sort" - "time" -) - -/* - * 173327 is too low - */ -var input []byte -var width int -var characters map[complex64]*Character -var charSlice []*complex64 -var turnCount int - -const ( - CLEAR_SCREEN = "\033[H\033[2J" - MAX_INT = int(^uint(0) >> 1) - - DIR_N = -1i - DIR_E = 1 - DIR_S = 1i - DIR_W = -1 ) func main() { - characters = make(map[complex64]*Character) - stdinToByteSlice() - setupBattle() - part1() + inp := StdinToStringSlice() + fmt.Println("# Part 1") + Pt1Combat(inp) + + fmt.Println("# Part 2") + Pt2Combat(inp) } -func part1() { - for { - // Sort the players on the field - charSlice = charSlice[0:0] - for i := 0; i < len(input); i++ { - pos := getPosFromInt(i) - bt := getByte(pos) - if bt == 'G' || bt == 'E' { - charSlice = append(charSlice, &pos) +func Pt1Combat(input []string) int { + cave := NewCave(input, defaultPower) + for i := 1; true; i++ { + hp, combat := cave.Status() + if !combat { + res := (i - 1) * hp + fmt.Printf("Result: %d\n", res) + return res + } + if cleanRound, _ := cave.Tick(false); !cleanRound { + i-- + } + } + return -1 +} + +func Pt2Combat(input []string) int { + for power := defaultPower; true; power++ { + cave := NewCave(input, power) + for i := 1; true; i++ { + hp, combat := cave.Status() + if !combat { + res := (i - 1) * hp + fmt.Printf("Result: %d; Attack Power: %d\n", res, power) + return (i - 1) * hp } - } - sort.Sort(ByPos(charSlice)) - - // Tick every player - for _, v := range charSlice { - if char, ok := characters[*v]; ok { - if !char.tick() { - break - } - } - } - if true { - time.Sleep(time.Millisecond * 250) - printBattlefield() - fmt.Println() - } - if checkBattleOver() { - break - } - turnCount++ - } - var totalHP int - sort.Sort(ByPos(charSlice)) - for _, v := range charSlice { - if c, ok := characters[*v]; ok { - fmt.Println(c.string()) - totalHP += c.health - } - } - fmt.Println(turnCount, totalHP) - fmt.Println("Result:", (totalHP * turnCount)) -} - -func checkBattleOver() bool { - var elves, gobs int - for _, v := range characters { - if v.tp == 'E' { - elves++ - } else if v.tp == 'G' { - gobs++ - } - } - return elves == 0 || gobs == 0 -} - -type Character struct { - tp byte - health int - power int - pos complex64 - kills int -} - -func (c *Character) hasEnemies() bool { - for _, oppPos := range charSlice { - if v, ok := characters[*oppPos]; ok { - if v.tp != c.tp { - return true + if cleanRound, elfDied := cave.Tick(true); elfDied { + break + } else if !cleanRound { + i-- } } } - return false + return -1 } -func (c *Character) tick() bool { - // Check if this character has any enemies on the field - if !c.hasEnemies() { - fmt.Println(c.string(), "is unopposed") - return false - } - // Now move/attack - if _, err := c.easiestAdjacentTarget(); err != nil { - // Ok, figure out a move - nxt, tgt := c.findMove() - if nxt != nil { - fmt.Println(c.string(), "is moving to", *nxt, "(", *tgt, ")") - c.moveTo(*nxt) - } - } - if tPos, err := c.easiestAdjacentTarget(); err == nil { - c.attack(tPos) - } - return c.hasEnemies() -} - -// findMove returns the position this character should move to and the position -// that is it's ultimate target -func (c *Character) findMove() (*complex64, *complex64) { - var opps []*complex64 - closestTargetDistance := MAX_INT - dist, path := findAllPaths(&c.pos) - - for _, v := range characters { - if v.health <= 0 { - continue - } - if v.tp != c.tp { - for _, t := range v.getOpenSides() { - if d, ok := dist[t]; ok && d <= closestTargetDistance { - if d < closestTargetDistance { - closestTargetDistance = d - opps = []*complex64{} - } - opps = append(opps, &t) - } - } - } - sort.Sort(ByPos(opps)) - if len(opps) > 0 { - t := opps[0] - curr := *t - for { - if pv == c.pos { - return &curr, t - } - curr = pv - } - } - } - return nil, nil -} - -func (c *Character) string() string { - return fmt.Sprintf("[%s%s:%d]", string(c.tp), getCoordString(c.pos), c.health) -} - -func (c *Character) attack(p complex64) { - fmt.Println(c.string(), "attacks", characters[p].string()) - characters[p].health -= c.power - if characters[p].health <= 0 { - c.kills++ - delete(characters, p) - setByte(p, '.') - } -} - -func (c *Character) easiestAdjacentTarget() (complex64, error) { - var wrk *Character - if v, ok := characters[c.pos+DIR_N]; ok && v != nil && v.tp != c.tp { - wrk = v - } - if v, ok := characters[c.pos+DIR_W]; ok && v != nil && v.tp != c.tp { - if wrk == nil || v.health < wrk.health { - wrk = v - } - } - if v, ok := characters[c.pos+DIR_E]; ok && v != nil && v.tp != c.tp { - if wrk == nil || v.health < wrk.health { - wrk = v - } - } - if v, ok := characters[c.pos+DIR_S]; ok && v != nil && v.tp != c.tp { - if wrk == nil || v.health < wrk.health { - wrk = v - } - } - if wrk != nil { - return wrk.pos, nil - } - return 0i, errors.New("No adjacent target") -} - -func (c *Character) isAdjacentTo(p complex64) bool { - return c.pos+DIR_N == p || c.pos+DIR_E == p || - c.pos+DIR_S == p || c.pos+DIR_W == p -} - -func (c *Character) getOpenSides() []complex64 { - var ret []complex64 - for _, d := range []complex64{DIR_N, DIR_W, DIR_E, DIR_S} { - if getByte(c.pos+d) == '.' { - ret = append(ret, c.pos+d) - } - } - return ret -} - -func (c *Character) hasOpenFlank() bool { - for _, d := range []complex64{DIR_N, DIR_E, DIR_S, DIR_W} { - if getByte(c.pos+d) == '.' { - return true - } - } - return false -} - -func (c *Character) moveTo(pos complex64) bool { - if getByte(pos) != '.' { - return false - } - delete(characters, c.pos) - setByte(c.pos, '.') - c.pos = pos - characters[c.pos] = c - setByte(c.pos, c.tp) - return true -} - -func getOpenSides(c complex64) []complex64 { - var ret []complex64 - for _, d := range []complex64{DIR_N, DIR_E, DIR_S, DIR_W} { - if getByte(c+d) == '.' { - ret = append(ret, c+d) - } - } - return ret -} - -// findAllPaths returns a map of all distances and a map of all paths -func findAllPaths(start *complex64) (map[complex64]int, map[complex64]complex64) { - all := []complex64{*start} - dist := map[complex64]int{*start: 0} - prev := map[complex64]complex64{*start: 0} - - for len(all) > 0 { - c := all[0] - all = all[1:] - for _, n := range getOpenSides(c) { - if _, ok := dist[n]; !ok { - all = append(all, n) - dist[n] = dist[c] + 1 - prev[n] = c - } - } - } - - return dist, prev -} - -func printBattlefield() { - //fmt.Print(CLEAR_SCREEN) - for i := 0; i < len(input)/width; i++ { - fmt.Println(string(input[i*width : (i+1)*width])) - } -} - -func setupBattle() { - characters = make(map[complex64]*Character) - for i := 0; i < len(input); i++ { - pos := getPosFromInt(i) - bt := getByte(pos) - if bt == 'G' || bt == 'E' { - characters[pos] = &Character{ - tp: bt, - pos: pos, - power: 3, - health: 200, - } - charSlice = append(charSlice, &pos) - } - } -} - -func isInMap(pos complex64) bool { - idx := int(real(pos)) + int(imag(pos))*width - return idx >= 0 && idx < len(input) -} - -// getByte pulls a byte from the given position in the input -func getByte(pos complex64) byte { - return input[int(real(pos))+int(imag(pos))*width] -} - -// setByte sets a byte in the input -func setByte(pos complex64, b byte) { - input[int(real(pos))+int(imag(pos))*width] = b -} - -func getPosFromInt(i int) complex64 { - return complex(float32(i%width), float32(i/width)) -} - -func getCoordString(p complex64) string { - return fmt.Sprintf("(%d,%d)", int(real(p)), int(imag(p))) -} - -func stdinToByteSlice() { +func StdinToStringSlice() []string { + var input []string scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() { - data := scanner.Bytes() - if width == 0 { - width = len(data) - } - - input = append(input, data...) + input = append(input, scanner.Text()) } -} - -// Returns the next move for the shortest path from p1 to p2 -type pathPoint struct { - pos complex64 - count int -} - -func findMove(p1 complex64, paths []pathPoint) (complex64, int) { - pathCount := len(paths) - // First check if p1 has _any_ possible moves - lowest := pathPoint{ - pos: 0 - 1i, - count: MAX_INT, - } - for _, v := range paths { - for _, d := range []complex64{DIR_N, DIR_E, DIR_S, DIR_W} { - wrkPt := pathPoint{ - pos: v.pos + d, - count: v.count + 1, - } - if !isInMap(wrkPt.pos) { - continue - } - if wrkPt.pos == p1 { - if wrkPt.count < lowest.count { - lowest.pos = v.pos - lowest.count = v.count - } - } - if getByte(wrkPt.pos) != '.' { - continue - } - var skip bool - for _, v2 := range paths { - if v2.pos == wrkPt.pos && v2.count <= wrkPt.count { - skip = true - break - } - } - if skip { - continue - } - paths = append(paths, wrkPt) - } - } - if len(paths) != pathCount && lowest.count == MAX_INT { - return findMove(p1, paths) - } - // We hit the end, return the lowest part - return lowest.pos, lowest.count -} - -// (Manhattan Distance, thanks earlier day) -func distance(p1, p2 complex64) int { - x1, y1, x2, y2 := real(p1), imag(p1), real(p2), imag(p2) - return int(math.Abs(float64(x1)-float64(x2)) + math.Abs(float64(y1)-float64(y2))) -} - -type ByPos []*complex64 - -func (c ByPos) Len() int { return len(c) } -func (c ByPos) Swap(i, j int) { c[i], c[j] = c[j], c[i] } -func (c ByPos) Less(i, j int) bool { - return imag(*c[i]) < imag(*c[j]) || - (imag(*c[i]) == imag(*c[j]) && real(*c[i]) < real(*c[j])) + return input } diff --git a/2018/day15/go.mod b/2018/day15/go.mod new file mode 100644 index 0000000..e49c518 --- /dev/null +++ b/2018/day15/go.mod @@ -0,0 +1,3 @@ +module git.bullercodeworks.com/brian/adventofcode/2018/day15 + +go 1.13 diff --git a/2018/day15/map.go b/2018/day15/map.go new file mode 100644 index 0000000..a5a3d7b --- /dev/null +++ b/2018/day15/map.go @@ -0,0 +1,49 @@ +package main + +type Coordinate struct { + X, Y int +} + +var offsets = []Coordinate{ + {0, -1}, + {-1, 0}, + {1, 0}, + {0, 1}, +} + +type Map map[int]map[int]*Tile + +func (m Map) SetTile(t *Tile, x, y int) { + if m[y] == nil { + m[y] = make(map[int]*Tile) + } + m[y][x] = t + t.X = x + t.Y = y + t.Map = m +} + +func (m Map) Tile(x, y int) *Tile { + if m[y] == nil { + return nil + } + return m[y][x] +} + +func (m Map) FindWalkableTiles(t *Tile) (map[*Tile]int, map[*Tile]*Tile) { + frontier := []*Tile{t} + distance := map[*Tile]int{t: 0} + cameFrom := map[*Tile]*Tile{t: nil} + for len(frontier) > 0 { + current := frontier[0] + frontier = frontier[1:] + for _, next := range current.WalkableNeighbors() { + if _, ok := distance[next]; !ok { + frontier = append(frontier, next) + distance[next] = distance[current] + 1 + cameFrom[next] = current + } + } + } + return distance, cameFrom +} diff --git a/2018/day15/tile.go b/2018/day15/tile.go new file mode 100644 index 0000000..39b2ba0 --- /dev/null +++ b/2018/day15/tile.go @@ -0,0 +1,34 @@ +package main + +type Tile struct { + Kind int + X, Y int + Map Map + Unit *Unit +} + +func (t Tile) WalkableNeighbors() []*Tile { + var neighbors []*Tile + for _, offset := range offsets { + if n := t.Map.Tile(t.X+offset.X, t.Y+offset.Y); n != nil && n.Kind == KindSpace { + neighbors = append(neighbors, n) + } + } + return neighbors +} + +type SortableTiles []*Tile + +func (s SortableTiles) Len() int { + return len(s) +} +func (s SortableTiles) Less(i, j int) bool { + if s[i].Y == s[j].Y { + return s[i].X < s[j].X + } + return s[i].Y < s[j].Y +} + +func (s SortableTiles) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} diff --git a/2018/day15/unit.go b/2018/day15/unit.go new file mode 100644 index 0000000..1fdf890 --- /dev/null +++ b/2018/day15/unit.go @@ -0,0 +1,144 @@ +package main + +import ( + "math" + "sort" +) + +type Unit struct { + Kind int + Hitpoints int + Power int + Tile *Tile +} + +const ( + defaultHitpoints = 200 + defaultPower = 3 +) + +type SortableUnits []*Unit + +func (s SortableUnits) Len() int { + return len(s) +} +func (s SortableUnits) Less(i, j int) bool { + if s[i].Tile.Y == s[j].Tile.Y { + return s[i].Tile.X < s[j].Tile.X + } + return s[i].Tile.Y < s[j].Tile.Y +} + +func (s SortableUnits) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func NewUnit(tile *Tile, kind, elfPower int) *Unit { + unit := &Unit{ + Kind: kind, + Hitpoints: defaultHitpoints, + Power: defaultPower, + Tile: tile, + } + tile.Unit = unit + if unit.Kind == KindElf { + unit.Power = elfPower + } + return unit +} + +func (u Unit) Targets(c *Cave) bool { + for _, unit := range c.Units { + if unit.Kind != u.Kind && unit.Hitpoints > 0 { + return true + } + } + return false +} + +// NextTile returns the next tile to move to and the target tile (or nil if no target found) +func (u *Unit) NextTile(c *Cave) (*Tile, *Tile) { + var targets SortableTiles + closestTargetDistance := math.MaxInt32 + distances, path := c.Map.FindWalkableTiles(u.Tile) + enemies := u.Enemies(c) + for _, enemy := range enemies { + for _, target := range enemy.Tile.WalkableNeighbors() { + if distance, ok := distances[target]; ok && distance <= closestTargetDistance { + if distance < closestTargetDistance { + closestTargetDistance = distance + targets = SortableTiles{} + } + targets = append(targets, target) + } + } + } + sort.Sort(targets) + if len(targets) > 0 { + target := targets[0] + current := target + for { + if path[current] == u.Tile { + return current, target + } + current = path[current] + } + } + return nil, nil +} + +// Enemies returns enemy units sorted by map position in reading order +func (u *Unit) Enemies(c *Cave) SortableUnits { + var enemies SortableUnits + for _, unit := range c.Units { + if unit.Kind != u.Kind && unit.Hitpoints > 0 { + enemies = append(enemies, unit) + } + } + sort.Sort(enemies) + return enemies +} + +func (u *Unit) EnemyNeighbor(c *Cave) *Unit { + var target *Unit + for _, offset := range offsets { + if t := c.Map.Tile(u.Tile.X+offset.X, u.Tile.Y+offset.Y); t != nil && t.Unit != nil && t.Unit.Kind != u.Kind && t.Unit.Hitpoints > 0 { + if target == nil || t.Unit.Hitpoints < target.Hitpoints { + target = t.Unit + } + } + } + return target +} + +func (u *Unit) Move(c *Cave) { + if u.EnemyNeighbor(c) != nil { + return + } + next, _ := u.NextTile(c) + if next != nil { + next.Unit = u + next.Kind = u.Kind + u.Tile.Kind = KindSpace + u.Tile.Unit = nil + u.Tile = next + } +} + +func (u *Unit) Attack(c *Cave) bool { + enemy := u.EnemyNeighbor(c) + if enemy != nil { + killed := enemy.Damage(c, u.Power) + return killed && enemy.Kind == KindElf + } + return false +} + +func (u *Unit) Damage(c *Cave, damage int) bool { + u.Hitpoints = u.Hitpoints - damage + if u.Hitpoints <= 0 { + c.RemoveUnit(u) + return true + } + return false +} diff --git a/go.mod b/go.mod index 39dfbf8..f075442 100644 --- a/go.mod +++ b/go.mod @@ -34,3 +34,5 @@ require ( gopkg.in/yaml.v2 v2.2.2 // indirect honnef.co/go/tools v0.0.0-20180920025451-e3ad64cb4ed3 // indirect ) + +go 1.13