privyet/Privyet/Privyet.swift

320 lines
10 KiB
Swift

//
// Privyet.swift
// Privyet
//
// Created by Amy Bowersox on 5/24/20.
// Copyright © 2020 Erbosoft Metaverse Design Solutions. All rights reserved.
//
import Foundation
let NumColumns = 10
let NumRows = 20
let StartingColumn = 4
let StartingRow = 0
let PreviewColumn = 12
let PreviewRow = 3
let HoldColumn = 12
let HoldRow = 10
// number of points for single (1 line), double (2 lines), triple (3 lines), Privyet (4 lines)
let PointsPerLine = [1, 2, 5, 10]
let LevelThreshold = 50
protocol PrivyetDelegate {
// Invoked when the current round of Privyet ends
func gameDidEnd(privyet: Privyet)
// Invoked after a game has begun
func gameDidBegin(privyet: Privyet)
// Invoked when the falling shape has become part of the game board
func gameShapeDidLand(privyet: Privyet)
// Invoked when the falling shape has changed its location
func gameShapeDidMove(privyet: Privyet)
// invoked when the falling shape has changed its location after being dropped
func gameShapeDidDrop(privyet: Privyet)
// invoked when the current shape is put on hold
func gameShapePutOnHold(privyet: Privyet, firstHold: Bool)
// invoked when the game has reached a new level
func gameDidLevelUp(privyet: Privyet)
}
class Privyet {
var bucket: Array2D<Block>
var nextShape: Shape?
var fallingShape: Shape?
var heldShape: Shape?
var delegate: PrivyetDelegate?
var score = 0
var level = 1
init() {
fallingShape = nil
nextShape = nil
heldShape = nil
bucket = Array2D<Block>(columns: NumColumns, rows: NumRows)
}
// Starts the game by initializing game data
func beginGame() {
if (nextShape == nil) {
nextShape = Shape.random(startingColumn: PreviewColumn, startingRow: PreviewRow)
}
delegate?.gameDidBegin(privyet: self)
}
// Advances the current falling shape and the "next" shape. Returns both these shapes as a tuple.
func newShape() -> (fallingShape: Shape?, nextShape: Shape?) {
fallingShape = nextShape
nextShape = Shape.random(startingColumn: PreviewColumn, startingRow: PreviewRow)
fallingShape?.moveTo(column: StartingColumn, row: StartingRow)
guard detectIllegalPlacement() == false else {
nextShape = fallingShape
nextShape!.moveTo(column: PreviewColumn, row: PreviewRow)
endGame()
return (nil, nil)
}
return (fallingShape, nextShape)
}
// Returns true if the falling shape is in an "illegal" placement (outside bucket bounds or "colliding"
// with existing blocks in the bucket).
func detectIllegalPlacement() -> Bool {
guard let shape = fallingShape else {
return false
}
for block in shape.blocks {
if block.column < 0 || block.column >= NumColumns || block.row < 0 || block.row >= NumRows {
return true
} else if bucket[block.column, block.row] != nil {
return true
}
}
return false
}
// "Settle" the falling shape by transferring its blocks to the bucket.
func settleShape() {
guard let shape = fallingShape else {
return
}
for block in shape.blocks {
bucket[block.column, block.row] = block
}
fallingShape = nil
delegate?.gameShapeDidLand(privyet: self)
}
// Returns true if the falling block is "landing" (at the bottom of the bucket or directly above existing blocks)
func detectTouch() -> Bool {
guard let shape = fallingShape else {
return false
}
for bottomBlock in shape.bottomBlocks {
if bottomBlock.row == NumRows - 1 || bucket[bottomBlock.column, bottomBlock.row + 1] != nil {
return true
}
}
return false
}
// Ends the current game
func endGame() {
score = 0
level = 1
delegate?.gameDidEnd(privyet: self)
}
// Detect and remove completed lines. Returns a tuple of two arrays of arrays:
// linesRemoved - The blocks that will be removed as a result of completed lines.
// fallenBlocks - The blocks that will drop to a lower line in the bucket when the above blocks are removed.
// If no lines are to be removed, returns a pair of empty lists.
func removeCompletedLines() -> (linesRemoved: Array<Array<Block>>, fallenBlocks: Array<Array<Block>>) {
var removedLines = Array<Array<Block>>()
for row in (1..<NumRows).reversed() {
var rowOfBlocks = Array<Block>()
for column in 0..<NumColumns {
guard let block = bucket[column, row] else {
continue
}
rowOfBlocks.append(block)
}
if rowOfBlocks.count == NumColumns {
// filled line detected, remove it
removedLines.append(rowOfBlocks)
for block in rowOfBlocks {
bucket[block.column, block.row] = nil
}
}
}
if removedLines.count == 0 {
return ([], []) // no lines filled
}
// Advance score and game level as appropriate
let pointsEarned = PointsPerLine[removedLines.count - 1] * max(level / 2, 1)
score += pointsEarned
if score >= level * LevelThreshold {
level += 1
delegate?.gameDidLevelUp(privyet: self)
}
var fallenBlocks = Array<Array<Block>>()
for column in 0..<NumColumns {
var fallenBlocksArray = Array<Block>()
for row in (1..<removedLines[0][0].row).reversed() {
guard let block = bucket[column, row] else {
continue
}
var newRow = row
while (newRow < NumRows - 1 && bucket[column, newRow + 1] == nil) {
newRow += 1
}
block.row = newRow
bucket[column, row] = nil
bucket[column, newRow] = block
fallenBlocksArray.append(block)
}
if fallenBlocksArray.count > 0 {
fallenBlocks.append(fallenBlocksArray)
}
}
return (removedLines, fallenBlocks)
}
// Removes all blocks from the bucket (at end of game). Returns all the removed blocks.
func removeAllBlocks() -> Array<Array<Block>> {
var allBlocks = Array<Array<Block>>()
for row in 0..<NumRows {
var rowOfBlocks = Array<Block>()
for column in 0..<NumColumns {
guard let block = bucket[column, row] else {
continue
}
rowOfBlocks.append(block)
bucket[column, row] = nil
}
allBlocks.append(rowOfBlocks)
}
return allBlocks
}
// Removes all stored shapes from their slots (at end of game). Returns all the removed shapes.
func removeAllShapes() -> Array<Shape> {
var allShapes = Array<Shape>()
for shape in [fallingShape, nextShape, heldShape] {
guard let s = shape else {
continue
}
allShapes.append(s)
}
fallingShape = nil
nextShape = nil
heldShape = nil
return allShapes
}
// Drop the falling shape down as far as it will go.
func dropShape() {
guard let shape = fallingShape else {
return
}
while detectIllegalPlacement() == false {
shape.lowerShapeByOneRow()
}
shape.raiseShapeByOneRow()
delegate?.gameShapeDidDrop(privyet: self)
}
// Let the falling shape fall by one row.
func letShapeFall() {
guard let shape = fallingShape else {
return
}
shape.lowerShapeByOneRow()
if detectIllegalPlacement() {
shape.raiseShapeByOneRow()
if detectIllegalPlacement() {
endGame() // shape was in illegal place to start, this ends the game
} else {
settleShape() // shape can land
}
} else {
delegate?.gameShapeDidMove(privyet: self)
/* AGRB 5/29/2020 - removed to allow "finessing" moves
if detectTouch() {
settleShape()
}
*/
}
}
// Rotate the falling shape clockwise by 90 degrees.
func rotateShape() {
guard let shape = fallingShape else {
return
}
shape.rotateClockwise()
guard detectIllegalPlacement() == false else {
shape.rotateCounterClockwise()
return
}
delegate?.gameShapeDidMove(privyet: self)
}
// Move the falling shape left by one column.
func moveShapeLeft() {
guard let shape = fallingShape else {
return
}
shape.shiftLeftByOneColumn()
guard detectIllegalPlacement() == false else {
shape.shiftRightByOneColumn()
return
}
delegate?.gameShapeDidMove(privyet: self)
}
// Move the falling shape right by one column.
func moveShapeRight() {
guard let shape = fallingShape else {
return
}
shape.shiftRightByOneColumn()
guard detectIllegalPlacement() == false else {
shape.shiftLeftByOneColumn()
return
}
delegate?.gameShapeDidMove(privyet: self)
}
// Places current shape "on hold."
func holdShape() {
guard let shape = fallingShape else {
return
}
guard let held = heldShape else {
shape.moveTo(column: HoldColumn, row: HoldRow)
heldShape = shape
fallingShape = nil
delegate?.gameShapePutOnHold(privyet: self, firstHold: true)
return
}
shape.exchangePositions(other: held)
fallingShape = held
heldShape = shape
delegate?.gameShapePutOnHold(privyet: self, firstHold: false)
}
}