3 - Player movement
Jun 4, 2021
tdd
rust
srl
|
Prev
|
Next
|
Code
Next on the list is the ability to move the player:
- [X] Render tiles
- [ ] Move player
- [ ] Run in terminal
In our game, the player will be able to move around using the arrow keys on the keyboard. This will come to fruition in the next step (Run in terminal), but we can lay most of the groundwork now.
The second failing test
So let’s write a test that will verify that the game can act on some kind of changing input.
Since we are only interested in the player here, we don’t need to add any walls or enemies.
#[test]
fn player_moves_according_to_input() {
let mut game = Game::new();
game.set_player_position(2, 1);
let mut input = InputSimulator::new();
let mut renderer = RenderingSpy::new(5, 4);
game.render(&mut renderer);
renderer.assert_frame(vec![
". . . . .",
". . @ . .",
". . . . .",
". . . . ."
]);
input.simulate_move_right();
game.tick(&input);
game.render(&mut renderer);
renderer.assert_frame(vec![
". . . . .",
". . . @ .",
". . . . .",
". . . . ."
]);
// assert other directions below
}
- Assert that the player is at the initial position
- Call the new
InputSimulator::simulate_move_right
to simulate to player’s intention to move to the right - Call the new
Game::tick
, passing an instance ofInputSimulator
- Assert that the player has indeed moved to the right, by rendering the game again and looking at the output.
Again very minimal design work: The tick method will take some kind of input object and use it to determine the player’s intentions.
Specifically, Game::tick
takes a trait called Input
:
pub fn tick<T: Input>(&mut self, input: &T) {
// does nothing yet
}
pub trait Input {
fn move_left(&self) -> bool;
fn move_right(&self) -> bool;
fn move_up(&self) -> bool;
fn move_down(&self) -> bool;
}
The tick code is expected to use these functions to determine if and where to move the player.
Simulating Input
We can’t use “real” input in this test, so we need a way to simulate it.
So, in addition to implementing the Input
trait, InputSimulator
has additonal functions like simulate_move_right
, to control the return value of the trait functions. This way we can trick the game code into thinking the player wants to move in a given direction.
To make the test compile (and fail) we implement an empty InputSimulator:
impl InputSimulator {
pub fn simulate_move_left(&mut self) {}
pub fn simulate_move_right(&mut self) {}
pub fn simulate_move_up(&mut self) {}
pub fn simulate_move_down(&mut self) {}
}
impl Input for InputSimulator {
fn move_left(&self) -> bool { false }
fn move_right(&self) -> bool { false }
fn move_up(&self) -> bool { false }
fn move_down(&self) -> bool { false }
}
We achieve test failure:
Because Game::tick
does nothing.
The complete InputSimulator
is not very interesting, but here is a snippet:
impl InputSimulator {
pub fn new() -> Self {
Self::default()
}
pub fn simulate_move_right(&mut self) {
self.simulating_move_right = true
}
// ...
}
impl Input for InputSimulator {
fn move_right(&self) -> bool {
self.simulating_move_right
}
/// ...
}
Querying input
The Game::tick
function is only slightly more interesting:
pub fn tick<T: Input>(&mut self, input: &T) {
let (mut player_x, mut player_y) = self.player;
if input.move_left() { player_x -= 1 }
if input.move_right() { player_x += 1 }
if input.move_up() { player_y -= 1 }
if input.move_down() { player_y += 1 }
self.set_player_position(player_x, player_y);
}
We are fast forwarding here - in reality I was only writing as much code as needed to make the initial test pass, which only asserts that the player can move to the right.
At any rate, the above code passes the test for all four directions. The full test tells the story of the moving player:
fn player_moves_according_to_input() {
let mut game = Game::new();
game.set_player_position(2, 1);
let mut input = InputSimulator::new();
let mut renderer = RenderingSpy::new(5, 4);
game.render(&mut renderer);
renderer.assert_frame(vec![
". . . . .",
". . @ . .",
". . . . .",
". . . . ."
]);
input.simulate_move_right();
game.tick(&input);
game.render(&mut renderer);
renderer.assert_frame(vec![
". . . . .",
". . . @ .",
". . . . .",
". . . . ."
]);
input.simulate_move_down();
game.tick(&input);
game.render(&mut renderer);
renderer.assert_frame(vec![
". . . . .",
". . . . .",
". . . @ .",
". . . . ."
]);
input.simulate_move_left();
game.tick(&input);
game.render(&mut renderer);
renderer.assert_frame(vec![
". . . . .",
". . . . .",
". . @ . .",
". . . . ."
]);
input.simulate_move_up();
game.tick(&input);
game.render(&mut renderer);
renderer.assert_frame(vec![
". . . . .",
". . @ . .",
". . . . .",
". . . . ."
]);
And that’s “Move player”:
- [X] Render tiles
- [X] Move player
- [ ] Run in terminal
In the next installment, input and rendering will finally see some “real” implementations, and we will roll everything into an executable. Should be fun!