From 516a9183bde3595acb197eea161c99082967766f Mon Sep 17 00:00:00 2001 From: RochesterX Date: Tue, 31 Mar 2026 18:42:04 -0400 Subject: [PATCH] Basically done --- Sources/A-Star/A_Star.swift | 448 +++++++++++++++++++++++++++++++++++- 1 file changed, 438 insertions(+), 10 deletions(-) diff --git a/Sources/A-Star/A_Star.swift b/Sources/A-Star/A_Star.swift index a64af39..57a9019 100644 --- a/Sources/A-Star/A_Star.swift +++ b/Sources/A-Star/A_Star.swift @@ -1,34 +1,458 @@ +#if os(macOS) +import Darwin +#elseif os(Linux) +import Glibc +#elseif os(Windows) +import ucrt +#endif + + @main struct A_Star { static func main() { - print("Hello, world!") + print("\u{1B}[2J", terminator: "") + print("\u{1B}[H", terminator: "") + // From https://patorjk.com/software/taag + print(#""" + $$$$$$\ $$$$$$\ $$\ $$\ +$$ __$$\ $$\$$\ $$ __$$\ $$ |$$ | +$$ / $$ | \$$$ | $$ / \__| $$$$$$\ $$$$$$$\ $$$$$$$ |$$$$$$$\ $$$$$$\ $$\ $$\ +$$$$$$$$ |$$$$$$$\ \$$$$$$\ \____$$\ $$ __$$\ $$ __$$ |$$ __$$\ $$ __$$\ \$$\ $$ | +$$ __$$ |\_$$$ __| \____$$\ $$$$$$$ |$$ | $$ |$$ / $$ |$$ | $$ |$$ / $$ | \$$$$ / +$$ | $$ | $$ $$\ $$\ $$ |$$ __$$ |$$ | $$ |$$ | $$ |$$ | $$ |$$ | $$ | $$ $$< +$$ | $$ | \__\__| \$$$$$$ |\$$$$$$$ |$$ | $$ |\$$$$$$$ |$$$$$$$ |\$$$$$$ |$$ /\$$\ +\__| \__| \______/ \_______|\__| \__| \_______|\_______/ \______/ \__/ \__| + +Welcome to the A* Sandbox, an interactive terminal-based visualizer for the A* graph traversal +algorithm. You can enter a width and height for the grid, then use the following controls to +experiment with the algorithm. The optimal path will refresh every time you make a change to +the grid. + +Controls: +- Cursor movement: WASD, HJKL, or Arrow Keys +- Wall placement: Space +- Origin movement: [ +- Target movement: ] + +"""#) + print("Enter the grid width: ", terminator: "") + let width: Int = Int(readLine() ?? "") ?? 16 + print("Enter the grid height: ", terminator: "") + let height: Int = Int(readLine() ?? "") ?? 8 + print("\u{1B}[?25l") + + Terminal.enableRawMode() + + var cursorPosition = Vector2(x: 0, y: 0) + + defer { + quit() + } + + var grid: [[Int]] = Array(repeating: Array(repeating: 0, count: width), count: height) + var start: Vector2 = Vector2(x: 0, y: 0) + var goal: Vector2 = Vector2(x: width - 1, y: 0) + + var aStar = A_Star() + + var path = aStar.pathfind(grid: grid, start: start, goal: goal) + + 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 .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 } + } } - func A_Star(start: Vector2, goal: Vector2) { + 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 = [:] + var gScore: Dictionary = [:] gScore[start] = 0 - var fScore: Dictionary = [:] - fScore[start] = h(start) + var fScore: Dictionary = [:] + fScore[start] = h(current: start, goal: goal) while openSet.count != 0 { - var current: Vector2 = getLowest(openSet) + 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) { + 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) + 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 Unknown +} + +struct Terminal { + nonisolated(unsafe) static var originalTerm = termios() + + 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 .Dow + 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 + default: + return .Unknown } } - func h(_: Vector2) -> Int { - return 0 + 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, quit) +#endif } - func getLowest(_: [Vector2]) -> Vector2 { - for + static func disableRawMode() { +#if os(macOS) || os(Linux) + tcsetattr(STDIN_FILENO, TCSANOW, &originalTerm) +#endif } } +func quit(_ signal: Int32 = 0) { + Terminal.disableRawMode() + // Show cursor + print("Exit") + print("\u{1B}[?25h", terminator: "") + print("\u{1B}[2J", terminator: "") + print("\u{1B}[H", terminator: "") + exit(0) +} + struct Vector2: Hashable { var x: Int var y: Int @@ -37,6 +461,10 @@ struct Vector2: Hashable { 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)