Category: iOS


Swiftris – Fixing Gravity

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:

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)
}

and this is my implementation (different name but same signatures so you can just replace in GameViewController):

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)
    }

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:

// ...
    func collapsingLines(linesToRemove: Array<Array<Block>>, fallenBlocks: Array<Array<Block>>, completion:() -> ()) {
    // ...
    // ...
                let delay = (NSTimeInterval(columnIdx) * 0.05) + (NSTimeInterval(blockIdx) * 0.05)
    /// ...
}

To this:

// ...
    func collapsingLines(linesToRemove: Array<Array<Block>>, fallenBlocks: Array<Array<Block>>, completion:() -> ()) {
    // ...
    // ...
                let delay = (NSTimeInterval(columnIdx) * 0.05) + (NSTimeInterval(1) * 0.05)
    /// ...
}

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.

Swiftris – The last minute move

This post is of the Swiftris series so if you have no idea what I’m talking about, read this.

If you’ve played a lot of tetris in your life you understand that not all is over when one ‘tetrino’ touches the settled rubble at the bottom of the pit – when that happens you still have 1 ‘tick’ to move it sideways or even spin the piece if it fits! Let’s call that the ‘last move rule’. Swiftris ADDS code to prevent you from doing that – it prematurely settles the tetrino.

Somewhere in your Swiftris.swift file you’ll have something like:

    func letShapeFall() {
        if let shape = fallingShape {
            shape.lowerShapeByOneRow()
            if detectIllegalPlacement() {
                shape.raiseShapeByOneRow()
                if detectIllegalPlacement() {
                    endGame()
                } else {
                    settleShape()
                }
            } else {
                delegate?.gameShapeDidMove(self)
                if detectTouch() {
                    settleShape()
                }
            }
        }
    }

‘letShapeFall()’ is the one method executed over and over when a shape is falling and ‘detectIllegalPlacement()’ is the method that verifies either the board contains an illegal placement.
Additionally, it introduces the ‘detectTouch()’ which simply duplicates the functionality of ‘detectIllegalPlacement()’ but implemented separately.. for no apparent reason.

‘detectTouch()’ is only used to ‘settle’ a tetrino when it touches the rubble at the bottom of the pit. Basically, only used to ‘ruin’ the ‘last move’ rule. Suppose we force ‘detectTouch()’ to always return false, that way, the ‘life’ of that tetrino will be extended by one ‘tick’ cycle only because as soon as ‘letShapeFall()’ is called again at the next cycle ‘detectIllegalPlacement()’ will return true and effectively ‘settleShape()’. You see, the current implementation offers two different paths for the ‘settleShape()’ calls where logically they should happen under the same circumstances. By any standards that stinks a bit. So, let’s just get rid of it. Try changing your code to this:

    func letShapeFall() {
        if let shape = fallingShape {
            shape.lowerShapeByOneRow()
            if detectIllegalPlacement() {
                shape.raiseShapeByOneRow()
                if detectIllegalPlacement() {
                    endGame()
                } else {
                    settleShape()
                }
            } else {
                delegate?.gameShapeDidMove(self)

                // Comment out this.
                // if detectTouch() {
                //     settleShape()
                // }
            }
        }
    }

Try it out.. everything still works fine and now the life of the Tetrinos at their last hopeless moment of fall is a tiny bit extended! Yay!

Now, if you look around you’ll see that there’s a LOT of code that is there for nothing.. detectTouch() is never used, and so isn’t ‘bottomBlocksForOrientations’ or ‘bottomBlocks’ in Shape.swift and all it’s child (JShape, LineShape, LShape, SquareShape, SShape, TShape, ZShape). You can safely remove all that and it’s about 80 lines.. that serve no purpose but be buggy and confuse ppl 🙂

For example, some shape implementations become really simple and short. TShape for example:

class TShape:Shape {
    /* ... */

    override var blockRowColumnPositions: [Orientation: Array<(cDiff: Int, rDiff: Int)>] {
        return [
            Orientation.Zero:       [(1, 0), (0, 1), (1, 1), (2, 1)],
            Orientation.Ninety:     [(2, 1), (1, 0), (1, 1), (1, 2)],
            Orientation.OneEighty:  [(1, 2), (0, 1), (1, 1), (2, 1)],
            Orientation.TwoSeventy: [(0, 1), (1, 0), (1, 1), (1, 2)]
        ]
    }
}

Try out the game after the changes.. it all works fine.. I hope!

Another problem with Swiftris is that whenever you complete a line it treats all blocks above that line as independently free falling – that’s not how original Tetris works and it makes the game a heck easier than the original. In the original, the fallen blocks all fall as a single connected mass leaving whatever small holes you had below it untouched. Plus, the logic for the fallen blocks, something intrinsic to the game is written inside Swiftris.swift but it’s called from the GameViewController.swift.. a pretty awkward implementation of any Model-View-Controller or Model-View-ViewModel pattern they got in there – it works and all but.. My next post will get a bit into that.

I’ve done it.. curiosity killed the cat and I plunged into the iOS world

I’ve had A LOT of free time lately. So I’ve ticked all the top items on the list: I’ve been surfing a lot, I finished reading two books I was stuck on, I visited all coffee places within an 30 minutes walk from home that had anything more than 3 stars on yelp.. yeah.. done it all..

And I still had some free time and some spare cash.. Then someone gave me a used MacBook (Thanks Christopher :), wherever you are!) so I’ve finally paid to be a ‘apple developer’ just so I could code something and run it on my phone (well, it’s technically not my phone but that’s a longer story).

First questions I was faced with was Objective-C vs. Swift. Well.. anyone in their sane minds will stay away from Objective-C, so Swift it is.

After some useless samples/tutorials I found this one: https://www.bloc.io/tutorials/swiftris-build-your-first-ios-game-with-swift  it’s premise is perfect. Not too simple, not too complex. It starts at a really nice pace (explaining all that deserves explanation and nothing that is too obvious). Later on you can see the writer getting annoyed/tired and flying past stuff. Well, can’t blame him: most people give up half way through anyway/it’s not like you couldn’t just read the code FFS/it works FFS! Anyway, highly recommend it as a starting point – it has worked well for me so far!

The thing is, the final game you develop has some things that really annoy me (code and functionality wise) so I’ll try to tip in my 20¢ on the next few posts.. 😀I'm Interested, go on!