As I mentioned on my previous post, Swiftris’ gravity is different than the http://en.wikipedia.org/wiki/Tetris#Gravity and that annoys anyone who played Tetris enough.
On the original Switris source, the gravity/collapsing behavior is implemented in ‘removeCompletedLines()’ which is called (weirdly) from within GameViewController’s implementation of the ‘gameShapeDidLand()’. Let’s start by just reimplementing removeCompletedLines() without changing its interface.
Here is the original implementation:
[sourcecode language=”javascript” title=”Swiftris.swift excerpt at the original removeCompletedLines()”]
func removeCompletedLines() -> (linesRemoved: Array<Array<Block>>, fallenBlocks: Array<Array<Block>>) {
var removedLines = Array<Array<Block>>()
for var row = (NumRows – 1) ; row > 0 ; row– {
var rowOfBlocks = Array<Block>()
for column in 0..<NumColumns {
//if block not null
if let block = blockArray[column, row] {
//add to the rowOfBlocks
rowOfBlocks.append(block)
}
}
//if amount of blocks equals the number of Columns
if rowOfBlocks.count == NumColumns {
removedLines.append(rowOfBlocks)
for block in rowOfBlocks {
blockArray[block.col, block.row] = nil
}
}
}
if removedLines.count == 0 {
return ([],[])
}
let pointsEarned = removedLines.count * PointsPerLine * level
score += pointsEarned
if score >= level * LevelThreshold {
level += 1
delegate?.gameDidLevelUp(self)
}
var fallenBlocks = Array<Array<Block>>()
for column in 0..<NumColumns {
var fallenBlocksArray = Array<Block>()
for var row = (removedLines[0][0]).row – 1; row > 0; row– {
if let block = blockArray[column, row] {
var newRow = row
//falls independently?
while (newRow < (NumRows-1) && blockArray[column, newRow+1] == nil) {
newRow++
}
block.row = newRow
blockArray[column, row] = nil
blockArray[column, newRow] = block
fallenBlocksArray.append(block)
}
}
if fallenBlocksArray.count > 0 {
fallenBlocks.append(fallenBlocksArray)
}
}
return (removedLines, fallenBlocks)
}
[/sourcecode]
and this is my implementation (different name but same signatures so you can just replace in GameViewController):
[sourcecode language=”javascript” title=”A redesigned gravity/collapsing lines with same signature”]
func removeFullLines() -> (linesRemoved: Array<Array<Block>>, fallenBlocks: Array<Array<Block>>) {
// Just so we can mock the original signature
var externalRemovedBlocks = Array<Array<Block>>()
var removedBlocks = Array<Block>()
// Just so we can mock the original signature
var externalFallenBlocks = Array<Array<Block>>()
var fallenBlocks = Array<Block>()
//Iterating from bottom of the Tetris grid; this is the summary of that loop:
// -> dropHeight = 0 (number of dropped rows to that point in the loop)
// for rows, bottom up
// if row is full
// -> save that row’s blocks to removedBlocks
// -> set nil for the whole row
// -> incread dropHeight
// else (row is not full)
// if dropHeight > 0
// -> lower that row by dropHeight rows
var dropHeight:Int = 0
for var row = (NumRows – 1) ; row > 0 ; row– {
if isRowFilled(row) {
for column in 0..<NumColumns {
//save row (we know its not null because its filled)
removedBlocks.append(blockArray[column, row]!)
//set nil
blockArray[column, row] = nil
}
//increase drop height
dropHeight += 1
} else {
if dropHeight > 0 {
//lower row by drop height
for column in 0..<NumColumns {
if let block = blockArray[column, row] {
block.row += dropHeight
blockArray[column, row] = nil
blockArray[column, block.row] = block
fallenBlocks.append(block)
}
}
}
}
}
externalRemovedBlocks.append(removedBlocks)
externalFallenBlocks.append(fallenBlocks)
if dropHeight == 0 {
return ([],[])
}
let pointsEarned = dropHeight * PointsPerLine * level
score += pointsEarned
if score >= level * LevelThreshold {
level += 1
delegate?.gameDidLevelUp(self)
}
return (externalRemovedBlocks, externalFallenBlocks)
}
[/sourcecode]
As you can see, the original Gravity is a lot simpler and makes the game harder & more fun. Also, note that ‘removeFullLines()’ has unnecessary complexity on its return values (Array of Arrays instead of simple Arrays) – we’ll fix that later.
And there’s a little problem with simply replacing ‘removeCompletedLines()’ by ‘removeFullLines()’; removeFullLines returns all fallen blocks in the same/first subarray of fallenBlocks; where the original one would return on subarray for each column. This causes a problem since the blockIdx within each of the subarrays is used to time a animation on the GameScene side so everything will work fine but the game will lock for a few seconds every time you fill a line (linear to the full amount of blocks falling), I plan to provide a way better fix for this later but for now this solves the immediate problem:
[sourcecode language=”javascript” title=”GameScene.swift before”]
// …
func collapsingLines(linesToRemove: Array<Array<Block>>, fallenBlocks: Array<Array<Block>>, completion:() -> ()) {
// …
// …
let delay = (NSTimeInterval(columnIdx) * 0.05) + (NSTimeInterval(blockIdx) * 0.05)
/// …
}
[/sourcecode]
To this:
[sourcecode language=”javascript” title=”GameScene.swift after”]
// …
func collapsingLines(linesToRemove: Array<Array<Block>>, fallenBlocks: Array<Array<Block>>, completion:() -> ()) {
// …
// …
let delay = (NSTimeInterval(columnIdx) * 0.05) + (NSTimeInterval(1) * 0.05)
/// …
}
[/sourcecode]
Now the game works as it should but the architecture of it is still a mess and all my fixes are basically hacks inside it. For my next post I’ll try to redesign/cleanup.. let’s hope for the best.