Networked Connect4

Networked Connect4

Due incrementally - see submission checklist at the bottom of this page.

Objectives

Develop a simple client-server game system employing multiple threads and communication over a network using sockets.

Description

Part 0: Playing games


Begin by familiarising yourself with using the gameclient program. It is a linux-based program so copy it into a directory called Project5 under your CS home folder, and run it using the following command:


gameclient stinking-cloud.cs.utexas.edu

The single command line argument is the host-name of the CS network machine on which the gameserver program is running. Play some games against other members of the class, or even against other instances of the gameclient which you run up. During game-play, if a participating gameclient process is killed or loses its connection to the server then that game instance will crash, but because the gameserver runs each game instance on a separate thread, other games that are in progress will not be affected. Once a game begins, it must be played to completion before the gameclient will return to the main menu. Consequently, one player walking away from their computer forces the opponent to wait. For this reason it is usually best to run each gameclient process in a separate screen session, which allows you to detach from a waiting gameclient without killing it, switch to another one, and switch back later.


Part 1: Synchronous communication with sockets

Due 10/14 by midnight


Sockets provide the abstraction for network communication used in this project. This project uses sockets according to the TCP connection protocol shown in Figure 21.1 on PER page 450. The design of the gameclient and gameserver is such that all socket communication is client-server. There is no direct client-client communication. The server maintains a single passive socket whose sole purpose is to listen for clients trying to connect and provide each connecting client with its own dedicated active socket. Communication proceeds according to the following steps, which are repeated multiple times during the lifetime of each gameclient process:


  1. A client requests a connection to the server
  2. The server provides the client with a dedicated active socket
  3. Client and server communicate over the active socket:
    1. The server waits for a message from the client
    2. The client sends a message to the server and waits for a response
    3. The server sends a response message to the client (Goto step 3(i))
  4. The active socket is closed (Restart from step 1 for further communication)

How many messages are sent between client and server over the active socket before it is closed depends on the purpose of the communication. Before game-play starts (pre-game) the purpose of the communication is to build menus on the client and handle user-requests to initiate and join games. Therefore, each active socket is connected for only the duration of one message-response roundtrip, i.e. one iteratiobn of step 3 above. After game-play starts (in-game) the purpose of the communication is to send user-moves and updated game-state an arbitrary number of times, so the server maintains a connected active socket to each participating client for the entire duration of the game, i.e. multiple iterations of step 3 above. The communication is synchronous in that when either process expects to receive a message, it will block progress of its own code until either it receives a message or times out. The socket methods that block are accept (used only by the server) and recv (used by both client and server).


In this part of the project you will become familiar with the socket methods that are used by the client. Specifically:


import socket

# make a new socket object
channel = socket.socket()
# request a connection from the gameserver passive socket
HOST = 'stinking-cloud.cs.utexas.edu'
PORT = 1025
channel.connect((HOST, PORT))
# send an invalid message to the server
msgbody = 'the socket send method can only send strings'
channel.send(msgbody)
# block until a message is received from the server (1024 byte limit)
response = channel.recv(1024)
print response
# close the socket (cannot be re-opened)
channel.close()

The restriction on sending strings is incovenient and doesn't specify a protocol for identifying where one message ends and another begins in the buffer. For this reason a communication helper class MsgProxy has been defined that handles the sending and receiving of multiple messages where each message is a separate python object. Any object can be sent and received using MsgProxy but in this project all pre-game messages are objects of type tuple, which provides an immutable container for multi-part message data. (All in-game messages will simply be whatever string the user entered, not a tuple). Copy the msgproxy.py file to your Project5 directory and study the code in it.


Your first task is to turnin a file msgproxy.txt in which you describe (in English) how the MsgProxy works. In particular, you should make sure your explanation includes answers to the following questions:


  1. What state is maintained by an instance of MsgProxy?
  2. How and why is the marshal module used?
  3. How do the MsgProxy.send and MsgProxy.recv methods work?
  4. What happens if an exception occurs calling the socket send or recv methods?
  5. What assumptions are made by the MsgProxy class?
  6. Which aspects of communication does MsgProxy not attempt to take responsibility for?

You might find it helpful to consider the previous code example modified to use MsgProxy


import socket
import msgproxy

# make a new socket object
channel = socket.socket()
# make a new MsgProxy
msg = msgproxy.MsgProxy()
# request a connection from the gameserver passive socket
HOST = 'stinking-cloud.cs.utexas.edu'
PORT = 1025
channel.connect((HOST, PORT))
# send a valid message to the server
msgbody = ('OPPONENTS',)
msg.send(channel, msgbody)
# try to read one message from the buffer, will block if empty
response = msg.recv(channel)
print response
# close the socket (cannot be re-opened)
channel.close()

Try entering this example code at the python interactive prompt. Copy and paste your interactive session into msgproxy.txt after your answers. Then try running the same example code as a python script, and again copy and paste the terminal trace into your msgproxy.txt file before turning it in. Finally, see how far you can get writing scripts that communicate with the server in this way. Can you get the server to send you an initial Connect4 game board? If so, add the trace to msgproxy.txt.


Part 2: Write your own gameclient

Due 10/18 by midnight


Your next task is to write a program gameclient.py that exactly replicates the behavior of the gameclient program that you familiarised yourself with in Part 0. The example code given in Part 1 will be helpful. An important point is that the gameclient does not know anything about the details of the game. It is a 'thin' client in that it handles only the user interface, not game-specific logic, and is therefore generic for any two-player turn-based game for which a nicely formatted string (sent from the server) is sufficient to describe the game state. Your program can make the following assumptions:


  1. I will test your program with the command:
    python gameclient.py server-hostname
    so you need to make sure your script reads the command-line argument given at test time, i.e. hard-coding stinking-cloud.cs.utexas.edu will not work. You may however assume the port number is 1025.
  2. All messages must be sent and received using an instance of MsgProxy
  3. Pre-game messages from client to sever must adhere to one of the formats given in the server timeout message you should have seen in Part 1.
  4. The server's pre-game responses are limited to:
    1. A list of strings representing the kinds of games that can be played, e.g. ['Connect4', 'Chess']
    2. A list of waiting opponent tuples
      (int(gameID), str(plyr1Name), str(plyr2Name), str(gameName))
      e.g.
      [(1, 'Julian', None, 'Connect4'), (3, None, 'Bob', 'Chess')]
    3. True for granted ('REQUEST', ...) messages
    4. False for denied ('REQUEST', ...) messages
    5. ('ERROR', str(exceptionName), tuple(exceptionData)) if an unexpected error occurs
  5. The server will close its connection to the client after sending a pre-game response, except in the case that its response was to grant a ('REQUEST', ...) message by sending True.
  6. Upon receiving any pre-game response other than True, the client should close its socket immediately.
  7. Upon receiving the pre-game response True, the client should not close its socket until the game is over.
  8. In-game messages from client to server should not be pre-validated on the client. Just send the user's raw_input string because only the game-specific code on the server can validate it.
  9. The server's in-game responses are limited to:
    1. Strings that have 'INVALID MOVE' as a sub-string
    2. Well formatted strings that represent the state of the game
    3. Strings that have 'GAME OVER' as a sub-string
  10. In-game communication involves a loop in which the client sends once, but receives twice: once for the response to its move, and then once for the opponent's move.

It will also be helpful to use the techniques from Project1 for validating user input and handling exceptions. Exception handling is particularly important because any attempt to connect, send or receive can result in an exception. You must ensure that any open socket is closed before the program exits. The client program does not need a lot of code: my solution is 133 lines long.


Part 3: Write your own gameserver

Due 10/23 by class - no slip days


In this part of the project you will write a program gameserver.py that exactly replicates the behavior of the game server program that is running on stinking-cloud. DO NOT RUN YOUR OWN GAME SERVER ON STINKING-CLOUD! To be safe, you should not use port 1025 either in case one of your classmates coincidentally chooses to run their server on the same machine as you (but then remember to change it in your client too).


Your gameserver.py program will provide a rudimentary socket server that handles client connection attempts and pre-game communication with each client. It is not responsible for in-game communication or defining game-specific logic, but will have to import modules that are. Hence you are provided with the exact code files that implement Connect4 for the server you have been using so far: connect4.py and gamebases.py which are posted on Blackboard under 'Course Documents'. DO NOT MODIFY THE CODE IN THESE FILES! It is important that you understand the code in these files BEFORE you write your gameserver. I will test your gameserver.py program assuming it uses these files and also msgproxy.py. I will test using the command python gameserver.py on a machine of my choosing.


The gameserver is a multi-threaded application. See PER pp 436-447 for a description of Python's multi-threading capabilities. The files clock_server2.py and clock_client2.py (posted to Blackboard under 'Course Documents') illustrate some basic concepts for making and using a multi-threaded socket server. Make sure you understand these files and are comfortable playing with them yourself as shown in class. This project improves on them by using the WorkerThread class (in gamebases.py and described in PER pp 444-446) which avoids the problem of synchronizing access to shared state, i.e. you will not need to use Locks, Semaphores, Condition Variables etc to successfully complete this project. The transition between pre-game and in-game communication should be handled by instantiating a Connect4 object, calling its start method which runs it on a separate thread, and using its send method to pass it players when granting their REQUESTs. (The send method of a Connect4 object has nothing to do with the send method of a socket or MsgProxy object and performs a totally different function).


Begin by studying the code in the TwoPlayerGame class. The do_work method coordinates the communication and turn taking for one game. It expects to be passed an object representing a player. Notice that it will only begin game-play after it has recevied two players. You must define the Player class as part of your gameserver.py program. The attributes of a Player object can be inferred by looking at how it is used in the do_work method.


As always, pay particular care to Exception handling. By now you should be aware that an instance of MsgProxy actually protects the rest of the application from Exceptions that occur at the point of sending or receiving over a socket. The MsgProxy.send and MsgProxy.recv methods never raise an Exception back to the calling code, but instead indicate success or failure with their return values.


FYI, my solution to gameserver.py is shorter than my solution to gameclient.py.


Part 4: Write your own game (extra credit)


A game can be added to the system by writing a file equivalent to connect4.py and making sure your gameserver.py imports the appropriate game class from it and includes that game in response to the pre-game ('GAMES',) message. If your game is robust and well written enough then it will be included in the main gameserver in future years. Suggested games are Chess, Checkers, and Othello, but feel free to be creative.

Grading Criteria

Your code will be graded on style and correctness and should be as elegant as possible. Marks will be deducted for needlessly long code.

Submission Checklist

  • msgproxy.txt (due 10/8 by midnight)
  • gameclient.py (due 10/15 by midnight)
  • gameserver.py (due 10/24 by class - no slip days)