import Foundation typealias CellCost = (cell: Cell, cost: Int) enum Dir : String, CaseIterable, CustomStringConvertible { case n = "▲" case s = "▼" case e = "▶" case w = "◀" var description: String { return self.rawValue } func turnCost(to dir: Self) -> Int? { if self == dir { return 0 } if (self == .n && dir == .s) || (self == .s && dir == .n) || (self == .e && dir == .w) || (self == .w && dir == .e) { return nil } return 1000 } } struct Cell : Hashable, CustomStringConvertible { let (i, j): (Int, Int) let dir: Dir var description: String { return "(\(i),\(j))\(dir)" } func neighbor(_ dir: Dir) -> CellCost? { if let cost = self.dir.turnCost(to: dir) { let c = cost + 1 switch dir { case Dir.n: return (cell: Cell(i: i-1, j: j, dir: dir), cost: c) case Dir.s: return (cell: Cell(i: i+1, j: j, dir: dir), cost: c) case Dir.e: return (cell: Cell(i: i, j: j+1, dir: dir), cost: c) case Dir.w: return (cell: Cell(i: i, j: j-1, dir: dir), cost: c) } } else { return nil } } } struct QueueEntry : Hashable, CustomStringConvertible { let cell: Cell let origin: Cell let costSoFar: Int var description: String { return "\(cell) from \(origin) @\(costSoFar)" } } struct IJ : Hashable { //TODO: delete let i: Int let j: Int } struct CellPair : Hashable { let a: Cell let b: Cell } struct Maze : CustomStringConvertible { let walls: [[Bool]] let (w, h): (Int, Int) var start: Cell { return Cell(i: h-2, j: 1, dir: Dir.e) } var ends: Set { return Set(Dir.allCases.map { Cell(i: 1, j: w-2, dir: $0) }) } var description: String { return "┌" + String(repeating: "─", count: w*2) + "┐\n" + walls.map { "│" + $0.map { $0 ? "██" : " " }.joined() + "│" }.joined(separator: "\n") + "\n└" + String(repeating: "─", count: w*2) + "┘" } init(fromFile f: String) throws { let content = try String(contentsOfFile: f, encoding: .ascii) let lines = content.split(separator: "\n") (h, w) = (lines.count, lines[0].count) walls = lines.map { line in line.map { cell in cell == "#" } } } func neighbors(of cell: Cell) -> [CellCost] { return Dir.allCases .compactMap { cell.neighbor($0) } .filter { nb in !walls[nb.cell.i][nb.cell.j] } } func graph() -> [Cell:[CellCost]] { var edges: [Cell:[(Cell, Int)]] = [start: []] var q: [QueueEntry] = [ QueueEntry(cell: start, origin: start, costSoFar: 0) ] var seen: Set = [] while !q.isEmpty { let e = q.removeLast() var origin = e.origin var costSoFar = e.costSoFar let nbs = neighbors(of: e.cell) //xprint(q: q, cell: e.cell) //print("q: \(q)") //print("c: \(e.cell) -> \(nbs)") if ends.contains(e.cell) || (nbs.count > 1 && e.cell != start) { if seen.contains(CellPair(a: origin, b: e.cell)) { continue } let upd = edges[origin, default: []] + [(e.cell, costSoFar)] edges[origin] = upd seen.insert(CellPair(a: origin, b: e.cell)) origin = e.cell costSoFar = 0 //print("!!!! \(e.cell)") //edges.forEach { k, v in print(" - \(k): \(v)") } } if !ends.contains(e.cell) { nbs.map { cell, cost in QueueEntry( cell: cell, origin: origin, costSoFar: costSoFar + cost ) }.forEach { q.append($0) } } } ends.forEach { edges[$0] = [] } return edges } func xprint(q: [QueueEntry], cell: Cell) { let qx = Set(q.map { IJ(i: $0.cell.i, j: $0.cell.j) }) let s: [[String]] = walls.enumerated().map { i, r in [String(format: "%02d ", i)] + r.enumerated().map { j, c in if c { return "██" } if cell.i == i && cell.j == j { return "🯇🯈" } if qx.contains(IJ(i: i, j: j)) { return "▒▒" } return " " } } print(" 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4") print(s.map { $0.joined() }.joined(separator: "\n")) } func search() -> Int { let edges = graph() edges.forEach { k, v in print(" - \(k): \(v)") } var q: [CellCost] = [(cell: start, cost: 0)] var visited: [Cell:Int] = [:] while !q.isEmpty { let e = q.removeLast() print("visiting \(e)") for (cell, cost) in edges[e.cell]! { if let prevCost = visited[cell] { if e.cost + cost >= prevCost { continue } } visited[cell] = e.cost + cost q.append((cell: cell, cost: e.cost + cost)) } } print(visited) return ends.compactMap { visited[$0] }.min()! } } let maze = try Maze(fromFile: CommandLine.arguments[1]) print(maze) print(maze.search())