# Tic Tac Toe example
This is a simple Tic Tac Toe example in Typescript, wrapped with the APIs to be integrated in our platform.
# Engine
We need to define the game state. Since this is a very basic example, we'll avoid everything optional.
Here is our gamestate:
type Player = 0 | 1;
type Coord = {
x: 0 | 1 | 2;
y: 0 | 1 | 2;
};
type Board = [
[Player | null, Player | null, Player | null],
[Player | null, Player | null, Player | null],
[Player | null, Player | null, Player | null]
];
type GameState = {
winner?: Player;
board: Board;
// all moves played
moves: Array<{
player: Player;
coord: Coord;
}>;
};
Then we go through the required exported methods defined in the engine API.
# init
We create an empty board.
export function init(): GameState {
return {
board: [
[null, null, null],
[null, null, null],
[null, null, null],
],
moves: [],
};
}
# move
We handle a move from a player.
We don't have to check that it's the current player - it's already done by the game server.
The function could be as simple as this:
export function move(state: GameState, coord: Coord, player: Player) {
state.board[coord.x][coord.y] = player;
state.moves.push({ player, coord });
return state;
}
But we'll also add a check to see if the winner is decided here.
function winner(board: Board): Player | undefined {
// Check rows
for (const row of board) {
if (row[0] !== null && row[1] === row[0] && row[2] === row[0]) {
return row[0];
}
}
// Check columns
for (let i = 0; i < 3; i++) {
if (board[0][i] !== null && board[1][i] === board[0][i] && board[2][i] === board[0][i]) {
return board[0][i];
}
}
// Check diagonals
if (board[0][0] !== null && board[0][0] === board[1][1] && board[0][0] === board[2][2]) {
return board[0][0];
}
if (board[2][0] !== null && board[2][0] === board[1][1] && board[2][0] === board[0][2]) {
return board[2][0];
}
}
Then we add that info in the move
function:
export function move(state: GameState, coord: Coord, player: Player) {
state.board[coord.x][coord.y] = player;
state.moves.push({ player, coord });
// Either it stays undefined or is set to the winner
state.winner = winner(state.board);
return state;
}
# ended
The game can be ended two ways:
- One of the players won
- The board is full
export function ended(state: GameState) {
if (state.winner !== undefined) {
return true;
}
// The board is full
return state.moves.length === 9;
}
# scores
Tic Tac Toe doesn't really have score or victory points. We'll just give 100 points to the winner. If there is no winner, the score is 0 for for everybody.
export function scores(state: GameState) {
return [state.winner === 0 ? 100 : 0, state.winner === 1 ? 100 : 0];
}
# dropPlayer
When a player drops, the other player automatically wins.
function opponent(player: Player): Player {
return player === 0 ? 1 : 0;
}
export function dropPlayer(state: GameState, player: Player) {
state.winner = opponent(player);
return state;
}
# currentPlayer
The first player is 0
. Then the turns alternate.
We reuse the opponent
function defined in dropPlayer.
export function currentPlayer(state: GameState): Player {
if (state.moves.length === 0) {
return 0;
}
return opponent(state.moves[state.moves.length - 1].player);
}
# logLength
Our log is simple, it's just the moves played.
However, we want to tell the viewer when a player wins.
So our log will actually be:
- an event "start"
- the list of moves
- an event "end", with the winner
As such the log length is the number of moves + 1 if there is no winner, and the number of moves + 2 if there is.
export function logLength(state: GameState): number {
return 1 + state.moves.length + (state.winner !== undefined ? 1 : 0);
}
# logSlice
And here we actually generate the log. The logic is described in logLength.
type LogItem =
| {
kind: "event";
event: "start";
}
| {
kind: "event";
event: "end";
winner?: Player;
}
| {
kind: "move";
move: Coord;
player: Player;
};
export function logSlice(state: GameState, options: { start: number; end?: number }): LogItem[] {
// Add the starting event
const log = [{ kind: "event", event: "start" }];
// Add the log items for the moves
log.push(...state.moves.map((move) => ({ kind: "move", move: move.coord, player: move.player })));
// Add the end event, with the winner, if the winner is decided
if (state.winner !== undefined || state.moves.length === 9) {
log.push({ kind: "event", event: "end", winner: state.winner });
}
// Return the requested log items
return log.slice(options.start, options.end);
}
# Viewer
We're mainly used to Vue, but boardgames can use any UI system.
We're going to demonstrate implementing the viewer with Vue, because it's much more readable than jQuery.
Let's also assume that we exported the engine module in a tictactoe-engine
module.
First, we create the game itself. We'll worry about the API later.
Here's the board:
<template>
<div>
<!--rows -->
<div class="row" v-for="i in 3" :key=i>
<!-- columns -->
<div :class="['cell', {player1: cellPlayer(i-1, j-1) === 0, player2: cellPlayer(i-1, j-1) === 1}]" v-for="j in 3" :key=j>
</div>
</div>
<div>
</template>
<script lang="ts">
...
</script>
<style lang="scss">
.cell {
margin: 20px;
width: 100px;
height: 100px;
border: 1px solid black;
&.player1 {
background-color: red;
}
&.player2 {
background-color: blue;
}
}
.row {
display: flex;
}
</style>
It's a really simple 3x3 board, with player 1's cells being red, and player2's cell being blue.
Now we need to flesh out the interactions. First add a way to receive the state from the outisde:
... Template ...
<script lang="ts">
import type { GameState, Player } from "tictactoe-engine";
import { Vue, Component, Watch, Prop } from "vue-property-decorator";
@Component
export default class App extends Vue {
// State received from the outside
@Prop()
state!: GameSate;
/**
* Which player is in cell i/j, needed in the template code
*/
cellPlayer(i: number, j: number): Player {
return this.state.board[i][j];
}
}
</script>
... Style ...
Pretty simple, right?
Now we need to add code to give our App the necessary info:
- The player of the viewer
- Updated state when the opponent makes a move
It's time to look at the BGS API and do a wrapper. In a separate file:
import Vue from "vue";
import App from "./App.vue";
import { init } from "tictactoe-engine";
import type { GameState, Player } from "tictactoe-engine";
import { EventEmitter } from "events";
function launch(selector: string) {
const props: { state: GameState; player?: Player } = { state: init() };
// Create the vue app and mount it where we are told
const vue = new Vue({
render: (h) => h(App, { props }, []),
}).$mount(selector);
// Our App component
const app = vue.$children[0];
// Now we just need to modify `props` to reflect the changes on the App component
// First create what we will use to communicate with BGS
const emitter = new EventEmitter();
// Handle BGS API stuff
// ...
return emitter;
}
window.viewer = { launch };
This is what the wrapper will look like in general.
What we want to do:
- Get new state when it's there
- Get current player
- Forward moves to BGS
So now we can complete the code:
//...
function launch(...) {
// ...
// When we know new state is available
emitter.on("state:updated", () => {
// Tell the backend we want the new state
emitter.emit("fetchState");
});
// When we receive new state
emitter.on("state", state => {
props.state = state;
// wait for the DOM to render, and emit the ready event
vue.$nextTick().then(() => emitter.emit("ready"));
});
// When we receive log slices, when executing a move
emitter.on("gamelog", (logData) => {
// Ignore the log data and tell the backend we want the new state
emitter.emit("fetchState");
});
// Which player are we
emitter.on("player", (player: {index: number}) => {
// Props are passed to the vue component
props.player = player.index;
});
// Finally, transmit moves to BGS
app.on("move", move => emitter.emit("move", move));
return emitter;
}
//...
And with that... all that's left is finishing our App
component.
We need to add the player
info, as well as transmit moves.
<template>
<div>
<!--rows -->
<div class="row" v-for="i in 3" :key=i>
<!-- columns -->
<div
v-for="j in 3"
class="cell"
:class="{player1: player(i-1, j-1) === 0, player2: player(i-1, j-1) === 1}"
:role="clickable(i-1, j-1) ? 'button' : undefined"
:key=j
@click="move(i-1, j-1)">
</div>
</div>
<div>
</template>
<script lang="ts">
import { currentPlayer, ended } from "tictactoe-engine";
// ...
@Component
export default class App extends Vue {
// ...
@Prop()
player?: Player;
clickable(i: number, j: number) {
// First check it's our turn
if (this.player === undefined) {
return false;
}
if (ended(this.state)) {
return false;
}
if (currentPlayer(this.state) !== this.player) {
return false;
}
// Then check cell is empty
if (this.cellPlayer(i, j) !== undefined) {
return false;
}
// We can click!
return true;
}
move(i: number, j: number) {
if (!this.clickable(i, j)) {
return false;
}
// Emit a move in a format that our engine understands
this.$emit("move", {x: i, y: j});
}
}
// ...
</script>
<style lang="scss">
// ...
.clickable {
cursor: pointer;
}
</style>
# Testing
You probably want to be able to test the game before adding it to BGS.
In that case - you can simulate a backend very easily. In a separate file, local.ts
.
There are three files in the viewer so far:
App.vue
wrapper.ts
- And now,
local.ts
Here's the content:
import "./wrapper.ts";
import { init, move as engineMove, currentPlayer } from "tictactoe-engine";
const emitter = window.viewer.launch("#app");
let state = init();
let player = 0;
// Initial state / player
emitter.emit("state", state);
emitter.emit("player", { index: player });
emitter.on("move", (move) => {
state = engineMove(state, move, player);
player = currentPlayer(state);
// Give updated player (we want to be able to play both sides)
// and give updated player / state
emitter.emit("state", state);
emitter.emit("player", { index: player });
});
And that should be it.
# Conclusion
That's it for Tic Tac Toe. The game is ready to be added on the platform!
We only implemented the very basics, and we can go more in depth. But should you want to add any boardgame, we'll offer our full support and help you add the other stuff, like replay mode or log in the sidebar.