210 lines
6.0 KiB
Swift
210 lines
6.0 KiB
Swift
//
|
|
// GameController.swift
|
|
// TetrisKit
|
|
//
|
|
// Created by Shadowfacts on 10/13/19.
|
|
// Copyright © 2019 Shadowfacts. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import Combine
|
|
|
|
public class GameController: ObservableObject {
|
|
|
|
public let width = 10
|
|
public let height = 20
|
|
|
|
public var state: GameState = .waitingForStart
|
|
|
|
@Published public var board: GameBoard
|
|
|
|
@Published public var currentPiece: GamePiece? {
|
|
didSet {
|
|
updateCurrentPieceAtDropPoint()
|
|
}
|
|
}
|
|
@Published public var currentPieceAtDropPoint: GamePiece?
|
|
@Published public var nextTetrominoes: [Tetromino] = []
|
|
@Published public var heldTetromino: Tetromino?
|
|
|
|
var currentBag: [Tetromino] = []
|
|
|
|
@Published public var score = 0
|
|
var previousPieceWasTetris = false
|
|
|
|
public init() {
|
|
self.board = GameBoard(width: width, height: height)
|
|
self.currentPiece = nil
|
|
}
|
|
|
|
public func start() {
|
|
state = .playing(.normal)
|
|
generateBag()
|
|
nextPiece()
|
|
}
|
|
|
|
func nextPiece() {
|
|
let tetromino = nextTetrominoes.first!
|
|
let nextPiece = GamePiece(tetromino: tetromino, topLeft: ((width - tetromino.shape.count) / 2, 0))
|
|
|
|
if overlapsAny(nextPiece) {
|
|
state = .ended
|
|
} else {
|
|
nextTetrominoes.removeFirst()
|
|
if currentBag.isEmpty {
|
|
generateBag()
|
|
}
|
|
nextTetrominoes.append(currentBag.removeFirst())
|
|
|
|
currentPiece = nextPiece
|
|
}
|
|
}
|
|
|
|
func generateBag() {
|
|
currentBag = Array(0..<Tetromino.allCases.count).shuffled().map { Tetromino.allCases[$0] }
|
|
if nextTetrominoes.isEmpty {
|
|
nextTetrominoes = Array(currentBag[0..<3])
|
|
currentBag.removeFirst(3)
|
|
}
|
|
}
|
|
|
|
func finalizePiece() {
|
|
self.board.set(piece: currentPiece!)
|
|
clearLines()
|
|
nextPiece()
|
|
|
|
if case .playing(.dropped) = state {
|
|
state = .playing(.normal)
|
|
}
|
|
}
|
|
|
|
func clearLines() {
|
|
var cleared = 0
|
|
var row = height - 1
|
|
while row >= 0 {
|
|
if board.rowFull(row) {
|
|
board.tiles.remove(at: row)
|
|
cleared += 1
|
|
}
|
|
row -= 1
|
|
}
|
|
for _ in 0..<cleared {
|
|
board.tiles.insert(Array(repeating: nil, count: width), at: 0)
|
|
}
|
|
|
|
if cleared == 4 {
|
|
score += previousPieceWasTetris ? 1200 : 800
|
|
previousPieceWasTetris = true
|
|
} else if cleared == 3 {
|
|
score += 500
|
|
previousPieceWasTetris = false
|
|
} else if cleared == 2 {
|
|
score += 300
|
|
previousPieceWasTetris = false
|
|
} else if cleared == 1 {
|
|
score += 100
|
|
previousPieceWasTetris = false
|
|
} else {
|
|
previousPieceWasTetris = false
|
|
}
|
|
}
|
|
|
|
public func step() {
|
|
guard case .playing(_) = state,
|
|
let currentPiece = currentPiece else { return }
|
|
|
|
let modifiedPiece = currentPiece.moved(by: (0, 1))
|
|
if overlapsAny(modifiedPiece) {
|
|
finalizePiece()
|
|
} else {
|
|
self.currentPiece = modifiedPiece
|
|
}
|
|
}
|
|
|
|
public func rotate(direction: RotationDirection) {
|
|
guard case let .playing(playState) = state,
|
|
playState != .dropped,
|
|
let currentPiece = currentPiece else { return }
|
|
|
|
let modifiedPiece = currentPiece.rotated(direction)
|
|
if !overlapsAny(modifiedPiece) {
|
|
self.currentPiece = modifiedPiece
|
|
}
|
|
}
|
|
|
|
public func left() {
|
|
guard case let .playing(playState) = state,
|
|
playState != .dropped,
|
|
let currentPiece = currentPiece else { return }
|
|
let modifiedPiece = currentPiece.moved(by: (-1, 0))
|
|
if !overlapsAny(modifiedPiece) {
|
|
self.currentPiece = modifiedPiece
|
|
}
|
|
}
|
|
|
|
public func right() {
|
|
guard case let .playing(playState) = state,
|
|
playState != .dropped,
|
|
let currentPiece = currentPiece else { return }
|
|
let modifiedPiece = currentPiece.moved(by: (1, 0))
|
|
if !overlapsAny(modifiedPiece) {
|
|
self.currentPiece = modifiedPiece
|
|
}
|
|
}
|
|
|
|
public func drop() {
|
|
guard case let .playing(playState) = state, playState != .dropped else { return }
|
|
currentPiece = currentPieceAtDropPoint
|
|
state = .playing(.dropped)
|
|
}
|
|
|
|
public func hold() {
|
|
guard case .playing(.normal) = state,
|
|
let currentPiece = currentPiece else { return }
|
|
if let heldTetromino = heldTetromino {
|
|
self.heldTetromino = currentPiece.tetromino
|
|
self.currentPiece = GamePiece(tetromino: heldTetromino, topLeft: ((width - heldTetromino.shape.count) / 2, 0))
|
|
} else {
|
|
heldTetromino = currentPiece.tetromino
|
|
nextPiece()
|
|
}
|
|
state = .playing(.switched)
|
|
}
|
|
|
|
private func updateCurrentPieceAtDropPoint() {
|
|
guard let currentPiece = currentPiece else { return }
|
|
var prev = currentPiece
|
|
currentPieceAtDropPoint = currentPiece
|
|
while !overlapsAny(currentPieceAtDropPoint!) {
|
|
prev = currentPieceAtDropPoint!
|
|
currentPieceAtDropPoint = currentPieceAtDropPoint!.moved(by: (0, 1))
|
|
}
|
|
currentPieceAtDropPoint = prev
|
|
}
|
|
|
|
public func overlapsAny(_ piece: GamePiece) -> Bool {
|
|
let (left, top) = piece.topLeft
|
|
for y in 0..<piece.tiles.count {
|
|
for x in 0..<piece.tiles.first!.count where piece.tiles[y][x] {
|
|
if top + y >= height || left + x < 0 || left + x >= width || board[left + x, top + y] {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
}
|
|
|
|
public enum GameState {
|
|
case waitingForStart
|
|
case playing(PlayState)
|
|
case ended
|
|
}
|
|
|
|
public enum PlayState {
|
|
case normal
|
|
case dropped
|
|
case switched
|
|
}
|