Created
November 14, 2020 22:16
-
-
Save dduan/d498944d05fd8609b7113396ce97b7df to your computer and use it in GitHub Desktop.
Flappy bird as a CLI app written in Swift.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This is the code for the Flappy Bird game running in a Unix terminal. | |
// Demo: https://twitter.com/daniel_duan/status/1327735679657250816?s=21 | |
// To run it, simply do "swift bird.swift" in a Unix command line. | |
#if canImport(Darwin) | |
import Darwin | |
#else | |
import Glibc | |
#endif | |
enum RawModeError: Error { | |
case notATerminal | |
case failedToGetTerminalSetting | |
case failedToSetTerminalSetting | |
} | |
func runInRawMode(_ task: @escaping () throws -> Void) throws { | |
var originalTermSetting = termios() | |
guard isatty(STDIN_FILENO) != 0 else { | |
throw RawModeError.notATerminal | |
} | |
guard tcgetattr(STDIN_FILENO, &originalTermSetting) >= 0 else { | |
throw RawModeError.failedToGetTerminalSetting | |
} | |
var raw = originalTermSetting | |
raw.c_iflag &= ~(UInt(BRKINT) | UInt(ICRNL) | UInt(INPCK) | UInt(ISTRIP) | UInt(IXON)) | |
raw.c_oflag &= ~(UInt(OPOST)) | |
raw.c_cflag |= UInt(CS8) | |
raw.c_lflag &= ~(UInt(ECHO) | UInt(ICANON) | UInt(IEXTEN) | UInt(ISIG)) | |
raw.c_cc.16 = 0 | |
raw.c_cc.17 = 1 | |
guard tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) >= 0 else { | |
throw RawModeError.failedToSetTerminalSetting | |
} | |
defer { | |
tcsetattr(STDIN_FILENO, TCSAFLUSH, &originalTermSetting) | |
} | |
clear() | |
try task() | |
} | |
func positionAt(x: Int, y: Int) { | |
print("\u{1b}[\(y);\(x)H") | |
} | |
func clear() { | |
print("\u{1b}[2J") | |
} | |
enum Key { | |
static let q = Character("q").asciiValue! | |
static let j = Character("j").asciiValue! | |
static let k = Character("k").asciiValue! | |
static let space = Character(" ").asciiValue! | |
} | |
class Pillar { | |
var position: Double = 40 | |
let hole: Int | |
init(hole: Int) { | |
self.hole = hole | |
} | |
convenience init() { | |
self.init(hole: Int.random(in: 0 ..< (24 - holeGap))) | |
} | |
} | |
enum GameState: Equatable { | |
case launch | |
case inProgress | |
case ended | |
} | |
var lastTick = timeval() | |
var yPosition: Double = 11.0 | |
let thrust: Double = -0.026 | |
var ySpeed = thrust | |
let g: Double = 0.000035 | |
var state = GameState.launch | |
var pillars = [Pillar]() | |
let pillarSpeed: Double = 0.01// block per ms | |
let emptyBuffer = Array(repeating: Character(" "), count: 40 * 24) | |
var renderBuffer = Array(repeating: Character(" "), count: 40 * 24) | |
var lastRender = timeval() | |
let holeGap = 8 | |
var score = 0 | |
func initializeGame() { | |
ySpeed = thrust | |
yPosition = 11.0 | |
score = 0 | |
pillars = [] | |
pillars.append(Pillar()) | |
pillars.append(Pillar()) | |
pillars.append(Pillar()) | |
pillars[1].position = 53 | |
pillars[2].position = 66 | |
gettimeofday(&lastTick, nil) | |
} | |
func update(msPassed: Double) { | |
ySpeed += g * msPassed | |
yPosition = yPosition + ySpeed * msPassed | |
var needNewOne = false | |
let concreteY = Int(yPosition) | |
for p in pillars { | |
if concreteY < 0 || concreteY >= 24 || Int(p.position) == 17 && (p.hole > concreteY || p.hole + holeGap < concreteY) { | |
state = .ended | |
return | |
} | |
let startingPosition = p.position | |
p.position -= msPassed * pillarSpeed | |
if startingPosition > 17 && p.position < 17 { | |
score += 1 | |
} | |
if p.position < 0 { | |
needNewOne = true | |
} | |
} | |
if needNewOne { | |
pillars.removeFirst() | |
pillars.append(Pillar()) | |
} | |
} | |
func render() { | |
renderBuffer = emptyBuffer | |
let symbol: Character | |
switch ySpeed { | |
case -0.003 ... 0.003: | |
symbol = "➡️" | |
case _ where ySpeed < -0.003: | |
symbol = "↗️" | |
default: | |
symbol = "↘️" | |
} | |
for p in pillars { | |
guard p.position < 40 && p.position > 0 else { | |
continue | |
} | |
for y in 0 ..< 24 { | |
if y < p.hole || y > p.hole + holeGap { | |
renderBuffer[y * 40 + Int(p.position)] = "\u{2588}" | |
} | |
} | |
} | |
let y = Int(yPosition) | |
if y >= 0 && y < 24 { | |
renderBuffer[y * 40 + 17] = symbol | |
} | |
for line in 0 ..< 24 { | |
positionAt(x: 0, y: line + 1) | |
print(String(renderBuffer[line * 40 ..< (line+1) * 40])) | |
} | |
positionAt(x: 0, y: 25) | |
print(" Score: \(score)") | |
} | |
func drawStartingScreen() { | |
clear() | |
positionAt(x: 0, y: 11) | |
print(" ➡️ ") | |
positionAt(x: 0, y: 13) | |
print(" Press [Space] to Start] ") | |
positionAt(x: 40, y: 24) | |
} | |
try runInRawMode { | |
drawStartingScreen() | |
initializeGame() | |
while true { | |
var char: UInt8 = 0 | |
read(STDIN_FILENO, &char, 1) | |
switch char { | |
case Key.q: | |
exit(0) | |
case Key.space: | |
if state == .launch { | |
state = .inProgress | |
} else if state == .ended { | |
state = .launch | |
drawStartingScreen() | |
initializeGame() | |
} | |
ySpeed = thrust | |
default: | |
break | |
} | |
var now = timeval() | |
gettimeofday(&now, nil) | |
defer { | |
lastTick = now | |
} | |
if state == .ended { | |
positionAt(x: 0, y: 25) | |
print("Score: \(score). You died. [space] to restart.") | |
} | |
guard state == .inProgress else { | |
continue | |
} | |
let msPassedSinceRender = Double(now.tv_usec - lastRender.tv_usec) / 1000 + Double(now.tv_sec - lastRender.tv_sec) * 1000 | |
if msPassedSinceRender > 100 { | |
let msPassed = Double(now.tv_usec - lastTick.tv_usec) / 1000 + Double(now.tv_sec - lastTick.tv_sec) * 1000 | |
update(msPassed: msPassed) | |
render() | |
lastRender = now | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Here's a gif of me playing it
