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.

Bash prompt showing multiple bash commands and their successful completion. Source
                 is on my github at src/source/tetris_3/new_project.png
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: Bash prompt showing the bash command 'cargo install wasm-pack` and its 
              successful completion.
We’ll use this, combined with wasm-bindgen, to make importing and exporting from Javascript easier.

Now lets open Cargo.toml
Cargo.toml file with new lines highlighted in green. We've added two new 
              dependencies: 'wasm-bindgen' and 'web-sys'. You can see the source on my 
              github at src/source/tetris_3/new_dependencies.toml
We want to add two dependencies to our project. The wasm-bindgen crate and the web-sys crate. Wasm-bindgen is used to create javascript-to-webassembly bindings so we don’t need to do all that ugly string collecting I showed you in the last post. Web-sys gives us access to javascript functions like canvas.getContext().

Speaking of:
Some Rust code that I've visually divided into 3 parts. The first part is 4 'use'
             statements that import the external packages we need. The second part is the start 
             of a function 'hello_world()'. At the start we initialize a bunch of DOM elements. 
             The third part is the second half of the 'hello_world()' function. Here we draw to 
             a CanvasRenderingContext. You can see the source on my github at 
             src/source/tetris_3/hello_world.rs
This is our src/lib.rs. The blue imports our 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.
Bash prompt showing the command 'wasm-pack build --target web' and its successful
              completion.

Wasm-pack compiles our package into the pkg directory where we have a host of files.
Bash prompt showing prompt from the previous image with the same 'wasm-pack build 
              --target web' output. It then lists the pkg directory generated by the wasm-pack
              command by using the command'ls /pkg'.
The only important ones are the .js one and .wasm one. You use them like so:
Index.html file importing our WebAssembly as an ES6 module. The source can be 
              found on my github at src/source/tetris_3/index.html
tetris.js is glue code. It includes code to access memory, pass strings, and import/export functions. 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, init will call whatever function we labelled as #[wasm_bindgen(start)] . This is how we will enter our webapp.

Now, let’s see if it runs.
Firefox window displaying the index.html file running our WebAssembly code. 
              A red square is displayed near the center of our canvas.
Yay!

2 - Laying A Foundation

When writing any piece of code it’s important to lay a good foundation. When writing Rust, triply so. Javascript is extremely flexible. If you make an architecture mistake it’s easy to hack in a quick fix. Rust, you can’t hack.

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.
A version of lib.rs with 4 empty functions: 'move_piece()', 'draw_piece()', 
              'create_piece()', and 'detect_collisions()'. You can find the source on my 
              github at src/source/tetris_3/game_blueprint.rs
And then focus on draw_piece().
The same version of lib.rs, this time with all functions except 'draw_piece()'
              folded. Folding is a mechanism in Vim used to hide code. The purpose of this
              image is to demonstrate that functionality.
You can do the folding with zim fold, zfi{.

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.
lib.rs again with draw_piece filled out with our red-square-drawing code from 
              earlier. We've also removed our 'hello_world()' function and replaced it with a 
              function named 'run()'. You can find the source on my github at
              src/source/tetris_3/draw_piece_first_edition.rs
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 a lot of research on this part trying to find the best way to represent a piece. I settled on an array.
This is the same draw_piece function with a length-16 array shaped in a 4x4 square. 
              The values are either 0 or 1 with the 1s forming the shape of a T block. The source 
              can be found on my github at src/source/tetris_3/draw_piece_second_edition.rs
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.

IMPORTANT: ARRAYS START AT 0!!
The second-edition draw piece function with more additions. We define some 
              constants so we know they're there, I'm hesitant to use them for fear of 
              cluttering the screenshot. After that we've got our for loop that loops over 
              our piece array and draws a square every time it sees a '1'. The source can 
              be found on my github at src/source/tetris_3/draw_piece_third_edition.rs
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 organized our piece array.

 
    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 %, the remainder operator, and the ycoord uses /, the divide-and-truncate operator. It’s one of those beautiful coincidences in mathematics.
Firefox window with a red T-block drawn in the top left corner of the canvas

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 draw_piece() 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:
Our same lib.rs file with the 'run()' and 'draw_piece()' functions. In this file
              we've now added two enums, 'Tetronimo' and 'Rotation'. I've also separated some
              of the code to get the context into its own function. You can find the source on 
              my github at src/source/tetris_3/draw_piece_enums.rs
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 src/pieces.rs

A new file 'src/pieces.rs'. The only thing this file contains is a really big array.
                I've got the completed code on my github at https://github.com/robert-snakard/tetris
                but you can also find the source under src/source/tetris_3/pieces_rs.rs

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.
Our lib.rs file again. This time we have a 'use' statement importing the PIECE array
              from pieces.rs. There's also some new code in 'draw_piece()' that uses the PIECE
              array to draw. The source can be found on my github at 
              src/source/tetris_3/import_pieces.rs
And then use our function parameters.
lib.rs again. We've replaced our static Tetronimo and Rotation with our function
              paramters. We've also added our xcoord and ycoord parameters to our draw function.
              The source can be found on my github at src/source/tetris_3/call_draw_piece.rs
And while we’re at it, let’s split our code into game and app modules
This time I've split lib.rs into 'game.rs' and 'app.rs'. There is a little bit of
              code left in 'lib.rs'. You can find the full source in my robert-snakard/tetris.git
              repo but the source is also available at src/source/tetris_3/app_and_game_modules.rs.
              In this file the modules are separated with 'mod' brackets. In the actual code
              they're separate files.
And reorganize the whole thing altogether!
This is the final version of this week's code. There are four files and four modules,
              'lib.rs', 'game.rs', 'app.rs', 'pieces.rs'. You can find the full source in my tetris
              repo at https://github.com/robert-snakard/tetris but there is also a single-file source
              available at src/source/tetris_3/rearchitecture.rs
So what did we do?

We reorganized. I decided to split the game into two parts. The game module, which consists of everything we do when playing the game, and the app module, which contains all our javascript/web-sys code. Lastly I added our enums to the pieces file, they just seemed to fit there best.

A lot of organizing is arbitrary, like I could have put draw_piece in 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 web-sys to bleed into game or pieces to bleed into app. 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.

Firefox windown showing a purple T block near the middle of the canvas.