package day16 import ( "fmt" "log" "os" "strings" "sync" ) func Run() int { fmt.Println("hello from day 16") log.Println("starting") field := ReadField("day16/example") fmt.Println(field.String()) field.StartTraversal() fmt.Println(field.ShowEnergyzed()) return field.CountEnergized() } // 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() { reportedVisits := make(chan MovementPoint) var wg sync.WaitGroup go f.RecordVisits(reportedVisits) startPoint := MovementPoint { Row: 0, Col: 0, Direction: Rightward, } 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 }