Add 2048 Game

This commit is contained in:
Ivan Vorobei
2019-06-06 22:28:01 +03:00
parent bcedea412a
commit 5eaf1fbc68
20 changed files with 1390 additions and 1 deletions

View File

@@ -0,0 +1,127 @@
//
// BlockMatrix.swift
// SwiftUI2048
//
// Created by Hongyu on 6/5/19.
// Copyright © 2019 Cyandev. All rights reserved.
//
import Foundation
protocol Block {
associatedtype Value
var number: Value { get set }
}
struct IndexedBlock<T> {
let index: (Int, Int)
let item: T?
}
struct BlockMatrix<T> : CustomDebugStringConvertible where T: Block {
typealias Index = (Int, Int)
fileprivate var matrix: [[T?]]
init() {
matrix = [[T?]]()
for _ in 0..<4 {
var row = [T?]()
for _ in 0..<4 {
row.append(nil)
}
matrix.append(row)
}
}
var debugDescription: String {
matrix.map { row -> String in
row.map {
if $0 == nil {
return " "
} else {
return String(describing: $0!.number)
}
}.joined(separator: "\t")
}.joined(separator: "\n")
}
var flatten: [IndexedBlock<T>] {
return self.matrix.enumerated().flatMap { (y: Int, element: [T?]) in
element.enumerated().map { (x: Int, element: T?) in
return IndexedBlock(index: (x, y), item: element)
}
}
}
subscript(index: Self.Index) -> T? {
guard isIndexValid(index) else {
return nil
}
return matrix[index.1][index.0]
}
/// Move the block to specific location and leave the original location blank.
/// - Parameter from: Source location
/// - Parameter to: Destination location
mutating func move(from: Self.Index, to: Self.Index) {
guard isIndexValid(from) && isIndexValid(to) else {
// TODO: Throw an error?
return
}
guard let source = self[from] else {
return
}
matrix[to.1][to.0] = source
matrix[from.1][from.0] = nil
}
/// Move the block to specific location, change its value and leave the original location blank.
/// - Parameter from: Source location
/// - Parameter to: Destination location
/// - Parameter newValue: The new value
mutating func move(from: Self.Index, to: Self.Index, with newValue: T.Value) {
guard isIndexValid(from) && isIndexValid(to) else {
// TODO: Throw an error?
return
}
guard var source = self[from] else {
return
}
source.number = newValue
matrix[to.1][to.0] = source
matrix[from.1][from.0] = nil
}
/// Place a block to specific location.
/// - Parameter block: The block to place
/// - Parameter to: Destination location
mutating func place(_ block: T?, to: Self.Index) {
matrix[to.1][to.0] = block
}
fileprivate func isIndexValid(_ index: Self.Index) -> Bool {
guard index.0 >= 0 && index.0 < 4 else {
return false
}
guard index.1 >= 0 && index.1 < 4 else {
return false
}
return true
}
}

View File

@@ -0,0 +1,176 @@
//
// GameLogic.swift
// SwiftUI2048
//
// Created by Hongyu on 6/5/19.
// Copyright © 2019 Cyandev. All rights reserved.
//
import Foundation
import SwiftUI
import Combine
final class GameLogic : BindableObject {
enum Direction {
case left
case right
case up
case down
}
typealias BlockMatrixType = BlockMatrix<IdentifiedBlock>
let didChange = PassthroughSubject<GameLogic, Never>()
fileprivate var _blockMatrix: BlockMatrixType!
var blockMatrix: BlockMatrixType {
return _blockMatrix
}
fileprivate var _globalID = 0
fileprivate var newGlobalID: Int {
_globalID += 1
return _globalID
}
init() {
newGame()
}
func newGame() {
_blockMatrix = BlockMatrixType()
generateNewBlocks()
didChange.send(self)
}
func move(_ direction: Direction) {
defer {
didChange.send(self)
}
var moved = false
let axis = direction == .left || direction == .right
for row in 0..<4 {
var rowSnapshot = [IdentifiedBlock?]()
var compactRow = [IdentifiedBlock]()
for col in 0..<4 {
// Transpose if necessary.
if let block = _blockMatrix[axis ? (col, row) : (row, col)] {
rowSnapshot.append(block)
compactRow.append(block)
}
rowSnapshot.append(nil)
}
merge(blocks: &compactRow, reverse: direction == .down || direction == .right)
var newRow = [IdentifiedBlock?]()
compactRow.forEach { newRow.append($0) }
if compactRow.count < 4 {
for _ in 0..<(4 - compactRow.count) {
if direction == .left || direction == .up {
newRow.append(nil)
} else {
newRow.insert(nil, at: 0)
}
}
}
newRow.enumerated().forEach {
if rowSnapshot[$0]?.number != $1?.number {
moved = true
}
_blockMatrix.place($1, to: axis ? ($0, row) : (row, $0))
}
}
if moved {
generateNewBlocks()
}
}
fileprivate func merge(blocks: inout [IdentifiedBlock], reverse: Bool) {
if reverse {
blocks = blocks.reversed()
}
blocks = blocks
.map { (false, $0) }
.reduce([(Bool, IdentifiedBlock)]()) { acc, item in
if acc.last?.0 == false && acc.last?.1.number == item.1.number {
var accPrefix = Array(acc.dropLast())
var mergedBlock = item.1
mergedBlock.number *= 2
accPrefix.append((true, mergedBlock))
return accPrefix
} else {
var accTmp = acc
accTmp.append((false, item.1))
return accTmp
}
}
.map { $0.1 }
if reverse {
blocks = blocks.reversed()
}
}
@discardableResult fileprivate func generateNewBlocks() -> Bool {
var blankLocations = [BlockMatrixType.Index]()
for rowIndex in 0..<4 {
for colIndex in 0..<4 {
let index = (colIndex, rowIndex)
if _blockMatrix[index] == nil {
blankLocations.append(index)
}
}
}
guard blankLocations.count >= 2 else {
return false
}
// Don't forget to sync data.
defer {
didChange.send(self)
}
// Place the first block.
var placeLocIndex = Int.random(in: 0..<blankLocations.count)
_blockMatrix.place(IdentifiedBlock(id: newGlobalID, number: 2), to: blankLocations[placeLocIndex])
// Place the second block.
guard let lastLoc = blankLocations.last else {
return false
}
blankLocations[placeLocIndex] = lastLoc
placeLocIndex = Int.random(in: 0..<(blankLocations.count - 1))
_blockMatrix.place(IdentifiedBlock(id: newGlobalID, number: 2), to: blankLocations[placeLocIndex])
return true
}
// fileprivate func forEachBlockIndices(mode: ForEachMode = .rowByRow,
// reversed: Bool = false,
// _ action: (BlockMatrixType.Index) -> ()) {
// var indices = (0..<4).map { $0 }
// if reversed {
// indices = indices.reversed()
// }
//
// for row in indices {
// for col in indices {
// if mode == .rowByRow {
// action((col, row))
// } else {
// action((row, col)) // transpose
// }
// }
// }
// }
}

View File

@@ -0,0 +1,16 @@
//
// IdentifiedBlock.swift
// SwiftUI2048
//
// Created by Hongyu on 6/5/19.
// Copyright © 2019 Cyandev. All rights reserved.
//
import Foundation
struct IdentifiedBlock: Block {
let id: Int
var number: Int
}