advent-of-code-2023/day16/main.go

385 lines
9.3 KiB
Go

package main
import (
"errors"
"fmt"
"io"
"os"
"slices"
"strings"
"sync"
)
type Tile rune
const (
TileMirrorRight Tile = '/'
TileMirrorLeft Tile = '\\'
TileSplitterVertical Tile = '|'
TileSplitterHorizontal Tile = '-'
)
type Direction int
const (
DirectionNorth Direction = iota
DirectionEast
DirectionSouth
DirectionWest
)
type Coordinate struct {
row int
col int
}
type Beam struct {
position Coordinate
direction Direction
}
type TileGrid struct {
Height int
Width int
Tiles map[Coordinate]Tile
}
func (dir Direction) Horizontal() bool {
return dir == DirectionEast || dir == DirectionWest
}
func (dir Direction) Vertical() bool {
return dir == DirectionNorth || dir == DirectionSouth
}
func (grid TileGrid) Print() {
grid.PrintWithBeams(nil)
}
func (grid TileGrid) PrintWithBeams(beams []Beam) {
for row := 0; row < grid.Height; row++ {
for col := 0; col < grid.Width; col++ {
position := Coordinate{row: row, col: col}
tile, ok := grid.Tiles[position]
beamIdx := slices.IndexFunc(beams, func(beam Beam) bool {
return beam.position == position
})
if beamIdx != -1 && !ok {
beamAtPosition := beams[beamIdx]
switch beamAtPosition.direction {
case DirectionNorth:
fmt.Print("^")
case DirectionSouth:
fmt.Print("v")
case DirectionWest:
fmt.Print("<")
case DirectionEast:
fmt.Print(">")
default:
panic(fmt.Sprintf("invalid direction %d", beamAtPosition.direction))
}
} else if !ok {
fmt.Print(".")
} else {
fmt.Printf("%c", tile)
}
}
fmt.Println()
}
}
// InBounds checks if the given position is in bounds of the grid
func (grid TileGrid) InBounds(position Coordinate) bool {
return position.row >= 0 && position.col >= 0 && position.row < grid.Height && position.col < grid.Width
}
// MovedInDirection returns a new beam, which is the result of this beam having moved in the given direction.
func (beam Beam) MovedInDirection(dir Direction) Beam {
updPosition := beam.position
switch dir {
case DirectionNorth:
updPosition.row--
case DirectionSouth:
updPosition.row++
case DirectionEast:
updPosition.col++
case DirectionWest:
updPosition.col--
default:
panic(fmt.Sprintf("invalid direction value %d", dir))
}
return Beam{
position: updPosition,
direction: dir,
}
}
// RotatedInDirection returns a new beam, which is the result of this beam rotated to face that direction.
// Its position is not updated.
func (beam Beam) RotatedInDirection(dir Direction) Beam {
return Beam{
position: beam.position,
direction: dir,
}
}
func main() {
if len(os.Args) != 2 && len(os.Args) != 3 {
fmt.Fprintf(os.Stderr, "Usage: %s inputfile\n", os.Args[0])
os.Exit(1)
}
inputFilename := os.Args[1]
inputFile, err := os.Open(inputFilename)
if err != nil {
panic(fmt.Sprintf("could not open input file: %s", err))
}
defer inputFile.Close()
inputBytes, err := io.ReadAll(inputFile)
if err != nil {
panic(fmt.Sprintf("could not read input file: %s", err))
}
input := strings.TrimSpace(string(inputBytes))
inputLines := strings.Split(input, "\n")
grid, err := parseTileGrid(inputLines)
if err != nil {
panic(fmt.Sprintf("failed to parse input: %s", err))
}
fmt.Printf("Part 1: %d\n", part1(grid))
fmt.Printf("Part 2: %d\n", part2(grid))
}
func part1(grid TileGrid) int {
startingBeam := Beam{position: Coordinate{row: 0, col: 0}, direction: DirectionEast}
return simulate(grid, startingBeam)
}
func part2(grid TileGrid) int {
startingBeams := allStartingBeams(grid)
wg := sync.WaitGroup{}
answerChan := make(chan int)
for _, startingBeam := range startingBeams {
wg.Add(1)
startingBeam := startingBeam
go func() {
answerChan <- simulate(grid, startingBeam)
wg.Done()
}()
}
go func() {
wg.Wait()
close(answerChan)
}()
maxEnergy := 0
for energy := range answerChan {
maxEnergy = max(energy, maxEnergy)
}
return maxEnergy
}
// simulate will simulate the beam's movement starting at the given beam, returning the number of energized tiles
func simulate(grid TileGrid, startingBeam Beam) int {
beams := []Beam{startingBeam}
nextBeams := []Beam{}
beamHistory := map[Beam]struct{}{
startingBeam: {},
}
for len(beams) > 0 {
for _, beam := range beams {
updBeam := beam.MovedInDirection(beam.direction)
tile, ok := grid.Tiles[updBeam.position]
if ok {
resultingBeams := beamsFromCollision(tile, updBeam)
nextBeams = append(nextBeams, resultingBeams...)
} else {
nextBeams = append(nextBeams, updBeam)
}
}
beams = []Beam{}
for _, beam := range nextBeams {
_, seenBeam := beamHistory[beam]
if grid.InBounds(beam.position) && !seenBeam {
beams = append(beams, beam)
beamHistory[beam] = struct{}{}
}
}
nextBeams = []Beam{}
}
visitedPositions := map[Coordinate]struct{}{}
for beam := range beamHistory {
visitedPositions[beam.position] = struct{}{}
}
return len(visitedPositions)
}
// allStartingBeams gets all possible starting beams around the edges of the grid
func allStartingBeams(grid TileGrid) []Beam {
startingBeams := []Beam{}
for col := 0; col < grid.Width; col++ {
topBeam := Beam{
direction: DirectionSouth,
position: Coordinate{row: 0, col: col},
}
bottomBeam := Beam{
direction: DirectionNorth,
position: Coordinate{row: grid.Height - 1, col: col},
}
startingBeams = append(startingBeams, topBeam, bottomBeam)
}
for row := 0; row < grid.Height; row++ {
leftBeam := Beam{
direction: DirectionEast,
position: Coordinate{row: row, col: 0},
}
rightBeam := Beam{
direction: DirectionWest,
position: Coordinate{row: row, col: grid.Width - 1},
}
startingBeams = append(startingBeams, leftBeam, rightBeam)
}
return startingBeams
}
// beamsFromCollision will get the resulting beams when a beam collides with a tile
func beamsFromCollision(tile Tile, beam Beam) []Beam {
switch tile {
case TileMirrorLeft, TileMirrorRight:
return beamsFromMirror(tile, beam)
case TileSplitterHorizontal, TileSplitterVertical:
return beamsFromSplitter(tile, beam)
default:
panic(fmt.Sprintf("invalid tile %c", tile))
}
}
// beamsFromSplitter gets the resulting beams when a bema collides with a splitter. Panics if the given tile
// is not a splitter.
func beamsFromSplitter(tile Tile, beam Beam) []Beam {
if tile != TileSplitterHorizontal && tile != TileSplitterVertical {
panic("cannot treat a non-splitter tile as a splitter")
}
if beam.direction.Horizontal() && tile == TileSplitterHorizontal {
return []Beam{beam}
} else if beam.direction.Vertical() && tile == TileSplitterVertical {
return []Beam{beam}
} else if beam.direction.Horizontal() && tile == TileSplitterVertical {
return []Beam{
beam.RotatedInDirection(DirectionNorth),
beam.RotatedInDirection(DirectionSouth),
}
} else if beam.direction.Vertical() && tile == TileSplitterHorizontal {
return []Beam{
beam.RotatedInDirection(DirectionWest),
beam.RotatedInDirection(DirectionEast),
}
} else {
// This can't happen, we've hit all four permutations already
panic("invalid configuration of beam")
}
}
// beamsFromMirror gets the resulting beams (well, beam) when a beam collides with a mirror. Panics if the given tile
// is not a mirror.
func beamsFromMirror(tile Tile, beam Beam) []Beam {
switch tile {
case TileMirrorRight:
return beamsFromRightMirror(tile, beam)
case TileMirrorLeft:
return beamsFromLeftMirror(tile, beam)
default:
panic("cannot treat non-mirror as a mirror")
}
}
func beamsFromRightMirror(tile Tile, beam Beam) []Beam {
if tile != TileMirrorRight {
panic("cannot treat non-right mirror as right mirror")
}
switch beam.direction {
case DirectionEast:
return []Beam{beam.RotatedInDirection(DirectionNorth)}
case DirectionWest:
return []Beam{beam.RotatedInDirection(DirectionSouth)}
case DirectionNorth:
return []Beam{beam.RotatedInDirection(DirectionEast)}
case DirectionSouth:
return []Beam{beam.RotatedInDirection(DirectionWest)}
default:
panic(fmt.Sprintf("invalid direction value %d", beam.direction))
}
}
func beamsFromLeftMirror(tile Tile, beam Beam) []Beam {
if tile != TileMirrorLeft {
panic("cannot treat non-right mirror as right mirror")
}
switch beam.direction {
case DirectionEast:
return []Beam{beam.RotatedInDirection(DirectionSouth)}
case DirectionWest:
return []Beam{beam.RotatedInDirection(DirectionNorth)}
case DirectionNorth:
return []Beam{beam.RotatedInDirection(DirectionWest)}
case DirectionSouth:
return []Beam{beam.RotatedInDirection(DirectionEast)}
default:
panic(fmt.Sprintf("invalid direction value %d", beam.direction))
}
}
func parseTileGrid(inputLines []string) (TileGrid, error) {
if len(inputLines) == 0 {
return TileGrid{}, fmt.Errorf("no tiles in grid")
}
width := len(inputLines[0])
height := len(inputLines)
tiles := make(map[Coordinate]Tile)
for row, line := range inputLines {
if len(line) != width {
return TileGrid{}, errors.New("lines have unequal lengths")
}
for col, char := range line {
switch char {
case '.':
continue
case rune(TileMirrorLeft), rune(TileMirrorRight), rune(TileSplitterHorizontal), rune(TileSplitterVertical):
pos := Coordinate{row: row, col: col}
tiles[pos] = Tile(char)
default:
return TileGrid{}, fmt.Errorf("invalid tile character %c", char)
}
}
}
return TileGrid{
Height: height,
Width: width,
Tiles: tiles,
}, nil
}