The tutorial · Step 8

Polish

Play this step Its whole project

The game is whole. This step adds what makes it pleasant: a moves counter, and a way for players to change their keys. Both are short, because the work's already been done, by you or by the engine.

The board with Moves: 1 at the top right.
FileWhat changes
crates/game/src/board.rsShows the moves
crates/game/src/options.rsNew: which options to offer
crates/game/src/levels.rsAn Options item on the menu
crates/game/src/stages.rsNames the two new screens
crates/game/src/lib.rspub mod options;
crates/game/files/en.ronWords for all of it
crates/game/tests/board.rsTests of both

A moves counter

How many moves has the player made? You don't need a new variable, because you already keep that: it's how many commands are in the history. And undo already takes one away, and a walk into a wall already doesn't count, as it's never recorded.

Game idea: a second copy of a fact is a chance for the two to disagree. If you can work a thing out from what you already hold, do that.

Add a moves field to BoardView, as pub moves: String,, and fill it in view:

            moves: ctx.text_with("board.moves", &[("n", &self.history.len())]),

Rust: ("n", &self.history.len()) is a tuple, two values side by side, and &[...] is a borrowed list of those. This fills in {n} in the text.

Then draw it at the top right, in draw, by working out where it must start from how long it is:

        let width = i32::try_from(view.moves.chars().count()).unwrap_or(0);
        let right = i32::from(buf.width()) - width - 1;
        buf.print(right, 0, &view.moves, c(EngineColor::Text), bg);

Its words go in files/en.ron, as "board.moves": "Moves: {n}",.

And here's a nice thing about measuring. Use longer words, and it stays against the edge:

In crates/game/files/en.ron:

- "board.moves": "Moves: {n}",
+ "board.moves": "Steps taken: {n}",
The top right says Moves: 1.
Before
The top right says Steps taken: 1, and stays against the edge.
After

Or centre it, by changing the sum:

In crates/game/src/board.rs:

- let right = i32::from(buf.width()) - width - 1;
+ let right = (i32::from(buf.width()) - width) / 2;
The moves counter is at the top right.
Before
The moves counter is centred at the top.
After

The test presses keys, and reads the screen's text. It also shows what the counter doesn't count:

#[test]
fn the_moves_counter_counts_moves_and_undo_takes_one_back() {
    let mut h = on_board();
    assert!(h.snapshot().contains("Moves: 0"), "{}", h.snapshot());
    // Up, then a wall: the refused step is no move.
    h.keys("Up Up");
    assert!(h.snapshot().contains("Moves: 1"), "{}", h.snapshot());
    h.keys("Down u");
    assert!(h.snapshot().contains("Moves: 1"), "{}", h.snapshot());
}

Options and rebinding

Letting players change their keys is a lot of work to get right: reading the new key, refusing keys already taken, saving them, restoring the defaults. It's also just the same in every game, which makes it perfect for an engine. You only say what you want.

The options screen: Frame rate, Key bindings and Restore defaults.

Make crates/game/src/options.rs, which lists the rows you want, and every action a player can rebind:

use shuyi::prelude::*;
use shuyi::ui::screens::key_bindings::{KeyBindingsScreen, KeyRow, RowGroup};
use shuyi::ui::screens::options::{self, OptionRow, RowAction, RowValue};

use crate::Data;
use crate::action::Action;
/// The Key bindings screen's rows: every action must keep a key.
pub const KEY_ROWS: [KeyRow<Action>; 7] = [
    (Action::Up, "key_bindings.action.up", RowGroup::Required),
    (Action::Down, "key_bindings.action.down", RowGroup::Required),
    (Action::Left, "key_bindings.action.left", RowGroup::Required),
    (
        Action::Right,
        "key_bindings.action.right",
        RowGroup::Required,
    ),
    (
        Action::Confirm,
        "key_bindings.action.confirm",
        RowGroup::Required,
    ),
    (
        Action::Cancel,
        "key_bindings.action.cancel",
        RowGroup::Required,
    ),
    (Action::Undo, "key_bindings.action.undo", RowGroup::Required),
];

/// The options' rows: the frame rate, the keys, and Restore defaults.
pub fn option_rows(_: &Ctx<Data>) -> Vec<OptionRow<Data>> {
    vec![
        OptionRow::frame_cap(),
        OptionRow::new(
            "options.row.key_bindings",
            RowValue::action(
                |_| None,
                |ctx| RowAction::Open(Box::new(KeyBindingsScreen::new(ctx, &KEY_ROWS))),
                |_| options::HELP_OPEN,
            ),
        )
        .starting_group(),
        OptionRow::restore_defaults().starting_group(),
    ]
}

KEY_ROWS lists each action once, with the text key of its name. RowGroup::Required says it must keep a key.

option_rows builds the rows of the options screen. There's a frame rate, then a row that opens the Key bindings screen, then Restore defaults. The engine makes the common ones for you (OptionRow::frame_cap()). The engine has more that you could add, like text speed and volume, when your game has those things.

Rust: the two |ctx| ... and |_| ... are closures again, this time saying what the row shows and what it does when chosen. And .starting_group() adds a gap above that row.

The menu gets an item for it. In levels.rs, Choice gains Options, and the items go on as one list:

    /// The menu's items: a text key and a value each.
    pub fn menu(&self, ctx: &Ctx<Data>) -> Menu<Choice> {
        let levels = ctx.game.own.levels.len();
        let items = (0..levels).map(|n| {
            MenuItem::keyed("levels.level")
                .with_arg("n", n + 1)
                .with_enabled(n < self.open)
                .with_value(Choice::Level(n))
        });
        let options = MenuItem::keyed("levels.options").with_value(Choice::Options);
        Menu::new(items.chain([options]).collect()).focused(self.focus)
    }

items.chain([options]) joins two sequences, so the Options item comes after the levels. And in update, add an arm for it:

                Some(MenuEvent::Chosen(Choice::Options)) => {
                    return Transition::Push(Box::new(OptionsScreen::new(option_rows)));
                }

Rust: OptionsScreen::new(option_rows) passes the function itself, by name, without calling it: note there are no () after option_rows. The screen calls it when it needs its rows.

Notice you didn't have to change Choice's users in a hundred places. The compiler told you which match wanted a new arm, and there was only this one.

Next, screens in stages.rs names both new screens, so cargo shuyi check --target can check that their controls fit each machine:

/// Every screen the game can show past the title, for the check that its
/// controls fit each target.
pub fn screens(ctx: &mut Ctx<Data>) -> Vec<Box<dyn Screen<Data>>> {
    vec![
        Box::new(LevelMenu::new(ctx)),
        Box::new(Board::new(ctx, 0)),
        Box::new(OptionsScreen::new(option_rows)),
        Box::new(KeyBindingsScreen::new(ctx, &KEY_ROWS)),
    ]
}

Their text goes in files/en.ron. The engine's screens ask for a lot of words, one key each, so a game can be translated in full. You'll want all of them, and there's no need to type them: take them from this step's en.ron, as they are. If you miss one, a debug build stops and names the key it couldn't find.

#[test]
fn the_options_open_from_the_menu_and_reach_the_key_bindings() {
    let mut h = game::game().harness();
    let menu = "Enter Up";
    h.keys(menu);
    h.keys("Enter");
    assert_eq!(h.screens(), ["title", "levels", "options"]);
    h.keys("Down Enter");
    assert_eq!(h.screens(), ["title", "levels", "options", "key_bindings"]);
}

The Up in that test wraps round from the top of the menu to its last item, Options.

Run it, and open Options, then Key bindings:

The Key bindings screen: a row for each action, with its keys in columns.

Now change your Undo key. Then go back to a level, and read the help line. It names your new key, and you didn't do anything to make that happen. Every line of text has said {Undo} all along, since step 5. This is why text never names a key.

What you learned

Next: the game can test itself.