So, I've been addicted to Tetris for the last week or so. Last night I decided to do something else, and when I came across the Phoenix LiveView contest, I thought: why not try and make Tetris? #myelixirstatus
I was surprised by how far I got. I have watched videos of people coding games before, have been coding myself for years and years, but somehow games felt out of my league. But here, after a (long) evening, I got a game! I should've streamed it myself.

I mean I'm standing of shoulders of giants. I got so much for free from Erlang/Elixir, LiveView to hook it up, @chris_mccord's Snake example gave me the basic idea of using just <div>
's for blocks, the browser is doing the drawing and key-repeats for me.
But it felt magical, once the game got playable. I was hooked again to my own creation. And so many "features" from NES Tetris like sliding, tucking and spinning "just worked" in my first implementation (probably because I was close to their implementation).
It all started with this HTML based board, using flexbox
to to the hard works of blocks for me. I later wrapped the board in a Game-struct, and added bindings for the scores.

How to get a game loop in Elixir? Just send yourself a message after, say, 500ms. The LiveView component has a handle_info/2
, which queues the next tick and moves the current Tetromino down.

Thanks to the phx-keydown="keydown"
in the template, we get messages in handle_event/3
for each key. Just delegating to my Game module. And yes, I use Vim, so I need those H
, J
and L
. (Dropping with K
is not implemented.)

My Game.move/2
got a bit complicated, some refactoring is in place. But I started with a Tetromino struct, with :x
and :y
keys and a :color
. To move it down, you paint the current :x
and :y
:white
, and then paint :y + 1
to the :color
. Code below reduces for all four points:

To obtain all the points I got this wonderful Tetromino.points/1
function. It's ugly and was a pain to write out and get right, but it works like a charm.

Colors and rotations work similar, with Tetromino.rotate/1
. (color_for_type/1
gets called in new/1
, and it never changes once it is created.)

Then the messy Game.move/1
: can you move down? Move down. Otherwise, call Board.clear_lines/1
to remove full lines and obtain the score, get a new random Tetromino and put it on the board. The game-over handling is a bit buggy still, I had only one evening.

Clearing the board is simple: reject rows of which all cells are not empty and use counts to add new rows to the top. The Board.color_at/2
gives the color for that particular cell. The guards against negative numbers prevent List.pop_at/2
to get items from the back of the list :)

I think that's mostly it! Again, I wish I had streamed it, it was very weird watching myself doing it, but also very rewarding. Keep coding, people, you can do this too!