Last active
August 29, 2015 14:02
-
-
Save tfausak/0f31dd5583c4bc961a7d to your computer and use it in GitHub Desktop.
2048 implemented in Swift via Haskell.
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
// http://taylor.fausak.me/2014/04/28/cloning-2048-in-haskell/ | |
import func Cocoa.rand | |
/* | |
Utilities | |
*/ | |
// TODO: Can this be generalized to allow anything addable? | |
func combine(array: Int[]) -> Int[] { | |
if array.count < 2 { | |
return array | |
} | |
return [array[0] + array[1]] + array[2..array.endIndex] | |
} | |
func compact<T>(array: T?[]) -> T[] { | |
var result: T[] = [] | |
for optional in array { | |
if let element = optional { | |
result.append(element) | |
} | |
} | |
return result | |
} | |
func group<T: Equatable>(var array: T[]) -> T[][] { | |
if array.isEmpty { | |
return [] | |
} | |
let first = array.removeAtIndex(0) | |
var result = [first] | |
while !array.isEmpty && first == array[0] { | |
result.append(array.removeAtIndex(0)) | |
} | |
return [result] + group(array) | |
} | |
// TODO: Can this be an infinite generator? | |
func iterate<T>(function: (T -> T), value: T, times: Int) -> T { | |
if times < 1 { | |
return value | |
} | |
return iterate(function, function(value), times - 1) | |
} | |
func log_2(var number: Int) -> Int { | |
var result = 0 | |
while number > 1 { | |
++result | |
number /= 2 | |
} | |
return result | |
} | |
func pad<T>(var array: Array<T>, value: T, var count: Int) -> Array<T> { | |
count = count - array.count | |
if count < 0 { | |
count = 0 | |
} | |
array.extend(Array(count: count, repeatedValue: value)) | |
return array | |
} | |
// TODO: There's got to be a better way. | |
func random(limit: Int) -> Int { | |
return Int(rand()) % limit | |
} | |
func split(string: String, separator: Character) -> String[] { | |
var strings: String[] = [] | |
var buffer = "" | |
for character in string { | |
if character == separator { | |
strings.append(buffer) | |
buffer = "" | |
} else { | |
buffer += character | |
} | |
} | |
if !buffer.isEmpty { | |
strings.append(buffer) | |
} | |
return strings | |
} | |
// TODO: Can this work for ragged or empty multi-dimensional arrays? | |
func transpose<T>(matrix: T[][]) -> T[][] { | |
if matrix.isEmpty || matrix[0].isEmpty { | |
return matrix | |
} | |
let width = matrix.count | |
let height = matrix[0].count | |
let value = matrix[0][0] | |
var result: T[][] = [] | |
for _ in 0..height { | |
result.append(Array(count: width, repeatedValue: value)) | |
} | |
for (i, row) in enumerate(matrix) { | |
assert(row.count == height) | |
for (j, cell) in enumerate(row) { | |
result[j][i] = cell | |
} | |
} | |
return result | |
} | |
/* | |
Direction | |
*/ | |
enum Direction { | |
case Left | |
case Down | |
case Right | |
case Up | |
var description: String { | |
switch self { | |
case .Left: return "←" | |
case .Down: return "↓" | |
case .Right: return "→" | |
case .Up: return "↑" | |
} | |
} | |
} | |
func directions() -> Direction[] { | |
return [ | |
Direction.Left, | |
Direction.Down, | |
Direction.Right, | |
Direction.Up | |
] | |
} | |
/* | |
Point | |
*/ | |
typealias Point = (x: Int, y: Int) | |
/* | |
Tile | |
*/ | |
typealias Tile = Int? | |
func empty() -> Tile { | |
return nil | |
} | |
// TODO: Can this be generalized into "toString" or something? | |
func render(tile: Tile) -> String { | |
if let value = tile { | |
return String(value) | |
} else { | |
return " " | |
} | |
} | |
func parse(string: String) -> Tile { | |
if let value = string.toInt() { | |
return value | |
} else { | |
return nil | |
} | |
} | |
func rank(tile: Tile) -> Int { | |
if let value = tile { | |
return log_2(value) | |
} else { | |
return 0 | |
} | |
} | |
func score(tile: Tile) -> Int { | |
if let value = tile { | |
return value * (rank(tile) - 1) | |
} else { | |
return 0 | |
} | |
} | |
func randomTile() -> Tile { | |
if random(10) == 0 { | |
return 4 | |
} else { | |
return 2 | |
} | |
} | |
/* | |
Line | |
*/ | |
typealias Line = Tile[] | |
func empty(count: Int) -> Line { | |
return Array(count: count, repeatedValue: empty()) | |
} | |
func render(line: Line) -> String { | |
return "\t".join(line.map(render)) | |
} | |
func parse(string: String) -> Line { | |
return split(string, "\t").map(parse) | |
} | |
func score(line: Line) -> Int { | |
return line.map(score).reduce(0, +) | |
} | |
// TODO: My functional roots are showing. | |
func shift(line: Line) -> Line { | |
let a: Int[] = compact(line) | |
let b: Int[][] = group(a) | |
let c: Int[][] = b.map(combine) | |
let d: Int[] = c.reduce([], +) | |
let e: Line = d.map { $0 } // TODO: This is dumb. | |
let f: Line = pad(e, nil, line.count) | |
return f | |
} | |
// TODO: Is there a way to directly compare arrays for equality? | |
func canShift(line: Line) -> Bool { | |
return render(line) != render(shift(line)) | |
} | |
func emptyIndexes(line: Line) -> Int[] { | |
var result: Int[] = [] | |
for (index, tile) in enumerate(line) { | |
if !tile { | |
result.append(index) | |
} | |
} | |
return result | |
} | |
func set(var line: Line, tile: Tile, index: Int) -> Line { | |
assert(index < line.count) | |
line[index] = tile | |
return line | |
} | |
func randomEmptyIndex(line: Line) -> Int? { | |
let indexes = emptyIndexes(line) | |
if indexes.isEmpty { | |
return nil | |
} | |
return indexes[random(indexes.count)] | |
} | |
func randomlySet(line: Line, tile: Tile) -> Line { | |
if let index = randomEmptyIndex(line) { | |
return set(line, tile, index) | |
} else { | |
return line | |
} | |
} | |
/* | |
Grid | |
*/ | |
typealias Grid = Line[] | |
// TODO: This is dumb, but using "repeatedValue" doesn't generate unique arrays. | |
func empty(width: Int, height: Int) -> Grid { | |
var result: Grid = [] | |
for _ in 0..height { | |
result.append(empty(width)) | |
} | |
return result | |
} | |
func render(grid: Grid) -> String { | |
return "\n".join(grid.map(render)) | |
} | |
func parse(string: String) -> Grid { | |
return split(string, "\n").map(parse) | |
} | |
func score(grid: Grid) -> Int { | |
return grid.map(score).reduce(0, +) | |
} | |
func shift(grid: Grid) -> Grid { | |
return grid.map(shift) | |
} | |
// TODO: See (other) canShift. | |
func canShift(grid: Grid) -> Bool { | |
return grid.filter { !canShift($0) }.isEmpty | |
} | |
// TODO: Why can't this be shortened to `.map(reverse)`? | |
func rotate(grid: Grid) -> Grid { | |
return transpose(grid).map { $0.reverse() } | |
} | |
func rotateTo(grid: Grid, direction: Direction) -> Grid { | |
var times: Int | |
switch direction { | |
case .Left: times = 0 | |
case .Down: times = 1 | |
case .Right: times = 2 | |
case .Up: times = 3 | |
} | |
return iterate(rotate, grid, times) | |
} | |
func rotateFrom(grid: Grid, direction: Direction) -> Grid { | |
var times: Int | |
switch direction { | |
case .Left: times = 0 | |
case .Down: times = 3 | |
case .Right: times = 2 | |
case .Up: times = 1 | |
} | |
return iterate(rotate, grid, times) | |
} | |
func move(grid: Grid, direction: Direction) -> Grid { | |
return rotateFrom(shift(rotateTo(grid, direction)), direction) | |
} | |
// TODO: See canShift. | |
func canMove(grid: Grid, direction: Direction) -> Bool { | |
return render(grid) != render(move(grid, direction)) | |
} | |
func emptyPoints(grid: Grid) -> Point[] { | |
var result: Point[] = [] | |
for (y, line) in enumerate(grid) { | |
result.extend(emptyIndexes(line).map { (x: $0, y: y) }) | |
} | |
return result | |
} | |
func set(var grid: Grid, tile: Tile, point: Point) -> Grid { | |
assert(point.y < grid.count) | |
grid[point.y] = set(grid[point.y], tile, point.x) | |
return grid | |
} | |
func randomEmptyPoint(grid: Grid) -> Point? { | |
let points = emptyPoints(grid) | |
if points.isEmpty { | |
return nil | |
} | |
return points[random(points.count)] | |
} | |
func randomlySet(grid: Grid, tile: Tile) -> Grid { | |
if let point = randomEmptyPoint(grid) { | |
return set(grid, tile, point) | |
} else { | |
return grid | |
} | |
} | |
func randomlyAdd(grid: Grid) -> Grid { | |
let tile: Tile = randomTile() | |
return randomlySet(grid, tile) | |
} | |
func randomlyAdd(grid: Grid, count: Int) -> Grid { | |
if count < 1 { | |
return grid | |
} else { | |
return randomlyAdd(randomlyAdd(grid), count - 1) | |
} | |
} | |
func canMove(grid: Grid) -> Bool { | |
return !directions().filter { canMove(grid, $0) }.isEmpty | |
} | |
func moves(grid: Grid) -> Direction[] { | |
return directions().filter { canMove(grid, $0) } | |
} | |
/* | |
Settings | |
*/ | |
struct Settings { | |
var gridHeight = 4 | |
var gridWidth = 4 | |
var maximumTile = 2048 | |
var initialTileCount = 2 | |
} | |
/* | |
Game | |
*/ | |
struct Game { | |
var grid: Grid | |
var settings: Settings | |
var description: String { | |
let s = score(grid) | |
let g = render(grid) | |
let ms = " ".join(moves(grid).map { $0.description }) | |
return "Score: \(s)\n\(g)\nMoves: \(ms)" | |
} | |
init() { | |
self.init(settings: Settings()) | |
} | |
init(settings: Settings) { | |
self.settings = settings | |
grid = randomlyAdd(empty(settings.gridWidth, settings.gridHeight), settings.initialTileCount) | |
} | |
func hasWon() -> Bool { | |
for line in grid { | |
for tile in line { | |
if tile > settings.maximumTile { | |
return true | |
} | |
} | |
} | |
return false | |
} | |
// TODO: Calling this "move" breaks overloading. | |
mutating func go(direction: Direction) { | |
grid = move(grid, direction) | |
} | |
} | |
/* | |
Playground | |
*/ | |
var game = Game() | |
println(game.description) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment