Why Though?#
I’ve been teaching Rust to a couple of friends and colleagues in lots of different ways. In my latest sessions, I’ve been using this game I built, as a homage to the original, to apply their newly learned Rust skills to a project. It seems to go over well with people because it’s something real people can reason about, it’s fun to work on as you can add your own spin to it and it happens to touch on a lot of the important aspects of the language. So I thought I’d write it up in a series of blog posts.
A small note to start: I love the language and continue to learn, so if you find anything fishy in these posts (and it’s not a turbofish), do let me know by submitting a pull request or an issue.
What We Need#
I will assume you have some basic knowledge of Rust and won’t go too deep into how language primitives works. What I want to focus on is the use of the language for something you can see and play with. This is how I learn myself.
It’s not just knowing what each of the bits are in the language, it’s how you use them and how it all comes together.
I recommend you have read the official book and if you’re so inclined, do have a look at easy_rust, which is a great way to learn Rust as it is organized in small chapters, not longer than 20 minutes each, with videos in plain language. Lastly, checking out rustlings helps you to get a feel for the types of the language.
What We’re Building#
BEAST is a terminal-based action game developed for MS-DOS by Dan Baker, Alan Brown, Mark Hamilton, and Derrick Shadel. It was distributed as shareware in 1984.
It’s a game I grew up with back when I was young (everything was still black and white, there were no mobile phones, computer monitors were monochrome and emitted radiation).
To get a feel for the game, play it in the iframe above or on archive.org directly.
The broad strokes are:
- You’re a player on a 2D board ◀▶
- The board contains blocks you can push ░░
- … and blocks you can’t push ▓▓
- There are beasts trying to get you ├┤
- To win you have to squish the beasts between two blocks ◀▶░░├┤░░


There are more advanced challenges in later levels, but for this tutorial, we will focus only on the basics so you can add your own levels later yourself.
For part 1, this post, we will be doing a bit of setup, go through some of the tooling and build our board to allow our player to move around on it.
Setting It Up#
Let’s build a terminal game in Rust 🥳!
We start by creating our Rust project:
cargo new beast
cd beast
This will create a new Rust project named beast
with a
binary target:
.
├── Cargo.toml
└── src
└── main.rs
Cargo has two entry points for its crates, and you can choose either or both in your project:
- a binary, the
main.rs
file with amain()
function which executes when you run the program - a library, the
lib.rs
file which can be imported by other crates
For a game we don’t really need a library, so we will focus on the binary and cargo new
will default to a binary
target anyway.
Our Cargo.toml
file in the root is like the package.json
file of our project:
|
|
We have our name
and version
set for us and something called edition
.
The edition is the version of the Rust language we want
to use and 2024
is the latest as of this writing.
Our src/main.rs
file is our entry point.
This is where we will call our game logic and define our modules.
|
|
Let’s take our crate for a spin:
cargo run
Compiling beast v0.1.0 (/Users/code/beast)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.34s
Running `target/debug/beast`
Hello, world!
Look at us! Building binaries and executing them like there’s no tomorrow. 🙌
Let’s Think About This#
OK, what are we doing?

There are a couple of ways we could approach this.
You could create instances of each tile you expect to see on the board, give each of them a position and a way to
represent themselves and in the render function we just iterate over each entity and place them on a temporary buffer
that then gets to be iterated over to form a String we simply print to
stdout
.
This is indeed a good way to approach this for a game with a large number of entities which all require instances
to keep track of their own states.
But our board isn’t very large, our blocks don’t really need state and we have way more blocks than beasts. Beasts need state to keep moving and path-find their way to the player and the player itself should probably remember where it is. Knowing that, we could simplify the approach above by skipping instances for blocks, going straight to keeping a buffer of the board in memory and in the render function simply iterate over it and print it to stdout.
It’s a simple version of scan-line rendering as you would see in old CRT screens.

So, we could represent the buffer of our board as a two-dimensional array of tiles. Each inner array represents a row of tiles and contains all its columns:
[[Tile; BOARD_WIDTH]; BOARD_HEIGHT]
The render function would iterate over the array and print a new line for each row.
OK, this was a lot of text, let’s get out of our heads and into code.
Creating A Board#
The first thing we need to do is define our tiles. The tile should encapsulate what each tile on our board can represent. A natural fit for an enum.
|
|
That’s fine for now.
Now let’s work on the board.
Let’s keep any board logic in a single struct we call Board
.
Structs are there to encapsulate data and behavior.
|
|
So our board is a 2D array of the Tile
enum we defined earlier.
We use 39
as width because we know each tile will be 2 chars wide and our frame most likely will take up a space on
each side (39 * 2 + 1 + 1 = 80
) and so we stay within the
80 column width limit.
Let’s implement the new
method on the struct so we can get a squeaky clean new board out.
|
|
Our buffer
in the new()
method looks like this:
[
[Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty],
[Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty],
[Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty],
[Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty],
[Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty],
[Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty],
[Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty],
[Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty],
[Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty],
[Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty],
[Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty],
[Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty],
[Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty],
[Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty],
[Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty],
[Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty],
[Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty],
[Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty],
[Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty],
[Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty]
]
It helps to visualize it because if you squint a little, it actually looks like a board. We have rows and columns, we have items for each tile, it’s square.
The Compiler, Our Friend#
Now let’s actually try to get this output ourselves by printing our board:
|
|
But once we save it all, rust-analyzer will be upset with us and if we try running cargo run
, rustc will say this:
cargo run
Compiling beast v0.1.0 (/Users/dominik/beast)
error[E0277]: the trait bound `Tile: Copy` is not satisfied
--> src/main.rs:17:14
|
17 | buffer: [[Tile::Empty; 39]; 20],
| ^^^^^^^^^^^ the trait `Copy` is not implemented for `Tile`
|
= note: the `Copy` trait is required because this value will be copied for each element of the array
help: consider annotating `Tile` with `#[derive(Copy)]`
|
2 + #[derive(Copy)]
3 | enum Tile {
|
help: create an inline `const` block
|
17 - buffer: [[Tile::Empty; 39]; 20],
17 + buffer: [[const { Tile::Empty }; 39]; 20],
|
error[E0277]: `Board` doesn't implement `Debug`
--> src/main.rs:23:19
|
23 | println!("{:?}", Board::new());
| ^^^^^^^^^^^^ `Board` cannot be formatted using `{:?}`
|
= help: the trait `Debug` is not implemented for `Board`
= note: add `#[derive(Debug)]` to `Board` or manually `impl Debug for Board`
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0277`.
error: could not compile `beast` (bin "beast") due to 2 previous errors
We’re told about two errors here:
- error[E0277]: the trait bound
Tile: Copy
is not satisfied
This error occurs because ourTile
enum is being copied into our array but doesn’t currently have the ability (trait) to be copied. Rust will actually tell us how to solve it too in two different ways which is awesome. - error[E0277]:
Board
doesn’t implementDebug
The second error happens because we’re trying to print the struct and Rust doesn’t know how to display this custom data structure we have built even in the debug mode we choose here in the format macro.
It feels like the compiler is yelling at us and you’d be forgiven if this was your first impression but if you, right from the start, see the compiler more as a seasoned pair-coder sitting patiently next to you, trying to help you, you will have a much healthier relationship with it. It’s just trying to help, I promise.
So let’s fix 1.
: the compiler tells us Tile
needs the Copy
trait.
Let’s derive it:
|
|
Let’s check in with our friend:
cargo run
Compiling beast v0.1.0 (/Users/dominik/beast)
error[E0277]: the trait bound `Tile: Clone` is not satisfied
--> src/main.rs:1:10
|
1 | #[derive(Copy)]
| ^^^^ the trait `Clone` is not implemented for `Tile`
|
note: required by a bound in `Copy`
--> /Users/dominik/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/marker.rs:420:17
|
420 | pub trait Copy: Clone {
| ^^^^^ required by this bound in `Copy`
= note: this error originates in the derive macro `Copy` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider annotating `Tile` with `#[derive(Clone)]`
|
2 + #[derive(Clone)]
3 | enum Tile {
|
error[E0277]: `Board` doesn't implement `Debug`
--> src/main.rs:22:19
|
22 | println!("{:?}", Board::new());
| ^^^^^^^^^^^^ `Board` cannot be formatted using `{:?}`
|
= help: the trait `Debug` is not implemented for `Board`
= note: add `#[derive(Debug)]` to `Board` or manually `impl Debug for Board`
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0277`.
error: could not compile `beast` (bin "beast") due to 2 previous errors
The good news: we fixed our previous error:
error[E0277]: the trait bound Tile: Copy
is not satisfied
The bad news: a new one poped up:
error[E0277]: the trait bound Tile: Clone
is not satisfied
But that’s solvable since it seems we just have to add another trait to our derive proc macro.
|
|
And upon checking in with our trusty compiler/helper it appears we have solved the first issue.
cargo run
Compiling beast v0.1.0 (/Users/dominik/beast)
error[E0277]: `Board` doesn't implement `Debug`
--> src/main.rs:22:19
|
22 | println!("{:?}", Board::new());
| ^^^^^^^^^^^^ `Board` cannot be formatted using `{:?}`
|
= help: the trait `Debug` is not implemented for `Board`
= note: add `#[derive(Debug)]` to `Board` or manually `impl Debug for Board`
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0277`.
error: could not compile `beast` (bin "beast") due to 1 previous error
This feels great.
OK, let’s just ride this wave and solve the second issue as well.
We can see our Board
struct needs the Debug
trait.
But because we are at the top of our game and feel great, we notice that part of the Board
struct is the Tile
enum
and if we were to just give the Board
the debug trait we’re guessing the compiler will let us gently know that the
enum, being part of the thing we’re trying to display with the Debug
trait, will also need this trait.
So we boldly just add the trait to both elements:
|
|
And what do you know: the compiler shows us some warnings about unused options and fields but it does … compile. Yay us!
The output is … large and more importantly, it doesn’t look like a board yet. So let’s work on rendering the output in a way that feels more board-game-y.
Rendering#
To render the board we add a render
method to the Board
struct.
We will have to iterate over each row in our buffer
and within each row we will iterate over each column in each row.
Then we match
against the column which contains our tile.
Lastly we have to go into our main
function and create an instance of our Board
and then call the render method and
print it to stdout
.
|
|
So we made a new method that takes a reference to self
and returns a String
.
Then we make a new mutable String
called output
and iterate in a nested loop over each tile and push into output
what the tile we match should be displayed as, before returning it.
We also opted for a tile being 2 characters long from the terminal perspective. That’s what the original game does, (I’m just trying to stay consistent). You’re welcome to change it to anything you like.
Running cargo run
, we still get a few warnings about unused options, which is fair, but we also get a big empty blob
that gets printed.
Functionally this is correct, there is a board that is just completely empty so we really don’t print anything for
Tile::Empty
.
But we’re missing a reference to where the board starts and ends to really see the empty board.
So let’s add a frame that surrounds the board:
|
|
Instead of creating an empty String
at the start, we use the
format
macro which returns a String
.
Inside there we use the repeat
method to
allow us to not have to write the entire length of the border out.
We repeat the border 39 * 2
because the width of the board is 39
and the tile size is 2
.
Then we add the side border on line 25 before we start iterating over each item in this row and add the other side on
line 34.
We had to change our push
to push_str
because now we add more than a char into the String
.
Lastly we add the bottom border in a very similar way we added the top.
Taking The Magic Out Of Coding#
I don’t know about you, but I don’t like magic numbers in my code.
We now have 39 * 2
and multiple instances of hardcoded 39
and 20
throughout our code.
At some point our future-self is going to ask:
What does this number even mean?
or
Where else do I have to change this number to change the window size?
Let’s be kind to future-you and create a couple constants.
|
|
Now even future-me will understand what BOARD_WIDTH * TILE_SIZE
means and there is only one place to change the size
of the board.
OK, our game is getting closer:
cargo run
Compiling beast v0.1.0 (/Users/dominik/beast)
warning: variants `Player`, `Block`, and `StaticBlock` are never constructed
--> src/main.rs:8:2
|
6 | enum Tile {
| ---- variants in this enum
7 | Empty, // There will be empty spaces on our board " "
8 | Player, // We will need the player "◀▶"
| ^^^^^^
9 | Block, // Some tiles will be blocks "░░"
| ^^^^^
10 | StaticBlock, // Others will be blocks that can't be moved "▓▓"
| ^^^^^^^^^^^
|
= note: `Tile` has derived impls for the traits `Debug` and `Clone`, but these are intentionally ignored during dead code analysis
= note: `#[warn(dead_code)]` on by default
warning: `beast` (bin "beast") generated 1 warning
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.14s
Running `target/debug/beast`
▛▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▜
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▙▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▟
This looks good. Let’s hardcode some blocks and the player just to see what it would look like on the board:
|
|
We create a mutable variable called buffer
which we assign our nested array to and then set a couple tiles in that
buffer to Tile::Player
, Tile::Block
and Tile::StaticBlock
.
We don’t have to do buffer: buffer
in the Self
block because of
field init shorthand syntax
Rust has built in.
All that gets us this little preview via cargo run
:
cargo run
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.11s
Running `target/debug/beast`
▛▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▜
▌◀▶ ▐
▌ ▐
▌ ░░░░░░ ▐
▌ ▓▓ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▙▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▟
Oh and look, no more warnings ✨!
Next up: Adding colors.
A Brief Intro into ANSI Escape Sequences#
How do you even add color to a terminal?
All we have is our trusted println
macro.
How do you add color to the output if all you have is a pipe that expects a string?
This is where ANSI escape sequences come in.
From Wikipedia:
ANSI escape sequences are a standard for in-band signaling to control cursor location, color, font styling, and other options on video text terminals and terminal emulators. Certain sequences of bytes, most starting with an ASCII escape character and a bracket character, are embedded into text.
The syntax of them is: ESCAPE
[
CODE
and when you print this to most terminals it will be interpreted as a command
rather than as text.
There are many different things you can control with those sequences but for the purpose of this tutorial we will be focusing only on color and cursor position.
Here is a short summary of colors and cursor codes we might need:
Colors#
Code | What it does |
---|---|
ESCAPE [ 30m | Black font color |
ESCAPE [ 31m | Red font color |
ESCAPE [ 32m | Green font color |
ESCAPE [ 33m | Yellow font color |
ESCAPE [ 34m | Blue font color |
ESCAPE [ 35m | Magenta font color |
ESCAPE [ 36m | Cyan font color |
ESCAPE [ 37m | White font color |
ESCAPE [ 39m | Reset font color |
Cursor#
Code | What it does |
---|---|
ESCAPE [ ?25l | Hide cursor |
ESCAPE [ ?25h | Show cursor |
ESCAPE [ n F | Move cursor to beginning of line, n lines up |
ESCAPE [ n E | Move cursor to beginning of line, n lines down |
ESCAPE
in Rust via the print macro would be \x1B
so looking at this we could make something yellow within a sentence
so let’s try it out:
|
|
Which will give us:
cargo run
[..some warnings about unused items in our code..]
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.34s
Running `target/debug/beast`
This is normal color, this is yellow, and this is normal again
Now we know how to make our text colorful in the terminal which is something we will need for the frame, our blocks and eventually our beasts.
Another thing you can do in the terminal is animations.
I’m sure you’ve seen it before when installing things:
.
You still only have println!("My output");
though so how would you do something like a loading animation?
The answer again is ANSI escape sequences. If you look at our sequences for cursor movements then we spot our ability to move the cursor to the start of a line which means we can print a thing, reset the cursor to the start of that line, and print again over the previous output, slowly changing what we print, frame by frame, to make an animation.
|
|
This will just print out “World” after you run cargo run
.
This is what’s happening:

Let’s use sleep
from the standard library to make what is
happening more visible:
|
|
Now when you run cargo run
you see “Hello” printed first, then after 3 seconds it’s replaced by “World”.
This is how any animations in the terminal work, by moving the cursor we constantly just overwrite the previous frame
with the next frame.
We will use this technique later when we start moving around on the board.
Rendering But This Time Pretty#
Now that we know how to add colors to our output let’s make our render
method prettier:
|
|
This will give us a board that is pretty close to the original game:
cargo run
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/beast`
▛▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▜
▌◀▶ ▐
▌ ▐
▌ ░░░░░░ ▐
▌ ▓▓ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▙▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▟
How Did Magic Get Back Into The Code?#
But I gotta say: looking at the code, it’s hard to see where an ANSI escape sequence ends and our output starts. Let’s clean this up by adding some consts for each of the colors:
|
|
Because the method push_str
expects a
&str
and the macro
format
returns a String
we have to pass what format
returns by
reference so we end up doing this: output.push_str(&format!("Foo"));
.
Our code is much more readable now and we can start listening to keyboard input.
Listening To Keyboard Input#
I mentioned stdout
before but now it’s time to actually briefly talk about what that is.
stdout
stands for standard out
and is part of the three
standard streams between programs and their environment:
stdout
- “Standard out”; the stream we output our data intostderr
- “Standard error”; the stream we output all of our error intostdin
- “Standard in”; the stream we read for input
We’ve been using stdout
via the println
macro and you would have been using it via console.log
, print()
, echo
etc in other languages.
Now we need to listen for keyboard input because we want to know if the user of our game hit a key to move the player so
we need to listen to stdin
.
And if we think about it: we really only want to render the board when things have changed in our state so only when the
user has hit a key to move the player.
So we need a play
method that listens to keyboard input and calls render
when the right keys have been pressed.
Listening to stdin
means we have to lock stdin
for reading and direct that stream to a buffer which we can match
against:
|
|
We’re importing stdin
function and the
Read
trait from the io
module in the standard library at the top of our main.rs
file.
Then we call stdin()
to get a handle for the standard-in stream and then call
lock
on it so we can read from this stream
(and no-one else can).
Think of the way we read from a stream as the same as reading from a file, we have to put a read-lock on it to make sure
no other processes are making changes to the stream while we’re reading from it.
We’re expecting the user to use the A, W, S and D for direction (mainly
because it’s simpler to listen to letter keys than arrow keys for now) which means we will need a buffer with exactly
one byte and use the read_exact
method to fill
it.
read_exact
returns a Result
because reading from the stream
could fail.
While it doesn’t fail, and the Result
is Ok
, we loop over the input and match against the byte we’re getting back.
Since it’s easier to read characters than bytes I convert the byte into a char
and then match against it.
Inside the match we just check for the letter q
(lowercase) and print a good bye message and break our while
loop
thus ending our program.
When you run this you notice the program doesn’t finish until you hit q and Enter. This is great. Now we can add the four branches for our directions.
|
|
What Is This Smell?#
OK, point of order: Looking at our code I’m getting a code smell.
What is this smell?
We have a Board
struct that now contains new
, render
and now also play
.
The first two make sense to me, the board needs to be instantiated and rendered.
But play
?
Should the board deal with the play logic?
No, I think we should quickly clean up as we go and separate these things.
We will likely do more things within our game like deal with scores, game state and other things so why not just create
a Game
struct that contains all that logic and keep the Board
isolated to just board business?
Let’s create a new file in our src
folder called board.rs
and move all our board logic there:
.
├── Cargo.lock
├── Cargo.toml
└── src
├── board.rs
└── main.rs
|
|
And our main.rs
file can be cleaned up a little:
|
|
Now having separated these we need to tell Rust that we just created a new module.
|
|
This includes our board.rs
file into our codebase and we can watch the rust-analyzer errors flooding in.
The compiler reminds us that everything by default in Rust is private and has to be explicitly made public.
So let’s throw in some pub
keywords where we need them:
|
|
And import those public variables now in our board.rs
file and make Board
and its method new
public as well:
|
|
This compiles again and feels much cleaner.
Running this code we notice something odd though. You have to hit Enter before our game does anything with the input. Even when you hit a, w and s all after one another and then Enter we see this in our terminal:
awd
Go Left
Go Up
Go Right
A few issues:
- We are required to hit Enter before our program does anything
- Moving is bunched together until we hit Enter
- Hitting any of our direction keys echos them to our output
That’s not a good way for a game to operate. Having to hit Enter after each move or even seeing the letters appear in my terminal when playing. We can fix all that by setting our terminal to “raw mode”.
Would You Like Your Terminal Cooked Or Raw?#
Unix-style terminals have modes that have different purposes.
By default terminals are set to cooked mode
.
In this mode commands can be typed out, edited by deleting or adding to the text before hitting Enter which
sends it to the program and echos it back to the user.
This dates back to the days of hardware terminals connected to the computer via a serial line.
The computer expected the terminals to handle the low-level editing so it didn’t have to implement it itself.
In contrast, raw mode
sets up the TTY driver to pass every character to
the program as it’s typed and keeps it the program’s responsibility to echo anything back to the user.
Programs are started in cooked mode
by default and need to enable raw mode
because imagine the mayhem raw mode
would cause if every single keystroke you type would be sent to the shell instantly.
Switching to raw mode in our Linux-like-shell will be this command: stty -icanon -echo
.
Switching back is: stty icanon echo
.
So we have to call these commands in our program at the start and end to make sure we’re in the right mode for our game.
Let’s do this by creating a new module called raw_mode
in a new file raw_mode.rs
:
.
├── Cargo.lock
├── Cargo.toml
└── src
├── board.rs
├── main.rs
└── raw_mode.rs
In there we will use the std::process::Command
struct to
execute the commands and put it all into a struct called RawMode
:
|
|
We implemented a method on our struct called enter
which creates a new instance of Command
, adds two arguments to it
and calls spawn
to create a new child
process to execute this command in.
Then we use and_then
to unwrap the Result
which is returned from spawn
and call wait
on the child handle inside of it to make sure we return from our function
only after the command was executed.
We use let _ =
to ignore the Result
returned by the command execution because we don’t need it.
_
is a catch all convention in Rust that allows us to tell the compiler to ignore whatever is returned here.
Calling RawMode::enter()
will now execute our command telling our terminal to enter raw mode
.
Let’s add one more thing in here: let’s hide the cursor while the program is running because we don’t need a cursor and
we have learned we can use ANSI escape sequences to do this.
|
|
OK, now let’s include our new module into our codebase in the main.rs
file and call our enter
method at the start of
our main
function:
|
|
Running our program now we notice we instantly get feedback on each keystroke and we also don’t see the cursor anymore. This is great. Hitting q quits the game flawlessly and we feel a rush of accomplishment.
Oh but we also notice that the cursor is now hidden, even after the program has finished.
That’s a side effect we didn’t want.
Let’s quickly run this in our terminal to get the cursor back: echo "\x1b[?25h"
.
OK, we’re back to normal but we can’t expect our users to do this after they played our game so we need to do this in our
program.
We COULD create a new method now called leave
or something that just echos the sequence and we call it at the end of
the program but instead we will do it a bit more “rusty”.
As you know, Rust cleans up its variables whenever they go out of scope.
When cleaning up, Rust will call the destructor
via the Drop
trait.
Let’s use this to our advantage and implement this trait for our RawMode
struct and have Rust take care of when to
call the clean up crew.
|
|
The Drop
trait expects a function called drop
to be implemented and inside we just reverse whatever we did in
enter
and have Rust take care of the rest.
But when we run our program now this function doesn’t seem to be called at all because our cursor is still hidden after the program quits.
That’s because there is nothing to be cleaned up.
While we call enter
in our main
function, we don’t actually create an instance of RawMode
that can be cleaned up
by Rust so the destructor on our struct is never called.
Let’s change that by returning Self
from the enter
method.
|
|
But wait a minute, I hear you say.
Now running cargo run
gets us back to cooked mode
because we have to hit Enter after each keyboard hit
again.
Why is that happening?
Well if we look at our main
function we see what the issue is:
|
|
While we return an instance of RawMode
(via Self
) from the enter
function, we don’t actually put it anywhere.
The instance is created but because it’s never stored anywhere it’s also cleaned up instantly thus calling our drop
method and resetting our terminal.
We need to create a variable to keep this instance in but we also don’t need to do anything with it.
We just want to keep it around for the duration of the main
function which represents the duration of our program.
So to do that and prevent the linter to warn us about an unused variable, we can prefix our variable with an underscore
to communicate to the compiler this is a thing we don’t want to use any further.
|
|
Now running our game we get the benefits of our terminal being in raw mode
and things are cleaned up for us nicely
without side effects.
We now listen to the user’s keyboard and are executing functions on each key we’re interested in for navigation. Naturally our next step should be to actually navigate our player on the board.
Giving Our Player Legs#
How do we move our player around the board?
We have a board buffer that we need to manipulate in order for our render
method to work.
So when moving our player we would need to set the previous tile our player was in to Tile::Empty
and the new tile our
player is moving into, to Tile::Player
.
We have a branch for each direction in our play
method so we can easily pass an enum for each direction into our
function that calculates our move.
Though we need to know where the player is in order to calculate the new position for a given direction.
We could do that by scanning the board buffer and find the location of Tile::Player
.
That seems like a lot of work to do for each move.
Perhaps we keep track of our position each time we move and just recall that position from memory.
OK, that sounds good, now let’s think about where to put all this code.
Moving a player doesn’t seem appropriate for the board module.
Also doesn’t seem like a good fit for our Game
struct really?
Perhaps we create a new module just for the player that can handle movements, re-spawning and scores.
With all that in mind let’s create a new file called player.rs
:
.
├── Cargo.lock
├── Cargo.toml
└── src
├── board.rs
├── main.rs
├── player.rs
└── raw_mode.rs
In there we create a new struct called Player
that holds our position and implements a new
method.
|
|
Our position requires two things: column and row.
Let’s keep it simple for now and just use a tuple
for this until
it gets too messy.
Now we need a new method to move our player.
Since move
is a reserved word in Rust, let’s call the method advance
.
This method would need to know what direction we’re advancing in so perhaps we start with adding a new enum to our
main.rs
file which lays out each direction a player can go:
|
|
We will make this enum pub
because we will need to use (import) it in our player module:
|
|
Our new advance method now takes a mutable reference to self because we will have to change position
and direction
to tell us which direction the player is advancing in.
Inside our method naturally we use the all-powerful match
statement to branch off each Direction
value.
What do we do in each branch?
If we go to the right, for example, we have to:
- Change our old position on the buffer to
Tile::Empty
- Add
1
to the column of our position - Add
Tile::Player
to the board buffer at this new position
That way when we call render
next, the player has moved and the next time we call the advance
we go from the new
position.
|
|
Let’s add our new player instance to our Game
struct and call the advance
method on keypress:
|
|
This gets us this:
cargo run
Compiling beast v0.1.0 (/Users/code/beast)
error[E0616]: field `buffer` of struct `Board` is private
--> src/player.rs:13:9
|
13 | board.buffer[self.position.1][self.position.0] = Tile::Empty;
| ^^^^^^ private field
error[E0616]: field `buffer` of struct `Board` is private
--> src/player.rs:22:9
|
22 | board.buffer[self.position.1][self.position.0] = Tile::Player;
| ^^^^^^ private field
error[E0277]: `Player` doesn't implement `Debug`
--> src/main.rs:36:2
|
33 | #[derive(Debug)]
| ----- in this derive macro expansion
...
36 | player: Player,
| ^^^^^^^^^^^^^^ `Player` cannot be formatted using `{:?}`
|
= help: the trait `Debug` is not implemented for `Player`
= note: add `#[derive(Debug)]` to `Player` or manually `impl Debug for Player`
help: consider annotating `Player` with `#[derive(Debug)]`
--> src/player.rs:3:1
|
3 + #[derive(Debug)]
4 | pub struct Player {
|
error[E0624]: method `render` is private
--> src/main.rs:51:29
|
51 | println!("{}", self.board.render());
| ^^^^^^ private method
|
::: src/board.rs:24:2
|
24 | fn render(&self) -> String {
| -------------------------- private method defined here
error[E0624]: method `render` is private
--> src/main.rs:74:60
|
74 | println!("\x1B[{}F{}", BOARD_HEIGHT + 1 + 1, self.board.render());
| ^^^^^^ private method
|
::: src/board.rs:24:2
|
24 | fn render(&self) -> String {
| -------------------------- private method defined here
Some errors have detailed explanations: E0277, E0616, E0624.
For more information about an error, try `rustc --explain E0277`.
error: could not compile `beast` (bin "beast") due to 5 previous errors
OK that’s fair.
The compiler let’s us know that we’ve been using the buffer
and the render
method that hasn’t been set to public
yet:
|
|
It also told us that Player
doesn’t implement Debug
.
That’s also fair because the Player
struct is used in our Game
struct and that struct has the Debug
trait derived.
If that struct has it, all its data must have it too.
|
|
So we added the derive and all the other code but when we run our program again and walk left as the first thing we get a panic:
cargo run
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/beast`
▛▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▜
▌◀▶ ▐
▌ ▐
▌ ░░░░░░ ▐
▌ ▓▓ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▙▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▟
thread 'main' panicked at src/player.rs:20:32:
attempt to subtract with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
What does “attempt to subtract with overflow” mean?
Also our trusty compiler tells us where this error happened in our code: on line 20 in the player.rs
file.
|
|
When we start the game our position is (0,0)
and on line 20 we attempt to subtract 1
from 0
.
But the type of that number is usize
which means it can’t be a value of anything below 0
so Rust panics.
Now thinking about this, what would happen when you walk across to the right and further:
cargo run
▛▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▜
▌ ◀▶▐
▌ ▐
▌ ░░░░░░ ▐
▌ ▓▓ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▌ ▐
▙▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▟
thread 'main' panicked at src/player.rs:23:9:
index out of bounds: the len is 39 but the index is 39
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Yeah that panics too because we’re trying to index into our board buffer with a number larger than the index of our array.
Basically we need to guard against each boundary of our board.
|
|
Now we’re checking to make sure we don’t do anything illegal with our buffer or our position and running this code gives us a nice way to walk across our board.
We notice that we’re “eating” the blocks on the board as we walk over them but that’s OK for now.
We Have The Start Of A Game#
This is it. We did it! The first part of this tutorial is done and we got a board we can walk around on with a couple tiles hardcoded.
In the next part we will generate a terrain, implement pushing blocks around and look into adding beasts.