Learning Rust By Building The Old Terminal Game Beast From 1984, Part 2
60min read
In the last post we set up our board and made the player walk around. In this post we will generate terrain for each level and implement a way for the player to push blocks.
usestd::io::{Read,stdin};modboard;modplayer;modraw_mode;usecrate::{board::Board,player::Player,raw_mode::RawMode};pubconstBOARD_WIDTH: usize=39;pubconstBOARD_HEIGHT: usize=20;pubconstTILE_SIZE: usize=2;pubconstANSI_YELLOW: &str="\x1B[33m";pubconstANSI_GREEN: &str="\x1B[32m";pubconstANSI_CYAN: &str="\x1B[36m";pubconstANSI_RESET: &str="\x1B[39m";#[derive(Copy, Clone, Debug)]pubenumTile{Empty,// There will be empty spaces on our board " "
Player,// We will need the player "◀▶"
Block,// Some tiles will be blocks "░░"
StaticBlock,// Others will be blocks that can't be moved "▓▓"
}pubenumDirection{Up,Right,Down,Left,}#[derive(Debug)]structGame{board: Board,player: Player,}implGame{fnnew() -> Self{Self{board: Board::new(),player: Player::new(),}}fnplay(&mutself){letstdin=stdin();letmutlock=stdin.lock();letmutbuffer=[0_u8;1];println!("{}",self.board.render());whilelock.read_exact(&mutbuffer).is_ok(){matchbuffer[0]aschar{'w'=>{self.player.advance(&mutself.board,Direction::Up);},'d'=>{self.player.advance(&mutself.board,Direction::Right);},'s'=>{self.player.advance(&mutself.board,Direction::Down);},'a'=>{self.player.advance(&mutself.board,Direction::Left);},'q'=>{println!("Good bye");break;},_=>{},}println!("\x1B[{}F{}",BOARD_HEIGHT+1+1,self.board.render());}}}fnmain(){let_raw_mode=RawMode::enter();letmutgame=Game::new();game.play();}
Our board.rs file contains the Board struct with a few hardcoded obstacles and a method to render it all:
While we’re looking at our code, I feel like we should move our Game struct into its own module and only keep shared
types in our main.rs file.
It’s probably more of a personal preference but I like to keep the main.rs file as clean as possible since it’s the
entry point to our binary and is responsible for orchestrating everything together rather than implementing logic.
modboard;modgame;modplayer;modraw_mode;usecrate::{game::Game,raw_mode::RawMode};pubconstBOARD_WIDTH: usize=39;pubconstBOARD_HEIGHT: usize=20;pubconstTILE_SIZE: usize=2;pubconstANSI_YELLOW: &str="\x1B[33m";pubconstANSI_GREEN: &str="\x1B[32m";pubconstANSI_CYAN: &str="\x1B[36m";pubconstANSI_RESET: &str="\x1B[39m";#[derive(Copy, Clone, Debug)]pubenumTile{Empty,// There will be empty spaces on our board " "
Player,// We will need the player "◀▶"
Block,// Some tiles will be blocks "░░"
StaticBlock,// Others will be blocks that can't be moved "▓▓"
}pubenumDirection{Up,Right,Down,Left,}fnmain(){let_raw_mode=RawMode::enter();letmutgame=Game::new();game.play();}
After running cargo run we get an error:
cargo run
Compiling beast v0.1.0 (/Users/code/beast)
error[E0603]: struct `Game` is private--> src/main.rs:6:19
|6| use crate::{game::Game, raw_mode::RawMode};
|^^^^private struct|note: the struct `Game` is defined here
--> src/game.rs:6:1
|6| struct Game {
|^^^^^^^^^^^For more information about this error, try `rustc --explain E0603`.error: could not compile `beast` (bin "beast") due to 1 previous error
So we need to make our Game struct public because it’s now in a different module.
But we realize that also is true for our new and play method, even though Rust isn’t showing us these errors yet.
But we know our friend well and so let’s just make all three of them public:
We have our little hardcoded blocks we added in
the first part of the tutorial
but now we should look into generating our terrain.
We want the terrain to be random each time so that when we play the game, the challenge is always a little different.
How would you do that, though?
Let’s assume we have a function that generates random numbers for us within a range, how would you go about generating
your coordinates for each block?
Your first instinct might be to just generate a pair of numbers, check if the tile at that coordinate is Tile::Empty
and then place it.
It was my first thought too.
But this is pretty inefficient because you’re just brute-forcing your way to a full board and could get extraordinarily
unlucky by generating multiple coordinates in a row that are not Empty and the more blocks you place on the board, the
higher the chances of collisions like that.
Instead of that, let’s just collect every possible coordinate on the board into a collection type like a Vec and then
shuffle the vector and pop the last one out, one by one
to place each block.
That way we guarantee that each pick only exists once and is Empty on the board.
all_coords now contains every possible position on the board since our board at the start is completely empty.
Now we need to shuffle this vec and for that we will need the rand crate.
From the rand crate we will use the SliceRandom trait
which implements a shuffle method on
T which in our case will be our vec.
Let’s use it in our code:
Now all_coords contains all coordinates of our board in random order.
Before we continue though, we should remove the player position from the vec since the player is inhabiting a coordinate
on the board and we wouldn’t want to overwrite it’s position.
But we just shuffled our vec and have no idea where that coordinate now is.
Perhaps it’s best to remove the player position from the vec before we shuffle:
userand::seq::SliceRandom;usecrate::{ANSI_CYAN,ANSI_GREEN,ANSI_RESET,ANSI_YELLOW,BOARD_HEIGHT,BOARD_WIDTH,TILE_SIZE,Tile,};#[derive(Debug)]pubstructBoard{pubbuffer: [[Tile;BOARD_WIDTH];BOARD_HEIGHT],}implBoard{pubfnnew() -> Self{letmutbuffer=[[Tile::Empty;BOARD_WIDTH];BOARD_HEIGHT];letmutall_coords=(0..BOARD_HEIGHT).flat_map(|row|(0..BOARD_WIDTH).map(move|column|(column,row))).filter(|coord|!(coord.0==0&&coord.1==0)).collect::<Vec<(usize,usize)>>();letmutrng=rand::rng();all_coords.shuffle(&mutrng);for_in0..50{letcoord=all_coords.pop().expect("We tried to place more blocks than there were available spaces on the board",);buffer[coord.1][coord.0]=Tile::Block;}Self{buffer}}pubfnrender(&self) -> String{letmutoutput=format!("{ANSI_YELLOW}▛{}▜{ANSI_RESET}\n","▀".repeat(BOARD_WIDTH*TILE_SIZE));forrowsinself.buffer{output.push_str(&format!("{ANSI_YELLOW}▌{ANSI_RESET}"));fortileinrows{matchtile{Tile::Empty=>output.push_str(" "),Tile::Player=>{output.push_str(&format!("{ANSI_CYAN}◀▶{ANSI_RESET}"))},Tile::Block=>{output.push_str(&format!("{ANSI_GREEN}░░{ANSI_RESET}"))},Tile::StaticBlock=>{output.push_str(&format!("{ANSI_YELLOW}▓▓{ANSI_RESET}"))},}}output.push_str(&format!("{ANSI_YELLOW}▐{ANSI_RESET}\n"));}output.push_str(&format!("{ANSI_YELLOW}▙{}▟{ANSI_RESET}","▄".repeat(BOARD_WIDTH*TILE_SIZE)));output}}
We create a loop from 0 to 50 and in each iteration pop off the last item from our shuffled all_coords vec and
use it to place a Tile::Block on the buffer.
We use expect because pop returns an
Option because pop could very well fail when there is
nothing left in our vec.
Normally we would deal with the error case gracefully and not throw a panic but in this case we should stop our game
and throw our hands up because we tried to place more blocks than there are empty tiles so I think it’s OK to panic
here.
Also note that we’re doing buffer[coord.1][coord.0] and not buffer[coord.0][coord.1] because the second argument in
our coord tuple is the row and the first is the column and in our buffer it’s the other way around.
We will have to keep that in mind and I certainly have already mixed this up about three times.
When we run our binary, we get something similar to this:
cargo run
Compiling beast v0.1.0 (/Users/code/beast)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.17s
Running `target/debug/beast`
▛▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▜▌░░▐▌░░░░░░▐▌░░░░░░▐▌░░░░░░░░░░▐▌░░░░░░░░░░▐▌▐▌░░▐▌░░░░░░▐▌░░░░░░░░▐▌░░░░░░▐▌▐▌░░░░░░▐▌░░░░░░▐▌░░▐▌░░░░░░▐▌░░░░░░░░▐▌░░░░░░░░░░▐▌░░▐▌░░▐▌░░▐▙▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▟
This is great!
Let’s do the same thing again for StaticBlocks:
userand::seq::SliceRandom;usecrate::{ANSI_CYAN,ANSI_GREEN,ANSI_RESET,ANSI_YELLOW,BOARD_HEIGHT,BOARD_WIDTH,TILE_SIZE,Tile,};#[derive(Debug)]pubstructBoard{pubbuffer: [[Tile;BOARD_WIDTH];BOARD_HEIGHT],}implBoard{pubfnnew() -> Self{letmutbuffer=[[Tile::Empty;BOARD_WIDTH];BOARD_HEIGHT];letmutall_coords=(0..BOARD_HEIGHT).flat_map(|row|(0..BOARD_WIDTH).map(move|column|(column,row))).filter(|coord|!(coord.0==0&&coord.1==0)).collect::<Vec<(usize,usize)>>();letmutrng=rand::rng();all_coords.shuffle(&mutrng);for_in0..50{letcoord=all_coords.pop().expect("We tried to place more blocks than there were available spaces on the board",);buffer[coord.1][coord.0]=Tile::Block;}for_in0..5{letcoord=all_coords.pop().expect("We tried to place more static blocks than there were available spaces on the board",);buffer[coord.1][coord.0]=Tile::StaticBlock;}Self{buffer}}pubfnrender(&self) -> String{letmutoutput=format!("{ANSI_YELLOW}▛{}▜{ANSI_RESET}\n","▀".repeat(BOARD_WIDTH*TILE_SIZE));forrowsinself.buffer{output.push_str(&format!("{ANSI_YELLOW}▌{ANSI_RESET}"));fortileinrows{matchtile{Tile::Empty=>output.push_str(" "),Tile::Player=>{output.push_str(&format!("{ANSI_CYAN}◀▶{ANSI_RESET}"))},Tile::Block=>{output.push_str(&format!("{ANSI_GREEN}░░{ANSI_RESET}"))},Tile::StaticBlock=>{output.push_str(&format!("{ANSI_YELLOW}▓▓{ANSI_RESET}"))},}}output.push_str(&format!("{ANSI_YELLOW}▐{ANSI_RESET}\n"));}output.push_str(&format!("{ANSI_YELLOW}▙{}▟{ANSI_RESET}","▄".repeat(BOARD_WIDTH*TILE_SIZE)));output}}
And this now really makes our board look awesome:
cargo run
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s
Running `target/debug/beast`
▛▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▜▌░░░░░░░░▐▌░░▓▓░░▐▌▓▓░░░░▐▌░░░░░░▐▌░░░░▐▌▐▌░░░░░░░░▐▌░░░░░░░░▐▌░░░░░░▓▓▐▌░░▐▌░░▐▌░░░░░░░░▐▌░░░░░░▐▌░░░░▓▓▐▌▓▓▐▌░░░░░░░░░░░░▐▌░░▐▌░░░░░░▐▌░░▐▌░░░░░░░░▐▙▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▟
For the finishing touches: we add our player back on the board:
userand::seq::SliceRandom;usecrate::{ANSI_CYAN,ANSI_GREEN,ANSI_RESET,ANSI_YELLOW,BOARD_HEIGHT,BOARD_WIDTH,TILE_SIZE,Tile,};#[derive(Debug)]pubstructBoard{pubbuffer: [[Tile;BOARD_WIDTH];BOARD_HEIGHT],}implBoard{pubfnnew() -> Self{letmutbuffer=[[Tile::Empty;BOARD_WIDTH];BOARD_HEIGHT];letmutall_coords=(0..BOARD_HEIGHT).flat_map(|row|(0..BOARD_WIDTH).map(move|column|(column,row))).filter(|coord|!(coord.0==0&&coord.1==0)).collect::<Vec<(usize,usize)>>();letmutrng=rand::rng();all_coords.shuffle(&mutrng);buffer[0][0]=Tile::Player;for_in0..50{letcoord=all_coords.pop().expect("We tried to place more blocks than there were available spaces on the board",);buffer[coord.1][coord.0]=Tile::Block;}for_in0..5{letcoord=all_coords.pop().expect("We tried to place more static blocks than there were available spaces on the board",);buffer[coord.1][coord.0]=Tile::StaticBlock;}Self{buffer}}pubfnrender(&self) -> String{letmutoutput=format!("{ANSI_YELLOW}▛{}▜{ANSI_RESET}\n","▀".repeat(BOARD_WIDTH*TILE_SIZE));forrowsinself.buffer{output.push_str(&format!("{ANSI_YELLOW}▌{ANSI_RESET}"));fortileinrows{matchtile{Tile::Empty=>output.push_str(" "),Tile::Player=>{output.push_str(&format!("{ANSI_CYAN}◀▶{ANSI_RESET}"))},Tile::Block=>{output.push_str(&format!("{ANSI_GREEN}░░{ANSI_RESET}"))},Tile::StaticBlock=>{output.push_str(&format!("{ANSI_YELLOW}▓▓{ANSI_RESET}"))},}}output.push_str(&format!("{ANSI_YELLOW}▐{ANSI_RESET}\n"));}output.push_str(&format!("{ANSI_YELLOW}▙{}▟{ANSI_RESET}","▄".repeat(BOARD_WIDTH*TILE_SIZE)));output}}
cargo run
Compiling beast v0.1.0 (/Users/code/beast)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.28s
Running `target/debug/beast`
▛▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▜▌◀▶░░░░░░▐▌▐▌░░▐▌░░░░░░░░▓▓░░▐▌░░▐▌░░░░░░░░░░▐▌░░▐▌░░░░▐▌░░░░░░░░░░▐▌░░░░░░▐▌░░░░░░▐▌░░░░▐▌░░▐▌░░░░▐▌░░▐▌░░░░▐▌▓▓▓▓▐▌░░▐▌░░░░░░░░▓▓░░▐▌░░░░▓▓░░░░░░░░░░▐▙▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▟
Our board now looks like the real thing but we’ve written buffer[coord.1][coord.0] a couple times now and have
certainly stumbled writing this a few times.
Everytime I have to ask myself:
Was it row first or column? How did the buffer work again?
—Me
After bumping into this enough times, I think we had enough and should now implement a new Coord struct to hold
coordinates.
That way we never have to wonder if coord.1 was row or column.
Let’s add this new struct to the main.rs file because, much like Tile, it will be used throughout the game:
modboard;modgame;modplayer;modraw_mode;usecrate::{game::Game,raw_mode::RawMode};pubconstBOARD_WIDTH: usize=39;pubconstBOARD_HEIGHT: usize=20;pubconstTILE_SIZE: usize=2;pubconstANSI_YELLOW: &str="\x1B[33m";pubconstANSI_GREEN: &str="\x1B[32m";pubconstANSI_CYAN: &str="\x1B[36m";pubconstANSI_RESET: &str="\x1B[39m";#[derive(Copy, Clone, Debug)]pubenumTile{Empty,// There will be empty spaces on our board " "
Player,// We will need the player "◀▶"
Block,// Some tiles will be blocks "░░"
StaticBlock,// Others will be blocks that can't be moved "▓▓"
}pubenumDirection{Up,Right,Down,Left,}#[derive(Debug)]pubstructCoord{column: usize,row: usize,}fnmain(){let_raw_mode=RawMode::enter();letmutgame=Game::new();game.play();}
We can use that new Coord struct in our player module now:
userand::seq::SliceRandom;usecrate::{ANSI_CYAN,ANSI_GREEN,ANSI_RESET,ANSI_YELLOW,BOARD_HEIGHT,BOARD_WIDTH,Coord,TILE_SIZE,Tile,};#[derive(Debug)]pubstructBoard{pubbuffer: [[Tile;BOARD_WIDTH];BOARD_HEIGHT],}implBoard{pubfnnew() -> Self{letmutbuffer=[[Tile::Empty;BOARD_WIDTH];BOARD_HEIGHT];letmutall_coords=(0..BOARD_HEIGHT).flat_map(|row|(0..BOARD_WIDTH).map(move|column|Coord{column,row})).filter(|coord|!(coord.column==0&&coord.row==0)).collect::<Vec<Coord>>();letmutrng=rand::rng();all_coords.shuffle(&mutrng);buffer[0][0]=Tile::Player;for_in0..50{letcoord=all_coords.pop().expect("We tried to place more blocks than there were available spaces on the board",);buffer[coord.row][coord.column]=Tile::Block;}for_in0..5{letcoord=all_coords.pop().expect("We tried to place more static blocks than there were available spaces on the board",);buffer[coord.row][coord.column]=Tile::StaticBlock;}Self{buffer}}pubfnrender(&self) -> String{letmutoutput=format!("{ANSI_YELLOW}▛{}▜{ANSI_RESET}\n","▀".repeat(BOARD_WIDTH*TILE_SIZE));forrowsinself.buffer{output.push_str(&format!("{ANSI_YELLOW}▌{ANSI_RESET}"));fortileinrows{matchtile{Tile::Empty=>output.push_str(" "),Tile::Player=>{output.push_str(&format!("{ANSI_CYAN}◀▶{ANSI_RESET}"))},Tile::Block=>{output.push_str(&format!("{ANSI_GREEN}░░{ANSI_RESET}"))},Tile::StaticBlock=>{output.push_str(&format!("{ANSI_YELLOW}▓▓{ANSI_RESET}"))},}}output.push_str(&format!("{ANSI_YELLOW}▐{ANSI_RESET}\n"));}output.push_str(&format!("{ANSI_YELLOW}▙{}▟{ANSI_RESET}","▄".repeat(BOARD_WIDTH*TILE_SIZE)));output}}
This also made our code more readable but we’re noticing we’re doing a lot of typing with things like
buffer[coord.row][coord.column].
Having to type this every time we index into our board seems a bit too much.
Luckily Rust gives us the ability to define our own Index trait
to improve this:
userand::seq::SliceRandom;usecrate::{ANSI_CYAN,ANSI_GREEN,ANSI_RESET,ANSI_YELLOW,BOARD_HEIGHT,BOARD_WIDTH,Coord,TILE_SIZE,Tile,};usestd::ops::{Index,IndexMut};#[derive(Debug)]pubstructBoard{pubbuffer: [[Tile;BOARD_WIDTH];BOARD_HEIGHT],}implIndex<Coord>forBoard{typeOutput=Tile;fnindex(&self,coord: Coord) -> &Self::Output{&self.buffer[coord.row][coord.column]}}implIndexMut<Coord>forBoard{fnindex_mut(&mutself,coord: Coord) -> &mutSelf::Output{&mutself.buffer[coord.row][coord.column]}}implBoard{pubfnnew() -> Self{letmutbuffer=[[Tile::Empty;BOARD_WIDTH];BOARD_HEIGHT];letmutall_coords=(0..BOARD_HEIGHT).flat_map(|row|(0..BOARD_WIDTH).map(move|column|Coord{column,row})).filter(|coord|!(coord.column==0&&coord.row==0)).collect::<Vec<Coord>>();letmutrng=rand::rng();all_coords.shuffle(&mutrng);buffer[0][0]=Tile::Player;for_in0..50{letcoord=all_coords.pop().expect("We tried to place more blocks than there were available spaces on the board",);buffer[coord.row][coord.column]=Tile::Block;}for_in0..5{letcoord=all_coords.pop().expect("We tried to place more static blocks than there were available spaces on the board",);buffer[coord.row][coord.column]=Tile::StaticBlock;}Self{buffer}}pubfnrender(&self) -> String{letmutoutput=format!("{ANSI_YELLOW}▛{}▜{ANSI_RESET}\n","▀".repeat(BOARD_WIDTH*TILE_SIZE));forrowsinself.buffer{output.push_str(&format!("{ANSI_YELLOW}▌{ANSI_RESET}"));fortileinrows{matchtile{Tile::Empty=>output.push_str(" "),Tile::Player=>{output.push_str(&format!("{ANSI_CYAN}◀▶{ANSI_RESET}"))},Tile::Block=>{output.push_str(&format!("{ANSI_GREEN}░░{ANSI_RESET}"))},Tile::StaticBlock=>{output.push_str(&format!("{ANSI_YELLOW}▓▓{ANSI_RESET}"))},}}output.push_str(&format!("{ANSI_YELLOW}▐{ANSI_RESET}\n"));}output.push_str(&format!("{ANSI_YELLOW}▙{}▟{ANSI_RESET}","▄".repeat(BOARD_WIDTH*TILE_SIZE)));output}}
With the Index trait implemented we can now index into our board by simply writing board[coord] instead of
board[coord.row][coord.column].
That’s a massive improvement so let’s apply this to our player module:
cargo run
Compiling beast v0.1.0 (/Users/code/beast)
error[E0507]: cannot move out of `self.position` which is behind a mutable reference--> src/player.rs:16:9
|16| board[self.position] = Tile::Empty;
|^^^^^^^^^^^^^move occurs because `self.position` has type `Coord`, which does not implement the `Copy` trait|note: if `Coord` implemented `Clone`, you could clone the value
--> src/main.rs:34:1
|34| pub struct Coord {
|^^^^^^^^^^^^^^^^consider implementing `Clone` for this type|::: src/player.rs:16:9
|16| board[self.position] = Tile::Empty;
|-------------you could clone this valueerror[E0507]: cannot move out of `self.position` which is behind a mutable reference--> src/player.rs:41:9
|41| board[self.position] = Tile::Player;
|^^^^^^^^^^^^^move occurs because `self.position` has type `Coord`, which does not implement the `Copy` trait|note: if `Coord` implemented `Clone`, you could clone the value
--> src/main.rs:34:1
|34| pub struct Coord {
|^^^^^^^^^^^^^^^^consider implementing `Clone` for this type|::: src/player.rs:41:9
|41| board[self.position] = Tile::Player;
|-------------you could clone this valueFor more information about this error, try `rustc --explain E0507`.error: could not compile `beast` (bin "beast") due to 2 previous errors
Our trusted friend, the compiler, tells us that Coord doesn’t implement the Copy trait which is needed for us to
take ownership of the coord passed into our Index trait.
We have two options here now:
We could derive the Copy and Clone trait for our Coord struct.
This is a pretty low impact thing since the struct only takes usize types which are themselves copy types.
Or we could not take ownership of the Coord within our Index trait implementation in the first place.
Due to the relatively simple nature of the Coord struct the difference is much of a muchness really.
But because this is a tutorial and we’re learning still, I would go with 2 mainly because there isn’t a reason to take
ownership of the Coord within our Index trait.
And if we don’t need it, why work around it?
userand::seq::SliceRandom;usecrate::{ANSI_CYAN,ANSI_GREEN,ANSI_RESET,ANSI_YELLOW,BOARD_HEIGHT,BOARD_WIDTH,Coord,TILE_SIZE,Tile,};usestd::ops::{Index,IndexMut};#[derive(Debug)]pubstructBoard{pubbuffer: [[Tile;BOARD_WIDTH];BOARD_HEIGHT],}implIndex<&Coord>forBoard{typeOutput=Tile;fnindex(&self,coord: &Coord) -> &Self::Output{&self.buffer[coord.row][coord.column]}}implIndexMut<&Coord>forBoard{fnindex_mut(&mutself,coord: &Coord) -> &mutSelf::Output{&mutself.buffer[coord.row][coord.column]}}implBoard{pubfnnew() -> Self{letmutbuffer=[[Tile::Empty;BOARD_WIDTH];BOARD_HEIGHT];letmutall_coords=(0..BOARD_HEIGHT).flat_map(|row|(0..BOARD_WIDTH).map(move|column|Coord{column,row})).filter(|coord|!(coord.column==0&&coord.row==0)).collect::<Vec<Coord>>();letmutrng=rand::rng();all_coords.shuffle(&mutrng);buffer[0][0]=Tile::Player;for_in0..50{letcoord=all_coords.pop().expect("We tried to place more blocks than there were available spaces on the board",);buffer[coord.row][coord.column]=Tile::Block;}for_in0..5{letcoord=all_coords.pop().expect("We tried to place more static blocks than there were available spaces on the board",);buffer[coord.row][coord.column]=Tile::StaticBlock;}Self{buffer}}pubfnrender(&self) -> String{letmutoutput=format!("{ANSI_YELLOW}▛{}▜{ANSI_RESET}\n","▀".repeat(BOARD_WIDTH*TILE_SIZE));forrowsinself.buffer{output.push_str(&format!("{ANSI_YELLOW}▌{ANSI_RESET}"));fortileinrows{matchtile{Tile::Empty=>output.push_str(" "),Tile::Player=>{output.push_str(&format!("{ANSI_CYAN}◀▶{ANSI_RESET}"))},Tile::Block=>{output.push_str(&format!("{ANSI_GREEN}░░{ANSI_RESET}"))},Tile::StaticBlock=>{output.push_str(&format!("{ANSI_YELLOW}▓▓{ANSI_RESET}"))},}}output.push_str(&format!("{ANSI_YELLOW}▐{ANSI_RESET}\n"));}output.push_str(&format!("{ANSI_YELLOW}▙{}▟{ANSI_RESET}","▄".repeat(BOARD_WIDTH*TILE_SIZE)));output}}
We simply take Coord by reference and thus don’t have to copy or clone anything.
Now we need to change how we index into our board in the player module:
userand::seq::SliceRandom;usecrate::{ANSI_CYAN,ANSI_GREEN,ANSI_RESET,ANSI_YELLOW,BOARD_HEIGHT,BOARD_WIDTH,Coord,TILE_SIZE,Tile,};usestd::ops::{Index,IndexMut};#[derive(Debug)]pubstructBoard{pubbuffer: [[Tile;BOARD_WIDTH];BOARD_HEIGHT],}implIndex<&Coord>forBoard{typeOutput=Tile;fnindex(&self,coord: &Coord) -> &Self::Output{&self.buffer[coord.row][coord.column]}}implIndexMut<&Coord>forBoard{fnindex_mut(&mutself,coord: &Coord) -> &mutSelf::Output{&mutself.buffer[coord.row][coord.column]}}implBoard{pubfnnew() -> Self{letmutbuffer=[[Tile::Empty;BOARD_WIDTH];BOARD_HEIGHT];letmutall_coords=(0..BOARD_HEIGHT).flat_map(|row|(0..BOARD_WIDTH).map(move|column|Coord{column,row})).filter(|coord|!(coord.column==0&&coord.row==0)).collect::<Vec<Coord>>();letmutrng=rand::rng();all_coords.shuffle(&mutrng);buffer[0][0]=Tile::Player;for_in0..50{letcoord=all_coords.pop().expect("We tried to place more blocks than there were available spaces on the board",);buffer[coord.row][coord.column]=Tile::Block;}for_in0..5{letcoord=all_coords.pop().expect("We tried to place more static blocks than there were available spaces on the board",);buffer[coord.row][coord.column]=Tile::StaticBlock;}Self{buffer}}pubfnrender(&self) -> String{letmutoutput=format!("{ANSI_YELLOW}▛{}▜{ANSI_RESET}\n","▀".repeat(BOARD_WIDTH*TILE_SIZE));forrowsinself.buffer{output.push_str(&format!("{ANSI_YELLOW}▌{ANSI_RESET}"));fortileinrows{matchtile{Tile::Empty=>output.push_str(" "),Tile::Player=>{output.push_str(&format!("{ANSI_CYAN}◀▶{ANSI_RESET}"))},Tile::Block=>{output.push_str(&format!("{ANSI_GREEN}░░{ANSI_RESET}"))},Tile::StaticBlock=>{output.push_str(&format!("{ANSI_YELLOW}▓▓{ANSI_RESET}"))},}}output.push_str(&format!("{ANSI_YELLOW}▐{ANSI_RESET}\n"));}output.push_str(&format!("{ANSI_YELLOW}▙{}▟{ANSI_RESET}","▄".repeat(BOARD_WIDTH*TILE_SIZE)));output}}
The idea is that in later levels the Block tiles are reduced and the StaticBlocks increased to give us fewer
opportunities to squish the beasts, making each level a little harder.
Thus we need to find a way to change the number of blocks and static blocks for each level.
OK that’s fair, we will need a way to express levels and then a way to get a level config for each level.
An enum here seems to be the right fit and we can implement a function on the enum that returns a struct with the
config per level.
For this let’s create a new module called level.rs and add our code there:
We added the LevelConfig struct for the return value and made sure we mark each field as public so that our other
modules can read it.
We also made our Level enum and get_level_config method on the enum public.
Now we just need to include this new module in our code:
modboard;modgame;modlevel;modplayer;modraw_mode;usecrate::{game::Game,raw_mode::RawMode};pubconstBOARD_WIDTH: usize=39;pubconstBOARD_HEIGHT: usize=20;pubconstTILE_SIZE: usize=2;pubconstANSI_YELLOW: &str="\x1B[33m";pubconstANSI_GREEN: &str="\x1B[32m";pubconstANSI_CYAN: &str="\x1B[36m";pubconstANSI_RESET: &str="\x1B[39m";#[derive(Copy, Clone, Debug)]pubenumTile{Empty,// There will be empty spaces on our board " "
Player,// We will need the player "◀▶"
Block,// Some tiles will be blocks "░░"
StaticBlock,// Others will be blocks that can't be moved "▓▓"
}pubenumDirection{Up,Right,Down,Left,}#[derive(Debug)]pubstructCoord{column: usize,row: usize,}fnmain(){let_raw_mode=RawMode::enter();letmutgame=Game::new();game.play();}
userand::seq::SliceRandom;usecrate::{ANSI_CYAN,ANSI_GREEN,ANSI_RESET,ANSI_YELLOW,BOARD_HEIGHT,BOARD_WIDTH,Coord,TILE_SIZE,Tile,level::{Level,LevelConfig},};usestd::ops::{Index,IndexMut};#[derive(Debug)]pubstructBoard{pubbuffer: [[Tile;BOARD_WIDTH];BOARD_HEIGHT],}implIndex<&Coord>forBoard{typeOutput=Tile;fnindex(&self,coord: &Coord) -> &Self::Output{&self.buffer[coord.row][coord.column]}}implIndexMut<&Coord>forBoard{fnindex_mut(&mutself,coord: &Coord) -> &mutSelf::Output{&mutself.buffer[coord.row][coord.column]}}implBoard{pubfnnew() -> Self{letmutbuffer=[[Tile::Empty;BOARD_WIDTH];BOARD_HEIGHT];letmutall_coords=(0..BOARD_HEIGHT).flat_map(|row|(0..BOARD_WIDTH).map(move|column|Coord{column,row})).filter(|coord|!(coord.column==0&&coord.row==0)).collect::<Vec<Coord>>();letmutrng=rand::rng();all_coords.shuffle(&mutrng);buffer[0][0]=Tile::Player;letLevelConfig{block_count,static_block_count,}=Level::One.get_level_config();for_in0..block_count{letcoord=all_coords.pop().expect("We tried to place more blocks than there were available spaces on the board",);buffer[coord.row][coord.column]=Tile::Block;}for_in0..static_block_count{letcoord=all_coords.pop().expect("We tried to place more static blocks than there were available spaces on the board",);buffer[coord.row][coord.column]=Tile::StaticBlock;}Self{buffer}}pubfnrender(&self) -> String{letmutoutput=format!("{ANSI_YELLOW}▛{}▜{ANSI_RESET}\n","▀".repeat(BOARD_WIDTH*TILE_SIZE));forrowsinself.buffer{output.push_str(&format!("{ANSI_YELLOW}▌{ANSI_RESET}"));fortileinrows{matchtile{Tile::Empty=>output.push_str(" "),Tile::Player=>{output.push_str(&format!("{ANSI_CYAN}◀▶{ANSI_RESET}"))},Tile::Block=>{output.push_str(&format!("{ANSI_GREEN}░░{ANSI_RESET}"))},Tile::StaticBlock=>{output.push_str(&format!("{ANSI_YELLOW}▓▓{ANSI_RESET}"))},}}output.push_str(&format!("{ANSI_YELLOW}▐{ANSI_RESET}\n"));}output.push_str(&format!("{ANSI_YELLOW}▙{}▟{ANSI_RESET}","▄".repeat(BOARD_WIDTH*TILE_SIZE)));output}}
We call get_level_config on Level::One because we find ourselves in the new method of the board module and a new
board will always start with level one.
But this brings us to the next step: we need to store our current level somewhere so that we can increment it when we
finish a level:
We should probably display our level in the footer?
The issue is that we store our level value on our Game struct and the render method of the board is implemented on
our Board struct.
We would have to pass in our level in order to print it in that method.
I don’t like passing things around like this.
You end up drilling function arguments all over the place and quickly lose track plus strictly speaking the board
shouldn’t be concerned about things outside of its own domain which is the board only.
So let’s create a new method on the Game struct that wraps our render method from our Board.
That way we keep everything strictly within their own area and avoid having to pass arguments around.
OK what’s going on here?
We’re making use of the format macro and its superpowers.
We create a new String, then push a reference of what our format macro returns into it.
To increase the macro’s readability we named each item.
You can always do that but it’s mostly not needed since we often don’t use more than two or three items.
That explains the names but what is this: {footer:>width$}?
We basically tell our macro to take an amount of characters of width and then place our footer variable, right
aligned, into it.
That effectively guarantees us always the same space taken up by this variable as long as the size of the variable is
smaller than width.
How did we come up with the width, you may ask?
1 + BOARD_WIDTH * 2 + 1 - 1
^-- Border size
^-- Board width
^-- Each tile is two columns wide
^-- Border size
^-- Level number width
We could leave this illustration as a comment in our code… or we could just not use magic numbers and stick them
into named variables:
That’s at least readable and we may even understand what’s happening here in a few months from now when we come back to
this code.
But when we run this code we notice, as we move along the board, the output is eating its way downward through our
terminal buffer.
cargo run
warning: variants `Two` and `Three` are never constructed--> src/level.rs:9:2
|7| pub enum Level {
|-----variants in this enum8| One,
9| Two,
|^^^10| Three,
|^^^^^|= note: `Level` has a derived impl for the trait `Debug`, but this is 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.10s
Running `target/debug/beast`
▛▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▜▛▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▜▛▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▜▌◀▶▐▌░░ ░░ ░░░░▐▌░░ ░░▐▌░░ ░░ ░░▐▌▓▓▐▌▐▌░░ ░░░░ ░░▐▌░░▐▌░░ ░░ ░░▐▌▓▓▐▌░░▐▌░░ ░░▐▌░░ ░░▐▌▓▓░░▓▓▐▌░░▐▌░░ ░░▐▌░░▐▌░░▐▌▓▓░░▐▌░░▐▙▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▟
Level: 1
That’s because we have added our footer which increases the height of our board and have not adjusted our ANSI escape
sequence that moves our cursor up n lines.
We’re printing our sequence right now in our play function and now we would need to add a magic number to that
output but we just named all of those numbers nicely within our render function.
So let’s move this reset into our render function and clean it up:
Now our board renders again nicely, we display a footer with a right aligned level display and we kept each of our
render functions to their respective areas of concerns.
Now let’s stop the player from eating everything on the board.
Right now, when we move around the board, we just overwrite anything in our path with Tile::Empty which isn’t right.
Ideally we need to push blocks and stop at static blocks.
So what does our advance method look like right now?
Regardless of what the next tile is we move into, we just overwrite it with Tile::Player and when we leave the tile we
set it to Tile::Empty.
We’re probably going to have to match on the Tile we’re about to move into and then decide what to do there:
usecrate::{BOARD_HEIGHT,BOARD_WIDTH,Coord,Direction,Tile,board::Board};#[derive(Debug)]pubstructPlayer{position: Coord,}implPlayer{pubfnnew() -> Self{Self{position: Coord{column: 0,row: 0},}}pubfnadvance(&mutself,board: &mutBoard,direction: Direction){letmutnext_position=self.position;matchdirection{Direction::Up=>{ifnext_position.row>0{next_position.row-=1}},Direction::Right=>{ifnext_position.column<BOARD_WIDTH-1{next_position.column+=1}},Direction::Down=>{ifnext_position.row<BOARD_HEIGHT-1{next_position.row+=1}},Direction::Left=>{ifnext_position.column>0{next_position.column-=1}},}matchboard[&next_position]{Tile::Empty=>{board[&self.position]=Tile::Empty;self.position=next_position;board[&next_position]=Tile::Player;},Tile::Block=>{// TODO: we need to move this block and any behind it
},Tile::Player|Tile::StaticBlock=>{},}}}
Instead of manipulating self.position in place, we change a copy of it and then match the tile for that position from
our board.
When we find an Empty we do what we did before: set our last position to Empty, store our new position
and set the new position on the board to Player.
When we find that the next tile is of type Player or StaticBlock we do nothing because our player should be
prevented from walking into these.
But when we find a Block, we note we will push it, which we will implement in the next section.
For now when we’re walking around the board, we can bump into obstacles but never overwrite them or move them.
OK let’s think about what we expect to happen when we hit a block while moving around.
If we move the player to the right:
◀▶░░
We would expect the player to push the block to the right:
◀▶░░
But it’s entirely possible there are more blocks than just one:
◀▶░░░░░░
Or there is a static block at the end:
◀▶░░░░▓▓
Or the board ends:
◀▶░░░░▐
The problem is, we don’t know what’s beyond our next_position yet and we will have to search in a direction until we
find anything other than a Tile::Block.
We will need to loop in a given direction and calculate the next position for each iteration.
Best to move our next position logic into its own function so we can use it in our loop later:
usecrate::{BOARD_HEIGHT,BOARD_WIDTH,Coord,Direction,Tile,board::Board};#[derive(Debug)]pubstructPlayer{position: Coord,}implPlayer{pubfnnew() -> Self{Self{position: Coord{column: 0,row: 0},}}fnget_next_position(position: Coord,direction: Direction) -> Coord{letmutnext_position=position;matchdirection{Direction::Up=>{ifnext_position.row>0{next_position.row-=1}},Direction::Right=>{ifnext_position.column<BOARD_WIDTH-1{next_position.column+=1}},Direction::Down=>{ifnext_position.row<BOARD_HEIGHT-1{next_position.row+=1}},Direction::Left=>{ifnext_position.column>0{next_position.column-=1}},}next_position}pubfnadvance(&mutself,board: &mutBoard,direction: Direction){letnext_position=Self::get_next_position(self.position,direction);matchboard[&next_position]{Tile::Empty=>{board[&self.position]=Tile::Empty;self.position=next_position;board[&next_position]=Tile::Player;},Tile::Block=>{// TODO: we need to move this block and any behind it
},Tile::Player|Tile::StaticBlock=>{},}}}
All we did here is we moved our logic into a new private method called get_next_position and use that in our advance
method.
This all works but if we walk against the boundary of our board we will just get back the same coordinate as we passed
into the function and end up setting the same tile first to Empty and then to Player right after.
This isn’t just inefficient, it also makes it hard for us to know we bumped against the wall of the board.
So let’s change our function signature to return an Option and return None when we hit the board walls.
usecrate::{BOARD_HEIGHT,BOARD_WIDTH,Coord,Direction,Tile,board::Board};#[derive(Debug)]pubstructPlayer{position: Coord,}implPlayer{pubfnnew() -> Self{Self{position: Coord{column: 0,row: 0},}}fnget_next_position(position: Coord,direction: Direction) -> Option<Coord>{letmutnext_position=position;matchdirection{Direction::Up=>{ifnext_position.row>0{next_position.row-=1}else{returnNone;}},Direction::Right=>{ifnext_position.column<BOARD_WIDTH-1{next_position.column+=1}else{returnNone;}},Direction::Down=>{ifnext_position.row<BOARD_HEIGHT-1{next_position.row+=1}else{returnNone;}},Direction::Left=>{ifnext_position.column>0{next_position.column-=1}else{returnNone;}},}Some(next_position)}pubfnadvance(&mutself,board: &mutBoard,direction: Direction){ifletSome(next_position)=Self::get_next_position(self.position,direction){matchboard[&next_position]{Tile::Empty=>{board[&self.position]=Tile::Empty;self.position=next_position;board[&next_position]=Tile::Player;},Tile::Block=>{// TODO: we need to move this block and any behind it
},Tile::Player|Tile::StaticBlock=>{},}}}}
Now that we’re returning an Option, we can use
if let Some which is pretty cool.
We don’t have to match on the Option since we’re only interested in the Some case.
Now we can look into pushing a bunch of blocks, a “chain” of blocks, if you will.
What do we actually need to execute a “blockchain move”?
Your first instinct might be to take each block in the chain and move it, one by one.
Consider this scenario:
0
1
2
3
4
5
6
7
8
9
10
◀▶
░░
The changes required to move would be this:
0
1
2
3
4
5
6
7
8
9
10
◀▶
░░
Position at coordinate 0 has to be set to Empty
Position at coordinate 1 has to be set to Player
Position at coordinate 2 has to be set to Block
That makes sense.
But what is required to do the same for a longer chain?
0
1
2
3
4
5
6
7
8
9
10
◀▶
░░
░░
░░
░░
░░
░░
░░
░░
A successful push would look like this:
0
1
2
3
4
5
6
7
8
9
10
◀▶
░░
░░
░░
░░
░░
░░
░░
░░
Position at coordinate 0 has to be set to Empty
Position at coordinate 1 has to be set to Player
Position at coordinate 9 has to be set to Block
Even though the chain is much longer we’re still only doing 3 operations!
So, what we really need to execute the blockchain push, however long it might be, is:
The previous position the player was at
The new position the player is moving into
The first Empty tile at the end of the block chain we’re pushing
So as soon as we hit a Block, when calculating the next position, we need to start iterating over each tile in that
direction until we hit anything other than Block.
usecrate::{BOARD_HEIGHT,BOARD_WIDTH,Coord,Direction,Tile,board::Board};#[derive(Debug)]pubstructPlayer{position: Coord,}implPlayer{pubfnnew() -> Self{Self{position: Coord{column: 0,row: 0},}}fnget_next_position(position: Coord,direction: Direction) -> Option<Coord>{letmutnext_position=position;matchdirection{Direction::Up=>{ifnext_position.row>0{next_position.row-=1}else{returnNone;}},Direction::Right=>{ifnext_position.column<BOARD_WIDTH-1{next_position.column+=1}else{returnNone;}},Direction::Down=>{ifnext_position.row<BOARD_HEIGHT-1{next_position.row+=1}else{returnNone;}},Direction::Left=>{ifnext_position.column>0{next_position.column-=1}else{returnNone;}},}Some(next_position)}pubfnadvance(&mutself,board: &mutBoard,direction: Direction){ifletSome(next_position)=Self::get_next_position(self.position,direction){matchboard[&next_position]{Tile::Empty=>{board[&self.position]=Tile::Empty;self.position=next_position;board[&next_position]=Tile::Player;},Tile::Block=>{letmutcurrent_tile=Tile::Block;letmutcurrent_position=next_position;whilecurrent_tile==Tile::Block{ifletSome(next_position)=Self::get_next_position(current_position,direction){current_position=next_position;current_tile=board[¤t_position];matchcurrent_tile{Tile::Block=>{/* continue looking */},Tile::Empty=>{// we found the end
},Tile::StaticBlock|Tile::Player=>break,}}}},Tile::Player|Tile::StaticBlock=>{},}}}}
When we find a Block in the position the player is about to move into, we first store our tile and position into a
mutable variable.
Then we start a while loop that will go on until current_tile isn’t Tile::Block anymore.
Inside the loop we get the next tile for our given direction with our handy get_next_position method and re-assign our
current_tile to the tile we find in this iteration.
Now we can do things inside this loop, like matching on that tile.
If that tile is a Block we just continue our search.
If the tile is Empty we have found the end of the blockchain and can execute our push.
If the tile is StaticBlock or Player we break from our loop because we now know the blocks we’re trying to move
push up against an immovable object.
Now when we try to run the compiler, we are told about an issue:
cargo run
Compiling beast v0.1.0 (/Users/code/beast)
error[E0382]: use of moved value: `direction`--> src/player.rs:67:50
|51| pub fn advance(&mut self, board: &mut Board, direction: Direction) {
|---------move occurs because `direction` has type `Direction`, which does not implement the `Copy` trait52| if let Some(next_position) =
53| Self::get_next_position(self.position, direction)
|---------value moved here...67| Self::get_next_position(current_position, direction)
|^^^^^^^^^value used here after move|note: consider changing this parameter type in method `get_next_position` to borrow instead if owning the value isn't necessary
--> src/player.rs:15:51
|15| fn get_next_position(position: Coord, direction: Direction) -> Option<Coord> {
|-----------------in this method^^^^^^^^^this parameter takes ownership of the valueFor more information about this error, try `rustc --explain E0382`.error: could not compile `beast` (bin "beast") due to 1 previous error
We’re taking ownership of direction when we wrote the advance method’s function signature but the type Direction
isn’t a copy type.
Then we’re trying to pass direction by value to the get_next_position method twice which is also told to own it.
That’s no good so let’s fix that up.
We don’t need ownership, we can just pass direction by reference:
And our friend the compiler is happy again.
We haven’t done anything when we try to push a block, though.
Naming is hard which we’re now seeing in our code.
If the first step our player takes in our algorithm is called next_position, what do we call the steps within our
iterator?
We renamed our next_position to first_position because it’s the first tile we move into and we will need that
position when executing our push.
We name the variable to store positions within our loop next_position as that seems most fitting.
We could have also come up with a new name for the next_position variable inside our while loop but I can’t think of
a better name right now.
So we hit a block, seek until the end of the blockchain in the direction we’re going in until we find an empty tile.
Then we set our last position to Empty, our new position to Player, store our new position in our player instance
and set the first Empty tile we found at the end of the chain to Block.
But there is a bug!
When you push a bunch of blocks against the wall of our board, the game stops responding and eventually crashes.
That’s because we are doing nothing in our while loop when the if let statement is false, meaning when the next
position while we’re looking for the end of the blockchain, is outside the board.
Because we do nothing, the loop continues indefinitely.