Skip to content

Instantly share code, notes, and snippets.

@dduan
Created November 14, 2020 22:16
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dduan/d498944d05fd8609b7113396ce97b7df to your computer and use it in GitHub Desktop.
Save dduan/d498944d05fd8609b7113396ce97b7df to your computer and use it in GitHub Desktop.
Flappy bird as a CLI app written in Swift.
// 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
}
}
}
@dduan
Copy link
Author

dduan commented Nov 14, 2020

Here's a gif of me playing it
bird

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment