Stage 3

Introduction

In this tutorial, you’ll be splitting your code into a client and a server, enabling it to be played by multiple people across a network. However, before you get to that, we’ll do some refactoring of your code to make it easier to do.

Refactoring

Refactoring is a bit of an art form, requiring experience with both the programming language and knowing what other programmers naturally expect from APIs. Since you’ve been free to structure your code as you wished, there will naturally be some diversity between solutions, so you may wish to ask a leader to look at your code and provide assistance.

With all this in mind, here’s our recommendation for the functions you should have in your program:

stage3.first(items)

Return the first item from a list, or None if the list is empty.

>>> first([1, 2, 3])
1
>>> first([])
None
Parameters:items – a list
Return type:the first item in the list, or None
stage3.natural_list(items)

Format a list of strings into a comma-separated list in English. Best explained by example:

>>> natural_list(['red', 'green', 'blue'])
'red, green and blue'
>>> natural_list(['apples', 'oranges'])
'apples and oranges'
>>> natural_list(['a single phrase'])
'a single phrase'
>>> natural_list([])
''
Parameters:items – a list of strings
Return type:string
stage3.get_items_in_room(room, inventory)

Get a dictionary mapping item ids to item information for all items in a room. This is done by taking the item ids in a room, removing those in the player’s inventory, and then looking up the remaining item ids in WORLD['items'].

Parameters:
  • room – a dictionary from the world data file, with an “items” key
  • inventory – a list of item ids in the player’s inventory
Return type:

dictionary mapping item ids to item information

stage3.display_room(room, inventory)

Return a human-readable string describing the room, any exits from it, and any items in it.

Parameters:
  • room – a dictionary from the world data file, with “description”, “exits”, and “items” keys.
  • inventory – a list of item ids currently in the player’s inventory, which will hence be omitted from the output
Return type:

a string describing the room

stage3.filter_by_type(inventory, type_)

Find items of a certain kind. Takes the player’s inventory, looks up each item id in WORLD['items'], and includes it in the result if item['type'] matches the type_ parameter.

Parameters:
  • inventory – list of item ids
  • type – string
Return type:

list of item ids

stage3.filter_by_name(inventory, name)

Finds items with a given name. Takes the player’s inventory, looks up each item id in WORLD['items'], and includes it in the result if any of the item['names'] match the name parameter.

Parameters:
  • inventory – list of item ids
  • name – string
Return type:

list of item ids

Have a look through your program, and see if you recognise places where you can make the logic simpler by using these (or similar) functions.

Building an Echo Server

Building an application using just the modules in the Python standard library is a little tedious, so we’ll be using Simple Network [1], which is (unsuprisingly) a simple networking library that we’ve written. Let’s start by building something simple with it: an echo server.

If you haven’t come across this before, back in days of yore, many Unix servers ran a daemon called inetd which managed many other Internet services, including one for the Echo Protocol. This ran on UDP (a fast, unreliable protocol) and TCP (a slower, reliable protocol), and whenever you sent some data to it, it would send a copy of it back to you. This was used for testing that the network worked and the server was up, until it was replaced by ICMP Echo Requests (which is what the ping utility uses).

The Server

Anyhow, enough history, let’s write some code. First, we need to import and create a network.Server instance:

from network import Server

server = Server("Peter's Echo Server", "echo")

When creating the server on line 3, the first parameter is the name, and the second parameter is the protocol. The name is used to show users what this specific instance of the server is for, and so you should change it to include your name in it. The protocol is used so that clients can only show servers of a specific type: we don’t want our echo client to show web or print servers, for example.

@server.handler
def handle(message):
    return message

Next, we define a function which will be called whenever a client sends us a message. Ignore the first line for the moment, and it’s just a function which takes a message, and sends the same message back to the client.

So, what’s that first line? It’s a decorator which registers the function with the server, indicating that it should be used to handle each request sent to the server. Don’t worry if you clicked through to the Wikipedia article on decorators, and were then very confused: this is the only place we’ll be using them. But feel free to ask a leader to explain them in more detail if you’d like to know.

server.run()

Finally, we just need to tell the server to run: this function will loop forever, listening for requests, calling our function, and sending back the response. Note that once we call this function, we surrender control of our program [2]: handling requests is the only thing it does, which lets it run very efficiently, but means that any other functionality we want to add will need to fit inside the request handler.

[1]It’s built on Avahi and ZeroMQ, and the code is here, if you’re interested.
[2]Actually, this is not the full story: there are plenty of ways to work around this limitation, but it’s better to avoid them, if possible.

The Client

Great, so we’ve got a client, but we need some way to test it. Writing the client is just as simple:

from network import Client

client = Client('echo')
client.find_server()

We start by importing and creating a network.Client which speaks the “echo” protocol (matching the protocol advertised by our server above). Then we call network.Client.find_server(), which will (by default) show a console interface for choosing a server and connect to it.

while True:
    message = input('> ')
    response = client.send(message)
    print(response)

The rest of our program is just a simple loop: read a line of text from the user, send it to the server, and print the response.

Running it

Now that you’ve got both a server and a client, it’s time to try them out: run the server in one window, and the client in another. They can be started in any order: the client will wait for the server to start up.

Once the server is started, the client should show the server’s name, and you can choose it by entering the associated number and pressing enter. Then you can type messages and see them get sent back from the server!

Note

Unfortunately, if you restart the server, it will be started on a different TCP port than the previous instance, and so the client won’t be able to reconnect. You can fix this by specifying a port when you create the server.

The Task

OK, so you’ve got the hang of building a simple client and server. Your task is to take your existing MUD program, and split it into a client and a server.

For the moment, don’t worry about how multiple players will work: we’re just working on separating out the display from the backend. The server will have most of your previous work: it will read the world, but rather than calling input() in a loop, you’ll have a single function which takes a line of text, and returns the text to be printed. The client will be very similar to the echo client: read input, send it to the server, and print the output.

One important change you’ll need to make: you’ll need to add a look command, which prints out the room. This is so that the client can send it automatically at startup, so the user knows where they are when the game starts.

We still only have one player in the game, so if multiple clients connect to the server, they will all be controlling the same player. On this note, make sure your server’s name is distinct, so you don’t accidentally connect to someone else’s server!

Extension

Rather than sending plain text over the network, you can instead send JSON dictionaries, and hence add more information. For instance, you could do the formatting and printing of the room in the client, rather than the server.

Table Of Contents

Previous topic

Stage 2

Next topic

Stage 4

This Page