Check state in struct fns vs separate structs for each state

Hello, I am writing a small card game to practice Rust. The game has some internal state + a GamePhase which controls what can/cannot be done. Currently, I have all functions in one big Game struct, and each function checks for correct state before continuing + whether to transition to next state afterwards; ie like:

enum GamePhase { Draw, Play };

struct Game {
    /* internal state (players, cards etc) */
    phase: GamePhase
}

impl Game {
    pub fn draw(&mut self) -> Result<(), ()> {
        match self.phase {
            GamePhase::Draw => { /* draw card */ },
            GamePhase::Play => { return Err(()) },
        }

        if /* ready to transition state */ {
            self.phase = GamePhase::Play;
        }

        Ok(())
    }

    /* same idea for play() */
}

This feels wrong as I need to explicitly check state and state transition for each function, and all functions are available regardless of the state.

An alternative is, I could have a struct for each GamePhase that holds the game's state and has specific functions, and have a next() function to explicitly consume self and return the next state if ready; thus behaviour is locked to the state:

enum GamePhase { 
    Draw(Draw), 
    Play(Play) 
}

struct GameState {
    /* internal state (players, cards etc) */
}

struct Draw {
    state: GameState
}

impl Draw {
    pub fn draw(&mut self) {
        /* draw a card for current player */
    }

    pub fn next(mut self) -> Result<GamePhase::Play, Self> {
        if /* ready to transition state */ {
            return Ok(GamePhase::Play(self.state));
        }
        else {
            return Err(self);
        }
    }
}

/* same idea for Play */

However, writing the gameflow would be less intuitive as I would have to construct a new struct for each phase, and next() always consumes self so I would need to re-assign it if it failed.

I'm wondering which way is more sensible? Or if there was a better way to do this. Thanks!

The pattern where you use distinct types to track state is called the "typestate" pattern. A subpattern that you will see a lot are builders such as OpenOptions or Command or thread::Builder. As you note, an upside is that you can omit impossible state transitions. On the otherhand, the more logic you push into compile-time checks by using typestate, the more rigid your framework may become. (If you're attacking a specific problem and not a framework, this may not be much of a concern.)

Typestates arguably also work best when most state transitions are infallible rather than fallible, as then you don't have to deal with Result<NextState, Self> return types (and thus, dynamic checks and reasoning) as much.

I think ultimately which approach is better depends on your specific library/program.

1 Like

Ah, thanks for putting a name to the pattern. Since it seems pretty natural for Rust I will head in that direction. The part about the fallibility makes sense, I will probably put in some default behaviour for the state transition to always be valid. Thank you!

1 Like

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.