2
\$\begingroup\$

This Rust code plays the Silver Dollar Game. I've put some effort into trying to utilize the appropriate parts of std.

This is the first program in the first real programming book I ever received. Practical Programs for the BBC Computer and Acorn ATOM

I have not factored any code out to structs or traits.

Listing of Cargo.toml:

[package]
name = "pp01_sd"
version = "0.1.0"
authors = ["Jonathan Watmough <[email protected]>"]

[dependencies]
rand = "0.3"

Listing of main.rs:

extern crate rand;

use rand::Rng;
use std::io;

// From "Practical Programs for the BBC Computer and Acorn ATOM"
// (c) David Johnson-Davies
//     Sigma Technical Press 1983
// Silver Dollar Game
// http://www.acornatom.nl/atom_plaatjes/boeken/pracprog/pp.htm

// In this version of the game the computer is one player; there are five coins identified with
// the letters A to E, and they move along a line of 32 dots. The starting position is chosen by
// the program at random, and the player moves a letter by typing the letter once for each place
// it is to be moved. The move is entered by typing 'return', and the computer will then make
// its move. The game continues in this way until one player has won.

fn score_position(gaps: &Vec<usize>) -> usize {
    let left_of_a = gaps[0];
    let b_to_c = gaps[2];
    let d_to_e = gaps[4];
    left_of_a ^ b_to_c ^ d_to_e
}

// choose a move to eliminate largest unmatched power of two gap
fn computer_move(gaps: &Vec<usize>, position_score: usize) -> (usize, usize) {
    let mut position_score = position_score >> 1;
    let mut move_spaces = 1_usize;
    while position_score > 0 {
        position_score = position_score >> 1;
        move_spaces = move_spaces << 1;
    }
    let (counter, _) = gaps.iter()
        .enumerate()
        .find(|&(_, gap)| *gap >= move_spaces)
        .unwrap();
    (counter, move_spaces)
}

fn won(gaps: &Vec<usize>) -> bool {
    let sum_of_gaps: usize = gaps.iter().take(5).sum();
    sum_of_gaps == 0
}

fn game_board(gaps: &Vec<usize>) -> String {
    fn push_dots(board: &mut String, count: usize) {
        for _ in 0..count {
            board.push('.');
        }
    }
    let mut board = String::new();
    push_dots(&mut board, gaps[0]);
    board.push('A');
    push_dots(&mut board, gaps[1]);
    board.push('B');
    push_dots(&mut board, gaps[2]);
    board.push('C');
    push_dots(&mut board, gaps[3]);
    board.push('D');
    push_dots(&mut board, gaps[4]);
    board.push('E');
    push_dots(&mut board, gaps[5]);
    board
}

fn user_move(gaps: &Vec<usize>) -> (usize, usize) {
    loop {
        println!("Enter the letter however many spaces to move and press enter.");
        let mut input = String::new();
        let _ = io::stdin().read_line(&mut input).expect("Failed to read from stdin.");
        let mut input: Vec<char> = input.chars()
            .filter({
                |ch| *ch >= 'a' && *ch <= 'e'
            })
            .collect();
        input.sort();
        let spaces = input.len();
        if spaces == 0 || input[0] != input[spaces - 1] {
            continue;
        }
        let move_counter = (input[0] as usize) - ('a' as usize);
        if spaces > gaps[move_counter] {
            continue;
        }
        return (move_counter, spaces);
    }
}

fn random_board(gaps: &mut Vec<usize>) {
    let mut counters = Vec::new();
    let mut rng = rand::thread_rng();
    loop {
        counters.push((rng.gen::<u8>() % 32) as i8);
        counters.sort();
        counters.dedup();
        if counters.len() == 5 {
            counters.push(32_i8);
            break;
        }
    }
    let mut previous = -1_i8;
    for counter in counters {
        gaps.push((counter - previous - 1) as usize);
        previous = counter;
    }
}

fn move_counter(gaps: &mut Vec<usize>, counter: usize, spaces: usize) {
    gaps[counter] -= spaces;
    gaps[counter + 1] += spaces;
}

fn main() {
    let mut gaps = Vec::new();
    random_board(&mut gaps);
    println!("{}", game_board(&gaps));
    loop {
        let (counter, spaces) = user_move(&gaps);
        move_counter(&mut gaps, counter, spaces);
        println!("{}", game_board(&gaps));
        if won(&gaps) {
            println!("You WIN!!!");
            break;
        }
        let score = score_position(&gaps);
        let (counter, spaces) = computer_move(&gaps, score);
        move_counter(&mut gaps, counter, spaces);
        println!("{} <-- computer moved", game_board(&gaps));
        if won(&gaps) {
            println!("COMPUTER is the WINNER!!!");
            break;
        }
    }
}

#[test]
fn test_game_board() {
    let gaps = vec![0, 0, 0, 0, 0, 27];
    let board = game_board(&gaps);
    println!("test_game_board: {:?}", board);
    assert_eq!(board, "ABCDE...........................");
    let gaps = vec![1, 1, 1, 1, 1, 22];
    let board = game_board(&gaps);
    println!("test_game_board: {:?}", board);
    assert_eq!(board, ".A.B.C.D.E......................");
}

#[test]
fn test_won() {
    let gaps = vec![0, 0, 0, 0, 0, 27];
    assert_eq!(won(&gaps), true);
    let gaps = vec![0, 2, 2, 2, 2, 19];
    assert_eq!(won(&gaps), false);
}

#[test]
fn test_score_position() {
    let gaps = vec![0, 0, 0, 0, 0, 27];
    assert_eq!(score_position(&gaps), 0);
    // odd 1 space
    let gaps = vec![0, 0, 1, 0, 0, 26];
    assert_eq!(score_position(&gaps), 1);
    // odd 2 space
    let gaps = vec![0, 0, 2, 0, 0, 25];
    assert_eq!(score_position(&gaps), 2);
    // even 2 spaces
    let gaps = vec![0, 0, 2, 0, 2, 23];
    assert_eq!(score_position(&gaps), 0);
}

#[test]
#[should_panic]
fn test_computer_move_panic() {
    // win - no move to be found
    let gaps = vec![0, 1, 2, 3, 4];
    assert_eq!(computer_move(&gaps, 1), (0, 0));
}

#[test]
fn test_computer_move_each_counter() {
    let gaps = vec![1, 0, 0, 0, 0, 26];
    assert_eq!(computer_move(&gaps, 1), (0, 1));
    let gaps = vec![0, 1, 0, 0, 0, 26];
    assert_eq!(computer_move(&gaps, 1), (1, 1));
    let gaps = vec![0, 0, 1, 0, 0, 26];
    assert_eq!(computer_move(&gaps, 1), (2, 1));
    let gaps = vec![0, 0, 0, 1, 0, 26];
    assert_eq!(computer_move(&gaps, 1), (3, 1));
    let gaps = vec![0, 0, 0, 0, 1, 26];
    assert_eq!(computer_move(&gaps, 1), (4, 1));
}

#[test]
fn test_computer_move_single_gaps() {
    let counters = vec![8, 0, 0, 0, 0, 19];
    assert_eq!(computer_move(&counters, 8), (0, 8));
    let counters = vec![0, 8, 0, 0, 0, 19];
    assert_eq!(computer_move(&counters, 8), (1, 8));
    let counters = vec![0, 0, 8, 0, 0, 19];
    assert_eq!(computer_move(&counters, 8), (2, 8));
    let counters = vec![0, 0, 0, 8, 0, 19];
    assert_eq!(computer_move(&counters, 8), (3, 8));
    let counters = vec![0, 0, 0, 0, 8, 19];
    assert_eq!(computer_move(&counters, 8), (4, 8));
}

#[test]
fn test_100_random_boards() {
    for _ in 0..10 {
        let mut counters = Vec::new();
        random_board(&mut counters);
        println!("board: {:?}", game_board(&counters));
    }
}
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$
  1. Prefer &[T] over &Vec<T>. Note that tests can now be arrays instead of allocating on the heap needlessly.

  2. The program is not documented enough; it's unclear to me why you'd XOR values to generate a position, or why powers of two are important. When documenting a function, consider using Rustdoc formatting.

  3. Prefer expect over unwrap, when it breaks you will be happier. Think about returning a Result to denote failure cases.

  4. iter::all is easier to understand when testing if the game is won, especially since there's no negative values.

  5. The multiline closure passed to filter is very non-idiomatic. It doesn't actually need to be multiline. When you do have multiline, the braces come after the closure arguments. When dereferencing, you'd put an & in the pattern binding whenever they are equivalent.

    .filter(|&ch| {
         ch >= 'a' && ch <= 'e'
    })
    
  6. Collect to a Vec<_>; there's no need to specify the inner type again.

  7. Use a set instead of a Vec if you want unique values. If you care about sorted values, a BTreeSet is a perfect match.

  8. There's no reason to take a mutable Vec if it's always going to be a fresh vector; just allocate it inside the function and return it.

  9. It's not idiomatic to do 12_u8; normally the type just attaches to the number: 12u8. Either way, the explicit type isn't needed as it can be inferred.

  10. Prefer &mut [T] over &mut Vec<T> if you are not adding to or removing from the Vec.

  11. assert_eq!(..., true) should just be !assert(...) or flipped for false.

  12. Tests wthout assertions are questionable at best.
  13. Tests with comments dividing them up should be separate tests; when they fail, the test running will tell you exactly what failed by the name.
extern crate rand;

use rand::Rng;
use std::io;
use std::collections::BTreeSet;

// In this version of the game the computer is one player; there are five coins identified with
// the letters A to E, and they move along a line of 32 dots. The starting position is chosen by
// the program at random, and the player moves a letter by typing the letter once for each place
// it is to be moved. The move is entered by typing 'return', and the computer will then make
// its move. The game continues in this way until one player has won.

fn score_position(gaps: &[usize]) -> usize {
    let left_of_a = gaps[0];
    let b_to_c = gaps[2];
    let d_to_e = gaps[4];
    left_of_a ^ b_to_c ^ d_to_e
}

// choose a move to eliminate largest unmatched power of two gap
fn computer_move(gaps: &[usize], position_score: usize) -> (usize, usize) {
    let mut position_score = position_score >> 1;
    let mut move_spaces = 1;
    while position_score > 0 {
        position_score = position_score >> 1;
        move_spaces = move_spaces << 1;
    }
    let (counter, _) = gaps.iter()
        .enumerate()
        .find(|&(_, &gap)| gap >= move_spaces)
        .unwrap();
    (counter, move_spaces)
}

fn won(gaps: &[usize]) -> bool {
    gaps.iter().take(5).all(|&x| x == 0)
}

fn game_board(gaps: &[usize]) -> String {
    fn push_dots(board: &mut String, count: usize) {
        for _ in 0..count {
            board.push('.');
        }
    }
    let mut board = String::new();
    push_dots(&mut board, gaps[0]);
    board.push('A');
    push_dots(&mut board, gaps[1]);
    board.push('B');
    push_dots(&mut board, gaps[2]);
    board.push('C');
    push_dots(&mut board, gaps[3]);
    board.push('D');
    push_dots(&mut board, gaps[4]);
    board.push('E');
    push_dots(&mut board, gaps[5]);
    board
}

fn user_move(gaps: &[usize]) -> (usize, usize) {
    loop {
        println!("Enter the letter however many spaces to move and press enter.");
        let mut input = String::new();
        let _ = io::stdin().read_line(&mut input).expect("Failed to read from stdin.");
        let mut input: Vec<_> = input.chars()
            .filter(|&ch| ch >= 'a' && ch <= 'e')
            .collect();
        input.sort();
        let spaces = input.len();
        if spaces == 0 || input[0] != input[spaces - 1] {
            continue;
        }
        let move_counter = (input[0] as usize) - ('a' as usize);
        if spaces > gaps[move_counter] {
            continue;
        }
        return (move_counter, spaces);
    }
}

fn random_board() -> Vec<usize> {
    let mut counters = BTreeSet::new();
    let mut rng = rand::thread_rng();
    while counters.len() < 5 {
        counters.insert((rng.gen::<u8>() % 32) as i8);
    }
    counters.insert(32);

    let mut gaps = Vec::new();
    let mut previous = -1;
    for counter in counters {
        gaps.push((counter - previous - 1) as usize);
        previous = counter;
    }
    gaps
}

fn move_counter(gaps: &mut [usize], counter: usize, spaces: usize) {
    gaps[counter] -= spaces;
    gaps[counter + 1] += spaces;
}

fn main() {
    let mut gaps = random_board();
    println!("{}", game_board(&gaps));
    loop {
        let (counter, spaces) = user_move(&gaps);
        move_counter(&mut gaps, counter, spaces);
        println!("{}", game_board(&gaps));
        if won(&gaps) {
            println!("You WIN!!!");
            break;
        }
        let score = score_position(&gaps);
        let (counter, spaces) = computer_move(&gaps, score);
        move_counter(&mut gaps, counter, spaces);
        println!("{} <-- computer moved", game_board(&gaps));
        if won(&gaps) {
            println!("COMPUTER is the WINNER!!!");
            break;
        }
    }
}

#[test]
fn test_game_board() {
    let gaps = [0, 0, 0, 0, 0, 27];
    let board = game_board(&gaps);
    println!("test_game_board: {:?}", board);
    assert_eq!(board, "ABCDE...........................");
    let gaps = [1, 1, 1, 1, 1, 22];
    let board = game_board(&gaps);
    println!("test_game_board: {:?}", board);
    assert_eq!(board, ".A.B.C.D.E......................");
}

#[test]
fn test_won() {
    let gaps = [0, 0, 0, 0, 0, 27];
    assert!(won(&gaps));
    let gaps = [0, 2, 2, 2, 2, 19];
    assert!(!won(&gaps));
}

#[test]
fn test_score_position() {
    let gaps = [0, 0, 0, 0, 0, 27];
    assert_eq!(score_position(&gaps), 0);
    // odd 1 space
    let gaps = [0, 0, 1, 0, 0, 26];
    assert_eq!(score_position(&gaps), 1);
    // odd 2 space
    let gaps = [0, 0, 2, 0, 0, 25];
    assert_eq!(score_position(&gaps), 2);
    // even 2 spaces
    let gaps = [0, 0, 2, 0, 2, 23];
    assert_eq!(score_position(&gaps), 0);
}

#[test]
#[should_panic]
fn test_computer_move_panic() {
    // win - no move to be found
    let gaps = [0, 1, 2, 3, 4];
    assert_eq!(computer_move(&gaps, 1), (0, 0));
}

#[test]
fn test_computer_move_each_counter() {
    let gaps = [1, 0, 0, 0, 0, 26];
    assert_eq!(computer_move(&gaps, 1), (0, 1));
    let gaps = [0, 1, 0, 0, 0, 26];
    assert_eq!(computer_move(&gaps, 1), (1, 1));
    let gaps = [0, 0, 1, 0, 0, 26];
    assert_eq!(computer_move(&gaps, 1), (2, 1));
    let gaps = [0, 0, 0, 1, 0, 26];
    assert_eq!(computer_move(&gaps, 1), (3, 1));
    let gaps = [0, 0, 0, 0, 1, 26];
    assert_eq!(computer_move(&gaps, 1), (4, 1));
}

#[test]
fn test_computer_move_single_gaps() {
    let counters = [8, 0, 0, 0, 0, 19];
    assert_eq!(computer_move(&counters, 8), (0, 8));
    let counters = [0, 8, 0, 0, 0, 19];
    assert_eq!(computer_move(&counters, 8), (1, 8));
    let counters = [0, 0, 8, 0, 0, 19];
    assert_eq!(computer_move(&counters, 8), (2, 8));
    let counters = [0, 0, 0, 8, 0, 19];
    assert_eq!(computer_move(&counters, 8), (3, 8));
    let counters = [0, 0, 0, 0, 8, 19];
    assert_eq!(computer_move(&counters, 8), (4, 8));
}

#[test]
fn test_100_random_boards() {
    for _ in 0..10 {
        let counters = random_board();
        println!("board: {:?}", game_board(&counters));
    }
}

You didn't say why you didn't implement any structs or traits. I would at least create a type for the board, implement inherent methods and the From and Display traits. Implementing Display also makes it more efficient to print the board.

There's also a lot of name mismatches in tests: sometimes it's called gaps, sometimes counters, etc.

extern crate rand;

use rand::Rng;
use std::{fmt, io};
use std::collections::BTreeSet;

// In this version of the game the computer is one player; there are five coins identified with
// the letters A to E, and they move along a line of 32 dots. The starting position is chosen by
// the program at random, and the player moves a letter by typing the letter once for each place
// it is to be moved. The move is entered by typing 'return', and the computer will then make
// its move. The game continues in this way until one player has won.

struct Board {
    gaps: Vec<usize>,
}

impl Board {
    fn random() -> Self {
        let mut counters = BTreeSet::new();
        let mut rng = rand::thread_rng();
        while counters.len() < 5 {
            counters.insert((rng.gen::<u8>() % 32) as i8);
        }
        counters.insert(32);

        let mut gaps = Vec::new();
        let mut previous = -1;
        for counter in counters {
            gaps.push((counter - previous - 1) as usize);
            previous = counter;
        }

        Board {
            gaps: gaps,
        }
    }

    fn score_position(&self) -> usize {
        let left_of_a = self.gaps[0];
        let b_to_c = self.gaps[2];
        let d_to_e = self.gaps[4];
        left_of_a ^ b_to_c ^ d_to_e
    }

    // choose a move to eliminate largest unmatched power of two gap
    fn computer_move(&self, position_score: usize) -> (usize, usize) {
        let mut position_score = position_score >> 1;
        let mut move_spaces = 1;
        while position_score > 0 {
            position_score = position_score >> 1;
            move_spaces = move_spaces << 1;
        }
        let (counter, _) = self.gaps.iter()
            .enumerate()
            .find(|&(_, &gap)| gap >= move_spaces)
            .unwrap();
        (counter, move_spaces)
    }

    fn won(&self) -> bool {
        self.gaps.iter().take(5).all(|&x| x == 0)
    }

    fn user_move(&self) -> (usize, usize) {
        loop {
            println!("Enter the letter however many spaces to move and press enter.");
            let mut input = String::new();
            let _ = io::stdin().read_line(&mut input).expect("Failed to read from stdin.");
            let mut input: Vec<_> = input.chars()
                .filter(|&ch| ch >= 'a' && ch <= 'e')
                .collect();
            input.sort();
            let spaces = input.len();
            if spaces == 0 || input[0] != input[spaces - 1] {
                continue;
            }
            let move_counter = (input[0] as usize) - ('a' as usize);
            if spaces > self.gaps[move_counter] {
                continue;
            }
            return (move_counter, spaces);
        }
    }

    fn move_counter(&mut self, counter: usize, spaces: usize) {
        self.gaps[counter] -= spaces;
        self.gaps[counter + 1] += spaces;
    }
}

impl<T> From<T> for Board
    where T: AsRef<[usize]>
{
    fn from(other: T) -> Self {
        Board {
            gaps: other.as_ref().into(),
        }
    }
}

impl fmt::Display for Board {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        fn push_dots(f: &mut fmt::Formatter, count: usize) -> fmt::Result {
            for _ in 0..count {
                write!(f, ".")?;
            }
            Ok(())
        }

        push_dots(f, self.gaps[0])?;
        write!(f, "A")?;
        push_dots(f, self.gaps[1])?;
        write!(f, "B")?;
        push_dots(f, self.gaps[2])?;
        write!(f, "C")?;
        push_dots(f, self.gaps[3])?;
        write!(f, "D")?;
        push_dots(f, self.gaps[4])?;
        write!(f, "E")?;
        push_dots(f, self.gaps[5])?;

        Ok(())
    }
}
fn main() {
    let mut board = Board::random();
    println!("{}", board);
    loop {
        let (counter, spaces) = board.user_move();
        board.move_counter(counter, spaces);
        println!("{}", board);
        if board.won() {
            println!("You WIN!!!");
            break;
        }
        let score = board.score_position();
        let (counter, spaces) = board.computer_move(score);
        board.move_counter(counter, spaces);
        println!("{} <-- computer moved", board);
        if board.won() {
            println!("COMPUTER is the WINNER!!!");
            break;
        }
    }
}

#[test]
fn test_game_board() {
    let board = Board::from([0, 0, 0, 0, 0, 27]);
    let board_str = format!("{}", board);
    println!("test_game_board: {:?}", board_str);
    assert_eq!(board_str, "ABCDE...........................");

    let board = Board::from([1, 1, 1, 1, 1, 22]);
    let board_str = format!("{}", board);
    println!("test_game_board: {:?}", board_str);
    assert_eq!(board_str, ".A.B.C.D.E......................");
}

#[test]
fn test_won() {
    let board = Board::from([0, 0, 0, 0, 0, 27]);
    assert!(board.won());
    let board = Board::from([0, 2, 2, 2, 2, 19]);
    assert!(!board.won());
}

#[test]
fn test_score_position() {
    let board = Board::from([0, 0, 0, 0, 0, 27]);
    assert_eq!(board.score_position(), 0);
    // odd 1 space
    let board = Board::from([0, 0, 1, 0, 0, 26]);
    assert_eq!(board.score_position(), 1);
    // odd 2 space
    let board = Board::from([0, 0, 2, 0, 0, 25]);
    assert_eq!(board.score_position(), 2);
    // even 2 spaces
    let board = Board::from([0, 0, 2, 0, 2, 23]);
    assert_eq!(board.score_position(), 0);
}

#[test]
#[should_panic]
fn test_computer_move_panic() {
    // win - no move to be found
    let board = Board::from([0, 1, 2, 3, 4]);
    assert_eq!(board.computer_move(1), (0, 0));
}

#[test]
fn test_computer_move_each_counter() {
    let board = Board::from([1, 0, 0, 0, 0, 26]);
    assert_eq!(board.computer_move(1), (0, 1));
    let board = Board::from([0, 1, 0, 0, 0, 26]);
    assert_eq!(board.computer_move(1), (1, 1));
    let board = Board::from([0, 0, 1, 0, 0, 26]);
    assert_eq!(board.computer_move(1), (2, 1));
    let board = Board::from([0, 0, 0, 1, 0, 26]);
    assert_eq!(board.computer_move(1), (3, 1));
    let board = Board::from([0, 0, 0, 0, 1, 26]);
    assert_eq!(board.computer_move(1), (4, 1));
}

#[test]
fn test_computer_move_single_gaps() {
    let board = Board::from([8, 0, 0, 0, 0, 19]);
    assert_eq!(board.computer_move(8), (0, 8));
    let board = Board::from([0, 8, 0, 0, 0, 19]);
    assert_eq!(board.computer_move(8), (1, 8));
    let board = Board::from([0, 0, 8, 0, 0, 19]);
    assert_eq!(board.computer_move(8), (2, 8));
    let board = Board::from([0, 0, 0, 8, 0, 19]);
    assert_eq!(board.computer_move(8), (3, 8));
    let board = Board::from([0, 0, 0, 0, 8, 19]);
    assert_eq!(board.computer_move(8), (4, 8));
}

#[test]
fn test_100_random_boards() {
    for _ in 0..10 {
        let board = Board::random();
        println!("board: {}", board);
    }
}
\$\endgroup\$
1
  • \$\begingroup\$ Awesome comments. I will take these and work through my own code, then compare to your rewrite. The XOR is a Nim sum. \$\endgroup\$ Commented Dec 17, 2016 at 12:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.