My game engine for playing a new game: FEMTO


use std::collections::HashMap;
use rand::seq::{IndexedRandom, SliceRandom};

const fn wins_round(played: u8, other: u8) -> bool {
    (played > other && 2 * other > played) || 2 * played < other
}

/// The main engine function. Evaluates a list of cards that will produce the card with the most probable win outcome.
fn engine(my_cards: &[u8], opp_cards: &[u8], can_recurse: bool) -> u8 {
    let mut rng = rand::rng();
    let mut solution_list = HashMap::new();
    for card in my_cards {
        solution_list.insert(
            card,
            opp_cards
                .iter()
                .filter(|x| wins_round(*card, **x))
                .count()
        );
    }
    let main_solutions = solution_list
        .iter()
        .filter(|(_, value)| *value == solution_list.values().max().expect("solution_list should not be empty"))
        .map(|(key, _)| *key)
        .collect::<Vec<_>>();
    if main_solutions.len() > 1 && can_recurse {
        let mut solutions_that_win_round = Vec::new();
        let engine_value = engine(opp_cards, my_cards, false);
        for &main_solution in &main_solutions {
            if wins_round(*main_solution, engine_value) {
                solutions_that_win_round.push(*main_solution);
            }
        }
        let return_value = solutions_that_win_round.choose(&mut rng).unwrap_or(main_solutions.choose(&mut rng).expect("Ideally you shouldn't have an empty list like that."));
        *return_value
    } else {
        **main_solutions
            .choose(&mut rng)
            .expect("main_solutions should not be empty")
    }
}

fn main() {
    let mut rng = rand::rng();
    let mut buffer = String::new();
    let mut all_cards = [2, 3, 4, 5, 6, 7, 8, 10];
    all_cards.shuffle(&mut rng);
    let mut player_1_cards = all_cards[..4].to_vec();
    let mut player_2_cards = all_cards[4..].to_vec();
    let mut my_played_card: u8;
    let mut your_played_card: u8;
    let mut my_trophies = Vec::new();
    let mut your_trophies = Vec::new();
    while !player_1_cards.is_empty() && !player_2_cards.is_empty() {
        println!("My cards: {:?}", player_1_cards);
        println!("Your cards: {:?}", player_2_cards);
        println!("My trophies: {:?}", my_trophies);
        println!("Your trophies: {:?}", your_trophies);
        my_played_card = engine(&player_1_cards, &player_2_cards, true);
        your_played_card = loop {
            println!("What card do you wish to play?");
            std::io::stdin().read_line(&mut buffer).unwrap();
            let ans = buffer.trim().parse::<u8>();
            match ans {
                Ok(answer) => {
                    if !player_2_cards.contains(&answer) {
                        println!("You do not have {} in your cards. Do you wish to play another card?", answer);
                        continue;
                    }
                }
                Err(_) => {
                    println!("Uh-oh, your answer isn't a number!");
                    continue;
                }
            };
            break ans.unwrap();
        };
        buffer.clear();
        println!("I played: {}", my_played_card);
        println!("You played: {}", your_played_card);
        player_1_cards
            .remove(
                player_1_cards
                    .iter()
                    .position(|card| *card == my_played_card)
                    .expect(&format!("Uh-oh, my engine has a problem! It shouldn't give me {}", my_played_card))
            );
        player_2_cards
            .remove(
                player_2_cards
                    .iter()
                    .position(|card| *card == your_played_card)
                    .unwrap()
            );
        if wins_round(my_played_card, your_played_card) {
            println!("My card won!");
            let card_to_be_trophied = engine(&[my_played_card, your_played_card], &player_1_cards, false);
            if card_to_be_trophied == my_played_card {
                my_trophies.push(my_played_card);
                player_2_cards.push(your_played_card);
                println!("I choose to trophy {} and give you {}", my_played_card, your_played_card);
            } else if card_to_be_trophied == your_played_card {
                my_trophies.push(your_played_card);
                player_2_cards.push(my_played_card);
                println!("I choose to trophy {} and give you {}", your_played_card, my_played_card);
            } else {
                panic!("My engine is not working!");
            }
        } else {
            println!("Your card won!");
            let card_to_be_trophied = loop {
                println!("What card do you wish to add to your trophies?");
                std::io::stdin().read_line(&mut buffer).unwrap();
                let ans = buffer.trim().parse::<u8>();
                match ans {
                    Ok(answer) => {
                        if my_played_card != answer && your_played_card != answer {
                            println!("Uh-oh, neither of us played {}.", answer);
                            continue;
                        }
                    }
                    Err(_) => {
                        println!("Uh-oh, your answer isn't a number!");
                        continue;
                    }
                }
                break ans.unwrap();
            };
            buffer.clear();
            if card_to_be_trophied == your_played_card {
                your_trophies.push(your_played_card);
                player_1_cards.push(my_played_card);
                println!("You gave me {} and trophied {}", my_played_card, your_played_card);
            } else if card_to_be_trophied == my_played_card {
                your_trophies.push(my_played_card);
                player_1_cards.push(your_played_card);
                println!("You gave me {} and trophied {}", your_played_card, my_played_card);
            }
        }
    }
    println!("My trophies: {:?}", my_trophies);
    println!("Your trophies: {:?}", your_trophies);
    if my_trophies.len() > your_trophies.len() {
        println!("I won!");
    } else if your_trophies.len() > my_trophies.len() {
        println!("You won!");
    } else {
        println!("It's a draw.");
    }
}

Rules of Femto:

  1. Femto is a card game for two players; a Femto pack consists of eight cards numbered 2,3,4,5,6,7,8,10.

  2. The cards are shuffled and dealt, so each player gets four cards.

  3. In each round of play each player puts out one card, face down. The two cards are then turned face up.

  4. The round is won by the higher value card, unless the higher card is more than twice the value of the lower, in which case the lower card wins. e.g. 10 beats 8, 6 beats 5, 3 beats 10, 10 beats 5, …

  5. Whoever plays the winning card chooses one of the two cards and puts it, face up, on the table in front of him/her. The player of the losing card takes the remaining card and puts it back into his/her hand.

  6. More rounds are played until one player has no cards left.

  7. The winner is the player with the greater total value of cards in front of them at the end of the hand.

What’s your opinion?

There's a lot of good stuff here. You're taking slices as arguments which is using borrowing well.

You're iterating with a mixture of for .. in and iterator chains like filter and map. It reads well to me.

There's a lot going on in these functions, I'd think about writing unit tests and how engine and main could be split into smaller functions. It'll help overall readability to have more, smaller functions.

You've got recursion, always fun. I don't see any risk to blowing the stack by recursing too far (can_recurse makes sure of that!).

I do think it's a shame that Vec and Hashmap are created new when recursing, it's not a deal breaker because the recursion is at most once, but for learning purposes it might be interesting to try and reuse those allocations (i.e. create them outside of engine and pass them in, might be tricky, see how it goes).

Describing this as a “game engine” is funny because that is quite a loaded term: bevy, Godot, unreal and unity are “game engines”. This is more like a bot or AI (from the good old days when AIs and Bots were written by game developers and modders), or maybe like a chess engine, but not chess.

Also the game seems interesting! Did you come up with it yourself? Very cool.

2 Likes

If you run clippy (cargo clippy or in bacon) you will see a bunch of hints, e.g. to put variables in the format string - which I think is generally nicer when possible:

62 - println!("My cards: {:?}", player_1_cards);
62 + println!("My cards: {player_1_cards:?}");

Since you using the terminal, perhaps you could look at tui-cards.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.