diff --git a/Privyet/Base.lproj/LaunchScreen.storyboard b/Privyet/Base.lproj/LaunchScreen.storyboard
index 124e445..6b66ff1 100644
--- a/Privyet/Base.lproj/LaunchScreen.storyboard
+++ b/Privyet/Base.lproj/LaunchScreen.storyboard
@@ -14,7 +14,8 @@
-
+
+
diff --git a/Privyet/Block.swift b/Privyet/Block.swift
index 251fce3..7366c08 100644
--- a/Privyet/Block.swift
+++ b/Privyet/Block.swift
@@ -11,6 +11,7 @@ import SpriteKit
let NumberOfColors: UInt32 = 6
+// Colors that may be applied to blocks (whcih controls the sprite image they use)
enum BlockColor: Int, CustomStringConvertible {
case Blue = 0, Orange, Purple, Red, Teal, Yellow
@@ -40,6 +41,7 @@ enum BlockColor: Int, CustomStringConvertible {
}
}
+// Represents a single block in the game.
class Block: Hashable, CustomStringConvertible {
// Constants
let color: BlockColor
diff --git a/Privyet/GameScene.swift b/Privyet/GameScene.swift
index 1ceafc9..99ee8ad 100644
--- a/Privyet/GameScene.swift
+++ b/Privyet/GameScene.swift
@@ -54,15 +54,17 @@ class GameScene: SKScene {
shapeLayer.addChild(gameBoard)
gameLayer.addChild(shapeLayer)
+ // Set the theme music to play infinitely.
run(SKAction.repeatForever(SKAction.playSoundFileNamed("Sounds/theme.mp3", waitForCompletion: true)))
}
+ // Convenience function to play a sound file.
func playSound(sound: String) {
run(SKAction.playSoundFileNamed(sound, waitForCompletion: false))
}
+ // Called once per frame by the framework, before it's rendered.
override func update(_ currentTime: TimeInterval) {
- // Called before each frame is rendered
guard let lastTick = lastTick else {
return
}
@@ -71,23 +73,26 @@ class GameScene: SKScene {
self.lastTick = NSDate()
tick?()
}
-
}
+ // Starts the game tick going.
func startTicking() {
lastTick = NSDate()
}
+ // Stops the game tick.
func stopTicking() {
lastTick = nil
}
+ // Convert bucket row/column coordinates to display coordinates.
func pointForColumn(column: Int, row: Int) -> CGPoint {
let x = LayerPosition.x + (CGFloat(column) * BlockSize) + (BlockSize / 2)
let y = LayerPosition.y - ((CGFloat(row) * BlockSize) + (BlockSize / 2))
return CGPoint(x: x, y: y)
}
+ // Add the "preview" shape to the current scene. This also "rezzes in" a shape's block sprites.
func addPreviewShapeToScene(shape: Shape, completion: @escaping () -> ()) {
for block in shape.blocks {
var texture = textureCache[block.spriteName]
@@ -115,6 +120,7 @@ class GameScene: SKScene {
run(SKAction.sequence([waitAction, completeAction]))
}
+ // Move the "preview" shape into position so it becomes the new "falling" shape.
func movePreviewShape(shape: Shape, completion: @escaping () -> ()) {
for block in shape.blocks {
let sprite = block.sprite!
@@ -128,6 +134,7 @@ class GameScene: SKScene {
run(SKAction.sequence([waitAction, completeAction]))
}
+ // Redraw the falling shape by shifting the position of its component blocks.
func redrawShape(shape: Shape, completion: @escaping () -> ()) {
for block in shape.blocks {
let sprite = block.sprite!
@@ -141,9 +148,11 @@ class GameScene: SKScene {
}
}
+ // Animate the removed lines "exploding" and the remaining blocks dropping into place.
func animateCollapsingLines(linesToRemove: Array>, fallenBlocks: Array>, completion: @escaping () -> ()) {
var longestDuration: TimeInterval = 0
+ // Animate the falling blocks dropping into place
for (columnIdx, column) in fallenBlocks.enumerated() {
for (blockIdx, block) in column.enumerated() {
let newPosition = pointForColumn(column: block.column, row: block.row)
@@ -158,6 +167,7 @@ class GameScene: SKScene {
}
}
+ // Animate the "removed" blocks "exploding"
for rowToRemove in linesToRemove {
for block in rowToRemove {
let randomRadius = CGFloat(UInt(arc4random_uniform(400) + 100))
diff --git a/Privyet/GameViewController.swift b/Privyet/GameViewController.swift
index 92e7dd0..c7f81c4 100644
--- a/Privyet/GameViewController.swift
+++ b/Privyet/GameViewController.swift
@@ -43,10 +43,12 @@ class GameViewController: UIViewController, PrivyetDelegate, UIGestureRecognizer
return true
}
+ // Called when the user taps the screen
@IBAction func didTap(_ sender: UITapGestureRecognizer) {
privyet.rotateShape()
}
+ // Called when the user slides their finger across the screen
@IBAction func didPan(_ sender: UIPanGestureRecognizer) {
let currentPoint = sender.translation(in: self.view)
if let originalPoint = panPointReference {
@@ -64,14 +66,17 @@ class GameViewController: UIViewController, PrivyetDelegate, UIGestureRecognizer
}
}
+ // Called when the user swipes the screen in a downward direction
@IBAction func didSwipe(_ sender: UISwipeGestureRecognizer) {
privyet.dropShape()
}
+ // Used to allow multiple gesture recognizers to be used simultaneously
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
+ // Used to prioritize one gesture recognizer over another
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer is UISwipeGestureRecognizer {
if otherGestureRecognizer is UIPanGestureRecognizer {
@@ -85,10 +90,12 @@ class GameViewController: UIViewController, PrivyetDelegate, UIGestureRecognizer
return false
}
+ // Called once per game tick (interval over which blocks drop)
func didTick() {
privyet.letShapeFall()
}
+ // Advances the "next' and "falling" shapes and places them on the screen
func nextShape() {
let newShapes = privyet.newShape()
guard let fallingShape = newShapes.fallingShape else {
@@ -101,6 +108,7 @@ class GameViewController: UIViewController, PrivyetDelegate, UIGestureRecognizer
}
}
+ // Called when the game starts
func gameDidBegin(privyet: Privyet) {
levelLabel.text = "\(privyet.level)"
scoreLabel.text = "\(privyet.score)"
@@ -116,6 +124,7 @@ class GameViewController: UIViewController, PrivyetDelegate, UIGestureRecognizer
}
}
+ // Called when the game ends
func gameDidEnd(privyet: Privyet) {
view.isUserInteractionEnabled = false
scene.stopTicking()
@@ -125,6 +134,7 @@ class GameViewController: UIViewController, PrivyetDelegate, UIGestureRecognizer
}
}
+ // Called when the game advances in level (gets faster)
func gameDidLevelUp(privyet: Privyet) {
levelLabel.text = "\(privyet.level)"
if scene.tickLengthMillis >= 100 {
@@ -135,6 +145,7 @@ class GameViewController: UIViewController, PrivyetDelegate, UIGestureRecognizer
scene.playSound(sound: "Sounds/levelup.mp3")
}
+ // Called when a shape is "dropped"
func gameShapeDidDrop(privyet: Privyet) {
scene.stopTicking()
scene.redrawShape(shape: privyet.fallingShape!) {
@@ -143,6 +154,7 @@ class GameViewController: UIViewController, PrivyetDelegate, UIGestureRecognizer
scene.playSound(sound: "Sounds/drop.mp3")
}
+ // Called when a shape "lands" on top of existing blocks or at the bottom of the bucket
func gameShapeDidLand(privyet: Privyet) {
scene.stopTicking()
self.view.isUserInteractionEnabled = false
@@ -158,6 +170,7 @@ class GameViewController: UIViewController, PrivyetDelegate, UIGestureRecognizer
}
}
+ // Called when a shape moves on the screen
func gameShapeDidMove(privyet: Privyet) {
scene.redrawShape(shape: privyet.fallingShape!) { }
}
diff --git a/Privyet/Privyet.swift b/Privyet/Privyet.swift
index bb51dcf..b077920 100644
--- a/Privyet/Privyet.swift
+++ b/Privyet/Privyet.swift
@@ -41,7 +41,7 @@ protocol PrivyetDelegate {
}
class Privyet {
- var blockArray: Array2D
+ var bucket: Array2D
var nextShape: Shape?
var fallingShape: Shape?
var delegate: PrivyetDelegate?
@@ -52,9 +52,10 @@ class Privyet {
init() {
fallingShape = nil
nextShape = nil
- blockArray = Array2D(columns: NumColumns, rows: NumRows)
+ bucket = Array2D(columns: NumColumns, rows: NumRows)
}
+ // Starts the game by initializing game data
func beginGame() {
if (nextShape == nil) {
nextShape = Shape.random(startingColumn: PreviewColumn, startingRow: PreviewRow)
@@ -62,6 +63,7 @@ class Privyet {
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)
@@ -77,6 +79,8 @@ class Privyet {
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
@@ -84,82 +88,93 @@ class Privyet {
for block in shape.blocks {
if block.column < 0 || block.column >= NumColumns || block.row < 0 || block.row >= NumRows {
return true
- } else if blockArray[block.column, block.row] != nil {
+ } 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 {
- blockArray[block.column, block.row] = block
+ 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 || blockArray[bottomBlock.column, bottomBlock.row + 1] != nil {
+ 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>, fallenBlocks: Array>) {
var removedLines = Array>()
for row in (1..()
for column in 0..= level * LevelThreshold {
level += 1
delegate?.gameDidLevelUp(privyet: self)
}
+
var fallenBlocks = Array>()
for column in 0..()
for row in (1.. 0 {
@@ -169,22 +184,24 @@ class Privyet {
return (removedLines, fallenBlocks)
}
+ // Removes all blocks from the bucket (at end of game). Returns all the removed blocks.
func removeAllBlocks() -> Array> {
var allBlocks = Array>()
for row in 0..()
for column in 0.. Orientation {
var rotated = orientation.rawValue + (clockwise ? 1 : -1)
if rotated > Orientation.TwoSeventy.rawValue {
@@ -51,6 +53,8 @@ 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 color of the shape
let color: BlockColor
@@ -65,15 +69,18 @@ class Shape: Hashable, CustomStringConvertible {
// Required overrides
// 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] {
return [:]
}
+ // Returns the blocks in this shape that are currently "on the bottom"
var bottomBlocks: Array {
guard let bottomBlocks = bottomBlocksForOrientations[orientation] else {
return []
@@ -110,6 +117,7 @@ class Shape: Hashable, CustomStringConvertible {
self.init(column: column, row: row, color: BlockColor.random(), 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
@@ -120,6 +128,7 @@ class Shape: Hashable, CustomStringConvertible {
}
}
+ // 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
@@ -130,34 +139,41 @@ class Shape: Hashable, CustomStringConvertible {
}
}
+ // 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
@@ -167,12 +183,14 @@ class Shape: Hashable, CustomStringConvertible {
}
}
+ // Moves the shape to an absolute position.
final func moveTo(column: Int, row: Int) {
self.column = column
self.row = row
rotateBlocks(orientation: orientation)
}
+ // Generates a random shape.
final class func random(startingColumn: Int, startingRow: Int) -> Shape {
switch Int(arc4random_uniform(NumShapeTypes)) {
case 0: