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 4×4 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.

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. 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