import Foundation #if os(macOS) import Darwin #elseif os(Linux) import Glibc #elseif os(Windows) import ucrt #endif @main struct A_Star { static func main() { var initfilePath = "-" var noInteract = false if CommandLine.arguments.count == 2 { initfilePath = CommandLine.arguments[1] } else if CommandLine.arguments.count == 3 && CommandLine.arguments[2] == "--nointeract" { initfilePath = CommandLine.arguments[1] noInteract = true } else if (CommandLine.arguments.count != 1) { print(""" Improper syntax. Usage: binary-path [initfile-path [--nointeract]] Example: ./A-Star Tests/Test1 --nointeract """) return } var grid: [[Int]] = [[]] var start: Vector2 = Vector2(x: -1, y: -1) var goal: Vector2 = Vector2(x: -1, y: -1) parseInitfile(initfilePath, grid: &grid, start: &start, goal: &goal) // Hide cursor print("\u{1B}[?25l") Terminal.enableRawMode() var cursorPosition = Vector2(x: 0, y: 0) defer { quit(clear: !noInteract) } var aStar = A_Star() var path = aStar.pathfind(grid: grid, start: start, goal: goal) if noInteract { aStar.display(path, on: grid) // If noInteract, print full path print("\nFull path: ") for vector in path { let index = path.firstIndex(of: vector)! print("(\(vector.x), \(vector.y))\(index != path.count - 1 ? (index % 5 == 4 ? " ⏎ " : " → ") : "")\(index % 5 == 4 ? "\n" : "")", terminator: "") } print() quit(clear: false) } while true { aStar.display(path, on: grid, cursorPosition: cursorPosition) let key = Terminal.readKey() switch key { case .Left: cursorPosition += Vector2(x: -1, y: 0) case .Right: cursorPosition += Vector2(x: 1, y: 0) case .Up: cursorPosition += Vector2(x: 0, y: -1) case .Down: cursorPosition += Vector2(x: 0, y: 1) default: switch key { case .Quit: quit(clear: true) case .Start: start = cursorPosition case .Finish: goal = cursorPosition case .Wall: if cursorPosition != goal && cursorPosition != start { grid[cursorPosition.y][cursorPosition.x] = grid[cursorPosition.y][cursorPosition.x] == 1 ? 0 : 1 } case .NW: aStar.directions[0] = !aStar.directions[0] case .N: aStar.directions[1] = !aStar.directions[1] case .NE: aStar.directions[2] = !aStar.directions[2] case .W: aStar.directions[3] = !aStar.directions[3] case .E: aStar.directions[4] = !aStar.directions[4] case .SW: aStar.directions[5] = !aStar.directions[5] case .S: aStar.directions[6] = !aStar.directions[6] case .SE: aStar.directions[7] = !aStar.directions[7] default: break } path = aStar.pathfind(grid: grid, start: start, goal: goal) continue } if cursorPosition.x < 0 { cursorPosition.x = 0 } if cursorPosition.y < 0 { cursorPosition.y = 0 } if cursorPosition.x > grid[0].count - 1 { cursorPosition.x = grid[0].count - 1 } if cursorPosition.y > grid.count - 1 { cursorPosition.y = grid.count - 1 } } } var directions: [Bool] = Array(repeating: true, count: 8) func pathfind(grid: [[Int]], start: Vector2, goal: Vector2) -> [Vector2] { var openSet: [Vector2] = [start] var cameFrom: Dictionary = [:] var gScore: Dictionary = [:] gScore[start] = 0 var fScore: Dictionary = [:] fScore[start] = h(current: start, goal: goal) while openSet.count != 0 { var current: Vector2 = getLowest(openSet, map: fScore) if current == goal { var totalPath: [Vector2] = [current] while cameFrom.keys.contains(current) { current = cameFrom[current]! totalPath.append(current) } totalPath.reverse() return totalPath } openSet.remove(at: openSet.firstIndex(of: current)!) let neighbors: [Vector2] = [ Vector2(x: current.x - 1, y: current.y - 1), Vector2(x: current.x, y: current.y - 1), Vector2(x: current.x + 1, y: current.y - 1), Vector2(x: current.x - 1, y: current.y), Vector2(x: current.x + 1, y: current.y), Vector2(x: current.x - 1, y: current.y + 1), Vector2(x: current.x, y: current.y + 1), Vector2(x: current.x + 1, y: current.y + 1), ] for i: Int in 0..= grid[0].count || neighbor.x < 0 || neighbor.y >= grid.count || neighbor.y < 0 { continue } if grid[neighbor.y][neighbor.x] == 1 { continue } let tentative_gScore: Float = gScore[current]! + d(current: current, neighbor: neighbor) if tentative_gScore < gScore[neighbor, default: Float.infinity] { cameFrom[neighbor] = current gScore[neighbor] = tentative_gScore fScore[neighbor] = tentative_gScore + h(current: neighbor, goal: goal) if !openSet.contains(neighbor) { openSet.append(neighbor) } } } } return [start, goal] } func h(current: Vector2, goal: Vector2) -> Float { let x = Float(abs(current.x - goal.x)) let y = Float(abs(current.y - goal.y)) return abs(x - y) + 1.4142 * min(x, y) } func d(current: Vector2, neighbor: Vector2) -> Float { if current.x == neighbor.x || current.y == neighbor.y { return 1 } return 1.4142 } func getLowest(_ set: [Vector2], map: Dictionary) -> Vector2 { var tentativeLowest = set[0] for item in set { if map[item]! < map[tentativeLowest]! { tentativeLowest = item } } return tentativeLowest } func display(_ path: [Vector2], on: [[Int]], cursorPosition: Vector2 = Vector2(x: -1, y: -1)) { let draw: Dictionary = [ "nw": "┏", "ne": "┓", "sw": "┗", "se": "┛", "n": "━", "e": "┃", "s": "━", "w": "┃", "start": "S", "goal": "F", "path": "*", "empty": "·", "wall": "▓", "clear": "\u{1B}[2J", "clearLine": "\u{1B}[2K", "home": "\u{1B}[H", "hide": "\u{1B}[?25l", "show": "\u{1B}[?25h", ] var output: String = "" output += draw["nw"] ?? "?" output += String(repeating: draw["n"] ?? "?", count: on[0].count + 2) output += draw["ne"] ?? "?" output += "\n" for y: Int in 0.. Int { if value < low { return low } if value > high { return high } return value } } enum Key { case Up case Down case Left case Right case Wall case Start case Finish case NW case N case NE case E case SE case S case SW case W case Quit case Unknown } struct Terminal { #if os(macOS) || os(Linux) nonisolated(unsafe) static var originalTerm = termios() #endif static func readKey() -> Key { var byte: UInt8 = 0 #if os(Windows) let ch = _getch() if ch == 224 || ch == 0 { let nextCh = _getch() switch nextCh { case 72: return .Up case 80: return .Down case 75: return .Left case 77: return .Right default: return .Unknown } } byte = UInt8(ch) #else read(STDIN_FILENO, &byte, 1) if byte == 27 { var seq1: UInt8 = 0 var seq2: UInt8 = 0 read(STDIN_FILENO, &seq1, 1) read(STDIN_FILENO, &seq2, 1) if seq1 == 91 { switch seq2 { case 65: return .Up case 66: return .Down case 67: return .Right case 68: return .Left default: return .Unknown } } } #endif let character = String(UnicodeScalar(byte)) switch character { case "w": return .Up case "k": return .Up case "a": return .Left case "h": return .Left case "s": return .Down case "j": return .Down case "d": return .Right case "l": return .Right case " ": return .Wall case "[": return .Start case "]": return .Finish case "7": return .NW case "8": return .N case "9": return .NE case "4": return .W case "6": return .E case "1": return .SW case "2": return .S case "3": return .SE case "q": return .Quit default: return .Unknown } } static func enableRawMode() { #if os(macOS) || os(Linux) tcgetattr(STDIN_FILENO, &originalTerm) var rawTerm = originalTerm rawTerm.c_lflag &= ~tcflag_t(ICANON | ECHO) tcsetattr(STDIN_FILENO, TCSANOW, &rawTerm) signal(SIGINT, quitHandler) #endif } static func disableRawMode() { #if os(macOS) || os(Linux) tcsetattr(STDIN_FILENO, TCSANOW, &originalTerm) #endif } } func quitHandler(_ signal: Int32) { quit(clear: true) } func quit(clear: Bool = true) { Terminal.disableRawMode() // Show cursor print("\u{1B}[?25h", terminator: "") // Clear + return home if clear { print("\u{1B}[2J", terminator: "") print("\u{1B}[H", terminator: "") } exit(0) } struct Vector2: Hashable { var x: Int var y: Int static func == (lhs: Vector2, rhs: Vector2) -> Bool { return lhs.x == rhs.x && lhs.y == rhs.y } static func += (lhs: inout Vector2, rhs: Vector2) { lhs = Vector2(x: lhs.x + rhs.x, y: lhs.y + rhs.y) } func hash(into hasher: inout Hasher) { hasher.combine(x) hasher.combine(y) } } struct Color { static let Black = "\u{1B}[90m" static let Red = "\u{1B}[91m" static let Green = "\u{1B}[92m" static let Yellow = "\u{1B}[93m" static let Blue = "\u{1B}[94m" static let Magenta = "\u{1B}[95m" static let Cyan = "\u{1B}[96m" static let White = "\u{1B}[97m" static let Default = "\u{1B}[39m" } enum InitfileError: Error { case emptyFile case missingStart case missingGoal case multipleStarts case multipleGoals case unevenRowLengths }