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).
333 lines
7.8 KiB
Go
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)
|
|
}
|