Making Tetris in Rust and Wasm (Part 1)
The Tetris Video Framework (Part 3)
Architecting the Tetris Video Framework
Hello and welcome one and all to Robert Snakard’s World Famous Tetris Video Framework! Step right up and enjoy this Rust library of epic proportions! Only here will you see Results tried and returned, Options twisted so tightly unwraps are used without measure, and who could forget the illustrious Piece museum!
In our main file you’ll find the request_animation_frame loop. Trust me, this little bit of recursion is one you won’t want to miss. You’ll be coming back again and again and again and again. To the left you’ll find our App module which performs unwrap after unwrap to create a beautiful CanvasRenderingContext and to the right is the Game module. While its small we promise it will grow bigger in size as we add more rules to the game.
So please folks. Sit back, relax, and enjoy this edu-tainment today!
1 - Creating a new project
We’re finally starting our project now. We’ve done the background research. We know how to write HTML, we know how to write Rust, now let’s make something useful.
You’ve done this before. Create a new Cargo project, initialize your git repository (if things aren’t working double check your ssh keys!), and push your first commit to master. Oh, and install wasm-pack:
We’ll use this, combined with
wasm-bindgen, to make importing and exporting
Now lets open
We want to add two dependencies to our project. The
wasm-bindgen crate and the
web-sys crate. Wasm-bindgen is
This is our
src/lib.rs. The blue
dependencies, the purple
sets up our DOM, and the green calls
fillRect. Notice the
#[wasm_bindgen(start)]. This lets us compile with wasm-pack instead of cargo
so we can import our package as an ES6 module.
Wasm-pack compiles our package into the pkg directory where we have a host of files.
The only important ones are the
.js one and
.wasm one. You
use them like so:
tetris.js is glue code. It includes code to access memory, pass strings, and
tetris_bg.wasm is our binary. We import the glue
code so it’s accessible for everyone, then init our binary. When we call
init will call whatever function we labelled as
. This is how we will enter our webapp.
Now, let’s see if it runs.
2 - Laying A Foundation
So what does a game of Tetris require? We need some way to draw pieces, we need some
way to create pieces, we need some way to move pieces, and we need some way to detect
collisions. Today we’re focusing on the “draw pieces” side of things but we’ll add everything
to the blueprint.
And then focus on
You can do the folding with zim fold,
3 - Beginning Construction
Now we ask ourselves a question. Where do we start drawing a piece? In this case we have some drawing functionality already, so let’s start with what we already have.
I’ve moved our
hello_world code into
draw_piece() and added
comments. I like to organize my code into lots of small messes so refactoring is easier.
Ok. But we still need to draw a piece, and drawing the piece is a very key part of Tetris.
I’ll admit, I actually got a little bit of analysis paralysis at this part. If we want
to draw a piece we have to know what a piece is. And depending on how you look at it a piece
can be a lot of different things. Now I did
research on this part trying to
find the best way to represent a piece. I settled on an
Which is great and all, but using an array means we have to traverse it. And traversing arrays is an entire category of programming all by itself. We’ll just use a for loop because it is the most straight forward to understand.
ARRAYS START AT 0!!
Whew, that's a mouthful!
We’ve got our for loop, which starts at 0 (the first item in the list), and ends one before the length, which leaves us with a bit of math.
Since you’ve played around with fillRect you’ll know we have to set the location of
the square and then the size. The size can be pretty much anything, set it first though
cause we also use it in our location. To set the location, take a look at the way we
0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
16 spots: 4 rows, 4 in a row. Now
4x4=16 so we know to
go backwards we’ll need to divide.
Let’s checkout spot number 6. We start at 0, count left to right, and hit this one. Row 1, Index 2.
0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
What’s 6 divided by 4? 1 remainder 2.
You can try the math for any index in the array, 3/4 = 0R3, 13/4 = 3R1. As long as
you start counting at 0 the math will always work out to be your graphical coordinates.
The xcoord uses
remainder operator, and
the ycoord uses
operator. It’s one of those beautiful coincidences in mathematics.
4 - Pack up the Tent
The whole point of architecting and modularizing and organizing your code is to make
it portable. We want to organize the code into bite-sized chunks so we can create a sane
API and call them where they’re needed. We currently have a
function. When I call it I’d like to specify which piece I’m drawing, how it’s rotated,
and where on the tetris board it’s located, so let’s do that:
You'll notice I changed the function signature, and we now get the context outside of the `draw_piece()` function. These are done for two different reasons. I want my function to take shape, rotation, and coordinate arguments because we're going to be calling it to draw every piece, no matter its shape, rotation, and location. I want my function to take a context argument because we're going to be calling this function a lot. We don't want to create and destroy the context every time we have to redraw a piece, it's better to create it once and pass it around as a permanent resource.
Create another file named
I won’t make you define that whole thing, you can find the source on my github. What
we’ve done though is define every piece and each of their rotations in a file named
src/pieces.rs. Now we can import them just like we did with our dependencies.
And then use our function parameters.
And while we’re at it, let’s split our code into
And reorganize the whole thing altogether!
So what did we do?
We reorganized. I decided to split the game into two parts. The
which consists of everything we do when playing the game, and the
enums to the
pieces file, they just seemed to fit there best.
A lot of organizing is arbitrary, like I could have put
app. I mean it does use the
CanvasRenderingContext! The thing is,
it also uses the
PIECE array and I had to decide whether I wanted
to bleed into
pieces to bleed into
I chose the former.
You’re always going to have tradeoffs when programming and re-architecturing sucks so
oftentimes it will be on you to decide if the duct-tape fix is safe enough. In one of these
articles I’ll show you how to make a wrapper and we’ll wrap
ctx with a nice
pretty bow, but for now it’s exposed to the world. Because it’s easier that way.