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: