276 lines
8.7 KiB
Swift
276 lines
8.7 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 PointsPerLine = 10
|
|
let LevelThreshold = 500
|
|
|
|
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 game has reached a new level
|
|
func gameDidLevelUp(privyet: Privyet)
|
|
}
|
|
|
|
class Privyet {
|
|
var bucket: Array2D<Block>
|
|
var nextShape: Shape?
|
|
var fallingShape: Shape?
|
|
var delegate: PrivyetDelegate?
|
|
|
|
var score = 0
|
|
var level = 1
|
|
|
|
init() {
|
|
fallingShape = nil
|
|
nextShape = 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 = removedLines.count * PointsPerLine * level
|
|
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
|
|
}
|
|
|
|
// 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)
|
|
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)
|
|
}
|
|
}
|