The Bitwise Challenge Post-Mortem, Part 3

This is part 3 of the post-mortem I’m doing for the Bitwise Challenge, in which I made a Tetris-like game called Loftis as an Addon in WildStar in four hours. Here are links to parts one and two.

Just a quick aside: I’ve now spent more time writing the post-mortem than the challenge itself, and I’m only about halfway done. Okay, moving on.

By the end of part two, we had an object representing the game field, a way to make new blocks and to rotate them, and I had described the process by which I would simulate rising blocks. Now let’s look at the logic for whether a proposed position of a block is a valid location. This is essentially the meat of Loftis, right here:

function Loftis:IsPositionValid(tBlock, iRow, iColumn)
 local tGame = self.tGame
 for y = 1,4 do
   for x = 1,4 do
     if tBlock[y][x] then
       if iRow + y - 1 < 1 or iRow + y - 1 > 28 or iColumn + x - 1 < 1 or iColumn + x - 1 > 10 then
         return false
       end
       if tGame.arField[iRow + y - 1][iColumn + x - 1] then
         return false
       end
    end
   end
 end
 return true
end

This is pretty straightforward, but it is made a little less clear because of Lua's one-based nature, which is something that I curse on an almost-daily basis. Let's look at what's going on, because this is the function that really determines what happens next at any given point.

Basically what I'm doing here is testing the passed block (tBlock), which is represented by a 4x4 array of boolean values, and testing whether the position passed in (iRow, iColumn) is valid. The way to test that is simple: for each of those sixteen booleans that are true, if the corresponding block in the game's field of blocks is also true, the block cannot be placed there. Also, if the corresponding "on" block would be outside the game field, (x < 1 or x > 10) then the block cannot be placed in the proposed position.

IsPositionValid is called 6 times in Loftis. The first one I wrote is right after creating a new block:

if not self:IsPositionValid(tBlock, 24, 5) then
  self:DoGameOver()
end
tGame.tBlock = tBlock
tGame.iColumn = 4
tGame.iRow = 24

If it is time to create a new block, and a new block cannot be placed, it's because the field has been filled up with blocks, so in that case, the game is over. And hey look! Remember when I talked about magic numbers before? For those who aren't familiar with the programmer lingo, a "magic number" is a hard-coded literal constant. In this case 24, 5, 24, and.... 4! If you are understanding the code I am writing here, you may have just realized I have a bug in Loftis, a bug I just found in this post-mortem!

Even during the challenge I knew I shouldn't have been using magic numbers. I tweeted this at the 2:25 mark:

This rather conveniently illustrates the exact danger in writing magic numbers. Obviously I had started the new block in column 5, and then realized it needs to be in column 4 instead, but since I used literal 5s when writing the code, changing it required me to change all the instances of 5 when I was referring to the starting column. But I clearly forgot to do that in the call to IsPositionValid. If I had taken the time to create a constant like this:

local kStartCol = 4
local kStartRow = 24

and written the code like this:

if not self:IsPositionValid(tBlock, kStartRow, kStartCol) then
  self:DoGameOver()
end
tGame.tBlock = tBlock
tGame.iColumn = kStartCol
tGame.iRow = kStartRow

There would have been no bug when I decided that starting column needed to be 4 instead of 5. I really couldn't have found a better illustration as to why magic numbers are so bad. Don't use them! Even in 4-hour coding challenges, in case you are ever in one...

Let's move on to the next usage.

if tGame.fProgress >= 1.0 then
 if tGame.iRow > 1 and self:IsPositionValid(tGame.tBlock, tGame.iRow - 1, tGame.iColumn) then
   tGame.fProgress = 0
   tGame.iRow = tGame.iRow - 1
 else
   self:PlaceBlock()
   return
 end
end

This second use of IsPositionValid is called after the progress for the current block has exceeded 1.0. When it does so, we check to see if it is still possible to fall or if it has come to rest on another (higher) block. If the new position check fails, it's time to place the block. If it succeeds, we update our new row (subtracting one) and reset progress to be 0.0.

After I had written this code it was possible for me to start a game and watch blocks rise to the top, collide, and get placed. I sent out this tweet:

After that there were two calls to handle attempts to move left and right. These were very straightforward:

if self:IsPositionValid(tGame.tBlock, tGame.iRow, tGame.iColumn - 1) then
  tGame.iColumn = tGame.iColumn - 1
end
if self:IsPositionValid(tGame.tBlock, tGame.iRow, tGame.iColumn + 1) then
  tGame.iColumn = tGame.iColumn + 1
end

Check the space on your left or right. Can we move there? If so, cool, do it. If not, do nothing. In a more polished game, we might play a buzz sound here when we fail, but not in a 4-hour challenge.

Finally the last two calls to IsPositionValid are used when attempting to rotate the current block. Remember how I said I rewrote RotateBlockLeft and RotateBlockRight to return a new block instead of changing the current block in place? This is why. In Tetris Loftis, it's possible that your block could be in a position where attempting to rotate would cause the block to collide with the field. This is how I handle it:

function Loftis:OnRotateLeft( wndHandler, wndControl, eMouseButton )
 local tGame = self.tGame
 local tNewBlock = RotateBlockLeft(tGame.tBlock)
 if self:IsPositionValid(tNewBlock, tGame.iRow, tGame.iColumn) then
   tGame.tBlock = tNewBlock
 end
 self:UpdateBlockPixies()
end

I call my rotate function which returns a rotated copy of my current block. I then check to see if that new block's position/orientation is valid. If it is, I assign it to be the current block. (The old current block loses its reference and is collected here. Thanks garbage collector!) If the new block is not valid, I do nothing and it is collected when I exit the function.

Okay, that does it for today. I should be able to finish this thing up tomorrow.

Bitwise out.

About Wiesman

Husband, father, video game developer, liberal, and perpetual Underdog.
This entry was posted in Videogames, WildStar and tagged , , , . Bookmark the permalink.

2 Responses to The Bitwise Challenge Post-Mortem, Part 3

  1. Pingback: The Bitwise Challenge Post-Mortem, Part 4 | Some Disagree

  2. Hi !

    Nice post about wildstar add-on development ! It’s hard to find information about add-on dev without having a beta access and this post is welcome :)
    I know it was a 4 hours coding challenge but here is a trick used in board games that’s fun to know : working with a larger board than the real one to avoid “out of board” tests. You just fill the case which are out of the board with blocks and the collision function will do all the work freely for you :)

    Anyway, thanks for sharing you experience with theses tools, i can’t wait to get my hands over :)

    Regards,
    Julien

Disagree?

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s