This week didn’t have much consistency. I did a lot of work on this game last Sunday, and then I was busy most of the week, and I barely touched the game. I finished it yesterday. I think this shows in the final product; last week I wrote about being glad of having a refactoring pass on the code, something which I didn’t manage to have this time.
I did make some progress though, I think. I had a conversation with a friend last week about
snake.py, and how I did at some points have interesting ideas which I ended up walking back because of the complexity it added to the code. I realised that was stupid, and that I know plenty of tricks to manage complexity in Python code; I just had this ‘everything in a single file’ constraint in my head, which didn’t make any sense.
So, with that constraint lifted, and some ideas about common things which I needed to implement in both Week 1, Week 2, and Week 3. So I began extracting some items into an
engine package. Right now these aren’t anything special; but there is a useful TwoDArray class which I think I’ll use a lot in the future.
Tetris is a game which I have played a lot; I had a fairly good idea before I began of the data structures behind Tetris, and how its implemented on the NES and the Gameboy. Tetris is interesting, because it has a sort of nested gameplay loop; the playfield advances continuously based on the level, but within that loop you can make multiple inputs and rotations.
I began by putting together an implementation of the data structures for the pieces; there is some interesting dynamics for how the irregular shapes rotate (not always around the pivot you’d expect), and I wanted to create something which was classic-Tetris accurate:
After this, I began placing the pieces in the playfield, establishing constraints on their movement (you can see a bug below where certain pieces can’t move across the whole X dimension due to how the pieces were modeled). So far, this was a really naive implementation:
Next, I began actually modelling the game state. The game is a 2d array of integers. I use certain integers for different tasks:
0 is empty space,
1 is a solid wall (ie. the edge of the playfield),
2 is a ‘settled’ block,
3 is an active block. The falling piece data is additively combined with the playfield for collision detection: so for example, if our active play piece
3 combines with the wall of the playfield
1, then we get
4, which is a collision, and the move can be rejected.
This additive model produces some interesting bugs, for example when you constantly add each game tick rather than correctly resetting state:
I’m pretty sure there are still bugs in this approach; occasionally due to how this collision is calculated, you can get a piece stuck in the wall with the right combination of input mashing and spins.
I’m reasonably happy with the end result; this is a minimum viable Tetris game, but it supports advanced movement (for example, you can properly T-spin if you’re fast enough), and the gameover state works similarly to the Gameboy where you see the blocks overwrite eachother for a few game ticks. This is pleasingly authentic.
Here’s the code.