How to Create a Two-Player Tic-Tac-Toe Game with Python and Vue
Online multiplayer games are extremely popular, but building one can seem daunting, especially if you‘re new to web development. In this guide, we‘ll walk through how to create a simple two-player tic-tac-toe game using Python, Vue, and Pusher Channels.
We‘ll cover everything you need to know, from setting up the project to deploying the final version. By the end, you‘ll have a working game you can play with your friends, and a solid foundation for building more complex multiplayer webapps.
Prerequisites
To follow along with this tutorial, you should have a basic understanding of:
- Python
- Flask (a Python web framework)
- Vue.js
- JavaScript (ES6+ syntax)
- HTML/CSS
You‘ll also need Python 3.6+ and Node.js installed on your machine.
Game Overview
Here‘s a quick overview of how the game will work:
- Players can create a new game or join an existing one by entering a username
- The game board is a simple 3×3 grid where players take turns marking spaces
- Player moves are synchronized in realtime across different browser sessions
- The game ends when a player gets three in a row or the board is full
- Players can just refresh the page to start a new game
Behind the scenes, the application consists of three main parts:
- A Python backend API built with Flask
- A Vue frontend for the user interface
- Pusher for realtime communication between clients and the server
The backend will manage the game state and expose HTTP endpoints for the frontend to retrieve data and send user actions. Pusher channels will be used to broadcast game updates to each connected client.
Project Setup
Let‘s get started by setting up a new project. Create a directory for the app and initialize a virtual environment:
mkdir tictactoe-app
cd tictactoe-app
python3 -m venv venv
source venv/bin/activate # Linux/macOS
venv\Scripts\activate # Windows
Install Flask and the Pusher library:
pip install flask pusher
For the frontend, we‘ll use the Vue CLI to bootstrap a new project:
npm install -g @vue/cli
vue create frontend
Choose the default preset and wait for the installation to complete.
Creating the Backend
Inside the project directory, create a new file app.py
for the Flask server code:
from flask import Flask, jsonify, request
from pusher import Pusher
app = Flask(__name__)
pusher = Pusher(app_id=PUSHER_APP_ID, key=PUSHER_KEY, secret=PUSHER_SECRET, cluster=PUSHER_CLUSTER)
@app.route(‘/game‘, methods=[‘POST‘])
def create_game():
# Generate a unique game ID
game_id = generate_game_id()
# Add the game to a store/database
add_game(game_id)
return jsonify(game_id=game_id)
@app.route(‘/game/<game_id>‘, methods=[‘GET‘])
def get_game(game_id):
game = get_game_from_store(game_id)
return jsonify(game)
@app.route(‘/game/<game_id>/move‘, methods=[‘POST‘])
def make_move(game_id):
# Parse the move from the request
player = request.json[‘player‘]
row = request.json[‘row‘]
col = request.json[‘col‘]
# Update the game state
game = update_game(game_id, player, row, col)
# Broadcast the move to all players
pusher.trigger(f‘game-{game_id}‘, ‘move‘, {‘player‘: player, ‘row‘: row, ‘col‘: col})
return jsonify(success=True)
# More code to handle game state...
if __name__ == ‘__main__‘:
app.run()
This sets up the basic endpoints for creating a game, getting the game state, and making a move. We use Pusher to broadcast move events to all players subscribed to a game-specific channel.
You‘ll need to fill in the functions for generating IDs, adding/retrieving games from a store, and updating the state with new moves. For now, we‘ll just hardcode these parts.
Be sure to replace PUSHER_APP_ID
, PUSHER_KEY
, PUSHER_SECRET
, PUSHER_CLUSTER
with your own Pusher credentials.
Building the Frontend
Navigate to the frontend
directory and install the Pusher JS library:
npm install pusher-js
Open src/App.vue
and replace the contents with:
<template>
<div id="app">
<!-- Show the game board or a join form -->
<div v-if="gameId">
<Board :game="game" @move="handleMove" />
<p>Game ID: {{gameId}}</p>
</div>
<div v-else>
<form @submit.prevent="joinGame">
<div>
<input placeholder="Enter your name" v-model="playerName">
</div>
<div>
<input placeholder="Enter a game ID" v-model="gameIdToJoin">
</div>
<button type="submit">Join Game</button>
<button @click="createGame">Create New Game</button>
</form>
</div>
</div>
</template>
<script>
import Pusher from ‘pusher-js‘;
import Board from ‘./components/Board.vue‘;
Pusher.logToConsole = true;
const pusher = new Pusher(PUSHER_APP_KEY, {
cluster: PUSHER_APP_CLUSTER,
});
export default {
name: ‘App‘,
components: {
Board,
},
data() {
return {
playerName: ‘‘,
game: {
board: [
[‘‘, ‘‘, ‘‘],
[‘‘, ‘‘, ‘‘],
[‘‘, ‘‘, ‘‘],
],
players: [],
turn: ‘X‘,
},
gameId: null,
gameIdToJoin: ‘‘,
};
},
methods: {
async createGame() {
const res = await fetch(‘/game‘, {
method: ‘POST‘,
});
const data = await res.json();
this.gameId = data.game_id;
this.joinGame();
},
async joinGame() {
const res = await fetch(`/game/${this.gameIdToJoin || this.gameId}`);
const data = await res.json();
// Subscribe to game events from Pusher
const channel = pusher.subscribe(`game-${this.gameId}`);
channel.bind(‘move‘, this.opponentMove);
this.game = data;
this.game.players.push(this.playerName);
},
async handleMove(row, col) {
await fetch(`/game/${this.gameId}/move`, {
method: ‘POST‘,
headers: {
‘Content-Type‘: ‘application/json‘,
},
body: JSON.stringify({
player: this.playerName,
row,
col,
}),
});
},
opponentMove(data) {
// Update the board
this.game.board[data.row][data.col] = data.player;
// Toggle the turn
this.game.turn = this.game.turn === ‘X‘ ? ‘O‘ : ‘X‘;
},
},
};
</script>
<style>
/* Add some styles */
</style>
This sets up the main Vue component with a form for joining/creating games, and a Board
component for rendering the tic-tac-toe grid (we‘ll create that next).
The createGame
and joinGame
methods make requests to the backend API to create a new game or join an existing one. When a game is joined, we subscribe to a Pusher channel to listen for moves made by the opponent.
The handleMove
method is called when the player clicks a square on the board, and it sends the move to the backend.
Now let‘s create the Board
component. Add a new file src/components/Board.vue
:
<template>
<div class="board">
<div v-for="(row, rowIndex) in game.board" :key="rowIndex" class="row">
<div
class="cell"
v-for="(cell, colIndex) in row"
:key="colIndex"
@click="handleClick(rowIndex, colIndex)"
>
{{cell}}
</div>
</div>
</div>
</template>
<script>
export default {
props: [‘game‘],
methods: {
handleClick(row, col) {
// Prevent clicking on already filled cells
if (this.game.board[row][col]) return;
this.$emit(‘move‘, row, col);
// Fill in the cell with the current player‘s symbol
this.game.board[row][col] = this.game.turn;
// Toggle the turn
this.game.turn = this.game.turn === ‘X‘ ? ‘O‘ : ‘X‘;
// Check for a win
const winner = this.getWinner();
if (winner) {
alert(`${winner} wins!`);
this.resetBoard();
}
},
getWinner() {
const lines = [
// Rows
[[0,0], [0,1], [0,2]],
[[1,0], [1,1], [1,2]],
[[2,0], [2,1], [2,2]],
// Columns
[[0,0], [1,0], [2,0]],
[[0,1], [1,1], [2,1]],
[[0,2], [1,2], [2,2]],
// Diagonals
[[0,0], [1,1], [2,2]],
[[0,2], [1,1], [2,0]],
];
for (let i = 0; i < lines.length; i++) {
const [[a, b], [c, d], [e, f]] = lines[i];
if (
this.game.board[a][b] &&
this.game.board[a][b] === this.game.board[c][d] &&
this.game.board[a][b] === this.game.board[e][f]
) {
return this.game.board[a][b];
}
}
return null;
},
resetBoard() {
this.game.board = [
[‘‘, ‘‘, ‘‘],
[‘‘, ‘‘, ‘‘],
[‘‘, ‘‘, ‘‘],
];
},
},
};
</script>
<style scoped>
.board {
display: flex;
flex-direction: column;
width: 300px;
height: 300px;
margin: 20px auto;
}
.row {
display: flex;
flex: 1;
}
.cell {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid black;
cursor: pointer;
font-size: 50px;
}
</style>
This component renders the game board as a 3×3 grid of cells. Clicking on a cell triggers the handleClick
method, which emits a move
event up to the parent component (App.vue
).
After making a move, we check if there‘s a winner by iterating over all possible winning lines. If a winner is found, we show an alert and reset the board.
At this point, we have a working tic-tac-toe game! Run npm run serve
in the frontend
directory and python app.py
in the project root to start the development servers. Visit http://localhost:8080
to play the game.
Enhancing the Game
There are plenty of ways to expand on this basic implementation:
- Add user accounts and authentication
- Implement a lobby system to let players create and join games
- Show a game history with outcomes for each match
- Let players rematch after a game ends
- Add timers to limit how long players can take for a move
- Improve the UI with animations and sound effects
Deployment
To deploy the app, you‘ll need to:
- Choose a hosting provider (e.g., Heroku, AWS, DigitalOcean)
- Provision a server to run the Flask backend
- Set up a production database for storing game state
- Build the Vue frontend for production and serve it from a static file host or CDN
- Configure your Pusher app to accept requests from your production domain
Consult your hosting provider‘s documentation for specific instructions on deploying Flask and Vue apps.
Conclusion
Congratulations, you now have a working multiplayer game using Python, Vue, and Pusher!
This project demonstrates the key concepts behind building realtime apps, including creating a backend API, synchronizing state between clients, and managing UI updates.
You can use this as a starting point for building more complex games and applications. The possibilities are endless—from chat rooms and collaborative tools to live dashboards and location tracking services.
So what are you waiting for? Go forth and build something awesome!