Programming lesson
Build a Networked Tic-Tac-Toe Game in Python: A Step-by-Step Tutorial for COP4521
Learn how to develop a two-player Tic-Tac-Toe game over the internet using Python sockets. This tutorial covers client-server architecture, game loop design, and error handling—perfect for COP4521 Assignment 4.
Introduction
In this tutorial, you will learn how to build a networked Tic-Tac-Toe game application in Python. This project is inspired by the COP4521 assignment 4, where you develop a server and client that allow two players to play over the internet. By the end, you'll understand socket programming, game state management, and turn-based communication. This is a great way to practice networked application development skills that are essential for modern software—think of it like building the backend for a real-time multiplayer game, similar to how apps like Among Us or online chess platforms handle connections. Let's dive in!
Understanding the Client-Server Model
The core of this project is the client-server architecture. The server waits for a client to connect, and once connected, they exchange moves until the game ends. In our Tic-Tac-Toe game, the second player runs the server, and the first player runs the client. This is a common pattern in multiplayer game programming—for example, in online gaming tournaments, one machine acts as the authoritative server. Here's a high-level overview:
- Server: Binds to a port, listens for a connection, then alternates receiving and sending moves.
- Client: Connects to the server's IP and port, sends the first move, then waits for responses.
The server must handle multiple games sequentially—after one game ends, it loops back to wait for a new client. This is similar to how a gaming server handles matchmaking: one match finishes, then the server waits for the next opponent.
Setting Up the Project
We'll create two Python files: server.py and client.py. The server takes a port number as a command-line argument, and the client takes the server's hostname and port. For example:
python3 server.py 55000
python3 client.py localhost 55000Make sure to include a header comment with your name and assignment details, as required by your instructor. Use the socket module for networking and sys for command-line arguments.
Designing the Game Board
We'll represent the board as a 3x3 list of characters: '.' for empty, '#' for player 1 (client), and 'O' for player 2 (server). The board is displayed with row labels A, B, C and column labels 1, 2, 3. Here's a function to print the board:
def print_board(board):
print(" 1 2 3")
for i, row in enumerate(['A','B','C']):
print(row, ' '.join(board[i]))This function will be called after every move to show the current state. Think of it as the game UI—simple but effective.
Implementing the Server
The server performs these steps:
- Create a socket, bind to the given port, and listen for connections.
- Accept a client connection and print the client's IP and port.
- Initialize an empty board and send it to the client (or just display locally).
- Loop until the game ends: wait for the client's move, apply it, check for win/tie, display board, then prompt the local player (server) for a move, send it, and check again.
- After the game, send the result and close the client socket, then loop back to accept a new client.
Here's a skeleton for the server loop:
import socket
import sys
def main():
port = int(sys.argv[1])
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(('', port))
server_socket.listen(1)
print(f"Server listening on port {port}")
while True:
client_socket, addr = server_socket.accept()
print(f"Connected to {addr}")
board = [['.' for _ in range(3)] for _ in range(3)]
# Game loop...
client_socket.close()
if __name__ == "__main__":
main()Implementing the Client
The client is simpler: it connects to the server, then alternates sending its move and receiving the server's response. The client always moves first. Here's the flow:
- Connect to the server using host and port from command line.
- Display the initial board.
- Loop: prompt the user for a move, validate it, send it to the server, receive the server's move, display board, and check for game over.
- When the game ends, display the result and exit.
Example client code:
import socket
import sys
def main():
host = sys.argv[1]
port = int(sys.argv[2])
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect((host, port))
board = [['.' for _ in range(3)] for _ in range(3)]
# Game loop...
client_socket.close()
if __name__ == "__main__":
main()Validating Moves
Both server and client must check that a move is valid: the row must be A, B, or C; the column must be 1, 2, or 3; and the cell must be empty. If invalid, prompt again. This is crucial for error handling in networked games. For example:
def is_valid_move(move, board):
if len(move) != 2:
return False
row = move[0].upper()
col = move[1]
if row not in ['A','B','C'] or col not in ['1','2','3']:
return False
r = ord(row) - ord('A')
c = int(col) - 1
return board[r][c] == '.'If the move is invalid, send an error message and ask again. This prevents cheating and ensures a smooth experience.
Checking for Win or Tie
After each move, check if the current player has won by examining rows, columns, and diagonals. Also check if the board is full (tie). Return a status: 'win', 'tie', or 'continue'. This is a classic algorithm for Tic-Tac-Toe and can be reused in both server and client.
def check_winner(board):
# Check rows, columns, diagonals
for i in range(3):
if board[i][0] == board[i][1] == board[i][2] != '.':
return board[i][0]
if board[0][i] == board[1][i] == board[2][i] != '.':
return board[0][i]
if board[0][0] == board[1][1] == board[2][2] != '.':
return board[0][0]
if board[0][2] == board[1][1] == board[2][0] != '.':
return board[0][2]
return None
def is_board_full(board):
return all(cell != '.' for row in board for cell in row)Communication Protocol
Decide on a simple text-based protocol. For example:
- Send moves as strings like
A1,B2. - Send game state updates as the board string or a result message.
- Use
sendall()andrecv()with a fixed buffer size (e.g., 1024).
To avoid blocking, you can use select or threading, but for simplicity, assume players take turns correctly. This is a synchronous turn-based protocol, similar to online board games like chess.com.
Handling Game End and Restart
When the game ends, the server sends a message like WINNER:# or TIE to the client. The server then closes the client socket and loops back to accept(). The client prints the result and exits. This design is scalable for multiple game sessions—the server can run indefinitely, handling one game after another.
Testing Your Application
Test locally using localhost or with a partner on the same network. Try these scenarios:
- Client wins with three in a row.
- Server wins.
- Tie game (full board).
- Invalid moves (e.g.,
D4, occupied cell).
Make sure the board displays correctly on both sides. This is similar to debugging a multiplayer game—you need to simulate both players' actions.
Conclusion
You've built a fully functional networked Tic-Tac-Toe game! This project teaches you socket programming, client-server architecture, and game state management. These skills are directly applicable to building real-time multiplayer applications, chat systems, and even IoT device communication. As a next step, consider adding features like a graphical interface, multiple simultaneous games, or AI opponents. Happy coding!