Game state machine

Last class and homework, we worked on creating a networking protocol to implement our rock-paper-scissors game. This homework, we are going to start the basic framework for using the network protocol to implement the game. To do this, we need consider states the currently is in. Because RPS is a pretty simple game, we have only N states:

  1. Waiting for a game to start
  2. Players select weapon
  3. Player ready, waiting on others
  4. All players transmit weapons
  5. Winner of round determined
  6. Lost
  7. Won

It's important to understand that the states above are just a list. It helps to consider all the possible states a game can be in, and how the game moves from one state to another. Sometimes it helps to draw out the states in a diagram. Here is a diagram I drew to show the logic of the states:

Our job is to implement these states. To do this we will implement a very common design for games, which is called a state machine. A state machine is very simple to describe, and very useful for implementing all kinds of processes. In code, we use an enum to represent the states. An enum is a special type used to take on one of several values of a given type. You have already used enums in Raylib. The Colors enum for example is a list of predefined Color structs. Defining an enum is pretty easy:

enum GameState {
   Start,
   Playing,
   GameOver
}

Now the type GameState is usable just like an integer, and can have one of 3 values, GameState.Start, GameState.Playing, or GameState.GameOver. You can add as many states as you need.

To handle what to do at each state, we need some input. Something has to change either by the player, or by a timer, or a message received, or something, that moves the current game state to another state. The new state is waited on until something else happens. It is totally up to you what states you want to define, and how the game moves from one state to the next. Each of these moves is called a state transition.

Exercise: simple state machine

For an exercise before the real thing, let's implement a simple state machine. In the class, we will implement the real state machine, so it will be good to have some practice on this. Here is some starting code, including a state:


import std.stdio;
import std.string;
import std.conv;

void main()
{
    enum State {
        FirstNumber,
        SecondNumber,
        ThirdNumber,
        Unlocked
    }

    int first = 1;
    int second = 2;
    int third = 3;

    State state = State.FirstNumber;
    while(state != State.Unlocked)
    {
        // display a prompt
        // [Insert code here]

        // read a number
        string line = strip(readln());
        int num = to!int(line);
        // figure out next state
        // [Insert code here]
    }
    writeln("You have unlocked the lock!");
}

Imagine this is a combination lock, with 3 dials (the combination is 1 2 3, you may want to change that). But we want to give hints to the user that they completed each number. So here's what the code should do:

  1. If the state is State.FirstNumber, ask the user for the first number. If they enter it incorrectly, tell them, and let them try again. If they enter it correctly, set the state to State.SecondNumber
  2. Do the same for State.SecondNumber. This time, set the state to State.ThirdNumber if correct.
  3. Do the same for State.ThirdNumber, but this time set the state to State.Unlocked if correct.

Each of these things should be done in the loop, and the next step should be done after looping again. Why do we do this instead of just handling the next state? Because in a real game, you need to handle other things in the loop -- updating the display, reading network data, etc. You handle one thing, update the state, and then let the loop handle the next thing.

For the next class, we will be doing a very similar thing with the states above. If you haven't constructed the code yet from the discord messages, here is the whole starting point. Everyone get this code in their visual studio code, and have it running so we can work on it. If you need help with getting it running, let me know.

import std.stdio;
import raylib;
import enet.enet;
import client;

// this is the message we will send to the others
enum playerchoice {
    undecided,
    decided,
    paper,
    rock,
    scissors
}
struct PlayerInfo
{
    string name; // players name
    playerchoice weapon;
    bool playerActive;
}

// rules:
//   Send PlayerInfo out whenever
//      a. you connect for the first time
//      b. a new peer is added.
//      c. you change your choice of RPS
//   When PlayerInfo is received
//      store the player info associated with the Id of the player

struct GameStartMessage {

}

// running a game:
// 1. send message that a game should start
// 2. players pick a weapon
// 3. send PlayerInfo with decided message
// 4. once everyone has decided, every client then sends the true weapon picked.
// 5. all clients decide who wins the round.
// 6. in case of a tie, after 2 seconds reset all players to undecided. Goto step 2.
// 7. If there are winners and losers, all losers are out of the game, and winners continue
// 8. If there is only one winner, the game is over, reset.

void main()
{
    InitWindow(500, 500, "Rock Paper Scissors");

    PlayerInfo[size_t] players; // state of all the users

    enum State {
        GameStart,
        SelectWeapon,
        WaitForReady,
        WeaponTransmit,
        CheckRound,
        Lost,
        Won
    }

    State state = State.GameStart;
    playerchoice myWeapon = playerchoice.undecided;

    // initialize enet
    enet_initialize();

    // create the client
    Client client;
    client.initialize("codingcat.club", 6666);

    SetTargetFPS(60);

    while(!WindowShouldClose())
    {
        // handle network traffic
        client.process();

        BeginDrawing();
        ClearBackground(Colors.WHITE);

        // client.validPeers is an array of ids of other players that are
        // currently registered on the server.
        foreach(idx, id; client.validPeers)
        {
        }

        // Draw anything else you need for the User Interface.

        EndDrawing();
    }

    // disconnect the client from the server
    client.disconnect();
    // shut down the networking system
    enet_deinitialize();
    // close the window
    CloseWindow();
}

©2021 Steven Schveighoffer