package main import ( "fmt" "math" "slices" "strings" "git.mstar.dev/mstar/goutils/sliceutils" "git.mstar.dev/mstar/aoc24/util" ) // A crossing is a path location with 3 or 4 other paths next to it type Crossing struct { At util.Vec2 LowestScore int LowestFrom util.Vec2 // Index into list of crossings Neighbours []util.Vec2 // Directions where a path is } type Area map[int64]map[int64]*Crossing type Wrapper struct { A Area } var ( DirUp = util.Vec2{X: 0, Y: -1} DirRight = util.Vec2{X: 1, Y: 0} DirDown = util.Vec2{X: 0, Y: 1} DirLeft = util.Vec2{X: -1, Y: 0} ) var startDir = DirRight // Get the size of the bord func getSize(lines []string) util.Vec2 { return util.Vec2{X: int64(len(lines[0])), Y: int64(len(lines))} } // Count the paths next to a given positon func countPathsAt(lines [][]rune, at util.Vec2) []util.Vec2 { center := lines[at.Y][at.X] paths := []util.Vec2{} if center == '#' { return paths } if util.SafeGet(lines, at.Up()) != '#' { paths = append(paths, at.Up()) } if util.SafeGet(lines, at.Down()) != '#' { paths = append(paths, at.Down()) } if util.SafeGet(lines, at.Left()) != '#' { paths = append(paths, at.Left()) } if util.SafeGet(lines, at.Right()) != '#' { paths = append(paths, at.Right()) } return paths } func linesToMap(lines [][]rune, size util.Vec2) Area { area := Area{} // Ensure area has full maps for i := range size.Y { area[i] = map[int64]*Crossing{} } for iy, line := range lines { for ix, char := range line { pos := util.Vec2{X: int64(ix), Y: int64(iy)} switch char { case '.', 'E', 'S': dirs := countPathsAt(lines, pos) area[int64(ix)][int64(iy)] = &Crossing{ At: pos, LowestScore: math.MaxInt64, LowestFrom: util.Vec2{X: -1, Y: -1}, Neighbours: dirs, } default: } } } return area } func findStart(lines [][]rune) util.Vec2 { for iy, line := range lines { for ix, char := range line { if char == 'S' { return util.Vec2{X: int64(ix), Y: int64(iy)} } } } // Return invalid positon if none found return util.Vec2{X: -1, Y: -1} } func findEnd(lines [][]rune) util.Vec2 { for iy, line := range lines { for ix, char := range line { if char == 'E' { return util.Vec2{X: int64(ix), Y: int64(iy)} } } } // Return invalid positon if none found return util.Vec2{X: -1, Y: -1} } func walk( area *Wrapper, from util.Vec2, dir util.Vec2, score int, ) { // Idea: walk forward from start until crossing or wall // At crossing, defer a walker for both sides (if not walled off) but prioritise forwards // At crossing, check if own score is lower than current score // If it is, update score and set own origin // Else if crossing has a lower score than self, abort // Special case: // If at end, update end "crossing" following the earlier rules // Walk forward in dir until no node found type WalkTarget struct { Pos, Dir util.Vec2 Score int } // targets := []WalkTarget{} defer fmt.Println("Deffered done", from) fmt.Println("Starting", from) prev := from for pos := from.Add(dir); area.A[pos.X][pos.Y] != nil; pos = pos.Add(dir) { score++ node := area.A[pos.X][pos.Y] if node.LowestScore <= score { return } node.LowestScore = score node.LowestFrom = prev // node.LowestFrom = pos.Add(dir.Mult(-1)) fmt.Println("Setting node", pos, score, node.LowestFrom) // Get all directions that don't match current walking direction (and reverse) filtered := sliceutils.Filter(node.Neighbours, func(t util.Vec2) bool { return !t.Add(pos.Mult(-1)).Eq(dir) && !t.Add(pos.Mult(-1)).Eq(dir.Mult(-1)) }) // fmt.Println("Filtered neighbours of node", filtered) for _, neighbour := range filtered { fmt.Println("Adding target", neighbour) // targets = append(targets, WalkTarget{ // neighbour, neighbour.Add(pos.Mult(-1)), score + 1001, // }) defer walk(area, neighbour, neighbour.Add(pos.Mult(-1)), score+1001) } fmt.Println("Stepping", dir) prev = pos } // fmt.Println("Hitting stored targets", from) // for _, target := range targets { // walk(area, target.Pos, target.Dir, target.Score) // } fmt.Println("Done, walking deferred", from) } func visualisePath(area Area, endPos, size util.Vec2, inputLineRunes [][]rune) string { lineCopy := [][]rune{} for _, line := range inputLineRunes { lineCopy = append(lineCopy, slices.Clone(line)) } for pos := endPos; pos.IsInBounds(size); pos = area[pos.X][pos.Y].LowestFrom { fmt.Println("Path elem", pos) lineCopy[pos.Y][pos.X] = 'x' } builder := strings.Builder{} for _, line := range lineCopy { for _, char := range line { builder.WriteRune(char) } builder.WriteRune('\n') } return builder.String() } func main() { inputLines := util.FileContentToNonEmptyLines(util.LoadFileFromArgs()) size := getSize(inputLines) inputLineChars := sliceutils.Map(inputLines, func(t string) []rune { return []rune(t) }) area := linesToMap(inputLineChars, size) start := findStart(inputLineChars) end := findEnd(inputLineChars) // Fill entire map fmt.Println("Filling right") wrapped := Wrapper{area} walk(&wrapped, start, DirRight, 0) fmt.Println("Filling up") walk(&wrapped, start, DirUp, 1000) fmt.Println("Filling down") walk(&wrapped, start, DirDown, 1000) fmt.Println("Filling left") walk(&wrapped, start, DirLeft, 2000) fmt.Printf("%#v\n", area[start.X][start.Y]) fmt.Printf("Task 1: %d\n", area[end.X][end.Y].LowestScore) fmt.Println(visualisePath(area, end, size, inputLineChars)) }