Writing a Guess My Number Game

The purpose of this demo is to quickly give you a feel for how the kxg engine works by writing a Guess My Number game. If you’re not familiar with this game, the idea is that the players will race to guess a number that was picked randomly by the computer. Every time a player makes a guess, the computer will tell everyone if it was too high or too low. Guess My Number makes for a good demo because it’s simple enough that we won’t get lost in the details of writing the game itself, but real enough that we’ll be able to see the power of the game engine. For example, our implementation will have both multiplayer and single-player vs. AI modes.

Playing the demo

Before reading this article, you might want to play the demo to get a feel for how it works. Follow these instructions to install the game engine if you haven’t already, then download the complete demo from the demos folder in the kxg repository on GitHub. Run the demo with the --help flag to see how to play the game in all the single-player and multiplayer modes it supports:

$ python3 guess_my_number.py --help

For example, the command to play a simple single-player game against no one is:

$ python3 guess_my_number.py sandbox

The game architecture

In broad strokes, the game engine expects you to implement a handful of classes that collectively describe your game. This includes classes to represent all the players themselves (Actor), the actions the players can take (Message), the game objects that the players can interact with (World and Token). You then pass these classes to a main function function that orchestrates playing the game. 1

Setting up the world

The first class we’ll implement is World. This class is a container for every object that is part of the game. One of the game engine’s most important responsibilities is to keep the world in sync during multiplayer games. Normally the objects in the world would be mostly tokens (see How Tokens Work), but Guess My Number is a simple game so we only need to keep track of four numbers:

  • The number to guess.

  • The smallest number guessed so far.

  • The biggest number guessed so far.

  • The id number of the winner, or zero if the game isn’t over.

We don’t have to do much to implement the world. All we have to do is inherit from World and initialize our four numbers in the constructor:

import kxg

class World(kxg.World):
    """
    Keep track of the secret number, the range of numbers that
    haven't been eliminated yet, and the winner (if there is one).
    """

    def __init__(self):
        super().__init__()
        self.number = 0
        self.lower_bound = 0
        self.upper_bound = 0
        self.winner = 0

Note that we don’t want to pick a random number to guess here. The world is initialized separately on all the clients, so if we pick a number here every client will end up with a different number and the games will be out of sync. The game engine uses messages to communicate changes to the world, so in the next section we’ll use a message to pick a number.

Picking the number

As discussed above, the world can’t be initialized with a number to guess. Instead, the server will pick a number and communicate it to all the clients by sending a message. To do this, we will need to write a PickNumber message class. This class will contain any relevant information (e.g. the number as well as the upper and lower bounds on the guesses) along with methods to help carry out the message:

import kxg

class PickNumber(kxg.Message):
    """
    Pick the secret number and communicate that choice to all the
    clients.
    """

    def __init__(self, number, lower_bound, upper_bound):
        self.number = number
        self.lower_bound = lower_bound
        self.upper_bound = upper_bound

    def on_check(self, world):
        if world.number:
            raise kxg.MessageCheck("number already picked")

    def on_execute(self, world):
        world.number = self.number
        world.lower_bound = self.lower_bound
        world.upper_bound = self.upper_bound

The constructor will only be used by us and not by the game engine itself, so it doesn’t need to do anything but store the information that the message will use to change the world. It doesn’t even need to call the base class constructor. In this case we only need to store the number to guess and the upper and lower bounds to show the players.

on_check() is called by the game engine the confirm that the message should be allowed given the current state of the world. If there’s a problem, on_check() should raise a MessageCheck exception. This is important in multiplayer games, because it gives the server veto power over messages sent by the clients. In other words, before a message from one client is relayed to all the others, it has to pass the check on the server. This gives the server a way to prevent cheating and to detect when the clients are getting out of sync. For Guess My Number, we just check to make sure that the number to guess isn’t already set, which would suggest that this message had been sent twice somehow.

The on_execute() method is called by the game engine to let the message change the game world. This message only needs to copy its three attributes into the world, so that’s what it does.

Guessing the number

We’ll use a second message to communicate the guesses that players make. This GuessNumber message will be conceptually very similar to PickNumber. The only wrinkle is that we will use World.end_game() to end the game when the right number is guessed:

import kxg

class GuessNumber (kxg.Message):
    """
    Make a guess on behalf of the given player.  If the guess is
    right, that player wins the game.  If the guess is wrong, the
    range of numbers that the secret number could be is narrowed
    accordingly.
    """

    def __init__(self, player, guess):
        self.player = player
        self.guess = guess

    def on_check(self, world):
        pass

    def on_execute(self, world):
        if self.guess == world.number:
            world.winner = self.player
            world.end_game()

        elif self.guess < world.number:
            world.lower_bound = max(self.guess, world.lower_bound)

        elif self.guess > world.number:
            world.upper_bound = min(self.guess, world.upper_bound)

The constructor stores a player id number and a guess. The on_execute() method compares that guess to the secret number. If the guess is right, the message ends the game and sets the player that made the guess as the winner. If the guess is wrong, the message narrows the lower and upper bounds that are displayed to all the players.

Refereeing the Game

The next class we’ll implement is Referee. The referee runs only on the server and sends messages that wouldn’t be sent by any of the players. This often boils down to messages that start and end the game. Our referee will only need to concern itself with starting the game, because the GuessNumber message takes care of ending it:

import kxg
import random

LOWER_BOUND, UPPER_BOUND = 0, 5000

class Referee (kxg.Referee):
    """
    Pick the secret number.
    """

    def on_start_game(self):
        number = random.randint(LOWER_BOUND + 1, UPPER_BOUND - 1)
        self >> PickNumber(number, LOWER_BOUND, UPPER_BOUND)

The lower and upper bounds are global variables just so they can be changed without having to dig through too much code. In a more sophisticated game, these bounds might be read from a config file or set in some sort of lobby. The Actor.on_start_game() method is called automatically by the game engine when the game starts. It picks a random number within the given bounds, uses that number to construct a PickNumber message, then sends that message using the >> operator. We can be sure that only one number will be picked because the referee only runs on the server and Actor.on_start_game() is only called once.

Making a user interface

Below is a screenshot of the graphical user interface (GUI) we’ll make for Guess My Number. As you can see, it’s quite simple. The numbers on the right and left tell the player what numbers have already been guessed. To make a guess, the player just has to type in a number and press Enter. As the player types, their guess will replace the question marks in the middle.

../_images/screenshot.png

The game engine itself doesn’t care how the GUI is written, so for your own games you can use whatever graphics library best fits your needs. We’ll use pyglet in this tutorial, because it offers a good balance between power and ease of use. If you’re not familiar with pyglet, this brief tutorial covers everything we’ll need, which really isn’t much more than “Hello world!”

We’ll write the GUI in two classes, and the first will be Gui. The purpose of this class will be to store information about the GUI we’ll need before, during, and after the game itself. Typically this information will include a window handle and maybe some shared menu stuff. Guess My Number will only use Gui to display a post-game “You Won/Lost!” message, but a production game would also use it to put together menus and multiplayer lobbies and things like that:

import pyglet

class Gui:
    """
    Manage GUI objects like the window, which exist before and after the
    game itself.
    """

    def __init__(self):
        self.width, self.height = 600, 400
        self.window = pyglet.window.Window()
        self.window.set_size(self.width, self.height)
        self.window.set_visible(True)
        self.label = pyglet.text.Label(
                "",
                color=(255, 255, 255, 255),
                font_name='Deja Vu Sans', font_size=32,
                x=self.width//2, y=self.height//2,
                anchor_x='center', anchor_y='center',
        )

    def on_refresh_gui(self):
        self.window.clear()
        self.label.draw()

The constructor is where any shared GUI resources should be initialized. In this case, we need to initialize window and label objects. Hopefully it makes sense why the window needs to be used both during and after the game. We don’t necessarily need to use the same label object during and after the game, but doing so makes it easier to set the post-game message and keeps us from having to specify the font and positioning of the label twice. 2

The on_refresh_gui() method clears the screen and redraws the label, which may have been changed the game since the last redraw. The game engine calls this method automatically before and after the game, and we’ll call it manually during the game itself.

Our second GUI class will be GuiActor. Actors are the components of the game engine that represent individual players. We will write GuiActor to represent human players and (in the next section) AiActor to represent computer players. To represent human players, GuiActor will have to render the screenshot from the beginning of this section and send messages on behalf of the player:

 import kxg

class GuiActor(kxg.Actor):
    """
    Show the players the range of numbers that haven't been eliminated yet,
    and allow the player to guess what the number is.
    """

    def __init__(self):
        super().__init__()
        self.guess = None
        self.prompt = "{0.lower_bound} < {1} < {0.upper_bound}"

    def on_setup_gui(self, gui):
        self.gui = gui
        self.gui.window.set_handlers(self)

    def on_draw(self):
        self.gui.on_refresh_gui()

    def on_mouse_scroll(self, x, y, dx, dy):
        # If the user scrolls the mouse wheel, update the guess accordingly.
        if self.guess is None:
            if dy < 0:
                self.guess = self.world.upper_bound
            else:
                self.guess = self.world.lower_bound

        self.guess = sorted([
            self.world.lower_bound,
            self.guess + dy,
            self.world.upper_bound,
        ])[1]

        self.on_update_prompt()

    def on_key_press(self, symbol, modifiers):
        # If the user types a number, add that digit to the guess.
        try:
            digit = int(chr(symbol))
            self.guess = 10 * (self.guess or 0) + digit
        except ValueError:
            pass

        # If the user hits backspace, remove the last digit from the guess.
        if symbol == pyglet.window.key.BACKSPACE:
            if self.guess is not None:
                guess_str = str(self.guess)[:-1]
                self.guess = int(guess_str) if guess_str else None

        # If the user hits enter, guess the current number.
        if symbol == pyglet.window.key.ENTER:
            if self.guess:
                self >> GuessNumber(self.id, self.guess)
                self.guess = None

        self.on_update_prompt()

    @kxg.subscribe_to_message(PickNumber)
    @kxg.subscribe_to_message(GuessNumber)
    def on_update_prompt(self, message=None):
        guess_str = '???' if self.guess is None else str(self.guess)
        self.gui.label.text = self.prompt.format(self.world, guess_str)

    def on_finish_game(self):
        self.gui.window.pop_handlers()

        if self.world.winner == self.id:
            self.gui.label.text = "You won!"
        else:
            self.gui.label.text = "You lost!"

As usual, the game engine doesn’t much care what happens in the constructor. Here we just define two variables that we will use to manage the GUI: self.guess will keep track of the guesses as the player types them and self.prompt will be formatted and displayed to the player on each frame.

The Actor.on_setup_gui() method is called by the game engine to give GuiActor a chance to store a reference to the Gui object. This is how GUI information from outside the game can be used inside the game. The call to pyglet.event.EventDispatcher.set_handlers tells pyglet that it should use the on_draw() and on_key_press() methods to handle draw and keyboard events.

The on_draw() method manually calls Gui.on_refresh_gui(), which causes the window to be cleared and redrawn. Although on_refresh_gui() is called automatically before and after the game, is has to be called manually during the game. The reason is that many games require more complicated draw steps during game than they do before or after it.

The on_mouse_scroll() and on_key_press() methods handle keyboard input from the player. If the player scrolls up or down, the guess is updated accordingly. If he or she types a number, it is added to the guess. If he or she hits backspace, a digit is removed from the guess. If he or she hits enter, the guess is made into a message and processed by the game engine. This last step, sending messages on behalf of the player, is what GuiActor exists to do. We use self.id to tell GuessNumber which player is making the guess. This id is assigned by the engine as soon as the game starts and is guaranteed to be unique for each actor. Once the message is ready, we send it using the >> operator.

The on_update_prompt() method shows the player the latest bounds on the number to guess. The kxg.forums.subscribe_to_message() decorators tell the game engine to call this method whenever a PickNumber or GuessNumber message is received. There are other ways to subscribe to messages (described in How Messaging Works) but decorators are generally the most readable.

The Actor.on_finish_game() method is called by the game engine once the game has been ended by a GuessNumber message. This method removes the game’s event handlers from the window and sets the post-game “You Won/Lost!” message. The id number of the winning player is stored in the world, so we can compare that to self.id to figure out which message to use.

Making an AI opponent

As mentioned in the previous section, the AI player will be represented by the AiActor class. Like GuiActor, AiActor will interact with the rest of the game by sending and receiving messages. Unlike GuiPlayer, which lets the player make all the interesting decisions, AiActor will also play the game. Our AI won’t be too complicated. It will just wait a random amount of time, guess a random number, and repeat until the game ends:

class AiActor(kxg.Actor):
    """
    Wait a random amount of time, then guess a random number within the
    remaining range.
    """

    def __init__(self):
        super().__init__()
        self.reset_timer()

    def on_update_game(self, dt):
        self.timer -= dt

        if self.timer < 0:
            lower_bound = self.world.lower_bound + 1
            upper_bound = self.world.upper_bound - 1
            guess = random.randint(lower_bound, upper_bound)
            self >> GuessNumber(self.id, guess)
            self.reset_timer()

    def reset_timer(self):
        self.timer = random.uniform(1, 3)

The on_update_game() method is called by the game engine on every frame of the game. The dt argument is the amount of time that has elapsed since the last frame. The AI uses this information to update its internal timer. When that timer expires, the AI makes a random guess using the same GuessNumber message as GuiActor and resets the timer.

Putting it all together

We’ve now written classes that encompass all the logic needed to play Guess My Number. To tie them together for the game engine so it can play the game, we just need to pass them all to quickstart.main():

if __name__ == '__main__':
    kxg.quickstart.main(World, Referee, Gui, GuiActor, AiActor)

This function runs a no-frills game loop and makes it easy to get started developing and debugging your games. Once you’re done developing and want to make a production game, the game engine can also provide you with complete control over the game loop, but how to do that is beyond the scope of this demo.

Do as I say, not as I do

Our Guess My Number implementation cuts some corners for the sake of simplicity and clarity. Below is a list of the things that should be done more rigorously for full-fledged games:

  1. We should’ve checked that our messages were being sent by the expected players. For example, only the referee should send PickNumber messages and only the player making a guess should send GuessNumber messages (i.e. players shouldn’t be able to make guesses for each other). This is mostly important to prevent cheating, although it might also help you catch bugs during development. Here is some code showing what these checks would look like:

    # In PickNumber.on_check()
    if not self.was_sent_by_referee():
        raise kxg.MessageCheck("only the referee can pick the number")
    
    # In GuessNumber.on_check()
    if not self.was_sent_by(self.player):
        raise kxg.MessageCheck("can't make a guess for another player!")
    
  2. We should’ve had the referee send an EndGame message, rather than having the GuessNumber message end the game on its own if the right number was guessed. The reason is that whenever a client sends a message, the server might veto it and require it to be undone (see How Messaging Works for more information). Since ending the game cannot be undone, it’s better for that command to come from the server.

  3. It’s unusual to directly use the actors’ id numbers to refer to players. The usual approach is to have each actor create a player token that knows about its id number.

Footnotes

1

For production games, with menus and graphics settings and multiplayer lobbies and all that, you would write your own main function. But the game engine provides a default main function that’s convenient for developing and debugging games.

2

Storing a label in Gui makes sense for Guess My Number because the GUI is really nothing but a label. For a more real game, it would make more sense to store a pyglet.graphics.Batch object instead.