Tetris/TetrisKit/GameController.swift

168 lines
4.8 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] = [.random(), .random(), .random()]
@Published public var heldTetromino: Tetromino?
public var ended: Bool {
return (0..<width).first(where: { board[$0, 0] }) != nil
}
public init() {
self.board = GameBoard(width: width, height: height)
self.currentPiece = nil
}
public func start() {
state = .playing(.normal)
nextPiece()
}
func nextPiece() {
let tetromino = nextTetrominoes.removeFirst()
currentPiece = GamePiece(tetromino: tetromino, topLeft: ((width - tetromino.shape.count) / 2, 0))
nextTetrominoes.append(.random())
}
func finalizePiece() {
self.board.set(piece: currentPiece!)
clearLines()
nextPiece()
state = .playing(.normal)
}
func clearLines() {
var row = height - 1
while row >= 0 {
if board.rowFull(row) {
board.tiles.remove(at: row)
}
row -= 1
}
for _ in 0..<height - board.tiles.count {
board.tiles.insert(Array(repeating: nil, count: width), at: 0)
}
}
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)
}
public enum PlayState {
case normal
case dropped
case switched
}