diff --git a/Privyet/Base.lproj/Main.storyboard b/Privyet/Base.lproj/Main.storyboard
index 966bd70..c9c0c3c 100644
--- a/Privyet/Base.lproj/Main.storyboard
+++ b/Privyet/Base.lproj/Main.storyboard
@@ -15,13 +15,6 @@
-
@@ -60,6 +53,20 @@
+
+
diff --git a/Privyet/Block.swift b/Privyet/Block.swift
index aee499e..b66ee74 100644
--- a/Privyet/Block.swift
+++ b/Privyet/Block.swift
@@ -37,6 +37,45 @@ enum BlockColor: Int, CustomStringConvertible, CaseIterable {
}
}
+// Rectangles in block coordinates
+class BlockRect {
+ var left: Int
+ var top: Int
+ var right: Int
+ var bottom: Int
+
+ init(left: Int, top: Int, right: Int, bottom: Int) {
+ self.left = left
+ self.top = top
+ self.right = right
+ self.bottom = bottom
+ }
+
+ convenience init(x: Int, y: Int) {
+ self.init(left: x, top: y, right: x, bottom: y)
+ }
+
+ var width: Int {
+ return right - left + 1
+ }
+
+ var height: Int {
+ return bottom - top + 1
+ }
+
+ static func +(lhs: BlockRect, rhs: BlockRect) -> BlockRect {
+ return BlockRect(left: min(lhs.left, rhs.left), top: min(lhs.top, rhs.top), right: max(lhs.right, rhs.right), bottom: max(lhs.bottom, rhs.bottom))
+ }
+
+ static func +=(lhs: BlockRect, rhs: BlockRect) -> BlockRect {
+ lhs.left = min(lhs.left, rhs.left)
+ lhs.top = min(lhs.top, rhs.top)
+ lhs.right = max(lhs.right, rhs.right)
+ lhs.bottom = max(lhs.bottom, rhs.bottom)
+ return lhs
+ }
+}
+
// Represents a single block in the game.
class Block: Hashable, CustomStringConvertible {
// Constants
diff --git a/Privyet/GameScene.swift b/Privyet/GameScene.swift
index 87d5ffc..69192f4 100644
--- a/Privyet/GameScene.swift
+++ b/Privyet/GameScene.swift
@@ -23,6 +23,7 @@ class GameScene: SKScene {
var lastTick: NSDate?
var scaleFactor: CGFloat!
+ var holdControlRect: CGRect!
var textureCache = Dictionary()
var textureAtlas: SKTextureAtlas?
@@ -62,6 +63,14 @@ class GameScene: SKScene {
shapeLayer.addChild(gameBoard)
gameLayer.addChild(shapeLayer)
+ // Calculate area of "hold" piece for hit-testing
+ let holdBBox = Shape.boundingBox(baseColumn: HoldColumn, baseRow: HoldRow)
+ let holdWidth = CGFloat(holdBBox.width) * BlockSize
+ let holdHeight = CGFloat(holdBBox.height) * BlockSize
+ let holdX = LayerPosition.x + (CGFloat(HoldColumn) * BlockSize)
+ let holdY = LayerPosition.y + (CGFloat(HoldRow) * BlockSize)
+ holdControlRect = CGRect(x: holdX * scaleFactor, y: holdY * scaleFactor, width: holdWidth * scaleFactor, height: holdHeight * scaleFactor)
+
// Set the theme music to play infinitely.
run(SKAction.repeatForever(SKAction.playSoundFileNamed("Sounds/theme.mp3", waitForCompletion: true)))
}
@@ -163,9 +172,19 @@ class GameScene: SKScene {
}
// Animate the removed lines "exploding" and the remaining blocks dropping into place.
- func animateCollapsingLines(linesToRemove: Array>, fallenBlocks: Array>, completion: @escaping () -> ()) {
+ func animateCollapsingLines(linesToRemove: Array>, fallenBlocks: Array>, fadingShapes: Array, completion: @escaping () -> ()) {
var longestDuration: TimeInterval = 0
+ // Animate the shapes fading out
+ for fadeShape in fadingShapes {
+ for block in fadeShape.blocks {
+ let sprite = block.sprite!
+ let fadeOutAction: SKAction = SKAction.fadeOut(withDuration: TimeInterval(0.1))
+ fadeOutAction.timingMode = .easeIn
+ sprite.run(SKAction.sequence([fadeOutAction, SKAction.removeFromParent()]))
+ }
+ }
+
// Animate the falling blocks dropping into place
for (columnIdx, column) in fallenBlocks.enumerated() {
for (blockIdx, block) in column.enumerated() {
@@ -212,4 +231,28 @@ class GameScene: SKScene {
}
run(SKAction.sequence([SKAction.wait(forDuration: longestDuration), SKAction.run(completion)]))
}
+
+ // Animate the multiple-line clearance banner.
+ func animateClearanceBanner(bannerName: String?) {
+ guard let name = bannerName else {
+ return
+ }
+ var texture = textureCache[name]
+ if texture == nil {
+ texture = textureAtlas?.textureNamed(name)
+ textureCache[name] = texture
+ }
+ let sprite = SKSpriteNode(texture: texture)
+
+ sprite.position = CGPoint(x: LayerPosition.x + (CGFloat(NumColumns / 2) * BlockSize), y: LayerPosition.y - (CGFloat(NumRows / 2) * BlockSize))
+ sprite.zPosition = 20
+ sprite.alpha = 0
+ shapeLayer.addChild(sprite)
+
+ // Animate a quick fade-in followed by a slow fade out
+ let fadeInAction: SKAction = SKAction.fadeAlpha(to: 0.9, duration: 0.05)
+ let fadeOutAction: SKAction = SKAction.fadeOut(withDuration: 1.5)
+ fadeOutAction.timingMode = .easeIn
+ sprite.run(SKAction.sequence([fadeInAction, fadeOutAction, SKAction.removeFromParent()]))
+ }
}
diff --git a/Privyet/GameViewController.swift b/Privyet/GameViewController.swift
index 8c9a0d8..c903ab2 100644
--- a/Privyet/GameViewController.swift
+++ b/Privyet/GameViewController.swift
@@ -10,6 +10,8 @@ import UIKit
import SpriteKit
import GameplayKit
+let ClearanceAnimation = [nil, "double", "triple", "privyet"]
+
class GameViewController: UIViewController, PrivyetDelegate, UIGestureRecognizerDelegate {
var scene: GameScene!
var privyet: Privyet!
@@ -31,7 +33,8 @@ class GameViewController: UIViewController, PrivyetDelegate, UIGestureRecognizer
scene.scaleMode = .aspectFill
bucketRect = scene.rectForBucket()
- print("Computed bucket rectangle = \(bucketRect!)")
+ //print("Computed bucket rectangle = \(bucketRect!)")
+ //print("Computed hold rectangle = \(scene.holdControlRect!)")
scene.tick = didTick
@@ -52,6 +55,8 @@ class GameViewController: UIViewController, PrivyetDelegate, UIGestureRecognizer
let tapLoc = sender.location(in: view)
if bucketRect.contains(tapLoc) {
privyet.rotateShape()
+ } else if scene.holdControlRect.contains(tapLoc) {
+ privyet.holdShape()
}
}
@@ -136,7 +141,7 @@ class GameViewController: UIViewController, PrivyetDelegate, UIGestureRecognizer
view.isUserInteractionEnabled = false
scene.stopTicking()
scene.playSound(sound: "Sounds/gameover.mp3")
- scene.animateCollapsingLines(linesToRemove: privyet.removeAllBlocks(), fallenBlocks: privyet.removeAllBlocks()) {
+ scene.animateCollapsingLines(linesToRemove: privyet.removeAllBlocks(), fallenBlocks: privyet.removeAllBlocks(), fadingShapes: privyet.removeAllShapes()) {
privyet.beginGame()
}
}
@@ -161,20 +166,52 @@ class GameViewController: UIViewController, PrivyetDelegate, UIGestureRecognizer
scene.playSound(sound: "Sounds/drop.mp3")
}
+ // Called when the current shape is put "on hold"
+ func gameShapePutOnHold(privyet: Privyet, firstHold: Bool) {
+ guard let held = privyet.heldShape else {
+ return
+ }
+ scene.stopTicking()
+ view.isUserInteractionEnabled = false
+ scene.redrawShape(shape: held) {
+ if (firstHold) {
+ self.nextShape() // act like the previous shape settled
+ } else {
+ self.scene.redrawShape(shape: privyet.fallingShape!) {
+ self.scene.startTicking()
+ self.view.isUserInteractionEnabled = true
+ }
+ }
+ }
+ scene.playSound(sound: "Sounds/zap.mp3")
+ }
+
+ // Internal: called when a shape lands, used to keep from showing the "special" banner more than once
+ func internalDidLand(privyet: Privyet, showBanner: Bool) {
+ let removedLines = privyet.removeCompletedLines()
+ let linesCount = removedLines.linesRemoved.count
+ if linesCount > 0 {
+ self.scoreLabel.text = "\(privyet.score)"
+ if (showBanner) {
+ scene.animateClearanceBanner(bannerName: ClearanceAnimation[linesCount - 1])
+ }
+ scene.animateCollapsingLines(linesToRemove: removedLines.linesRemoved, fallenBlocks: removedLines.fallenBlocks, fadingShapes: []) {
+ self.internalDidLand(privyet: privyet, showBanner: (linesCount <= 1) && showBanner)
+ }
+ self.scene.playSound(sound: "Sounds/bomb.mp3")
+ if showBanner && linesCount == 4 {
+ self.scene.playSound(sound: "Sounds/privyet.mp3")
+ }
+ } else {
+ nextShape()
+ }
+ }
+
// 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
- let removedLines = privyet.removeCompletedLines()
- if removedLines.linesRemoved.count > 0 {
- self.scoreLabel.text = "\(privyet.score)"
- scene.animateCollapsingLines(linesToRemove: removedLines.linesRemoved, fallenBlocks: removedLines.fallenBlocks) {
- self.gameShapeDidLand(privyet: privyet)
- }
- self.scene.playSound(sound: "Sounds/bomb.mp3")
- } else {
- nextShape()
- }
+ internalDidLand(privyet: privyet, showBanner: true)
}
// Called when a shape moves on the screen
diff --git a/Privyet/Privyet.swift b/Privyet/Privyet.swift
index 216835a..5c7f375 100644
--- a/Privyet/Privyet.swift
+++ b/Privyet/Privyet.swift
@@ -17,8 +17,12 @@ let StartingRow = 0
let PreviewColumn = 12
let PreviewRow = 3
-let PointsPerLine = 10
-let LevelThreshold = 500
+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
@@ -36,6 +40,9 @@ protocol PrivyetDelegate {
// 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)
}
@@ -44,6 +51,7 @@ class Privyet {
var bucket: Array2D
var nextShape: Shape?
var fallingShape: Shape?
+ var heldShape: Shape?
var delegate: PrivyetDelegate?
var score = 0
@@ -52,6 +60,7 @@ class Privyet {
init() {
fallingShape = nil
nextShape = nil
+ heldShape = nil
bucket = Array2D(columns: NumColumns, rows: NumRows)
}
@@ -154,7 +163,7 @@ class Privyet {
}
// Advance score and game level as appropriate
- let pointsEarned = removedLines.count * PointsPerLine * level
+ let pointsEarned = PointsPerLine[removedLines.count - 1] * max(level / 2, 1)
score += pointsEarned
if score >= level * LevelThreshold {
level += 1
@@ -201,6 +210,21 @@ class Privyet {
return allBlocks
}
+ // Removes all stored shapes from their slots (at end of game). Returns all the removed shapes.
+ func removeAllShapes() -> Array {
+ var allShapes = Array()
+ 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 {
@@ -272,4 +296,22 @@ class Privyet {
}
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)
+ }
}
diff --git a/Privyet/Shape.swift b/Privyet/Shape.swift
index 77ca24c..9456895 100644
--- a/Privyet/Shape.swift
+++ b/Privyet/Shape.swift
@@ -131,6 +131,21 @@ class Shape: Hashable, CustomStringConvertible {
}
}
+ 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 {
@@ -193,6 +208,17 @@ class Shape: Hashable, CustomStringConvertible {
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) {
@@ -212,4 +238,14 @@ class Shape: Hashable, CustomStringConvertible {
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)
+ }
}
diff --git a/Privyet/Sounds/privyet.mp3 b/Privyet/Sounds/privyet.mp3
new file mode 100755
index 0000000..7797ac4
Binary files /dev/null and b/Privyet/Sounds/privyet.mp3 differ
diff --git a/Privyet/Sounds/zap.mp3 b/Privyet/Sounds/zap.mp3
new file mode 100755
index 0000000..06b4115
Binary files /dev/null and b/Privyet/Sounds/zap.mp3 differ
diff --git a/Privyet/Sprites.atlas/double.png b/Privyet/Sprites.atlas/double.png
new file mode 100644
index 0000000..642023f
Binary files /dev/null and b/Privyet/Sprites.atlas/double.png differ
diff --git a/Privyet/Sprites.atlas/double@2x.png b/Privyet/Sprites.atlas/double@2x.png
new file mode 100644
index 0000000..180d901
Binary files /dev/null and b/Privyet/Sprites.atlas/double@2x.png differ
diff --git a/Privyet/Sprites.atlas/privyet.png b/Privyet/Sprites.atlas/privyet.png
new file mode 100644
index 0000000..1a77610
Binary files /dev/null and b/Privyet/Sprites.atlas/privyet.png differ
diff --git a/Privyet/Sprites.atlas/privyet@2x.png b/Privyet/Sprites.atlas/privyet@2x.png
new file mode 100644
index 0000000..fd6c0cf
Binary files /dev/null and b/Privyet/Sprites.atlas/privyet@2x.png differ
diff --git a/Privyet/Sprites.atlas/triple.png b/Privyet/Sprites.atlas/triple.png
new file mode 100644
index 0000000..20a96fd
Binary files /dev/null and b/Privyet/Sprites.atlas/triple.png differ
diff --git a/Privyet/Sprites.atlas/triple@2x.png b/Privyet/Sprites.atlas/triple@2x.png
new file mode 100644
index 0000000..ce4266c
Binary files /dev/null and b/Privyet/Sprites.atlas/triple@2x.png differ