Multiplayer Game Programming for Teens with Python: Part 2

I’m sure once in a while, your friends and you go online to play a multiplayer game. In this tutorial, you will learn about multiplayer game programming by creating a simple game. By .

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

The Game Class

Next you’re going to implement the Game class, which will represent all of the game elements: a pair of clients, the board map and whose turn it is.

Add this right after the BoxesServer class in the server.py file:

class Game:
    def __init__(self, player0, currentIndex):
        # whose turn (1 or 0)
        self.turn = 0
        #owner map
        self.owner=[[False for x in range(6)] for y in range(6)]
        # Seven lines in each direction to make a six by six grid.
        self.boardh = [[False for x in range(6)] for y in range(7)]
        self.boardv = [[False for x in range(7)] for y in range(6)]
        #initialize the players including the one who started the game
        self.player0=player0
        self.player1=None
        #gameid of game
        self.gameid=currentIndex

This class represents the state of the game. The server will be the “official manager” of the state of the game, and will keep each client updated about what to display.

When the first client connects, the server should create a new game. The server will then have a list of games and queue of waiting players so that when another client connects, the server knows whether create a new game for them or have them join a player who is waiting. Let’s add that functionality now.

Add this method to the beginning of the BoxesServer class:

def __init__(self, *args, **kwargs):
    PodSixNet.Server.Server.__init__(self, *args, **kwargs)
    self.games = []
    self.queue = None
    self.currentIndex=0

See that crazy line in there that contains a bunch of confusing things that starts with PodSixNet? Since you are extending something, you still have it call the init of the class that you are extending. So this calls the PodSixNet server class’s initializer, passing all the arguments through.

The currentIndex variable keeps track of the existing games, incrementing one for every game created.

Let’s now add the part of code that puts a client into a queue or has them join a game. Add this to the end of Connected():

if self.queue==None:
    self.currentIndex+=1
    channel.gameid=self.currentIndex
    self.queue=Game(channel, self.currentIndex)
else:
    channel.gameid=self.currentIndex
    self.queue.player1=channel
    self.queue.player0.Send({"action": "startgame","player":0, "gameid": self.queue.gameid})
    self.queue.player1.Send({"action": "startgame","player":1, "gameid": self.queue.gameid})
    self.games.append(self.queue)
    self.queue=None

As you can see, the server checks if there is a game in the queue. If there isn’t, then the server creates a new game and puts it in the queue so that the next time a client connects, they are assigned to that game. Otherwise, it sends a “start game” message to both players.

Consider this: When a client places a line on the board, the server knows where they placed it on the grid. But – assuming there’s more than one game – the server doesn’t know which game the client is in. Thus, the server doesn’t know which map representation to update and which other client to inform that the map has changed.

To get this information to the server, you first need to send the client the gameid when they are assigned to a game. You can see that this is passed in the “startgame” message as one of the parameters. This will also be your notification to the client that the game has started.

You also need to make the client wait for that and when it gets the message, you need to figure out whose turn it is as well. This will tell both of the players that the game has started and that they are a specific gameid. Let’s do that next.

Add this to the class on the client side:

def Network_startgame(self, data):
    self.running=True
    self.num=data["player"]
    self.gameid=data["gameid"]

You want the client to wait until it receives the message to start the game. Add this to the end of __init__:

self.running=False
while not self.running:
    self.Pump()
    connection.Pump()
    sleep(0.01)
#determine attributes from player #
if self.num==0:
    self.turn=True
    self.marker = self.greenplayer
    self.othermarker = self.blueplayer
else:
    self.turn=False
    self.marker=self.blueplayer
    self.othermarker = self.greenplayer

Two Players, One Game

Remember that function that I said wouldn’t work yet that draws the boxes a certain color (drawOwnermap)? Well, now it will, since the client now knows if you are blue or green on the board. Add this right after the call to self.drawHUD() in update:

self.drawOwnermap()

Run the game now. You’ll need three Terminal windows this time – one to run the server, and two to run the clients, because the game won’t start unless there are two players. Not much works yet, but at least the two games are connected to the same server!

Boxes9

Let’s do a quick implementation of placing a line. First you need to add a method to the Game class in the server file for when a client wants to place a line. The Game class will then check if it is that client’s turn and if it is, add the piece to the map representations of both clients.

Add this method to the Game class in the server file:

def placeLine(self, is_h, x, y, data, num):
    #make sure it's their turn
    if num==self.turn:
        self.turn = 0 if self.turn else 1
        #place line in game
        if is_h:
            self.boardh[y][x] = True
        else:
            self.boardv[y][x] = True
        #send data and turn data to each player
        self.player0.Send(data)
        self.player1.Send(data)

This code checks if the move is valid and if it is, sends it off to both clients and updates the representations of the game board and the turn.

Next you need to enable the server to call this method. Add this to BoxesServer in server.py:

def placeLine(self, is_h, x, y, data, gameid, num):
    game = [a for a in self.games if a.gameid==gameid]
    if len(game)==1:
        game[0].placeLine(is_h, x, y, data, num)

This code loops through all of the games and finds the one with the same gameid as the client. Then it relays the information to the game by calling Game.placeline().

You have one last method to add to the ClientChannel class in the server file. Add this method to the ClientChannel class in server.py:

def Network_place(self, data):
    #deconsolidate all of the data from the dictionary
 
    #horizontal or vertical?
    hv = data["is_horizontal"]
    #x of placed line
    x = data["x"]
 
    #y of placed line
    y = data["y"]
 
    #player number (1 or 0)
    num=data["num"]
 
    #id of game given by server at start of game
    self.gameid = data["gameid"]
 
    #tells server to place line
    self._server.placeLine(hv, x, y, data, self.gameid, num)

This reads the message that you’ve been seeing the server print out when the player places a line. It pulls out each argument, and calls the placeLine method on the server.

Now you have your game so that the server relays information to the client. There is one big problem, though: The client doesn’t know what to do with this information. Let’s add another method to the client file to fix this problem.

Add this to the client file:

def Network_place(self, data):
    #get attributes
    x = data["x"]
    y = data["y"]
    hv = data["is_horizontal"]
    #horizontal or vertical
    if hv:
        self.boardh[y][x]=True
    else:
        self.boardv[y][x]=True

This is called when the client receives a place line message from the server. It reads out the parameters and updates the game’s state as appropriate.

Now try running the program. The first line you place will show up on the other side (but further lines won’t work – you’ll get to that soon).

You have just implemented your first multiplayer server! This was not easy to do, as you can attest. Look back at all of the things you’ve done to get here.