privyet/Privyet/Shape.swift

252 lines
9.1 KiB
Swift

//
// Shape.swift
// Privyet
//
// Created by Amy Bowersox on 5/24/20.
// Copyright © 2020 Erbosoft Metaverse Design Solutions. All rights reserved.
//
import Foundation
import SpriteKit
// Represents the orientation of a shape in increments of 90 degrees
enum Orientation: Int, CustomStringConvertible, CaseIterable {
case Zero = 0, Ninety, OneEighty, TwoSeventy
var description: String {
switch self {
case .Zero:
return "0"
case .Ninety:
return "90"
case .OneEighty:
return "180"
case .TwoSeventy:
return "270"
}
}
static func random() -> Orientation {
return Orientation.allCases.shuffled().first!
}
// Returns the orientation resulting from a rotation in either direction.
static func rotate(orientation: Orientation, clockwise: Bool) -> Orientation {
var rotated = orientation.rawValue + (clockwise ? 1 : -1)
if rotated > Orientation.TwoSeventy.rawValue {
rotated = Orientation.Zero.rawValue
} else if rotated < 0 {
rotated = Orientation.TwoSeventy.rawValue
}
return Orientation(rawValue:rotated)!
}
}
// The number of total shape varieties
let NumShapeTypes = 7
// Shape indexes
let FirstBlockIdx: Int = 0
let SecondBlockIdx: Int = 1
let ThirdBlockIdx: Int = 2
let FourthBlockIdx: Int = 3
// Base class representing a shape consisting of four blocks. Derived classes represent the different
// tetramino shapes.
class Shape: Hashable, CustomStringConvertible {
// The blocks comprising the shape
var blocks = Array<Block>()
// The current orientation of the shape
var orientation: Orientation
// The column and row representing the shape's anchor point
var column, row: Int
// Required overrides
// Subclasses must override this property
// The color of the shape
var color: BlockColor? {
return nil
}
// Subclasses must override this property
// Returns the relative positions of the shape blocks based on the shape orientation
var blockRowColumnPositions: [Orientation: Array<(columnDiff: Int, rowDiff: Int)>] {
return [:]
}
// Subclasses must override this property
// Returns the blocks in a shape that are "on the bottom" based on the shape orientation
var bottomBlocksForOrientations: [Orientation: Array<Block>] {
return [:]
}
// Returns the blocks in this shape that are currently "on the bottom"
var bottomBlocks: Array<Block> {
guard let bottomBlocks = bottomBlocksForOrientations[orientation] else {
return []
}
return bottomBlocks
}
// Equatable
static func == (lhs: Shape, rhs: Shape) -> Bool {
return lhs.row == rhs.row && lhs.column == rhs.column
}
// Hashable
func hash(into hasher: inout Hasher) {
blocks.forEach {b in
hasher.combine(b.hashValue)
}
}
// CustomStringConvertible
var description: String {
guard let color = color else {
return "<invalid color> block facing \(orientation): \(blocks[FirstBlockIdx]), \(blocks[SecondBlockIdx]), \(blocks[ThirdBlockIdx]), \(blocks[FourthBlockIdx])"
}
return "\(color) block facing \(orientation): \(blocks[FirstBlockIdx]), \(blocks[SecondBlockIdx]), \(blocks[ThirdBlockIdx]), \(blocks[FourthBlockIdx])"
}
init(column: Int, row: Int, orientation: Orientation) {
self.column = column
self.row = row
self.orientation = orientation
initializeBlocks()
}
convenience init(column: Int, row: Int) {
self.init(column: column, row: row, orientation: Orientation.random())
}
// Initialize the blocks of the shape based on the current position, color, and orientation.
final func initializeBlocks() {
guard let blockRowColumnTranslations = blockRowColumnPositions[orientation] else {
return
}
blocks = blockRowColumnTranslations.map { (diff) -> Block in
return Block(column: column + diff.columnDiff, row: row + diff.rowDiff, color: color!)
}
}
final func getBoundingBoxForOrientation(baseColumn: Int, baseRow: Int, orientation: Orientation) -> BlockRect {
guard let offsets = blockRowColumnPositions[orientation] else {
return BlockRect(x: baseColumn, y: baseRow)
}
let blockRects = offsets.map { (offset) -> BlockRect in
return BlockRect(x: baseColumn + offset.columnDiff, y: baseRow + offset.rowDiff)
}
return blockRects[0] + blockRects[1] + blockRects[2] + blockRects[3]
}
final func getBoundingBox(baseColumn: Int, baseRow: Int) -> BlockRect {
return getBoundingBoxForOrientation(baseColumn: baseColumn, baseRow: baseRow, orientation: .Zero) + getBoundingBoxForOrientation(baseColumn: baseColumn, baseRow: baseRow, orientation: .Ninety) + getBoundingBoxForOrientation(baseColumn: baseColumn, baseRow: baseRow, orientation: .OneEighty) + getBoundingBoxForOrientation(baseColumn: baseColumn, baseRow: baseRow, orientation: .TwoSeventy)
}
// Rotate the blocks in this shape to a new orientation.
final func rotateBlocks(orientation: Orientation) {
guard let blockRowColumnTranslation: Array<(columnDiff: Int, rowDiff: Int)> = blockRowColumnPositions[orientation] else {
return
}
for (idx, diff) in blockRowColumnTranslation.enumerated() {
blocks[idx].column = column + diff.columnDiff
blocks[idx].row = row + diff.rowDiff
}
}
// Rotate the blocks in this shape clockwise.
final func rotateClockwise() {
let newOrientation = Orientation.rotate(orientation: orientation, clockwise: true)
rotateBlocks(orientation: newOrientation)
orientation = newOrientation
}
// Rotate the blocks in this shape counterclockwise.
final func rotateCounterClockwise() {
let newOrientation = Orientation.rotate(orientation: orientation, clockwise: false)
rotateBlocks(orientation: newOrientation)
orientation = newOrientation
}
// Updates block positions in this shape to one row down.
final func lowerShapeByOneRow() {
shiftBy(columns: 0, rows: 1)
}
// Updates block positions in this shape to one row up.
final func raiseShapeByOneRow() {
shiftBy(columns: 0, rows: -1)
}
// Updates block positions in this shape to one colukmn to the right.
final func shiftRightByOneColumn() {
shiftBy(columns: 1, rows: 0)
}
// Updates block positions in this shape to one column to the left.
final func shiftLeftByOneColumn() {
shiftBy(columns: -1, rows: 0)
}
// Shifts plock positions in this shape in a relative fashion.
final func shiftBy(columns: Int, rows: Int) {
self.column += columns
self.row += rows
for block in blocks {
block.column += columns
block.row += rows
}
}
// Moves the shape to an absolute position.
final func moveTo(column: Int, row: Int) {
self.column = column
self.row = row
rotateBlocks(orientation: orientation)
}
final func exchangePositions(other: Shape) {
var tmp = other.column
other.column = self.column
self.column = tmp
tmp = other.row
other.row = self.row
self.row = tmp
other.rotateBlocks(orientation: other.orientation)
rotateBlocks(orientation: orientation)
}
// Generates a random shape.
final class func random(startingColumn: Int, startingRow: Int) -> Shape {
switch Int.random(in: 0 ..< NumShapeTypes) {
case 0:
return SquareShape(column: startingColumn, row: startingRow)
case 1:
return LineShape(column: startingColumn, row: startingRow)
case 2:
return TShape(column: startingColumn, row: startingRow)
case 3:
return LShape(column: startingColumn, row: startingRow)
case 4:
return JShape(column: startingColumn, row: startingRow)
case 5:
return SShape(column: startingColumn, row: startingRow)
default:
return ZShape(column: startingColumn, row: startingRow)
}
}
final class func boundingBox(baseColumn: Int, baseRow: Int) -> BlockRect {
return SquareShape(column: 0, row: 0).getBoundingBox(baseColumn: baseColumn, baseRow: baseRow) +
TShape(column: 0, row: 0).getBoundingBox(baseColumn: baseColumn, baseRow: baseRow) +
LineShape(column: 0, row: 0).getBoundingBox(baseColumn: baseColumn, baseRow: baseRow) +
LShape(column: 0, row: 0).getBoundingBox(baseColumn: baseColumn, baseRow: baseRow) +
JShape(column: 0, row: 0).getBoundingBox(baseColumn: baseColumn, baseRow: baseRow) +
SShape(column: 0, row: 0).getBoundingBox(baseColumn: baseColumn, baseRow: baseRow) +
ZShape(column: 0, row: 0).getBoundingBox(baseColumn: baseColumn, baseRow: baseRow)
}
}