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:

  1. A Python backend API built with Flask
  2. A Vue frontend for the user interface
  3. 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:

  1. Choose a hosting provider (e.g., Heroku, AWS, DigitalOcean)
  2. Provision a server to run the Flask backend
  3. Set up a production database for storing game state
  4. Build the Vue frontend for production and serve it from a static file host or CDN
  5. 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!

Similar Posts