3. A map
Chapter 2 clamped @ to the screen edge. This chapter replaces that with an actual level: a walled
room built once from a plain string, with movement blocked by the walls in it instead of by the edge
of the grid.
Building the level
Grid::from_charmap turns a multi-line string into a Grid, calling a closure once per character
to decide that character’s tile. It’s the same helper the 11_sokoban gallery example (a complete
small game) builds its level from:
/// The level: a single walled room. `#` is a wall, everything else (just `.` here) is floor.
const LEVEL: &str = "\
#############
#............#
#............#
#............#
#............#
#............#
#############";
/// Builds the level's [`Grid`]: one [`Tile`] per character, walls and floor each with their own
/// style so they read apart from the player drawn on top of them.
fn build_level() -> Grid {
let wall = Style::default();
let floor = Style::default();
Grid::from_charmap(LEVEL, |c| match c {
'#' => Tile::new('#', wall),
_ => Tile::new('.', floor),
})
}
Movement and collision
try_move no longer clamps to the screen; it asks is_wall whether the destination cell is
walkable before committing to it. Out-of-bounds counts as a wall too, so a level doesn’t need a
border check of its own as long as it’s, well, walled in:
/// Whether `(x, y)` is a wall, or outside the level entirely (out of bounds is treated the
/// same as a wall: nothing to walk onto there).
fn is_wall(&self, x: i32, y: i32) -> bool {
let (Ok(x), Ok(y)) = (u16::try_from(x), u16::try_from(y)) else {
return true;
};
self.level
.tile(0, (x, y))
.is_none_or(|tile| tile.glyph() == '#')
}
/// Moves the player by one cell in `(dx, dy)`, a no-op if the destination is a wall.
fn try_move(&mut self, dx: i32, dy: i32) {
let (nx, ny) = (i32::from(self.x) + dx, i32::from(self.y) + dy);
if self.is_wall(nx, ny) {
return;
}
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
{
self.x = nx as u16;
self.y = ny as u16;
}
}
/// Drains pending input: moves on an arrow key, returns `false` if the user asked to quit.
fn handle_events<B: Backend>(&mut self, term: &mut Terminal<B>) -> bool {
for event in term.drain_events() {
match event {
Event::Key(key) => match key.code {
KeyCode::Char('q') | KeyCode::Escape => return false,
KeyCode::Left => self.try_move(-1, 0),
KeyCode::Right => self.try_move(1, 0),
KeyCode::Up => self.try_move(0, -1),
KeyCode::Down => self.try_move(0, 1),
_ => {}
},
Event::Close => return false,
_ => {}
}
}
true
}
Drawing composes the two pieces from the last two chapters: Surface::blit stamps the whole level
grid onto the screen in one call, then @ is drawn on top of it the same way it always has been.
Running it
cargo run --example 03_a_map --features crossterm
cargo run --example 03_a_map --features software
cargo run --example 03_a_map # headless fallback, prints a few frames to stdout
Arrow keys move @, blocked by the walls (#). q or Escape quits.
This is the minimum viable tutorial: the how-to section covers what comes next task by task,
including drawing a status bar or log with retroglyph-ui widgets (see
Draw a panel and Handle a click).