# Tic-Tac-Toe PhaserJS

**URL:** https://heroiclabs.com/docs/nakama/tutorials/javascript/xoxo/
**Summary:** End to end tutorial for creating a tic-tac-toe game using Nakama and PhaserJS.
**Keywords:** tic-tac-toe phaserjs, nakama, xoxo
**Categories:** nakama, javascript, xoxo

---


# Tic-Tac-Toe PhaserJS Tutorial

{{< youtube "videoseries?list=PLOAExZcDNj9v8Ne6pXtOdhCycZIpubsZV" >}}

In this tutorial you will be creating XOXO, an online tic-tac-toe game, using [PhaserJS](https://phaser.io/) and your Nakama server.

This game and tutorial highlight the powerful [authentication](../../../concepts/authentication/), [matchmaking](../../../concepts/multiplayer/matchmaker/), and [real-time multiplayer](../../../concepts/multiplayer/relayed/) features of Nakama.

{{< note "important" >}}
The complete tutorial source code is available on [GitHub](https://github.com/heroiclabs/xoxo-phaserjs).
{{< / note >}}

## Prerequisites

To easily follow along with this tutorial, perform the following before proceeding:

* [Install Node LTS](https://nodejs.org/en/download/)
* [Install TypeScript](https://www.typescriptlang.org/download)
* [Install Phaser](https://phaser.io/download)
* [Install Nakama](../../../getting-started/install/docker/)
* [Install the Nakama JavaScript Client](../../../client-libraries/javascript/)
* [Install Svelte](https://svelte.dev/blog/the-easiest-way-to-get-started)
* [Clone the Nakama Project Template](https://github.com/heroiclabs/nakama-project-template)
* [Prepare PhaserJS Game Engine](#configuring-your-javascript-framework)

### Running the Nakama server

In order to run the XOXO game you must first make sure Nakama is running.

Once you've [installed Docker](https://docs.docker.com/get-docker/) and cloned the [Nakama Project Template](https://github.com/heroiclabs/nakama-project-template), navigate to the folder you cloned the repository to and run the following command:

```bash
docker compose up
```

### Configuring your JavaScript framework

Here you will install the TypeScript dependencies required for this project, transpile your TypeScript code to JavaScript, and add PhaserJS to your Svelte JavaScript framework.

1. Install NPM to manage your dependencies. From your terminal window:

```bash
npm install
```

2. Before starting your Nakama server, transpile the TypeScript code to JS:

```bash
npx tsc
```

3. Add the PhaserJS script tag to your `index.html` file:

```html
<!DOCTYPE html>
<html lang="en">
<head>
        <meta charset='utf-8'>
        <meta name='viewport' content='width=device-width,initial-scale=1'>
        <title>Svelte app</title>
        <link rel='icon' type='image/png' href='/favicon.png'>
        <link rel='stylesheet' href='/global.css'>
        <link rel='stylesheet' href='/build/bundle.css'>
        // PhaserJS script tag
        <script src="//cdn.jsdelivr.net/npm/phaser@3.54.0/dist/phaser.min.js"></script>
        <script defer src='/build/bundle.js'></script>
</head>
<body>
</body>
</html>
```

4. You can now run your application locally by running:

```bash
npm run dev
```

Your application will be available at `localhost:5000`.

## Building the UI

1. Start by adding the PhaserJS config to your `App.svelte` file (your game's main entry point, imported in your `main.js` file):

```svelte
<script>
        import MainMenu from "./scenes/MainMenu";
        import Matchmaking from "./scenes/Matchmaking";
        import InGame from "./scenes/InGame";
        import CONFIG from "./config";
        const config = {
                type: Phaser.AUTO,
                width: CONFIG.WIDTH,
                height: CONFIG.HEIGHT,
                backgroundColor: "#FF4C4C",
                scene: [MainMenu, Matchmaking, InGame],
        };
        new Phaser.Game(config);
</script>
```

```javascript
import App from './App.svelte';

const app = new App({
    target: document.body,
    props: {}
});

export default app;
```

2. Next, create a `config.js` file for you game settings. In this case, the height and width of the game canvas:

```javascript
const CONFIG = {
      WIDTH: 414,
      HEIGHT: 736
}
export default CONFIG
```

3. Now you can start creating the game's Phaser scenes to group related logic. For our game we have three, the main menu, matchmaking, and in game scenes:

**MainMenu.js**
```javascript

// ...

    create() {
        Nakama.authenticate()

        // Create the Welcome banner
        this.add
            .text(CONFIG.WIDTH / 2, 75, "Welcome to", {
                fontFamily: "Arial",
                fontSize: "24px",
            })
            .setOrigin(0.5);

        this.add
            .text(CONFIG.WIDTH / 2, 123, "XOXO", {
                fontFamily: "Arial",
                fontSize: "72px",
            })
            .setOrigin(0.5);

        this.add.grid(
            CONFIG.WIDTH / 2,
            CONFIG.HEIGHT / 2,
            300,
            300,
            100,
            100,
            0xffffff,
            0,
            0xffca27
        );

        // Create a button to start the game
        const playBtn = this.add
            .rectangle(CONFIG.WIDTH / 2, 625, 225, 70, 0xffca27)
            .setInteractive({ useHandCursor: true });

        const playBtnText = this.add
            .text(CONFIG.WIDTH / 2, 625, "Begin", {
                fontFamily: "Arial",
                fontSize: "36px",
            })
            .setOrigin(0.5);

        playBtn.on("pointerdown", () => {
            Nakama.findMatch()
            this.scene.start("in-game");
        });

        // ...
    }
}
```

**Matchmaking.js**
```javascript

// ...

    create() {
        this.add
            .text(CONFIG.WIDTH / 2, 125, "Searching for an opponent...", {
                fontFamily: "Arial",
                fontSize: "24px",
            })
            .setOrigin(0.5);

        this.anims.create({
            key: "spinnerAnimation",
            frames: this.anims.generateFrameNumbers("spinner"),
            frameRate: 30,
            repeat: Phaser.FOREVER,
        });

        this.add
            .sprite(CONFIG.WIDTH / 2, CONFIG.HEIGHT / 2, "spinner")
            .play("spinnerAnimation")
            .setScale(0.5);
    }
}
```

**InGame.js**
```javascript

// ...

    updateBoard(board) {
        board.forEach((element, index) => {
            let newImage = this.INDEX_TO_POS[index]

            if (element === 1) {
                this.phaser.add.image(newImage.x, newImage.y, "O");
            } else if (element === 2) {
                this.phaser.add.image(newImage.x, newImage.y, "X");
            }
        })
    }

    updatePlayerTurn() {
        this.playerTurn = !this.playerTurn

        if (this.playerTurn) {
            this.headerText.setText("Your turn!")
        } else {
            this.headerText.setText("Opponents turn!")
        }
    }

    setPlayerTurn(data) {
        let userId = localStorage.getItem("user_id");
        if (data.marks[userId] === 1) {
            this.playerTurn = true;
            this.playerPos = 1;
            this.headerText.setText("Your turn!")
        } else {
            this.headerText.setText("Opponents turn!")
        }
    }

    endGame(data) {
        this.updateBoard(data.board)

        if (data.winner === this.playerPos) {
            this.headerText.setText("Winner!")
        } else {
            this.headerText.setText("You loose :(")
        }
    }

    nakamaListener() {
        Nakama.socket.onmatchdata = (result) => {
            switch (result.op_code) {
                case 1:
                    this.gameStarted = true;
                    this.setPlayerTurn(result.data)
                    break;
                case 2:
                    console.log(result.data)
                    this.updateBoard(result.data.board)
                    this.updatePlayerTurn()
                    break;
                case 3:
                    this.endGame(result.data)
                    break;
            }
        };
    }

    // ...

        // Register the player move in the correct square
        this.nakamaListener()

        this.add
            .rectangle(
                gridCenterX - gridCellWidth,
                topY,
                gridCellWidth,
                gridCellWidth
            )
            .setInteractive({ useHandCursor: true })
            .on("pointerdown", async () => {
                await Nakama.makeMove(0)
            });

        this.add
            .rectangle(gridCenterX, topY, gridCellWidth, gridCellWidth)
            .setInteractive({ useHandCursor: true })
            .on("pointerdown", () => {
                Nakama.makeMove(1)
            });

        // ...
    }
}
```

In the Main Menu scene, you authenticate the user in Nakama (discussed below) and display a "Welcome to XOXO" banner and button that, on click, takes the user to the In Game scene.

The Matchmaking scene simply displays a waiting spinner while Nakama finds an opponent for the user.

For the In Game scene, you are creating the interactive board of nine individual squares for players to enter their X's and O's. You also define the gameplay functions to set and update players turns, and end the game. The `nakamaListener` defined here communicates these actions via websocket to the Nakama server.

## Connecting to Nakama

1. Next connect your client to Nakama server and configure it for [device authentication](../../../concepts/authentication/#device):

**nakama.js**
```javascript
import { Client } from "@heroiclabs/nakama-js";
import { v4 as uuidv4 } from "uuid";

class Nakama {
    constructor() {
        this.client
        this.session
        this.socket
        this.matchID
    }

    async authenticate() {
        const useSSL = false;
        this.client = new Client("defaultkey", "localhost", "7350", useSSL);

        let deviceId = localStorage.getItem("deviceId");
        if (!deviceId) {
            deviceId = uuidv4();
            localStorage.setItem("deviceId", deviceId);
        }

        this.session = await this.client.authenticateDevice(deviceId, true);
        localStorage.setItem("user_id", this.session.user_id);

        const trace = false;
        this.socket = this.client.createSocket(this.useSSL, trace);
        await this.socket.connect(this.session);

    }

    // ...
```

{{< note "important" >}}
When testing the game locally with two browser windows, you should set `deviceId` to `uuidv4()` rather than get it from `localStorage`, otherwise you will authenticate as the same user twice which will cause the first session to disconnect and make the game unplayable.
{{< / note >}}

2. Here you'll also configure the [multiplayer](../../../concepts/multiplayer/relayed/) functionality. Learn more about this in the [authoritative multiplayer](../../../concepts/multiplayer/authoritative/) and [matchmaker](../../../concepts/multiplayer/matchmaker/) documentation.

**nakama.js**
```javascript
import { Client } from "@heroiclabs/nakama-js";
import { v4 as uuidv4 } from "uuid";

class Nakama {
    constructor() {
        this.client
        this.session
        this.socket
        this.matchID
    }

    // ...

    async findMatch() {
        const rpcid = "find_match";
        const matches = await this.client.rpc(this.session, rpcid, {});

        this.matchID = matches.payload.matchIds[0]
        await this.socket.joinMatch(this.matchID);
        console.log("Matched joined!")
    }

    async makeMove(index) {
        var data = { "position": index };
        await this.socket.sendMatchState(this.matchID, 4, data);
        console.log("Match data sent")
    }
}

export default new Nakama()
```

And that's it, you're ready to play!

## Further reading

Learn more about the topics and features in this tutorial with the following:

* [JavaScript Client Guide](../../../client-libraries/javascript/)
* [Authentication](../../../concepts/authentication/)
* [Authoritative Multiplayer](../../../concepts/multiplayer/authoritative/)
* [Matchmaker](../../../concepts/multiplayer/matchmaker/)
