Multiplayer Game Programming for Teens with Python: Part 2

Julian Meyer
Learn how to make a multiplayer game with Python!

Learn how to make a multiplayer game with Python!

This is a post by Tutorial Team Member Julian Meyer, a 13-year-old python developer. You can find him on and Twitter.

Welcome back to our 2-part tutorial series on Multiplayer Game Programming for Teens with Python!

In the first part of the tutorial, you created most of the client for the game. You created the code that draws the lines to the screen, and allows you to put down new lines.

In this second and final part of the tutorial, you will finally get to the multiplayer part of the code by creating a server for the game that the clients connect to. Let’s get started!

Getting Started

The basic idea behind making a multiplayer game is that you have a game client, and your friend has a game client. To communicate between each other, both clients connect to a central server that coordinates the gameplay.

To create the server for the Boxes game, you’re going to use PodSixNet, a simple, lightweight Python networking library created by Chris McCormick. In layman’s terms, PodSixNet lets the player talk to the server. You’ll find that it’s easy to use and does exactly what you want it to do. You can learn more about PodSixNet here.

It works like this: First you run a PodSixNet custom server on a specific port (you can think of this like a mailbox with a particular address). You then run the client to communicate with the server through that port.

I’ve already included the PodSixNet code in the resources for the project which you downloaded earlier. But if you’d like to download a fresh version, you can download it here: PodSixNet-78

Go into the folder where you unzipped the file and you should see a file called setup.py. Follow the directions below to install PodSixNet on your computer.

For Mac Users:
Open a new Terminal window and type cd . Notice the space at the end of the command. Drag in the folder containing the unzipped PodSixNet file and press enter. Then type sudo python setup.py install and press enter. This will install PodSixNet so that you can use it anywhere on your computer.

For Windows Users:
Press shift while right-clicking the directory containing the unzipped PodSixNet file. Then click Open Command Prompt Here. Then, if you have Python in your path, type python setup.py install. If this command gives you a “Command Not Found” error, then you do not have Python in your path and you need to add it.

To add Python in your path, right-click My Computer and click Properties. Then click the Advanced Tab and then Environment Variables. Add one and put it in the directory to your Python folder, which should be something like C:\python2.7. Now proceed with the python setup.py install command.

To test out your installation, start up the Python interpreter by typing python in Terminal or the Command Prompt and pressing enter. Then type import PodSixNet and hit enter. If nothing happens, then you’re good. If there is some type of error, then post the error in the comments to this tutorial to get help fixing the problem.

The PodSixNet Skeleton

Let’s begin by creating a PodSixNet skeleton. This is something you can use whenever you want to use PodSixNet in your project just to make sure everything that is working.

Create a new file called server.py and put it in the same folder as boxes.py. Begin the file with this:

import PodSixNet.Channel
import PodSixNet.Server
from time import sleep
class ClientChannel(PodSixNet.Channel.Channel):
    def Network(self, data):
        print data
 
class BoxesServer(PodSixNet.Server.Server):
 
    channelClass = ClientChannel
 
    def Connected(self, channel, addr):
        print 'new connection:', channel
 
print "STARTING SERVER ON LOCALHOST"
boxesServe=BoxesServer()
while True:
    boxesServe.Pump()
    sleep(0.01)

This creates a simple bare bones connection that listens for connections on a default port. When someone connects, it prints out a message.

This code should work out-of-the-box (no pun intended), but it won’t do anything yet because you haven’t programmed the client-side. The changes you need to make to the client-side code are simple. All you have to do is initialize the client’s PodSixNet class and connect it to the server, and then you can send a variety of messages to the server.

Add this code to the top of boxes.py to import some needed PodSixNet libraries and timing:

from PodSixNet.Connection import ConnectionListener, connection
from time import sleep

Make BoxesGame extend ConnectionListener by changing class BoxesGame(): to:

class BoxesGame(ConnectionListener):

Add this to the bottom of the BoxesGame class’s init to initialize the PodSixNet client:

self.Connect()

Add this to the beginning of the update function to “pump” the client and the server so it looks for new events/messages:

connection.Pump()
self.Pump()

Now open up two Terminal windows. Use Python to run boxes.py in one window, and server.py in the other. When you start up the client, the server will tell you that a new client has connected successfully.

localhost:boxes rwenderlich$ python server.py 
STARTING SERVER ON LOCALHOST
new connection: <socket._socketobject object at 0x37ff48>

Adding Multiplayer Functionality

Your client and server are talking to each other! That’s great, but you still need to get your game on. Let’s add some functionality to do just that.

First you want your client to tell the server when it places a line. Find the code where you set a line as drawn on the board inside update. Make that part look like this:

if pygame.mouse.get_pressed()[0] and not alreadyplaced and not isoutofbounds:
    if is_horizontal:
        self.boardh[ypos][xpos]=True
        self.Send({"action": "place", "x":xpos, "y":ypos, "is_horizontal": is_horizontal, "gameid": self.gameid, "num": self.num})
    else:
        self.boardv[ypos][xpos]=True
        self.Send({"action": "place", "x":xpos, "y":ypos, "is_horizontal": is_horizontal, "gameid": self.gameid, "num": self.num})

Notice that this code refers to the new properties self.gameid and self.num. Add statements to initialize them to None in BoxesGame‘s __init__:

self.gameid = None
self.num = None

Now run the server and client and try placing a line. The server will log some messages showing the lines that you clicked on the client:

STARTING SERVER ON LOCALHOST
new connection: <socket._socketobject object at 0x3adb90>
new connection: <socket._socketobject object at 0x54db58>
{'gameid': None, 'is_horizontal': True, 'action': 'place', 'num': None, 'y': 5, 'x': 3}
{'gameid': None, 'is_horizontal': False, 'action': 'place', 'num': None, 'y': 5, 'x': 3}

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.

Taking Turns

Next you need to implement turns so that the players don’t cheat. Believe it or not, you’ve already set up a variable for this (the turn variable). But first, you need a delay function that waits for 10 frames before the game allows the player to place another line.

Add this variable to __init__ in boxes.py:

self.justplaced=10

You want this variable to decrement by 1 each frame, and then reset to 10 when the user places a line. Change the following code in the game:

if pygame.mouse.get_pressed()[0] and not alreadyplaced and not isoutofbounds:
#-----------to-----------#
if pygame.mouse.get_pressed()[0] and not alreadyplaced and not isoutofbounds and self.turn==True and self.justplaced<=0:

This checks to see if it's the player's turn and that they haven't already just placed a line.

Next add this at the top of the update function:

self.justplaced-=1

This decrements the variable by 1 every frame. Now you have to make it so that if a user does place a line, then the counter resets. Add this to inside the if statement that you changed earlier:

self.justplaced=10

Good. One problem you may have noticed is that the turn indicator is always green – and for both clients at the same time! This is only because you haven't hooked up the part of your code that switches the color back and forth.

Find the screen.blit function that draws the green indicator inside the drawHUD() method. Change it to this:

self.screen.blit(self.greenindicator if self.turn else self.redindicator, (130, 395))

Run the game again - one of the indicators should be green and one red.

Boxes10

The indicators are correct, but when you place a line, the game should obviously switch turns. Currently this isn't happening. Let's quickly add that in and then move on to the game logic on the server.

On the server-side, you need it to send the client a message whenever any event happens to tell the client whose turn it is. You can do this by simply adding on to the placeLine method in the Game class on the server. Put this right after the part in the if statement where you update the turn representation:

self.player1.Send({"action":"yourturn", "torf":True if self.turn==1 else False})
self.player0.Send({"action":"yourturn", "torf":True if self.turn==0 else False})

torf stands for “true or false”. This will tell the client whether or not it's their turn. You don't need to send this message when the game is starting up because the client figures out who takes the first turn according to which number player it is.

Let's implement this yourturn command on the client-side. This is fairly easy to do. Add this method to the Game class on the client:

def Network_yourturn(self, data):
    #torf = short for true or false
    self.turn = data["torf"]

Now run the server and two clients. You will have to restart the server since you made a change to it.

Boxes11

As you can see, the clients have to take turns now. Pretty neat, huh?

Now that you've taught your clients how to behave and take turns, you ought to reward them with something for their hard work. And that is - colored boxes!

Programming the Game Logic

This game has simple logic: to determine whether a player has made a square during their turn, the server loops through all of the possible squares and looks for one.

Create a new method in the BoxesServer class called tick(). Add the method as shown:

def tick(self):
    # 1
    index=0
    change=3
    # 2 
    for game in self.games:
        change=3
        for time in range(2):
            # 3
            for y in range(6):
                for x in range(6):
                    # 4
                    if game.boardh[y][x] and game.boardv[y][x] and game.boardh[y+1][x] and game.boardv[y][x+1] and not game.owner[x][y]:
                        if self.games[index].turn==0:
                            self.games[index].owner[x][y]=2
                            game.player1.Send({"action":"win", "x":x, "y":y})
                            game.player0.Send({"action":"lose", "x":x, "y":y})
                            change=1
                        else:
                            self.games[index].owner[x][y]=1
                            game.player0.Send({"action":"win", "x":x, "y":y})
                            game.player1.Send({"action":"lose", "x":x, "y":y})
                            change=0
        # 5
        self.games[index].turn = change if change!=3 else self.games[index].turn
        game.player1.Send({"action":"yourturn", "torf":True if self.games[index].turn==1 else False})
        game.player0.Send({"action":"yourturn", "torf":True if self.games[index].turn==0 else False})
        index+=1
    self.Pump()

Whoa, that's a lot of code! Let's break it down to see what's happening.

  1. At the top of the method, you declare some variables: index, which is commonly used with for loops to keep track of what item you are on, and change, which tells you whether the turn should change and whose turn it should be.
  2. Next you loop through all of the games, resetting the change to 3, which means no change.
  3. Then you loop through all of the possible squares. You do it twice, because it's possible a player could get two squares at once by drawing a middle line between two boxes.
  4. For each possible square, you check to see if there is a square and if so, make sure it wasn't drawn on an earlier turn.
  5. Finally, you check to see who placed the line that made the square and set the change variable correctly.

Now that you have this method, you need to add it to the server. This is easy to do. Go to the bottom of the file and find where it says:

boxesServe.Pump()

Change it to:

boxesServe.tick()

Now that you have some of the game logic on the server, let's add a method on the client to tell it that it either won a square or lost a square (i.e. the other player won a square).

Add these two methods to the client side:

def Network_win(self, data):
    self.owner[data["x"]][data["y"]]="win"
    self.boardh[data["y"]][data["x"]]=True
    self.boardv[data["y"]][data["x"]]=True
    self.boardh[data["y"]+1][data["x"]]=True
    self.boardv[data["y"]][data["x"]+1]=True
    #add one point to my score
    self.me+=1
def Network_lose(self, data):
    self.owner[data["x"]][data["y"]]="lose"
    self.boardh[data["y"]][data["x"]]=True
    self.boardv[data["y"]][data["x"]]=True
    self.boardh[data["y"]+1][data["x"]]=True
    self.boardv[data["y"]][data["x"]+1]=True
    #add one to other players score
    self.otherplayer+=1

These handle the win and lose messages that come over the network. It updates the game state appropriately.

Run your game using the server and two clients, and enjoy taking over the whole board from both sides!

Screen Shot 2013-06-20 at 8.48.01 AM

Game Over, Man!

But wait, when does this game end? You still need to instruct the server to implement the finished() method you added at the end of Part 1. Remember, that method shows either a winning or a losing screen and exits the game.

Add this at the top of the update function:

if self.me+self.otherplayer==36:
    self.didiwin=True if self.me>self.otherplayer else False
    return 1

This checks to see how many squares you have won and how many squares the other player has won. If the total is 36 (the number of squares on the board), the game is over. If the game is over, it checks to see who has the most squares to figure out who won, and returns 1.

Finally, find bg.update() at the very bottom of the file. Replace it with the following:

if bg.update()==1:
    break

At the very end of the file, add this with no indentation:

bg.finished()

Now you should be able to win the game. But before you test it out, let's add one more server and client function. When a client closes the game, you want the other client to close the game, too.

Add this to the ClientChannel class:

def Close(self):
    self._server.close(self.gameid)

Then add this to the BoxesServer class:

def close(self, gameid):
    try:
        game = [a for a in self.games if a.gameid==gameid][0]
        game.player0.Send({"action":"close"})
        game.player1.Send({"action":"close"})
    except:
        pass

To make the client understand the close() command, add this to the client class:

def Network_close(self, data):
    exit()

Run the game again with two clients and play until you win. It shouldn't be too hard. ;)

You are now officially done with the game. If you want to perfect it with music and sounds, continue on to the next section, where you will also give the game network connectivity. Otherwise, skip ahead to the last section to read a little about what you've done and where you can take it from here.

Finishing Touches

Add a new function to the main client game class (BoxesGame) as shown:

def initSound(self):
    pygame.mixer.music.load("music.wav")
    self.winSound = pygame.mixer.Sound('win.wav')
    self.loseSound = pygame.mixer.Sound('lose.wav')
    self.placeSound = pygame.mixer.Sound('place.wav')
    pygame.mixer.music.play()

This loads the music and sound files so that you can play them when needed. These .wav files were part of the resources package you downloaded at the beginning of Part 1 of the tutorial. I made these sound effects with cxfr and the sound effects come from Kevin MacLeod.

Now in __init__, add the following after initGraphics:

self.initSound()

Then add this code in the specified places:

#anywhere in Network_place
self.placeSound.play()

#anywhere in Network_win
self.winSound.play()

#anywhere in Network_lose
self.loseSound.play()

These play the sound effects in the appropriate spots.

Run the game again and make sure your computer's volume is up. Enjoy the groovy tunes!

Now that you've enhanced the game with music and sounds, let's make it so that you can play across your network instead of just on the same computer.

Replace self.Connect() in BoxesGame with this:

address=raw_input("Address of Server: ")
try:
    if not address:
        host, port="localhost", 8000
    else:
        host,port=address.split(":")
    self.Connect((host, int(port)))
except:
    print "Error Connecting to Server"
    print "Usage:", "host:port"
    print "e.g.", "localhost:31425"
    exit()
print "Boxes client started"

This lets you specify where the client should find the server. You will be prompted when you run the game to type in the server's IP address.

Now let's add the server version of this. Replace boxesServe = BoxesServer() in server.py with this:

# try:
address=raw_input("Host:Port (localhost:8000): ")
if not address:
    host, port="localhost", 8000
else:
    host,port=address.split(":")
boxesServe = BoxesServer(localaddr=(host, int(port)))

Run your game one last time.

Where to Go from Here?

Here is the finished sample project from the tutorial series.

Congrats! You have just finished your first multiplayer game using Python and PyGame. I hope you had fun with this project.

If you did and you want to keep working on it, here are some things you could try on your own:

  • Add random “bad” squares that cause the player who won them to lose a point.
  • Make some squares randomly worth more than one point.
  • Give the first player to join the game the ability to chose the board's size.
  • Make it possible for players to spend their turns removing lines added by the other player.

If you have any questions or comments about this tutorial, chime in the forum discussion below. Happy Pythoning!

Julian Meyer is a 13-year-old python programmer from Northern California. He has programmed PixelBuild, a simple block-building game and has his own blog about programming, arduino, and music here.

Other Items of Interest

Save time.
Learn more with our video courses.

raywenderlich.com Weekly

Sign up to receive the latest tutorials from raywenderlich.com each week, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

PragmaConf 2016 Come check out Alt U

Our Books

Our Team

Video Team

... 20 total!

iOS Team

... 78 total!

Android Team

... 26 total!

Unity Team

... 11 total!

Articles Team

... 15 total!

Resident Authors Team

... 18 total!

Podcast Team

... 7 total!

Recruitment Team

... 9 total!