import Foundation import simd enum Input: String, CaseIterable { case up = "▴" case right = "▸" case down = "▾" case left = "◂" var bytes: [UInt8] { switch self { case .up: return [27, 91, 65] case .right: return [27, 91, 67] case .down: return [27, 91, 66] case .left: return [27, 91, 68] } } init?(_ bytes: [UInt8]) { guard let match = Self.allCases.first(where: { $0.bytes == bytes }) else { return nil } self = match } } class InputHandler { static func initStruct() -> S { let struct_pointer = UnsafeMutablePointer.allocate(capacity: 1) let struct_memory = struct_pointer.pointee struct_pointer.deallocate() return struct_memory } static func enableRawMode(fileHandle: FileHandle) -> termios { var raw: termios = initStruct() tcgetattr(fileHandle.fileDescriptor, &raw) let original = raw raw.c_lflag &= ~(UInt(ECHO | ICANON)) tcsetattr(fileHandle.fileDescriptor, TCSAFLUSH, &raw); return original } static func restoreRawMode(fileHandle: FileHandle, originalTerm: termios) { var term = originalTerm tcsetattr(fileHandle.fileDescriptor, TCSAFLUSH, &term); } let originalTerm: termios init() { self.originalTerm = Self.enableRawMode( fileHandle: FileHandle.standardInput ) } deinit { Self.restoreRawMode( fileHandle: FileHandle.standardInput, originalTerm: originalTerm ) } var input: Input? } class Renderer { var buffer: [Character] var length: Int { buffer.count } let size: simd_int2 init(size: simd_int2) { self.size = size self.buffer = Array( repeating: "▓", count: Int(size.x * size.y) ) } func scale(_ position: simd_float2) -> simd_int2 { simd_int2( simd_int2.Scalar(position.x * Float(size.x)), simd_int2.Scalar(position.y * Float(size.y)) ) } func drawChar(_ char: Character, position: simd_float2) { let i = scale(position).index(rowLength: size.x) self.buffer[i % length] = char } func drawRect(frame: float2x2, char: Character) { let p1 = convertViewportPoint(frame.columns.0) let p2 = convertViewportPoint(frame.columns.0 + frame.columns.1) (p1.x..(repeating: "▓", count: length) } func clear() { buffer = Array(repeating: "░", count: length) } func blit() { print("\u{001B}[2J") print( String ( stride(from: 0, to: length, by: Int(size.x)).map { i in String(buffer[i..<(i + Int(size.x))]) }.joined(separator: "\n") ) ) } func convertViewportPoint(_ point: simd_float2) -> simd_int2 { simd_int2( x: Int32(Float(size.x) * point.x), y: Int32(Float(size.y) * point.y) ) } } extension simd_int2 { func index(rowLength: Scalar) -> Int { Int((y * rowLength) + (x % rowLength)) } } class Game { let inputHandler = InputHandler() let renderer = Renderer( size: simd_int2(x: 40, y: 10) ) var isRunning = false var pointer = simd_float2(0.5, 0.5) let speed: Float = 0.1 let guy = Character("☃︎") func start() { var data: Data repeat { data = FileHandle.standardInput.availableData let bytes = [UInt8](data) let input = Input(bytes) renderer.clear() if let input = input { switch input { case .up: pointer -= simd_float2(0, speed) case .right: pointer += simd_float2(speed, 0) case .down: pointer += simd_float2(0, speed) case .left: pointer -= simd_float2(speed, 0) } pointer.clamp( lowerBound: simd_float2.zero, upperBound: simd_float2.one ) } renderer.drawChar( guy, position: pointer ) renderer.blit() } while (data.count > 0) } func stop() { isRunning = false } } let game = Game() game.start()