ASSISTANT
Creating an online game using Python typically involves both backend and frontend development. Below, I'll guide you through creating a simple multiplayer **Tic-Tac-Toe** game using Python's Flask framework for the backend and Socket.IO for real-time communication between clients and the server. The frontend will use HTML, CSS, and JavaScript to interact with the backend.
### Table of Contents
1. [Prerequisites](#prerequisites)
2. [Project Structure](#project-structure)
3. [Backend Setup with Flask and Flask-SocketIO](#backend-setup-with-flask-and-flask-socketio)
4. [Frontend Setup](#frontend-setup)
5. [Running the Game](#running-the-game)
6. [Complete Code](#complete-code)
7. [Conclusion](#conclusion)
---
### Prerequisites
Before you begin, ensure you have the following installed:
- **Python 3.6+**
- **pip** (Python package installer)
- **Node.js and npm** (optional, if you want to manage frontend packages)
You can check if you have Python installed by running:
```bash
python --version
```
### Project Structure
Create a project directory with the following structure:
```
online-tic-tac-toe/
├── app.py
├── requirements.txt
├── templates/
│ └── index.html
└── static/
├── style.css
└── script.js
```
### Backend Setup with Flask and Flask-SocketIO
1. **Create a Virtual Environment (Optional but Recommended):**
```bash
python -m venv venv
source venv/bin/activate # On Windows use `venv\Scripts\activate`
```
2. **Install Dependencies:**
Create a `requirements.txt` file:
```txt
Flask==2.3.2
Flask-SocketIO==5.3.4
eventlet==23.1.0
```
Then install them:
```bash
pip install -r requirements.txt
```
3. **`app.py` – The Flask Application:**
```python
from flask import Flask, render_template
from flask_socketio import SocketIO, emit, join_room, leave_room
import eventlet
eventlet.monkey_patch()
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!'
socketio = SocketIO(app, cors_allowed_origins="*")
# Store game states
games = {} # room_id: {'players': [player1_sid, player2_sid], 'board': [...], 'turn': 'X'}
@app.route('/')
def index():
return render_template('index.html')
@socketio.on('create_game')
def handle_create_game(data):
room = data.get('room')
sid = request.sid
if room not in games:
games[room] = {'players': [sid], 'board': [''] * 9, 'turn': 'X'}
join_room(room)
emit('game_created', {'room': room}, room=sid)
else:
emit('error', {'message': 'Room already exists.'}, room=sid)
@socketio.on('join_game')
def handle_join_game(data):
room = data.get('room')
sid = request.sid
if room in games and len(games[room]['players']) < 2:
games[room]['players'].append(sid)
join_room(room)
emit('game_joined', {'room': room, 'board': games[room]['board'], 'turn': games[room]['turn']}, room=sid)
emit('start_game', {'board': games[room]['board'], 'turn': games[room]['turn']}, room=room)
else:
emit('error', {'message': 'Room is full or does not exist.'}, room=sid)
@socketio.on('make_move')
def handle_make_move(data):
room = data.get('room')
index = data.get('index')
player = data.get('player') # 'X' or 'O'
if room in games:
game = games[room]
if game['board'][index] == '' and game['turn'] == player:
game['board'][index] = player
game['turn'] = 'O' if player == 'X' else 'X'
emit('update_board', {'board': game['board'], 'turn': game['turn']}, room=room)
# Optionally, check for a winner here
else:
emit('error', {'message': 'Invalid move.'}, room=request.sid)
else:
emit('error', {'message': 'Game does not exist.'}, room=request.sid)
@socketio.on('disconnect')
def handle_disconnect():
for room, game in games.items():
if request.sid in game['players']:
game['players'].remove(request.sid)
emit('player_left', {'message': 'A player has left the game.'}, room=room)
if not game['players']:
del games[room]
break
if __name__ == '__main__':
socketio.run(app, debug=True)
```
**Explanation:**
- **Routes:**
- `/` serves the main HTML page.
- **Socket.IO Events:**
- `create_game`: Creates a new game room.
- `join_game`: Joins an existing game room.
- `make_move`: Handles player moves.
- `disconnect`: Cleans up when a player disconnects.
### Frontend Setup
1. **`templates/index.html`:**
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Online Tic-Tac-Toe</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.min.js" integrity="sha512-z/0Xwt2PBYqDaF2WCzBtgdrTXyN8ueBABeseN6C4bMvkUC83Gb7hAcqGjEcwGLr0RKI639akE+YgqWWVlx0NgA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</head>
<body>
<h1>Online Tic-Tac-Toe</h1>
<div id="login">
<input type="text" id="room" placeholder="Room ID">
<button id="create">Create Game</button>
<button id="join">Join Game</button>
</div>
<div id="game" style="display:none;">
<h2 id="status"></h2>
<div id="board">
<!-- 9 cells -->
<div class="cell" data-index="0"></div>
<div class="cell" data-index="1"></div>
<div class="cell" data-index="2"></div>
<div class="cell" data-index="3"></div>
<div class="cell" data-index="4"></div>
<div class="cell" data-index="5"></div>
<div class="cell" data-index="6"></div>
<div class="cell" data-index="7"></div>
<div class="cell" data-index="8"></div>
</div>
<button id="restart" style="display:none;">Restart Game</button>
</div>
<script src="{{ url_for('static', filename='script.js') }}"></script>
</body>
</html>
```
2. **`static/style.css`:**
```css
body {
font-family: Arial, sans-serif;
text-align: center;
background-color: #f0f0f0;
}
#board {
display: grid;
grid-template-columns: repeat(3, 100px);
grid-template-rows: repeat(3, 100px);
gap: 5px;
justify-content: center;
margin: 20px auto;
}
.cell {
background-color: #fff;
border: 1px solid #ccc;
display: flex;
align-items: center;
justify-content: center;
font-size: 2em;
cursor: pointer;
}
.cell:hover {
background-color: #e0e0e0;
}
#status {
margin-top: 20px;
font-size: 1.2em;
}
```
3. **`static/script.js`:**
```javascript
const socket = io();
const createBtn = document.getElementById('create');
const joinBtn = document.getElementById('join');
const roomInput = document.getElementById('room');
const gameDiv = document.getElementById('game');
const loginDiv = document.getElementById('login');
const statusText = document.getElementById('status');
const cells = document.querySelectorAll('.cell');
const restartBtn = document.getElementById('restart');
let room = '';
let player = '';
let myTurn = false;
createBtn.addEventListener('click', () => {
const roomID = roomInput.value.trim();
if (roomID) {
socket.emit('create_game', { room: roomID });
}
});
joinBtn.addEventListener('click', () => {
const roomID = roomInput.value.trim();
if (roomID) {
socket.emit('join_game', { room: roomID });
}
});
socket.on('game_created', data => {
room = data.room;
player = 'X';
statusText.innerText = `Waiting for opponent to join room: ${room}`;
loginDiv.style.display = 'none';
gameDiv.style.display = 'block';
});
socket.on('game_joined', data => {
room = data.room;
player = 'O';
myTurn = false;
statusText.innerText = `Joined room: ${room}. Waiting for your turn.`;
loginDiv.style.display = 'none';
gameDiv.style.display = 'block';
updateBoard(data.board);
});
socket.on('start_game', data => {
if (player === 'X') {
myTurn = true;
statusText.innerText = `Game started. Your turn (${player}).`;
} else {
statusText.innerText = `Game started. Opponent's turn (${data.turn}).`;
}
});
socket.on('update_board', data => {
updateBoard(data.board);
if (data.turn === player) {
myTurn = true;
statusText.innerText = `Your turn (${player}).`;
} else {
myTurn = false;
statusText.innerText = `Opponent's turn (${data.turn}).`;
}
// Optionally, you can add win/draw detection here
});
socket.on('player_left', data => {
alert(data.message);
resetGame();
});
socket.on('error', data => {
alert(data.message);
});
cells.forEach(cell => {
cell.addEventListener('click', () => {
if (myTurn && cell.innerText === '') {
const index = cell.getAttribute('data-index');
socket.emit('make_move', { room: room, index: index, player: player });
}
});
});
function updateBoard(board) {
board.forEach((mark, index) => {
cells[index].innerText = mark;
});
}
function resetGame() {
room = '';
player = '';
myTurn = false;
statusText.innerText = '';
cells.forEach(cell => cell.innerText = '');
loginDiv.style.display = 'block';
gameDiv.style.display = 'none';
}
restartBtn.addEventListener('click', () => {
// Implement restart functionality as needed
resetGame();
});
```
**Explanation:**
- **Creating and Joining Games:**
- Players can create a game by specifying a room ID.
- Another player can join using the same room ID.
- **Gameplay:**
- Players take turns clicking on cells to place their mark (X or O).
- The board updates in real-time for both players.
- **Handling Disconnections:**
- If a player disconnects, the other player is notified.
### Running the Game
1. **Start the Flask Server:**
Ensure you're in the project directory and your virtual environment is activated, then run:
```bash
python app.py
```
You should see output indicating that the server is running, e.g.:
```
* Serving Flask app 'app'
* Debug mode: on
...
```
2. **Access the Game in Browser:**
Open your web browser and navigate to `http://localhost:5000`. You should see the Tic-Tac-Toe interface.
3. **Playing the Game:**
- **Player 1:**
- Enters a unique Room ID and clicks "Create Game".
- Waits for another player to join.
- **Player 2:**
- Enters the same Room ID and clicks "Join Game".
- The game starts, and players can take turns making moves.
You can test the multiplayer functionality by opening the game in two different browser windows or tabs.
### Complete Code
For convenience, here's the complete code organized by file.
1. **`app.py`**
```python
from flask import Flask, render_template, request
from flask_socketio import SocketIO, emit, join_room, leave_room
import eventlet
eventlet.monkey_patch()
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!'
socketio = SocketIO(app, cors_allowed_origins="*")
# Store game states
games = {} # room_id: {'players': [player1_sid, player2_sid], 'board': [...], 'turn': 'X'}
@app.route('/')
def index():
return render_template('index.html')
@socketio.on('create_game')
def handle_create_game(data):
room = data.get('room')
sid = request.sid
if room not in games:
games[room] = {'players': [sid], 'board': [''] * 9, 'turn': 'X'}
join_room(room)
emit('game_created', {'room': room}, room=sid)
else:
emit('error', {'message': 'Room already exists.'}, room=sid)
@socketio.on('join_game')
def handle_join_game(data):
room = data.get('room')
sid = request.sid
if room in games and len(games[room]['players']) < 2:
games[room]['players'].append(sid)
join_room(room)
emit('game_joined', {'room': room, 'board': games[room]['board'], 'turn': games[room]['turn']}, room=sid)
emit('start_game', {'board': games[room]['board'], 'turn': games[room]['turn']}, room=room)
else:
emit('error', {'message': 'Room is full or does not exist.'}, room=sid)
@socketio.on('make_move')
def handle_make_move(data):
room = data.get('room')
index = data.get('index')
player = data.get('player') # 'X' or 'O'
if room in games:
game = games[room]
if game['board'][index] == '' and game['turn'] == player:
game['board'][index] = player
game['turn'] = 'O' if player == 'X' else 'X'
emit('update_board', {'board': game['board'], 'turn': game['turn']}, room=room)
# Optionally, check for a winner here
else:
emit('error', {'message': 'Invalid move.'}, room=request.sid)
else:
emit('error', {'message': 'Game does not exist.'}, room=request.sid)
@socketio.on('disconnect')
def handle_disconnect():
for room, game in list(games.items()):
if request.sid in game['players']:
game['players'].remove(request.sid)
emit('player_left', {'message': 'A player has left the game.'}, room=room)
if not game['players']:
del games[room]
break
if __name__ == '__main__':
socketio.run(app, debug=True)
```
2. **`requirements.txt`**
```txt
Flask==2.3.2
Flask-SocketIO==5.3.4
eventlet==23.1.0
```
3. **`templates/index.html`**
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Online Tic-Tac-Toe</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.min.js" integrity="sha512-z/0Xwt2PBYqDaF2WCzBtgdrTXyN8ueBABeseN6C4bMvkUC83Gb7hAcqGjEcwGLr0RKI639akE+YgqWWVlx0NgA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</head>
<body>
<h1>Online Tic-Tac-Toe</h1>
<div id="login">
<input type="text" id="room" placeholder="Room ID">
<button id="create">Create Game</button>
<button id="join">Join Game</button>
</div>
<div id="game" style="display:none;">
<h2 id="status"></h2>
<div id="board">
<!-- 9 cells -->
<div class="cell" data-index="0"></div>
<div class="cell" data-index="1"></div>
<div class="cell" data-index="2"></div>
<div class="cell" data-index="3"></div>
<div class="cell" data-index="4"></div>
<div class="cell" data-index="5"></div>
<div class="cell" data-index="6"></div>
<div class="cell" data-index="7"></div>
<div class="cell" data-index="8"></div>
</div>
<button id="restart" style="display:none;">Restart Game</button>
</div>
<script src="{{ url_for('static', filename='script.js') }}"></script>
</body>
</html>
```
4. **`static/style.css`**
```css
body {
font-family: Arial, sans-serif;
text-align: center;
background-color: #f0f0f0;
}
#board {
display: grid;
grid-template-columns: repeat(3, 100px);
grid-template-rows: repeat(3, 100px);
gap: 5px;
justify-content: center;
margin: 20px auto;
}
.cell {
background-color: #fff;
border: 1px solid #ccc;
display: flex;
align-items: center;
justify-content: center;
font-size: 2em;
cursor: pointer;
}
.cell:hover {
background-color: #e0e0e0;
}
#status {
margin-top: 20px;
font-size: 1.2em;
}
```
5. **`static/script.js`**
```javascript
const socket = io();
const createBtn = document.getElementById('create');
const joinBtn = document.getElementById('join');
const roomInput = document.getElementById('room');
const gameDiv = document.getElementById('game');
const loginDiv = document.getElementById('login');
const statusText = document.getElementById('status');
const cells = document.querySelectorAll('.cell');
const restartBtn = document.getElementById('restart');
let room = '';
let player = '';
let myTurn = false;
createBtn.addEventListener('click', () => {
const roomID = roomInput.value.trim();
if (roomID) {
socket.emit('create_game', { room: roomID });
}
});
joinBtn.addEventListener('click', () => {
const roomID = roomInput.value.trim();
if (roomID) {
socket.emit('join_game', { room: roomID });
}
});
socket.on('game_created', data => {
room = data.room;
player = 'X';
statusText.innerText = `Waiting for opponent to join room: ${room}`;
loginDiv.style.display = 'none';
gameDiv.style.display = 'block';
});
socket.on('game_joined', data => {
room = data.room;
player = 'O';
myTurn = false;
statusText.innerText = `Joined room: ${room}. Waiting for your turn.`;
loginDiv.style.display = 'none';
gameDiv.style.display = 'block';
updateBoard(data.board);
});
socket.on('start_game', data => {
if (player === 'X') {
myTurn = true;
statusText.innerText = `Game started. Your turn (${player}).`;
} else {
statusText.innerText = `Game started. Opponent's turn (${data.turn}).`;
}
});
socket.on('update_board', data => {
updateBoard(data.board);
if (data.turn === player) {
myTurn = true;
statusText.innerText = `Your turn (${player}).`;
} else {
myTurn = false;
statusText.innerText = `Opponent's turn (${data.turn}).`;
}
// Optionally, you can add win/draw detection here
});
socket.on('player_left', data => {
alert(data.message);
resetGame();
});
socket.on('error', data => {
alert(data.message);
});
cells.forEach(cell => {
cell.addEventListener('click', () => {
if (myTurn && cell.innerText === '') {
const index = cell.getAttribute('data-index');
socket.emit('make_move', { room: room, index: index, player: player });
}
});
});
function updateBoard(board) {
board.forEach((mark, index) => {
cells[index].innerText = mark;
});
}
function resetGame() {
room = '';
player = '';
myTurn = false;
statusText.innerText = '';
cells.forEach(cell => cell.innerText = '');
loginDiv.style.display = 'block';
gameDiv.style.display = 'none';
}
restartBtn.addEventListener('click', () => {
// Implement restart functionality as needed
resetGame();
});
```
### Enhancements and Considerations
The above implementation is a basic foundation for an online Tic-Tac-Toe game. Here are some suggestions for enhancements and considerations:
1. **Win and Draw Detection:**
- Implement logic on the server to detect when a player has won or when the game is a draw.
- Notify both players accordingly and offer options to restart or exit.
2. **Restart Functionality:**
- Allow players to restart the game after it ends without leaving the room.
3. **Room Management:**
- Generate unique room IDs automatically instead of manual entry.
- Implement features like listing available rooms or limiting the number of concurrent rooms.
4. **User Authentication:**
- Integrate user authentication to identify players uniquely.
5. **Persistent Storage:**
- Use a database to store game histories, player statistics, etc.
6. **Responsive Design:**
- Enhance the frontend to be responsive and visually appealing across different devices.
7. **Security:**
- Implement measures to prevent cheating, such as validating moves server-side.
- Secure the communication between clients and server.
8. **Scalability:**
- Deploy the application using scalable infrastructure if expecting high traffic.
- Consider using message brokers or scaling Socket.IO with multiple workers.
### Deployment
To make your game accessible online, consider deploying it to a cloud platform like **Heroku**, **AWS Elastic Beanstalk**, **Google App Engine**, or **DigitalOcean**. Ensure that the deployment platform supports WebSockets, which are essential for real-time communication with Socket.IO.
**Example Deployment Steps on Heroku:**
1. **Install the Heroku CLI:**
Follow instructions from [Heroku's official documentation](https://devcenter.heroku.com/articles/heroku-cli).
2. **Create a `Procfile`:**
```Procfile
web: python app.py
```
3. **Initialize Git and Commit Code:**
```bash
git init
git add .
git commit -m "Initial commit"
```
4. **Create a Heroku App and Deploy:**
```bash
heroku create your-app-name
git push heroku master
```
5. **Scale the Web Dyno:**
```bash
heroku ps:scale web=1
```
6. **Open the App:**
```bash
heroku open
```
*Note:* Heroku may require specific configurations for Flask and Socket.IO. Ensure proper setup as per [Flask-SocketIO deployment guidance](https://flask-socketio.readthedocs.io/en/latest/deployment.html).
### Conclusion
You've now built a basic online multiplayer Tic-Tac-Toe game using Python's Flask framework and Socket.IO for real-time communication. This foundational project can be expanded with additional features, improved UI/UX, and enhanced security measures. Exploring these enhancements will deepen your understanding of web development, real-time communication, and game design.
Feel free to customize and expand upon this project to create more complex and engaging online games!