# Play with Bun and Solid.js: Create a multi-player Tic-Tac-Toe, Part1

Recently, Bun reached version 1.0 and made a splash in the JavaScript ecosystem. I wanted to try it out and thought maybe good idea to create something in combination with Solid.js, the frontend framework I wanted to play with for a long time. As a person who is crazy about performance, this combination looks very fancy as both of them claim to be very fast and efficient. So, here I will share my journey to build a full-stack multi-player Tic-Tac-Toe game with you.

I split this post into two parts. First, we will create the backend for a tic-tac-toe game to provide APIs to create a new game and play over a WebSocket connection. Then we will implement the frontend part of it.

# What’s Bun?

Bun is a new JavaScript runtime. It’s a drop-in replacement for Node.js and promises higher performance, comes with a built-in package manager (which means no more discussion for using npm, yarn, or pnpm🙄), and even a bundler! But the most interesting part for me is that TypeScript is a first-class citizen of Bun, which is great, isn’t it?

So let’s get started.

# Install Bun and initialize the project

Install Bun using the command on [their website](https://bun.sh/).

Run `bun init` to create a new project using Bun. Choose a name for your project and use `src/index.ts` as your entry point.

Execute command `bun run src/index.ts`, and you should see this output.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699981720726/87475e69-80ad-4cf4-9b1d-59daedbb5e28.png align="center")

Voilà🎉 We're almost done with our project (●'◡'●).

Run `bun add -d bun-types` to add Typescript Bun types to the project. Now we are ready for the tic-tac-toe.

# Create Tic-Tac-Tobe!

We will use `Bun.serve` without any library and wrapper. We will create an REST endpoint for creating a game, and handle game communications on WebSocket.

## A brief look at the architecture

We create a `Game` class to represent a game. Game logic will be implemented here, it keeps the game state and handles player actions in the game (selecting a tile). To notify the WebSocket layer about events happening inside our game (game started, the game is over, etc.), we emit events from our game, so that our WebSocket layer can listen to Game events and push them to the users.

## Create Game class

As said, we want to emit events from our `Game` , so we extend `EventEmitter` class which provides us with `emit` method we can use inside `Game` to emit events, and also `on` method which we can use when using `Game` to listen for events.

```typescript
type Events = {
  start: [];
  over: [Player];
  tileChosen: [{ player: Player; row: number; col: number }];
};

export class Game extends EventEmitter<Events> {
  private turn: Player = Math.floor(Math.random() * 2);
  private gameState: GameState = GameState.WAITING;
  private winner: Player | null = null;
  private numberOfPlayers: number = 0;
  private boardState: Array<(Player | null)[]> = [
    [null, null, null],
    [null, null, null],
    [null, null, null],
  ];

  constructor(private readonly id: string) {
    super();
  }

  getId(): string {
    return this.id;
  }

  getGameState(): GameState {
    return this.gameState;
  }

  getWinner(): Player | null {
    return this.winner;
  }

  getTurn(): Player {
    return this.turn;
  }
}
```

Here, we pass the type parameter to `EventEmitter` to define types of events and their parameters. We have the `start` event which has no parameter, the `over` event which has one parameter of type `Player`, and the `tileChoosen` event which has one parameter of the object you see in the code.

When initializing, we set `turn` with a random value 0 or 1, set `GameState` to waiting, `winner` to null, `numberOfPlayers` to 0, and `boardState` to a 3x3 array which is initially empty (null) but can be chosen by `Player`s

Also, we defined some getters for getting `Id`, `gameState`, `winner`, and `turn`.

We used `Player` and `GameState` types which are simple enums as follows:

```typescript
export enum Player {
  PLAYER1,
  PLAYER2,
}

export enum GameState {
  WAITING,
  PLAYING,
  OVER,
}
```

## **Game methods**

In `Game` class, we have a method to add a player to the game, which checks if the game has not already started and then just increments the number of players. If the player we are adding is the second player, we start the game by changing the state and emitting the start event.

```typescript
  addPlayer(): Player {
    if (this.gameState !== GameState.WAITING) {
      throw new Error("Game is already started");
    }

    const idx = this.numberOfPlayers;
    this.numberOfPlayers += 1;

    // start the game if it's second player
    if (this.numberOfPlayers === 2) {
      this.gameState = GameState.PLAYING;
      this.emit("start");
    }

    return idx;
  }
```

Then comes the time a user wants to select one of the tiles.

```typescript
  chooseTile(player: Player, row: number, col: number) {
    if (player !== this.turn) {
      throw new Error("Not this player's turn");
    }

    if (this.boardState[row][col] === undefined) {
      throw new Error("Invalid tile address");
    }

    if (this.boardState[row][col] !== null) {
      throw new Error("Tile is already chosen");
    }

    this.boardState[row][col] = player;
    this.emit("tileChosen", { player, row, col });

    // toggle turn
    this.turn = this.turn === Player.PLAYER1 ? Player.PLAYER2 : Player.PLAYER1;

    const winner = this.checkWinner();
    if (winner !== null) {
      this.gameState = GameState.OVER;
      this.winner = winner;

      this.emit("over", winner);
    }
  }
```

First, we have some validation. Is it the requester’s turn or not? Are the coordinates inside our board? Isn’t the tile already chosen?

Then we assign the tile to the user, emit the event of `tileChosen` with the player and tile coordinates, and toggle turn.

Now we need to check if the game is finished with this action or not. The logic for checking the winner is implemented in another method, and if a player wins, we change the game state, set the winner, and emit over the event.

The method for checking the winner is as follows:

```typescript
  checkWinner(): Player | null {
    // check rows
    for (let row = 0; row < 3; row++) {
      if (
        this.boardState[row][0] === this.boardState[row][1] &&
        this.boardState[row][0] === this.boardState[row][2] &&
        this.boardState[row][0] !== null
      ) {
        return this.boardState[row][0];
      }
    }

    // check columns
    for (let col = 0; col < 3; col++) {
      if (
        this.boardState[0][col] === this.boardState[1][col] &&
        this.boardState[0][col] === this.boardState[2][col] &&
        this.boardState[0][col] !== null
      ) {
        return this.boardState[0][col];
      }
    }

    // check diagonals
    if (
      this.boardState[0][0] === this.boardState[1][1] &&
      this.boardState[0][0] === this.boardState[2][2] &&
      this.boardState[0][0] !== null
    ) {
      return this.boardState[0][0];
    }

    if (
      this.boardState[0][2] === this.boardState[1][1] &&
      this.boardState[0][2] === this.boardState[2][0] &&
      this.boardState[0][2] !== null
    ) {
      return this.boardState[0][2];
    }

    return null;
  }
```

This is just if conditions for checking rows, columns, or diagonals are the same and a user wins the game.

## **Games container**

To ease creating games and access to them, we create a class to just contain games, with static properties and methods, like this:

```typescript
import { randomUUID } from "crypto";
import { Game } from "./Game";

export class Games {
  private static games: Record<string, Game> = {};

  static newGame(): Game {
    const id = randomUUID();
    const game = new Game(id);
    this.games[id] = game;

    return game;
  }

  static deleteGame(id: string): void {
    delete this.games[id];
  }

  static getGame(id: string): Game {
    const game = this.games[id];
    if (!game) {
      throw new Error("Invalid game id");
    }

    return game;
  }
}
```

## **Now the Bun comes in…**

As said earlier, we use `Bun.serve` to create our HTTP and WebSocket server.

```typescript
Bun.serve({
  fetch(request, server) {
    console.log("Request received");
  },
  websocket: {
    open: (ws) => console.log("WebSocket connection opened", ws),
    message: (ws, message) => console.log("WebSocket message received", ws, message),
  },
  port: 8080,
});
```

Pretty simple, to handle HTTP requests, we implement the `fetch` method which receives requests and server objects, and we return a `Response`. In `websocke` property, we define methods for handling events. `open` is fired when a new WebSocket connection is opened, and `message` handles new message coming over WebSocket.

## **Fetch implementation**

We want to have two endpoints for our API, `/games` which accepts `POST` requests and creates a new game, another `/play` for upgrading to a WebSocket connection.

Inside the `fetch` method, first, we parse the request url using URL class.

```typescript
const url = new URL(request.url);
```

By that, we can check `pathname` and `method` for our endpoints. Inside this condition, we create a new game and create a JSON response containing the game id to send to the user. We also add `Access-Control-*` headers to allow CORS.

```typescript
    if (url.pathname === "/games" && request.method === "POST") {
      const game = Games.newGame();

      const response = Response.json({ id: game.getId() }, { status: 201 });
      response.headers.set("Access-Control-Allow-Origin", "*");
      response.headers.set("Access-Control-Allow-Methods", "POST");

      return response;
    }
```

### Upgrade WebSocket connection

Handling the WebSocket connection has two phases. First, the browser sends an HTTP request which we handle in the `fetch` method, by responding with `server.upgrade`, we are switching protocol and the two-way WebSocket connection will be established, so we receive `websocket.open` event.

So now, let's check `/play` endpoint and get `gameId` from URL search (query) params, check if the game exists, and upgrade the connection to WebSocket.

```typescript
    if (url.pathname === "/play") {
      const params = url.searchParams;
      const gameId = params.get("gameId");
      const game = Games.getGame(gameId ?? "");

      if (!game) {
        return;
      }

      server.upgrade(request, { data: { game, player: null } });
    }
```

As you see, we are passing some data as a second parameter to `server.upgrade` method. This data is attached to this specific WebSocket client and in WebSocket methods we can read and also mutate this data.

To make the type of this data value known to other parts of the application, we can define the type and pass it as the first type parameter to `Bun.serve`, like this:

```typescript
export type WebSocketData = {
  game: Game;
  player: Player | null;
};

Bun.serve<WebSocketData>({
...
```

## **WebSocket implementation**

In the WebSocket part, we implement `open` method as follows. We retrieve the game instance stored on [`ws.data`](http://ws.data) and check for its state. If already started we close the connection, otherwise, we listen to the game events and notify the user by `ws.send`, and after that, we add the user as a player to the game.

```typescript
    open: (ws) => {
      const { game } = ws.data;

      if (game.getGameState() !== GameState.WAITING) {
        ws.close(4000, "Bad state");
        return;
      }

      // send player
      ws.send(JSON.stringify({ event: "welcome", player: ws.data.player }));

      // set event listeners
      game.on("start", () =>
        ws.send(JSON.stringify({ event: "start", turn: game.getTurn() }))
      );
      game.on("over", (winner) =>
        ws.send(JSON.stringify({ event: "over", winner: winner }))
      );
      game.on("tileChosen", (data) =>
        ws.send(JSON.stringify({ event: "tileChosen", ...data }))
      );

      // add to game
      ws.data.player = game.addPlayer();
    }
```

Everything is wired together, we just need to handle WebSocket messages and do the action on the game instance. Just parse the message and get game and player from WebSocket data. If the command is `chooseTile`, perform it on the `game` object. All the logic required is implemented in this method.

```typescript
    message: (ws, message) => {
      const msg = JSON.parse(message.toString());
      const { game, player } = ws.data;

      if (msg.command === "chooseTile") {
        game.chooseTile(player!, msg.row, msg.col);
      }
    },
```

Now run the `bun run src/index.ts` and your server will be ready to accept gamers, Congratulations!

In the next part, we will implement the frontend part of this game using Solid.js and communicate with this backend. Stay tuned!

# Source code

To see the final code, you can refer to [this GitHub repository](https://github.com/Hossein-s/Tic-Tac-Tobe/tree/main/backend)
