Friday, December 13, 2013

Tetrocity

The source code for this project can be accessed here. This post is largely a collection of loosely related thoughts I have about the development process. Go play the game first! Do it!

Not too long ago I was trudging through one of my online classes when the instructor mentioned something interesting. He brought up the idea of an expansion of Tetris: instead of game pieces (Tetriminoes) with 4 blocks… why not 5!? I was more intrigued that I had never thought of this before, considering the dozens of hours I wasted becoming mediocre at Tetris With Friends. I had made small command-line games before, but nothing really substantial and, you know, actually fun.

So, I set off. Like every OOP project anyone has ever embarked on, I was set on making it as clean, organized, encapsulated, modular, blah blah blah as possible. Surprisingly, I actually managed to succeed, for the most part. I spent a huge amount of time with Microsoft Word, planning my objects and their roles, game features and mechanics, before writing a single line of code.

It was a pretty huge revelation in my development as a programmer to see just how much this helped. It’s easy to get so excited about your ideas that you jump straight in to the code and make up everything as you go along. Almost every time I ever did that, though, it would turn messy pretty fast. Didn’t see that problem coming? Just add a method to this class… I’ll mark it private so nobody will see how badly it doesn’t belong there. That kind of thing adds up fast. I would at this point like to give a shout-out to any recruiter that may be reading this blog post. Please ignore this paragraph.

This necessity is one of the biggest disadvantages and advantages of OOP in general. In functional programming, it’s much easier to implement the functions you need as you go along. Keeping them organized is still important, but not nearly to the degree of OOP. Developing a system in a language like Java requires a lot more forethought. If you put in that initial effort, though, the payoff is massive.

Anyway, after a few weeks of playing around with the idea on and off, getting it sorted out in my head and on paper, I was ready to embark. First things first: more administration. I began by creating every single class that I had anticipated needing, and writing full documentation for each one. This is really just a continuation of the idea from before, but a hell of a lot more boring. I felt it was a lot like eating a salad. Immensely painful and soul rendering, but you walk away feeling better about yourself.

One of the biggest challenges I faced was designing the game in such a way that it would be easy to implement new features down the line. Despite the planning, I really didn’t know what would set my version of Tetris apart from the hordes of Tetris clones already out there.

I began by developing a small set of “fundamental truths” about my game’s mechanics. Really basic stuff that you would expect from any Tetris adaptation: pieces always fall south, the game is over when the board fills, etc. Everything else was completely variable. Tetrimino size? Needs to be potentially infinite. The number of Tetriminoes falling at once? Needs to be potentially infinite. Board size? Needs to be potentially infinite. You see where I’m going with this.

This is where I see a lot of code fail. It’s absolutely possible to write Tetris in a few hundred lines of code, with full GUI support and everything. However, this approach is too static in nature. The code fulfills a very specific purpose: nothing more, nothing less. Avoiding this is a critical aspect of good code. Good code is a brick in a building. You shouldn’t have to demolish the entire thing to add a new floor.

***

Enough preaching! I won’t explain the details of the code or anything. That would be tedious and you can see all of the code on the GitHub repo (it's pretty well documented. Start with the model package if you're interested in learning the game's structure). Instead, I’ll talk about some important things I learned, and then present some interesting problems I ran into, as well as how I solved them.

My first vision for Tetrocity was that every game would be highly customizable. I would begin by asking the player for information such as the range of Tetrimino lengths (i.e. the number of blocks that make up the piece), and the game would begin with those parameters. This was going to be a real selling point. “Play with huge pieces! Infinite possibilities!” I was going to make my game capable of producing infinite-length Tetrimino pieces anyway, so may as well give the option, right?

The first harsh realization I had was there was a huge difference between novelty and genuine entertainment. This is what it looks like when you ask for length 20 Tetriminoes. 


It’s definitely interesting to look at, but you certainly wouldn’t want to play a game like that. It would be impossible. In fact, even length 7 pieces are way too hard to use. Here’s my friend’s best attempt at playing with them (he could barely clear a single line).


If these lengths are impossible to play with, why would anybody want to? After the novelty of seeing them for the first time, there really isn’t much appeal. 

The more I thought about it, the more I began to realize that giving the player all of that control was not a good idea. How would they know what a realistic range of lengths are? Who wants to spend all of that time figuring things out before being able to enjoy the game? On top of that, my own experience with videogames lead me to realize that players would rather overcome a challenge set out for them than overcome one they set out for themselves. By taking control of the course of the game, I could much more easily control things like the difficulty curve, and high scores would have actual meaning. Tetrocity went from fully customizable to “Press start to play”.

I ended up limiting the range of Tetriminoes ever produced in a game to a minimum of 3 and a maximum of 6. It was somewhat disappointing that I wouldn’t be taking full advantage of my infinite length Tetrimino-producing object, but it was a necessary sacrifice of ego.

***

Tetriminoes are randomly constructed in a pretty straightforward way. You begin with an empty grid, place a single block in the center, and then randomly choose grid coordinates that share an edge with a block until you run out of blocks to place. This seems straightforward enough, but ended up posing a lot of problems. First off, this method does not produce every shape with equal probability.

To see this, let’s consider the original set of length-4 Tetriminoes being randomly constructed with this method. Is a line piece just a likely to be constructed as a square piece? Well, there’s only one way to construct a square piece. It looks the exact same regardless of how it’s rotated. A line piece, however, can be constructed two ways: vertically, or horizontally. These two orientations are produced via an entirely different set of coordinates, but any player would consider them equivalent. The more rotational symmetry a shape has, the less likely it is to be produced.

The first question we ask is: is this really a problem? Is the effect noticeable? As it turns out, not only is it noticeable, but it’s also somewhat desirable. In a very hand-wavy sense, the more rotational symmetry a piece has, the less likely we’ll be able to find a place for it, since you can’t rotate it to fit various openings. This is a trivial concern for length-4 pieces, but would be a huge issue for length-5 pieces. I would argue that the game would be way too hard to play with length-5 pieces were they all produced with equal probability.

In true hacker fashion, we’ve turned a bug into a feature! Except for one issue: line pieces have relatively low rotational symmetry. When playing the game, I found that I would go ridiculously long periods of time without ever seeing a line piece. As any player of Tetris knows, they’re critical. Since they have such special meaning, it was necessary to implement some way to increase the probability they would appear.

The way Tetris decides what pieces to give you is actually pretty cool. It begins with a set of all 7 length-4 pieces, and shuffles them randomly. It then feeds you pieces from that set. As such, we expect a line piece to appear once out of every 7.

To mimic this behavior in Tetrocity, I had to implement a probabilistic model, since piece shapes are not stored in software. I made the Tetrimino-production class return random shapes with a certain probability, and straight-line pieces otherwise. This way, we can control the expected number of straight-line pieces fairly easily.

***

An interesting bug I ran into with my Tetrimino-production class TetriminoFactory had to do with Java’s Random class. We’re all guilty of mentally blanking the “pseudo” in “pseudorandom” from time to time, but after the several hours of grueling debugging that resulted from that mindset, I probably won’t make that mistake again.

The bug presented itself as pretty easy to fix. I noticed it when testing TetriminoFacotry’s random shape generation. Regardless of what range of lengths I would give it, it would always give me length-4 pieces. Okay, so the method probably isn’t receiving the correct range information, right? Nope, that was fine. I managed to narrow it down to single call to my Random object’s nextInt() method. Absolutely everything up to that point was working as intended, and everything after it was not. The nextInt() method would return the same number every time. Definitely not random, although still technically pseudorandom I guess.

I isolated the relevant code and began testing, and could not replicate the problem. It seemed like magic. Every relevant parameter was recreated, yet the bug only occurred when the code existed in the Tetrimino-production class. I even tracked down the source code and learned the underlying algorithm in an attempt to figure out why it was occurring. No luck.

Eventually desperation set in and I began to just randomly tweak parameters in some vain hope of something happening. Surprisingly enough, it worked! Apparently, the bug only occurs when the requested range is a power of 2. That was not something I replicated in my isolated code. In my original test code, the range of Tetrimino lengths was 3 to 5. 5 – 3 = 2 = 21.

Why is there such an arbitrary shortcoming in Java’s Random class? I have no idea!
To this day I have no clue why this happens. Regardless of the seed you give it, Java’s random object will return the same value if the range is a power of 2 (it actually begins exhibiting random behavior again after the 15th or so call). Weird.

***

Another interesting challenge was making the program user friendly. Anybody who makes software in industry is well aware of the importance of this, but Tetrocity was the first time I've ever really had to take that into account. 

Tetrocity isn't a particularly complex or deep game. It isn't World of Warcraft, where things like complicated UIs are a justified necessity. Tetrocity is a relatively simple, 2D game. Probably even a mini-game by today's standards. As such, it was very important to convey the new features of the game in as simple a fashion as possible. 

This was pretty easy for the most part, thanks to the success of Tetris. Most people seemed to be aware of the rules and such, so I had to do very little work in explaining that aspect of it. Controls were also borrowed from Tetris With Friends, which makes it intuitive for any Facebook-savvy players. 

The biggest challenge was introducing the ability system. It was certainly new to most players, yet it needed to remain somewhat mysterious in order to motivate people to continue playing. Apart from a single line on the "Controls" welcoming screen, there wasn't even a good way to introduce the concept of abilities without shoving it in the user's face.

Pictures proved to be the best way to get around this. I found a set of free vector icons available here, and sifted through them to find the ones that best described an ability I wanted to implement. By sticking huge icons right off the side of the main grid, I wanted the player to be able to quickly glance at the sidebar, and understand what the ability did. 

I'm not sure I entirely succeeded in that regard. I've found that new players generally don't even notice when a new ability is unlocked, let alone what it does. The main game is simply too intense; your eyes are completely locked on the grid. After a couple of tries people begin to notice the abilities, but I wanted people to know it was there immediately. 

This was partly what motivated me to add sound effects, the majority of which I found here. That way, I could add a little sound that would play every time an ability became available, so that the player wouldn't have to take their eyes off the screen. Again, this was less successful than I had hoped for the reason. Players are just too focused on the game to notice the sound. 

This seemed to work fairly well, but at the end of the day I think a "How to Play" screen is really the best solution. I haven't implemented it yet, nor am I sure if I will, but it's definitely something to keep in mind when your game is too fast paced for the player to really "figure things out". 

*** 

Another important factor in making the game 'easy to use' was the fluidity of the controls. This was actually pretty interesting, because it was the first time that the behavior of my methods became fairly "hazy". I began by developing a very strict definition of what it means to move and rotate a piece. If it was found that these transformations could not occur, they wouldn't. Period. 

From a "contract" point of view everything was fine, but what I found is that the controls became extremely rigid. For example, if a line piece were flush with the right edge of the board and you tried to rotate it, nothing would happen. Logically, that rotation would cause the line piece to protrude off of the board. In fact, the line piece didn't even have to be flush. There could be several spaces in between the line piece and the edge of the board and the rotation would still fail. 

It simply didn't feel right. Again, the game is intense. It simply isn't realistic to expect the player to be that meticulous about every single rotation. When a rotation like that fails, it's incredibly frustrating and confusing. This is was motivated me to begin coding my controls to be "error tolerant". In this example, I made it so that when a failed rotation is detected, the game will automatically try to shift the piece over to the left, and check again. This way, when we encounter that same rotation failure from before, the line piece will be "pushed" left, and rotation will succeed.

Of course, you have to be incredibly careful about just how error tolerant the controls are. How far should you try shifting if left before giving up? Does it have to be the edge of the board; what about another piece? What if it gets shifted into another piece? Once you work out all of the kinks, it feels much better.

***


These are some of the things I learned while making Tetrocity. At the end of the day, I’m extremely proud of how it turned out. It’s the most substantial amount of code I’ve written personally from scratch. The game runs smoothly and is fun as hell. I should go back to studying for finals now, but I’ll probably edit this post as more things come to mind. In the mean time feel free to give the game a go. My high score is about 300000; see if you can beat it!