385 lines
9.3 KiB
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
|
|
}
|