4 - Terminal output
Jun 9, 2021
tdd
rust
srl
|
Prev
|
Next
|
Code
The last item on the list is running the game, such as it is, in the terminal:
- [X] Render tiles
- [X] Move player
- [ ] Run in terminal
Without further ado, here is the main function:
const GAME_WIDTH: usize = 40;
const GAME_HEIGHT: usize = 20;
fn main() -> std::io::Result<()> {
let mut game = Game::new();
game.add_enemies(&vec![(1, 1), (4, 2), (2, 3)]);
game.add_walls(&vec![
(1, 0), (2, 0), (3, 1), (1, 2),
(0, GAME_HEIGHT as u32 - 1),
(GAME_WIDTH as u32 - 1, GAME_HEIGHT as u32 - 1)]);
game.set_player_position(3, 2);
let mut renderer = TerminalRenderer::new(GAME_WIDTH, GAME_HEIGHT);
let mut input = TerminalInput::new();
let mut term = Term::buffered_stdout();
while !input.quit_game() {
game.render(&mut renderer);
renderer.flush(&mut term);
input.on_key(term.read_key()?);
game.tick(&input);
term.clear_last_lines(GAME_HEIGHT - 1)?;
}
term.clear_to_end_of_screen()?;
}
That almost looks like the first test, right? Instantiate the game, add stuff, grab a renderer and an input provider, tick and render, except this time do it in a loop. Granted, there are some minor differences here, mostly owing to peculiarities having to do with the game using the terminal as input and output.
The key pieces to the puzzle are the production implementations of the Renderer
and Input
traits. Let’s have a look.
Rendering to the terminal
Turns out that TerminalRenderer::render_at
is an exact copy of RenderingSpy::render_at
fn render_at(&mut self, x: u32, y: u32, object_type: ObjectType) {
let ch = match object_type {
Wall => { '#' }
Player => { '@' }
Enemy => { 'E' }
};
self.frame[y as usize][x as usize] = ch;
}
Should we be concerned? Not overly. It is not surprising, since we are dealing with strings in both cases. We make a note clean up this duplication.
TerminalRenderer
also has a flush function:
pub fn flush<T: Write>(&mut self, write: &mut T) {
write.write(self.frame_as_string().as_bytes()).unwrap();
write.flush().unwrap();
}
We write the whole frame as a string to a Write
object, which is what Term::buffered_stdout()
gives us. And since it is buffered, we call its own flush function as well. The buffering is not strictly necessary, but it prevents intermittent flickering of the output.
Reading input from the terminal
Let’s look at TerminalInput
in its entirety:
use console::Key;
use crate::input::Input;
pub struct TerminalInput {
pressed_key: Key
}
impl TerminalInput {
pub fn new() -> Self {
Self {pressed_key: Key::Unknown}
}
pub fn on_key(&mut self, key: Key) {
self.pressed_key = key;
}
}
impl Input for TerminalInput {
fn move_left(&self) -> bool {
self.pressed_key == Key::ArrowLeft
}
fn move_right(&self) -> bool {
self.pressed_key == Key::ArrowRight
}
fn move_up(&self) -> bool {
self.pressed_key == Key::ArrowUp
}
fn move_down(&self) -> bool {
self.pressed_key == Key::ArrowDown
}
fn quit_game(&self) -> bool {
self.pressed_key == Key::Escape
}
}
Really nothing to write home about. That extra on_key
function is used to pass in the key returned from the terminal library’s read_key
function (which is blocking until a key is pressed).
Where do we go from here
Wait, not so fast, Wolfgang, I hear you say. Yes yes.
Hi, I am Wolfgang, and I wrote the main function without writing any tests.
True, a case can be made for taking that chunk of main code and writing a big end to end test for it. I am not against that, but end-to-end testing is not the theme of this project. Plus I expect the main loop to remain stable for a good while.
I also did not write tests for the trait implementations. I got too excited. What I usually do in such cases is to write tests when I need to touch that code again.
Anyway, in the coming episodes we will deal with collison, and we will start doing some random dungeon generation. Stay tuned? And in the meantime, look at the ugly duckling go: