Coding Challenge Postmortem And Analysis: Tetris in 8 Hours
You may have seen that yesterday I coded Tetris from scratch in 8 hours in C++ (source code and EXE links on that page). Today I shall reflect on the whys, the good and bad points and lessons learned.
I wanted to do something interesting for various selfish and non-selfish reasons:
- I’ve worked on plenty of bits of games. The last full game I wrote on my own from start to finish was a vertically scrolling shoot ’em up in ARM code when I was 14. That was 18 years ago.
- My old blog had 2 or 3 articles that were particularly popular talking points that drew in site traffic and reader interest, but they are rather old now. I wanted a new talking point.
- I wanted to find important bits that were missing in my Simple2D graphics wrapper for Direct2D and add them.
- I wanted to show beginning and intermediate programmers who are interested in writing video games that it is possible to create a video game in a short amount of time without a huge amount of resources.
- I’ve been out of work for a while so I wanted to test myself and push myself physically a little bit.
- I wanted to show future employers / clients that I can code well and fast at the same time.
Before The Session
I first thought about doing the challenge the previous evening. Prior to starting the code I had 3 hours of boring meetings to attend, and I used the time in my mind to try and solve a couple of design issues I anticipated.
In the real world, it’s a smart idea to look at other codebases to see how something is implemented. For this exercise I did not look at the source code of any other implementations of Tetris or related programs. I did check Tetris Battle on Facebook just to find the size of the grid (as part of the 8 hour coding time).
The main problem I imagined would be rotating the blocks. There are a whole variety of approaches to this: you can give each piece type a bounding box equivalent to its dimensions; you can store each piece as a series of relative X/Y co-ordinates of where the blocks should go to make up the piece; or you can take the quick and dirty approach I used: use the same bounding box (in this case 4×4) for every piece. Each method is a trade-off. Using the same size bounding box for every piece type makes collision detection harder, but makes rotating the pieces easier.
One of the problems with Tetris is that not all of the pieces use the same center of rotation. You cannot simply transpose an array’s x and y dimensions to perform a 90 degree rotation: in the scenario where each piece has a bounding box equivalent to its dimensions, this will cause the piece to always be rotated relative to its top-left corner and arbitrary corrections to its current position in the bucket will need to be made on a per piece type basis; on the other hand, in the scanerio where each piece has the same bounding box, transposing the array will cause asymmetrical rotation (it will always be around the centre point).
I used another quick and dirty solution to this: pre-define each possible rotation as additional bounding boxes to a piece type. Therefore, each piece type ended up as an array of 4 bounding boxes – one for each rotation. This allows precise control over each piece type’s center of rotation.
Fixed or modular? Procedural or object-oriented?
I always prefer to use a modular approach to code where possible. This includes two facets. First, abstracting behaviours – for example, a bucket or a piece should not be responsible for its own rendering as we want to be able to plug in different renderers (a 3D version or even an ASCII version for example), rather, classes that encapsulate game logic and game objects should only implement the logic and behaviour of those objects. Secondly, values should not be hard-coded. The resolution of the window should be able to be changed and everything should still render in the correct place, for example. Or, the bucket size could be changed, or the piece types, to provide variations on the original game.
Under tight deadlines often prevalent in the IT industry, there is often not time for modular code as it takes longer to produce. My initial intention was to make this version of Tetris neither modular nor object-oriented. For a simple application, both introduce unnecessary complexity and I wanted the source code to be understandable to beginners as much as humanly possible.
However, as work progressed, I quickly began to get frustrated with how messy the code was already becoming. I made a snap evaluation of whether I had enough remaining time to re-factor the code, which I did twice along the way to keep things neat and modular. This, of course, changes the design of the application without changing its functionality, so my progress was stalled a couple of times. In real world programming, such design changes mid-way through application development are usually a bad idea.
During The Session
Things started pretty well. It only took me 25 minutes to set up the graphics renderer and get a bucket on the screen.
With only a vague design outline in my head, not on paper and not in detail, I threw things in to the relevant places as I went and moved them around as necessary. I moved member variables in and out of public and private space and between classes quite often which was a minor irritation.
In the end there were four classes: TetrisPieceType (stored definitions of each of the 7 piece types; layout, rotations, colour and so on), TetrisBucket (contained the game board and collision detection logic for pieces already on the board), TetrisShape (two are used, storing the current and next shapes in play, with a TetrisPieceType as a member), and SimpleTetris (contained the renderer, application logic and game logic).
I started just with SimpleTetris and TetrisShape. Splitting the shapes up into TetrisPieceTypes made the code somewhat less messy but wasted a lot of time in re-factoring. By the time the program was complete, TetrisShape really doesn’t make sense as most of the properties and methods (collision detection against bucket edge and so on) only apply to the shape currently in play. The next shape could really have just been stored as a TetrisPieceType and the current shape information merged into SimpleTetris.
A lot of public members were used; it was for simplicity and speed but breaks encapsulation quite badly. Some methods are really in the wrong classes; I wanted to avoid callback function pointers and such. References or pointers to some objects are passed to other objects, which I had also wanted to avoid. On the whole, the object-oriented concept and overall structure of the work is correct, but a fair amount of re-factoring needs to be done to make it more sane.
In some cases I used pointers to avoid having to write copy constructors and assignment operator overloads, which would have introduced further boilerplate code and degrade readability.
I took advantage of Boost’s intrusive pointer template class to try and cut down on the COM object memory management needed by Direct2D paintbrushes and text layout descriptors. However it was certainly not well-tested, and there are probably memory leaks in the code.
I always knew that collision detection would be the biggest problem after piece rotation, but as it turns out, it was testing against the edges of the bucket that caused by far the most problems. By the time I wrote the collision detection for the pieces actually in the bucket, I had so many helper functions that it was really rather simple.
The next biggest problem was rendering text. DirectWrite has a needlessly over-complicated initialization and individual text item setup process and I wasted almost 90 minutes reading documentation and copying examples (modifying Simple2D, not the Tetris application – meaning I had to switch between solutions rapidly in Visual Studio) before I got any meaningful output. Converting between Unicode and multi-byte strings is a pain too.
At one point I was also stalled by a crash-on-exit bug which appeared only in the Release builds of the code. These are usually memory leaks and can be hard to track down. Fortunately I was lucky and managed to clear it up within 20 minutes or so.
Whenever I had to add something to Simple2D, I needed to make sure that it was generic and modular, easy to use and applicable to other applications besides Tetris. Therefore I was somewhat more careful and methodical with this code, and stopped to think about implementation choices. This slowed me down, but it is important to get re-usable functions right. The two main things I added were processing of keyboard input and basic text rendering. Both of these took longer than they needed to if I had just thrown them into Tetris itself, but in terms of long-term economic cost, it is better to implement them as well-defined library functions.
The Human Element
I woke up around 6:30am on the day I started to code. I left my house around 10:30am to go to some meetings and got back around 3pm. I suffer from ME / CFS so I was already tired and in pain by this point. I also hadn’t had anything to eat.
I coded and took cigarette breaks between each milestone I blogged about and once or twice more during long sections or parts where I had to stop and think, or do some research on the internet. I ate nothing and only drank cola. I had been in such a rush to start that I didn’t realise until midnight that I hadn’t even taken my shoes off. By the end I was sitting in the dark. It was so warm (with the windows open, despite being in Norway) that beads of sweat were coming off my forehead in the last 2 hours.
After I finished, I collapsed on the sofa for 15 minutes or so to recover, forced myself up, made dinner, watched a science documentary and went to bed around 1:15am.
This is not a healthy way to work. One should take regular breaks and step away from the computer. Even though I took regular breaks, I didn’t get up. I was still blogging or reading on ‘break’. Stepping away can actually be a great help even for just 20 minutes, as when you get crushed by an issue that you can’t seem to fix, coming back refreshed can often lead you to see the problem immediately.
I know that many developers work under these conditions, either out of passion and enthusiasm or job deadline necessity, but do try to take care of yourself a little. Stop and eat. Turn the lights on. Rest your eyes from the screen periodically.
When I originally said I would code Tetris in 8 hours, that is because I believed I would be able to do it in 5-6 – but, as we all know, developers are infamous for under-estimating the amount of time required to complete a coding task. Therefore when I make an estimate I think is believable, I usually double it when quoting an estimate to a client.
It is not a word of a lie that I finished with precisely 1 minute to spare. The last 15 minutes of coding were a furious blur of adding the scoring, leveling, complete line detection and removal and the game over state. I might have made sure it worked on one execution before I uploaded it and declared it complete. I didn’t check the leveling up at all (but found out it worked later, phew!). Of course I didn’t have time to do these last parts neatly: there are no score multipliers as the level increases, and the line check algorithm is much more inefficient than it needs to be, scanning over the whole bucket instead of just the lines where a new piece has been inserted.
The application is, in fact, peppered with unnecessary bits of code because time constraints prevented me from cleaning it up properly.
Time prohibited testing. There was of course, no unit testing and only a cursory “CTRL+F5… press some buttons… yep it works” style approach. Bad QA is one of the worst problems we have in the gaming industry, to the point where many gamers refer to newly released titles as betas. QA teams who test applications in the manner I just did should be promptly fired.
I had hoped that I would have time to add a menu and retry button, the Shift/Hold mechanic that modern Tetris games have (which lets you swap the next shape for the current shape once per move) and pretty up the graphics a little. I had predicted that having done all that extra topping, I would have about half an hour to spare. 2 hours into the challenge I had already realised that it was indeed going to be very close and I knew I would likely either be a few minutes under or a few minutes over. I had planned in the code for menus and such (there is a game state variable which has an “on menu screen” state) but did not get to filling in the blanks there, unfortunately.
As time went on, and at around 9:30pm – 10pm, I started to get rather tired and noticed the pace of my work slowing down. More breaks. More mistakes. Slower thought processes. I knew that it was going to be an uphill struggle to the finish as by then I had only just finished the bucket maintenance code and the goal posts were still a long way off. Blogging kept my spirits up, especially seeing the site stats that more and more people were following the article as the challenge went on (thank you all!).
The actual working game really did not come together until about 1 hour before the deadline, and of course I did not have time to play it. But the reward is always tremendous: as soon as I saw the pieces fall nicely on top of each other, rotate properly and fit together with correct collision detection, I knew I had a working baseline Tetris and it felt really good. Every programmer knows, this is the beauty of starting your own creation from scratch and seeing it come alive. It is the best feeling in software development. “Yes, I achieved something good today.”
A Note on Commenting
Despite the time pressure, if you browse the source code you will notice that I have commented it quite well. Commenting code is not a waste of time. It’s extremely important to help you and others remember how things work, and since I knew this code was going online and I wanted it to be accessible to beginning game programmers, commenting it well was especially high priority. I did not skimp on commenting.
To be honest I didn’t learn too much, but for the sake of completeness, had this been my first such escapade I would’ve learned something like the following:
- Design the structure of your code before you write it
- Unnecessary complexity is bad. K.I.S.S. principle applies.
- Design and implementation problems that you are not expecting will always come up, even when you plan ahead
- Plan ahead (when writing the implementation); this means, use hooks, placeholders and dummy variables to be utilized later if you anticipate needing to expand the functionality of a part of the code
- The project will always take way longer than you think, even if it is trivial for your skill level
- You will never be able to add in all the features you want before the deadline
- Spend more time on testing / QA
- Don’t code when you’re tired. You just write sloppy code and make lots of mistakes, and more slowly to boot.
I did learn a couple of new things though:
- Never do an 8 hour coding marathon when you’re tired and haven’t eaten all day to start with
- DirectWrite is a pain in the ass
Things I Didn’t Get Time To Include
- Shift / Hold mechanic of modern Tetris
- Score / bonus multipliers
- Silhouette of where piece will fall for quicker play (used in modern Tetris implementations)
- Background grid for quicker play
- Menu screen / Retry option at end
- High score table
These are things I would like to add that are fairly quick and painless. If I really wanted to push the boat out these items would take considerably longer:
- Network leaderboards
- Better graphics
I hope you had as much fun following along as I had doing the coding!
Want to challenge me? Write your challenge suggestions below!