4
\$\begingroup\$

I watched a YouTube video (https://www.youtube.com/watch?v=z-0-bbc80JM) talking about the power of enums in rust for data modelling. In the video, there's an example of a state machine of a simple Mario game using enum and match to define possible states and transitions.

I implemented the same code from the video and tried to add new things to it, and I got stuck because I don't know a good way to add new attributes to the transition. For example, I wanted to make some items on the game be able to revive the player, and items on the game are the transition between states, so for example if you pick up an flower it should be able to change your state to an FireMario, and an feather to the CapeMario, but only the Feather can revive the player.

Here is a code example:

#[derive(Debug)]
enum TransitionProperty {
    Revive,
    None,
}

#[derive(Debug)]
enum Transition {
    Mushroom(TransitionProperty),
    Flower(TransitionProperty),
    Feather(TransitionProperty),
    Damage(TransitionProperty),
}

impl Transition {
    fn new(transition: Self) -> Self {
        match transition {
            Transition::Mushroom(_) => Transition::Mushroom(TransitionProperty::None),
            Transition::Flower(_) => Transition::Flower(TransitionProperty::None),
            Transition::Damage(_) => Transition::Damage(TransitionProperty::None),
            Transition::Feather(_) => Transition::Feather(TransitionProperty::Revive),
        }
    }
}

#[derive(Debug)]
enum State {
    Mario,
    SuperMario,
    FireMario,
    CapeMario,
    Dead,
}

#[derive(Debug)]
struct Player {
    state: State,
    // health: usize,
}

impl Player {
    fn new() -> Self {
        let pl = Self {
            state: (State::Mario),
            // health: 1,
        };

        #[cfg(debug_assertions)]
        println!("Creating player, Starting fields: {:?}", pl);

        pl
    }

    fn consume(&mut self, item: Transition) {
        #[cfg(debug_assertions)]
        print!("Consuming {:?}, ", &item);

        match (&self.state, &item) {
            (State::Dead, _) => {}
            (State::Mario, Transition::Damage) => self.state = State::Dead,
            (State::Mario, Transition::Mushroom) => self.state = State::SuperMario,
            (_, Transition::Flower) => self.state = State::FireMario,
            (_, Transition::Feather) => self.state = State::CapeMario,
            (_, Transition::Mushroom) => {
                // if self.health < 3 {
                //     self.health += 1;
                //     #[cfg(debug_assertions)]
                //     print!("Won a extra life, health = {}, ", self.health);
                // }
            }
            (State::SuperMario, Transition::Damage) => self.state = State::Mario,
            (_, Transition::Damage) => self.state = State::SuperMario,
        };

        #[cfg(debug_assertions)]
        println!("Current state: {:?}", self.state);
    }
}

fn main() {
    let mut player1 = Player::new();
    player1.consume(Transition::Mushroom);
    player1.consume(Transition::Feather);
    player1.consume(Transition::Flower);
    player1.consume(Transition::Mushroom);
    player1.consume(Transition::Mushroom);
    player1.consume(Transition::Mushroom);
    player1.consume(Transition::Mushroom);
    player1.consume(Transition::Damage);
    player1.consume(Transition::Damage);
    player1.consume(Transition::Flower);
    player1.consume(Transition::Damage);
    player1.consume(Transition::Damage);
    player1.consume(Transition::Damage);
    player1.consume(Transition::Damage);

    #[cfg(debug_assertions)]
    println!("{:?}", player1);
    println!("Finish.");
}

To make the code compile, just remove everything related to the TransitionProperty. How could I approach the problem better?

New contributor
rafaelgdgs is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

You would potentially want to add a TransitionProperty to Transition::Flower only if you had multiple kinds of flowers: ones that can revive and ones that can't. But that doesn't look like that's what you intend.

If you just want to have transitions be classified by certain properties that multiple transitions have, that is just behavior that can be expressed by a method.

enum Transition {
    Mushroom,
    Flower,
    Feather,
    Damage,
}

impl Transition {
    fn property(&self) -> TransitionProperty {
        match self {
            Transition::Mushroom => TransitionProperty::None,
            Transition::Flower => TransitionProperty::None,
            Transition::Damage => TransitionProperty::None,
            Transition::Feather => TransitionProperty::Revive,
        }
    }
}

And then to handle transitions using that property in your state transition mapping, you can use a match guard (the if ... part of the arm):

fn consume(&mut self, item: Transition) {
    match (&self.state, &item) {
        (State::Dead, item) if item.property() == TransitionProperty::Revive => {
            // revive mario
            self.state = State::Mario
        }
        (State::Dead, _) => {
            // leave dead
        }
        ...
    };
}

Make sure you #[derive(PartialEq, Eq)] on your TransitionProperty for the above condition to work.

\$\endgroup\$

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.