package day16 import ( "fmt" "log" "math" "os" "strings" "sync" ) func Run() int { fmt.Println("hello from day 16") log.Println("starting") filename := "day16/input" field := ReadField(filename) startPoints := StartPoints(&field) var startPointsWaitGroup sync.WaitGroup startPointsWaitGroup.Add(len(startPoints)) results := make(chan int) go func() { startPointsWaitGroup.Wait() close(results) }() for _, start := range startPoints { go func(start MovementPoint) { cleanField := ReadField(filename) cleanField.StartTraversal(start) thisResult := cleanField.CountEnergized() results <- thisResult startPointsWaitGroup.Done() }(start) } max := math.MinInt for energized := range results { if energized > max { max = energized log.Println("found new max: ", max) } } // fmt.Println(field.String()) // field.StartTraversal() // fmt.Println(field.ShowEnergyzed()) return max } func StartPoints(f *Field) []MovementPoint { result := make([]MovementPoint, 0) for rowNum, row := range f.cells { result = append(result, MovementPoint{Row: rowNum, Col: 0, Direction: Rightward}, MovementPoint{Row: rowNum, Col: len(row) - 1, Direction: Leftward}) } for colNum, _ := range f.cells[0] { result = append(result, MovementPoint{Row: 0, Col: colNum, Direction: Downward}, MovementPoint{Row: len(f.cells) - 1, Col: colNum, Direction: Upward}) } return result } // have shared field // running traversal recursive function per ray // exit if going out of field or visiting cell that already had light in this direction // (i.e encountering loop) type CellType rune const ( Empty CellType = '.' SplitterNS = '|' SplitterEW = '-' MirrorBackslash = '\\' MirrorSlash = '/' ) type Direction int const ( Upward Direction = iota Downward Leftward Rightward ) type Cell struct { CellType CellType KnownBeams map[Direction]any } type Field struct { cells [][]*Cell } func (f *Field) isValid(mp MovementPoint) bool { if mp.Row < 0 || mp.Col < 0 { return false } if mp.Row >= len(f.cells) || len(f.cells) == 0 || mp.Col >= len(f.cells[0]) { return false } return true } func ReadField(filename string) Field { result := Field{} bytes, err := os.ReadFile(filename) if err != nil { panic(fmt.Sprint("cannot read file: ", filename)) } text := string(bytes) text = strings.TrimSpace(text) for _, line := range strings.Split(text, "\n") { rowCells := make([]*Cell, 0) for _, symb := range line { rowCells = append(rowCells, &Cell{ CellType: CellType(symb), KnownBeams: make(map[Direction]any), }) } result.cells = append(result.cells, rowCells) } return result } func (f *Field) String() string { result := "\n" for _, row := range f.cells { for _, cell := range row { result += string(cell.CellType) } result += "\n" } return result } func (f *Field) ShowEnergyzed() string { result := "\n" for _, row := range f.cells { for _, cell := range row { if len(cell.KnownBeams) > 0 { result += "#" } else { result += string(cell.CellType) } } result += "\n" } return result } type MovementPoint struct { Row, Col int Direction Direction } func (f *Field) StartTraversal(startPoint MovementPoint) { reportedVisits := make(chan MovementPoint) var wg sync.WaitGroup go f.RecordVisits(reportedVisits) wg.Add(1) go f.TraverseFrom(startPoint, reportedVisits, &wg) wg.Wait() close(reportedVisits) } func (f *Field) CountEnergized() (result int) { for _, row := range f.cells { for _, cell := range row { if len(cell.KnownBeams) > 0 { result += 1 } } } return } func (f *Field) RecordVisits(reportedPoints <-chan MovementPoint) { for point := range reportedPoints { cell := f.cells[point.Row][point.Col] // log.Printf("recording visit %+v to %+v at row %d col %d\n", point, cell, point.Row, point.Col) cell.KnownBeams[point.Direction] = struct{}{} } } // starting at point, mark as visited // move (concurrently if required) into next points // ends - when out of the field, or if encountering a cycle func (f *Field) TraverseFrom(current MovementPoint, reportVisits chan<- MovementPoint, wg *sync.WaitGroup) { // log.Printf("> starting traverse through %+v", current) if !f.isValid(current) { log.Println("invalid current ", current, " should be impossible") wg.Done() return } cell := f.cells[current.Row][current.Col] _, knownDirection := cell.KnownBeams[current.Direction] if knownDirection { // log.Printf("found cycle at %+v in %+v", current, cell) wg.Done() return } reportVisits <- current nextPoints := NextPoints(f, current) // log.Printf("for current %+v next are: %+v\n", current, nextPoints) switch len(nextPoints) { case 0: wg.Done() return case 1: f.TraverseFrom(nextPoints[0], reportVisits, wg) return case 2: wg.Add(1) go f.TraverseFrom(nextPoints[0], reportVisits, wg) f.TraverseFrom(nextPoints[1], reportVisits, wg) return } } func NextPoints(f *Field, current MovementPoint) []MovementPoint { cell := f.cells[current.Row][current.Col] nextDirections := cell.CellType.NextDirections(current.Direction) nextCells := make([]MovementPoint, 0) for _, direction := range nextDirections { nextMovementPoint := current.ApplyDirection(direction) if f.isValid(nextMovementPoint) { nextCells = append(nextCells, nextMovementPoint) } } return nextCells } // value receiver, can safely modify incoming mp // doesn't know about Field dimentions func (mp MovementPoint) ApplyDirection(d Direction) MovementPoint { switch d { case Upward: mp.Row -= 1 case Downward: mp.Row += 1 case Leftward: mp.Col -= 1 case Rightward: mp.Col += 1 } mp.Direction = d return mp } func (ct CellType) NextDirections(currentDirection Direction) (nextDirections []Direction) { switch ct { case Empty: nextDirections = []Direction{currentDirection} case SplitterNS: if currentDirection == Rightward || currentDirection == Leftward { nextDirections = []Direction{Upward, Downward} } else { nextDirections = []Direction{currentDirection} } case SplitterEW: if currentDirection == Downward || currentDirection == Upward { nextDirections = []Direction{Leftward, Rightward} } else { nextDirections = []Direction{currentDirection} } case MirrorBackslash: // mirror symbol is \ directionMappings := map[Direction]Direction{ Leftward: Upward, Rightward: Downward, Upward: Leftward, Downward: Rightward, } nextDirections = []Direction{directionMappings[currentDirection]} case MirrorSlash: // mirrow symbol is / directionMappings := map[Direction]Direction{ Leftward: Downward, Rightward: Upward, Upward: Rightward, Downward: Leftward, } nextDirections = []Direction{directionMappings[currentDirection]} } return }