mirror of
https://github.com/Threnklyn/advent-of-code-go.git
synced 2026-05-18 19:13:27 +02:00
435 lines
12 KiB
Go
435 lines
12 KiB
Go
/*
|
|
Intcode struct is defined within this file
|
|
Robot struct houses an Intcode computer and its RecursiveMove method populates a map of
|
|
coordinates to the floor type (-1: wall, 1: hallway, 2: O2 tank, 5: origin)
|
|
That map is converted into a 2D grid (slice)
|
|
2D slice is passed to a backtracking & searching algorithm to find the shortest path
|
|
*/
|
|
|
|
package main
|
|
|
|
import (
|
|
"github.com/alexchao26/advent-of-code-go/util"
|
|
"fmt"
|
|
"log"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
func main() {
|
|
// read the input file, modify it to a slice of numbers
|
|
inputFile := util.ReadFile("../input.txt")
|
|
|
|
splitStrings := strings.Split(inputFile, ",")
|
|
|
|
inputNumbers := make([]int, len(splitStrings))
|
|
for i, v := range splitStrings {
|
|
inputNumbers[i], _ = strconv.Atoi(v)
|
|
}
|
|
|
|
robot := MakeRobot(inputNumbers)
|
|
|
|
// fire off recursive move function to populate the robot's floorDetails property
|
|
robot.RecursiveMove()
|
|
|
|
// make grid from the map of coordinates to floor types
|
|
grid := Draw(robot.floorDetails)
|
|
|
|
// find the origin coordinates to know where to start recursive searcher from, it was tagged with a 5
|
|
var originX, originY int
|
|
for y, row := range grid {
|
|
for x, tileType := range row {
|
|
if tileType == 5 {
|
|
originX = x
|
|
originY = y
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
shortestLength := findShortestLength(grid, originX, originY)
|
|
fmt.Println("Shortest length is", shortestLength)
|
|
}
|
|
|
|
// Robot struct to maintain detail's on the Robot's coordinates, path
|
|
type Robot struct {
|
|
fromTop, fromLeft int
|
|
floorDetails map[string]int // maps coordinates and type of tile (0 == wall, 1 == path, 2 == oxygen)
|
|
computer *Intcode
|
|
}
|
|
|
|
// MakeRobot returns an instance of a Robot
|
|
func MakeRobot(intcodeInput []int) *Robot {
|
|
return &Robot{
|
|
0,
|
|
0,
|
|
map[string]int{"0,0": 5}, // mark the origin specially with a 5
|
|
MakeComputer(intcodeInput),
|
|
}
|
|
}
|
|
|
|
var backtrack map[int]int = map[int]int{
|
|
1: 2, // north (1), south (2)
|
|
2: 1,
|
|
3: 4, // west (3), east(4)
|
|
4: 3,
|
|
}
|
|
|
|
// dx is the difference to add when traveling in the given direction
|
|
// i.e. add 0 for north and south, for west decrement 1, for east add 1
|
|
var dx map[int]int = map[int]int{
|
|
1: 0,
|
|
2: 0,
|
|
3: -1,
|
|
4: 1,
|
|
}
|
|
|
|
// dy is the vertical distance traveled
|
|
var dy map[int]int = map[int]int{
|
|
1: 1,
|
|
2: -1,
|
|
3: 0,
|
|
4: 0,
|
|
}
|
|
|
|
// RecursiveMove will populate a robot's floor details property by traveling in all directions
|
|
// and
|
|
func (robot *Robot) RecursiveMove() {
|
|
for i := 1; i <= 4; i++ {
|
|
// if next coordinates have already been detailed, skip all calculations
|
|
nextCoords := fmt.Sprintf("%v,%v", robot.fromTop+dy[i], robot.fromLeft+dx[i])
|
|
|
|
if robot.floorDetails[nextCoords] == 0 {
|
|
robot.computer.Step(i)
|
|
computerOutput := robot.computer.Outputs[len(robot.computer.Outputs)-1]
|
|
|
|
switch computerOutput {
|
|
case 0: // hit a wall, do not recurse
|
|
// update robot's wall coords to include the wall
|
|
// note representing walls with a -1 to avoid the zero value detection
|
|
robot.floorDetails[nextCoords] = -1
|
|
case 1, 2: // walked and hit the O2 tank or not
|
|
// update floorDetails
|
|
robot.floorDetails[nextCoords] = computerOutput
|
|
|
|
// continue to walk the robot. walk the robot into the nextCoords spot
|
|
robot.fromLeft += dx[i]
|
|
robot.fromTop += dy[i]
|
|
|
|
// recurse
|
|
robot.RecursiveMove()
|
|
|
|
// backtrack so the robot walks in the remainder of directions from this output
|
|
robot.fromLeft -= dx[i]
|
|
robot.fromTop -= dy[i]
|
|
// backtrack the computer
|
|
robot.computer.Step(backtrack[i])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
Intcode is an OOP approach *************************************************
|
|
MakeComputer is equivalent to the constructor
|
|
Step takes in an input int and updates properties in the computer:
|
|
- InstructionIndex: where to read the next instruction from
|
|
- LastOutput, what the last opcode 4 outputted
|
|
- PuzzleIndex based if the last instruction modified the puzzle at all
|
|
****************************************************************************/
|
|
type Intcode struct {
|
|
PuzzleInput []int // file/puzzle input parsed into slice of ints
|
|
InstructionIndex int // stores the index where the next instruction is
|
|
RelativeBase int // relative base for opcode 9 and param mode 2
|
|
Outputs []int // stores all outputs
|
|
IsRunning bool // will be true until a 99 opcode is hit
|
|
}
|
|
|
|
// MakeComputer initializes a new comp
|
|
func MakeComputer(PuzzleInput []int) *Intcode {
|
|
puzzleInputCopy := make([]int, len(PuzzleInput))
|
|
copy(puzzleInputCopy, PuzzleInput)
|
|
|
|
comp := Intcode{
|
|
puzzleInputCopy,
|
|
0,
|
|
0,
|
|
make([]int, 0),
|
|
true,
|
|
}
|
|
return &comp
|
|
}
|
|
|
|
// Step will read the next 4 values in the input `sli` and make updates
|
|
// according to the opcodes
|
|
func (comp *Intcode) Step(input int) {
|
|
// read the instruction, opcode and the indexes where the params point to
|
|
opcode, paramIndexes := comp.GetOpCodeAndParamIndexes()
|
|
param1, param2, param3 := paramIndexes[0], paramIndexes[1], paramIndexes[2]
|
|
|
|
// ensure params are within the bounds of PuzzleInput, resize if necessary
|
|
// Note: need to optimize this to not resize if the params are not being used
|
|
switch opcode {
|
|
case 1, 2, 7, 8:
|
|
comp.ResizeMemory(param1, param2, param3)
|
|
case 5, 6:
|
|
comp.ResizeMemory(param1, param2)
|
|
case 3, 4, 9:
|
|
comp.ResizeMemory(param1)
|
|
}
|
|
|
|
switch opcode {
|
|
case 99: // 99: Terminates program
|
|
fmt.Println("Terminating...")
|
|
comp.IsRunning = false
|
|
case 1: // 1: Add next two paramIndexes, store in third
|
|
comp.PuzzleInput[param3] = comp.PuzzleInput[param1] + comp.PuzzleInput[param2]
|
|
comp.InstructionIndex += 4
|
|
comp.Step(input)
|
|
case 2: // 2: Multiply next two and store in third
|
|
comp.PuzzleInput[param3] = comp.PuzzleInput[param1] * comp.PuzzleInput[param2]
|
|
comp.InstructionIndex += 4
|
|
comp.Step(input)
|
|
case 3: // 3: Takes one input and saves it to position of one parameter
|
|
// check if input has already been used (i.e. input == -1)
|
|
// if it's been used, return out to prevent further Steps
|
|
// NOTE: making a big assumption that -1 will never be an input...
|
|
if input == -1 {
|
|
return
|
|
}
|
|
|
|
// else recurse with a -1 to signal the initial input has been processed
|
|
comp.PuzzleInput[param1] = input
|
|
comp.InstructionIndex += 2
|
|
comp.Step(-1)
|
|
case 4: // 4: outputs its input value
|
|
// set LastOutput of the computer & log it
|
|
comp.Outputs = append(comp.Outputs, comp.PuzzleInput[param1])
|
|
// fmt.Printf("Opcode 4 output: %v\n", comp.LastOutput)
|
|
comp.InstructionIndex += 2
|
|
|
|
// continue running until terminates or asks for another input
|
|
comp.Step(input)
|
|
// 5: jump-if-true: if first param != 0, move pointer to second param, else nothing
|
|
case 5:
|
|
if comp.PuzzleInput[param1] != 0 {
|
|
comp.InstructionIndex = comp.PuzzleInput[param2]
|
|
} else {
|
|
comp.InstructionIndex += 3
|
|
}
|
|
comp.Step(input)
|
|
// 6: jump-if-false, if first param == 0 then set instruction pointer to 2nd param, else nothing
|
|
case 6:
|
|
if comp.PuzzleInput[param1] == 0 {
|
|
comp.InstructionIndex = comp.PuzzleInput[param2]
|
|
} else {
|
|
comp.InstructionIndex += 3
|
|
}
|
|
comp.Step(input)
|
|
// 7: less-than, if param1 < param2 then store 1 in postion of 3rd param, else store 0
|
|
case 7:
|
|
if comp.PuzzleInput[param1] < comp.PuzzleInput[param2] {
|
|
comp.PuzzleInput[param3] = 1
|
|
} else {
|
|
comp.PuzzleInput[param3] = 0
|
|
}
|
|
comp.InstructionIndex += 4
|
|
comp.Step(input)
|
|
// 8: equals, if param1 == param2 then set position of 3rd param to 1, else store 0
|
|
case 8:
|
|
if comp.PuzzleInput[param1] == comp.PuzzleInput[param2] {
|
|
comp.PuzzleInput[param3] = 1
|
|
} else {
|
|
comp.PuzzleInput[param3] = 0
|
|
}
|
|
comp.InstructionIndex += 4
|
|
comp.Step(input)
|
|
// 9: adjust relative base
|
|
case 9:
|
|
comp.RelativeBase += comp.PuzzleInput[param1]
|
|
comp.InstructionIndex += 2
|
|
comp.Step(input)
|
|
default:
|
|
log.Fatalf("Error: unknown opcode %v at index %v", opcode, comp.PuzzleInput[comp.InstructionIndex])
|
|
}
|
|
}
|
|
|
|
/*
|
|
GetOpCodeAndParamIndexes will parse the instruction at comp.PuzzleInput[comp.InstructionIndex]
|
|
- opcode will be the left two digits, mod by 100 will get that
|
|
- rest of instructions will be grabbed via mod 10
|
|
- these also have to be parsed for the
|
|
*/
|
|
func (comp *Intcode) GetOpCodeAndParamIndexes() (int, [3]int) {
|
|
instruction := comp.PuzzleInput[comp.InstructionIndex]
|
|
|
|
// opcode is the lowest two digits, so mod by 100
|
|
opcode := instruction % 100
|
|
instruction /= 100
|
|
|
|
// assign the indexes that need to be read by reading the parameter modes
|
|
var paramIndexes [3]int
|
|
for i := 1; i <= 3 && comp.InstructionIndex+i < len(comp.PuzzleInput); i++ {
|
|
// grab the mode with a mod, last digit
|
|
mode := instruction % 10
|
|
instruction /= 10
|
|
|
|
switch mode {
|
|
case 0: // position mode, index will be the value at the index
|
|
paramIndexes[i-1] = comp.PuzzleInput[comp.InstructionIndex+i]
|
|
case 1: // immediate mode, the index itself
|
|
paramIndexes[i-1] = comp.InstructionIndex + i
|
|
case 2: // relative mode, like position mode but index is added to relative base
|
|
paramIndexes[i-1] = comp.PuzzleInput[comp.InstructionIndex+i] + comp.RelativeBase
|
|
}
|
|
}
|
|
|
|
return opcode, paramIndexes
|
|
}
|
|
|
|
// ResizeMemory will take any number of integers and resize the computer's memory appropriately
|
|
func (comp *Intcode) ResizeMemory(sizes ...int) {
|
|
// get largest of input sizes
|
|
maxArg := sizes[0]
|
|
for _, v := range sizes {
|
|
if v > maxArg {
|
|
maxArg = v
|
|
}
|
|
}
|
|
|
|
// resize if PuzzleInput's length is shorter
|
|
if maxArg >= len(comp.PuzzleInput) {
|
|
// make empty slice to copy into, of the new, larger size
|
|
resizedPuzzleInput := make([]int, maxArg+1)
|
|
// copy old puzzle input values in
|
|
copy(resizedPuzzleInput, comp.PuzzleInput)
|
|
|
|
// overwrite puzzle input
|
|
comp.PuzzleInput = resizedPuzzleInput
|
|
}
|
|
}
|
|
|
|
// Draw was copied from day11. It converts a map of points mapped from a (0,0) origin to a 2D grid
|
|
// The origin loses its reference...
|
|
func Draw(mapCoordsToType map[string]int) [][]int {
|
|
var lowX, highX, lowY, highY int
|
|
for key := range mapCoordsToType {
|
|
coords := strings.Split(key, ",")
|
|
x, _ := strconv.Atoi(coords[0])
|
|
y, _ := strconv.Atoi(coords[1])
|
|
switch {
|
|
case x < lowX:
|
|
lowX = x
|
|
case x > highX:
|
|
highX = x
|
|
}
|
|
switch {
|
|
case y < lowY:
|
|
lowY = y
|
|
case y > highY:
|
|
highY = y
|
|
}
|
|
}
|
|
|
|
// Determine the bounds of the grid
|
|
edgeLength := 2 * util.MaxInts(-lowY, -lowX, highY, highX)
|
|
|
|
grid := make([][]int, edgeLength)
|
|
for i := 0; i < edgeLength; i++ {
|
|
// each character will initialize as a space character
|
|
grid[i] = make([]int, edgeLength)
|
|
}
|
|
|
|
// Iterate through all coordinates and transcribe x,y onto a 2D grid
|
|
// where the math is a little different...
|
|
for key, val := range mapCoordsToType {
|
|
// key is string coords
|
|
coords := strings.Split(key, ",")
|
|
x, _ := strconv.Atoi(coords[0])
|
|
y, _ := strconv.Atoi(coords[1])
|
|
x += edgeLength / 2
|
|
y += edgeLength / 2
|
|
// val is color to paint (1 or 0)
|
|
if val != -1 {
|
|
grid[x][y] = val
|
|
}
|
|
}
|
|
|
|
// trim off due to making the initial grid too large
|
|
grid = trim(grid)
|
|
// rotate it because of how I coded up the robot's coordinates :/
|
|
grid = util.RotateGridInts(grid)
|
|
// retrim
|
|
grid = trim(grid)
|
|
|
|
return grid
|
|
}
|
|
|
|
// helper function for Draw to remove whitespace from overestimating the size
|
|
// of the drawing space
|
|
func trim(grid [][]int) [][]int {
|
|
// remove all empty rows at top and bottom
|
|
removeRowsTop:
|
|
for i := 0; i < len(grid); {
|
|
for j := 0; j < len(grid[i]); j++ {
|
|
if grid[i][j] != 0 {
|
|
break removeRowsTop
|
|
}
|
|
}
|
|
grid = grid[1:]
|
|
}
|
|
|
|
// remove empty columns on left
|
|
removeColsLeft:
|
|
for i := 0; i < len(grid[0]); {
|
|
for j := 0; j < len(grid); j++ {
|
|
if grid[j][i] != 0 {
|
|
break removeColsLeft
|
|
}
|
|
}
|
|
// if loop hasn't broken out, iterate over first "column" and slice off "0-index"
|
|
for j := 0; j < len(grid); j++ {
|
|
grid[j] = grid[j][1:]
|
|
}
|
|
}
|
|
|
|
return grid
|
|
}
|
|
|
|
// findShortestLength is a (fairly) standard brute force searching algorithm that will return the shortest path from origin to the O2 tank (cell with a 2)
|
|
func findShortestLength(grid [][]int, startX, startY int) int {
|
|
shortestLength := math.MaxInt32
|
|
|
|
var recurse func(x, y, pathLength int)
|
|
recurse = func(x, y, pathLength int) {
|
|
if grid[y][x] == 2 {
|
|
if pathLength < shortestLength {
|
|
shortestLength = pathLength
|
|
}
|
|
return
|
|
}
|
|
|
|
for i := 1; i <= 4; i++ {
|
|
nextX, nextY := x+dx[i], y+dy[i]
|
|
// if it is inbounds and the next coordinate is not a wall or something already traversed
|
|
inBounds := nextX >= 0 && nextX < len(grid[0]) && nextY >= 0 && nextY < len(grid)
|
|
if inBounds && grid[nextY][nextX] != 0 {
|
|
oldVal := grid[y][x]
|
|
|
|
// mark as "seen" by making it a "fake" wall
|
|
grid[y][x] = 0
|
|
|
|
// recurse into the next coordinate
|
|
recurse(nextX, nextY, pathLength+1)
|
|
|
|
// undo fake wall
|
|
grid[y][x] = oldVal
|
|
}
|
|
}
|
|
}
|
|
|
|
recurse(startX, startY, 0)
|
|
return shortestLength
|
|
}
|