Files
2019/days/15.go
Parnic ce6e63b5b6 Day 15 Part 2
Well...that turned out easier than I thought. I suspected this solution would work, but wasn't completely confident. It can only work for the type of maze used by this problem (where there are no loops of open areas).
2022-06-13 08:31:43 -05:00

333 lines
7.8 KiB
Go

package days
//go:generate stringer -type=cellStatus,responseType,dirType -output=15_types_string.go
import (
"fmt"
"math"
"sort"
u "parnic.com/aoc2019/utilities"
)
type point u.Pair[int, int]
type cellStatus int
type responseType int
type dirType int
const (
cellStatusUnknown cellStatus = iota
cellStatusWall
cellStatusOpen
cellStatusGoal
)
const (
responseWall responseType = iota
responseSuccess
responseFoundGoal
)
const maxVisited = 3
const (
dirNorth dirType = iota + 1
dirSouth
dirWest
dirEast
dirFirst = dirNorth
dirLast = dirEast
)
var dirOrders = [][]dirType{
{dirNorth, dirSouth, dirWest, dirEast},
{dirSouth, dirWest, dirEast, dirNorth},
{dirWest, dirEast, dirNorth, dirSouth},
{dirEast, dirNorth, dirSouth, dirWest},
}
// turned out to be unnecessary on multiple datasets i tried. increases the iterations 6x
// var dirOrders = u.GetPermutations(dirNorth, dirSouth, dirWest, dirEast)
type visitedStatus struct {
timesVisited int
distanceFromStart int
}
type Day15 struct {
program u.IntcodeProgram
grid map[point]cellStatus
visited map[point]*visitedStatus
shortestPath []point
pos point
goalPos point
}
func (d *Day15) Parse() {
d.program = u.LoadIntcodeProgram("15p")
d.grid = map[point]cellStatus{
{First: 0, Second: 0}: cellStatusOpen,
}
d.visited = map[point]*visitedStatus{
{First: 0, Second: 0}: {timesVisited: 1},
}
d.shortestPath = []point{{}}
}
func (d Day15) Num() int {
return 15
}
func (d Day15) getPointInDirection(pos point, dir dirType) point {
target := pos
switch dir {
case dirNorth:
target.First--
case dirSouth:
target.First++
case dirWest:
target.Second--
case dirEast:
target.Second++
}
return target
}
func (d Day15) getCellTypeInDirection(pos point, dir dirType) (cellStatus, point) {
target := d.getPointInDirection(pos, dir)
return d.grid[target], target
}
func (d Day15) getAdjacentCellsOfType(pos point, cellType cellStatus) []point {
points := make([]point, 0, 4)
for i := dirFirst; i <= dirLast; i++ {
adjacentCell := d.getPointInDirection(pos, i)
if d.grid[adjacentCell] == cellType {
points = append(points, adjacentCell)
}
}
return points
}
func (d Day15) getDirToNextCellType(pos point, t cellStatus, maxNumVisited int, dirs []dirType) (dirType, point, error) {
for _, dir := range dirs {
cellInDirection, targetCell := d.getCellTypeInDirection(pos, dir)
if cellInDirection == t {
_, visitedTargetExists := d.visited[targetCell]
foundUnknown := t == cellStatusUnknown && !visitedTargetExists
foundOther := t != cellStatusUnknown && visitedTargetExists && d.visited[targetCell].timesVisited <= maxNumVisited
if foundUnknown || foundOther {
return dir, targetCell, nil
}
}
}
return dirFirst, point{}, fmt.Errorf("no %v tiles around %v", t, pos)
}
func (d *Day15) Draw() {
min := point{First: math.MaxInt, Second: math.MaxInt}
max := point{First: math.MinInt, Second: math.MinInt}
for p := range d.grid {
if p.First < min.First {
min.First = p.First
}
if p.First > max.First {
max.First = p.First
}
if p.Second < min.Second {
min.Second = p.Second
}
if p.Second > max.Second {
max.Second = p.Second
}
}
for x := min.First; x <= max.First; x++ {
for y := min.Second; y <= max.Second; y++ {
p := point{First: x, Second: y}
switch d.grid[p] {
case cellStatusGoal:
fmt.Printf("%s@%s", u.ColorBrightGreen, u.TextReset)
case cellStatusOpen:
if p == d.pos {
fmt.Print(u.BackgroundBrightRed)
} else if x == 0 && y == 0 {
fmt.Print(u.BackgroundYellow)
} else if u.ArrayContains(d.shortestPath, p) {
fmt.Print(u.BackgroundGreen)
} else if d.visited[p] != nil && d.visited[p].timesVisited > maxVisited {
fmt.Print(u.ColorYellow)
fmt.Print(u.BackgroundBlack)
} else if d.visited[p] != nil && d.visited[p].timesVisited > 1 {
fmt.Print(u.BackgroundMagenta)
} else {
fmt.Print(u.BackgroundBlue)
}
fmt.Printf(".%s", u.TextReset)
case cellStatusWall:
fmt.Print("█")
case cellStatusUnknown:
fmt.Print(" ")
}
}
fmt.Println()
}
}
func (d *Day15) markShortestPath() {
pos := d.goalPos
checkOffsets := []point{
{First: -1, Second: 0},
{First: 1, Second: 0},
{First: 0, Second: -1},
{First: 0, Second: 1},
}
checkPt := func(pt point) (bool, int) {
if v, exists := d.visited[pt]; exists && d.grid[pt] == cellStatusOpen {
return true, v.distanceFromStart
}
return false, math.MaxInt
}
d.shortestPath = []point{d.goalPos}
for pos.First != 0 || pos.Second != 0 {
lowestDist := math.MaxInt
lowestPoint := point{}
for _, pt := range checkOffsets {
newPt := point{First: pos.First + pt.First, Second: pos.Second + pt.Second}
if found, dist := checkPt(newPt); found && dist < lowestDist {
lowestDist = dist
lowestPoint = newPt
}
}
d.shortestPath = append(d.shortestPath, lowestPoint)
pos = lowestPoint
}
}
func (d *Day15) exploreFullMap() map[point]*visitedStatus {
grids := make([]map[point]cellStatus, 0, len(dirOrders))
goalVisited := d.visited
for _, dirOrder := range dirOrders {
d.program.Reset()
targetPos := point{}
nextDir := dirFirst
distFromStart := 0
d.pos = point{}
d.visited = map[point]*visitedStatus{
{First: 0, Second: 0}: {timesVisited: 1},
}
d.grid = map[point]cellStatus{
{First: 0, Second: 0}: cellStatusOpen,
}
d.program.RunIn(func(inputStep int) int64 {
var err error
nextDir, targetPos, err = d.getDirToNextCellType(d.pos, cellStatusUnknown, 0, dirOrder)
if err != nil {
// ensure we never try to go back into the trapped spot
d.visited[d.pos].timesVisited = maxVisited + 1
for x := 1; x <= maxVisited && err != nil; x++ {
nextDir, targetPos, err = d.getDirToNextCellType(d.pos, cellStatusOpen, x, dirOrder)
}
}
if err != nil {
// d.Draw()
// panic(err)
d.program.Stop()
}
return int64(nextDir)
}, func(val int64, state u.IntcodeProgramState) {
rVal := responseType(val)
p := d.getPointInDirection(d.pos, nextDir)
shouldMove := true
switch rVal {
case responseWall:
d.grid[p] = cellStatusWall
shouldMove = false
case responseSuccess:
d.grid[p] = cellStatusOpen
case responseFoundGoal:
d.grid[p] = cellStatusGoal
}
if shouldMove {
d.pos = targetPos
if d.visited[d.pos] == nil {
d.visited[d.pos] = &visitedStatus{}
distFromStart++
} else {
distFromStart--
}
d.visited[d.pos].timesVisited++
d.visited[d.pos].distanceFromStart = distFromStart
}
if rVal == responseFoundGoal {
// d.Draw()
d.goalPos = targetPos
goalVisited = d.visited
}
})
grids = append(grids, d.grid)
}
d.grid = map[point]cellStatus{
{First: 0, Second: 0}: cellStatusOpen,
}
for _, grid := range grids {
keys := u.MapKeys(grid)
for _, key := range keys {
d.grid[key] = grid[key]
}
}
return goalVisited
}
func (d *Day15) tagDistanceRecursive(pos, last point, dist int, distances map[point]int) {
distances[pos] = dist
for _, cell := range d.getAdjacentCellsOfType(pos, cellStatusOpen) {
if cell == last {
continue
}
d.tagDistanceRecursive(cell, pos, dist+1, distances)
}
}
func (d *Day15) Part1() string {
d.visited = d.exploreFullMap()
d.markShortestPath()
for _, visited := range d.visited {
visited.timesVisited = 1
}
d.pos = point{}
d.Draw()
return fmt.Sprintf("Moves required to reach target: %s%d%s", u.TextBold, d.visited[d.goalPos].distanceFromStart, u.TextReset)
}
func (d *Day15) Part2() string {
startLoc := d.goalPos
distanceMap := map[point]int{startLoc: 0}
d.tagDistanceRecursive(startLoc, point{}, 0, distanceMap)
cellDistances := u.MapValues(distanceMap)
sort.Slice(cellDistances, func(i, j int) bool { return cellDistances[i] > cellDistances[j] })
return fmt.Sprintf("%s%d%s", u.TextBold, cellDistances[0], u.TextReset)
}