Tetris/TetrisKit/GameController.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
}