Creating the Game Definition
In this section, weâll implement the core game logic for our Tic-Tac-Toe game. This includes setting up the game state, validating moves, making moves, and checking for win or draw conditions.
Game Logic Overview
Our game logic will be contained in the src/game-logic.ts
file and will include the following key functions:
- setupTicTacToeTable: Initialize a new game table with default attributes
- isValidMove: Check if a move is valid given the current game state
- makeMove: Update the game state when a player makes a move
- checkGameStatus: Determine if the game has ended with a win or draw
- getCurrentPlayer: Get the current player whose turn it is
- resetGame: Reset the game to start a new round
Creating the Table Setup Function
First, letâs create the setup function that initializes a new Tic-Tac-Toe table:
// src/game-logic.ts
import { Table, Player } from 'shoehive';
import { GameState } from './events';
/**
* Initialize the table with Tic-Tac-Toe game attributes
*/
export function setupTicTacToeTable(table: Table): void {
// Initial game state
table.setAttribute('gameState', GameState.WAITING_FOR_PLAYERS);
// Game board: 3x3 grid, null means empty cell
table.setAttribute('board', [
[null, null, null],
[null, null, null],
[null, null, null]
]);
// Game metadata
table.setAttribute('currentPlayerIndex', 0);
table.setAttribute('moveCount', 0);
table.setAttribute('symbols', ['X', 'O']);
table.setAttribute('winner', null);
table.setAttribute('winningCells', null);
table.setAttribute('isGameOver', false);
}
This function sets up all the initial attributes we need to track for our game. The Table
object from Shoehive provides a convenient way to store and access game state.
Validating Moves
Next, letâs create a function to validate player moves:
/**
* Check if a move is valid
*/
export function isValidMove(table: Table, row: number, col: number): boolean {
// Game must be in progress
if (table.getAttribute('gameState') !== GameState.IN_PROGRESS) {
return false;
}
// Check if coordinates are valid
if (row < 0 || row > 2 || col < 0 || col > 2) {
return false;
}
// Check if the cell is empty
const board = table.getAttribute('board');
return board[row][col] === null;
}
This function performs three checks:
- Ensures the game is in progress
- Validates that the coordinates are within the 3x3 grid
- Verifies that the selected cell is empty
Making Moves
Now, letâs implement the function to make a move:
/**
* Make a move on the board
*/
export function makeMove(table: Table, playerId: string, row: number, col: number): void {
// Get the current board state
const board = table.getAttribute('board');
const currentPlayerIndex = table.getAttribute('currentPlayerIndex');
const symbols = table.getAttribute('symbols');
// Place the symbol
board[row][col] = symbols[currentPlayerIndex];
// Update the board
table.setAttribute('board', board);
// Increment move count
const moveCount = table.getAttribute('moveCount') + 1;
table.setAttribute('moveCount', moveCount);
// Switch to the next player
table.setAttribute('currentPlayerIndex', (currentPlayerIndex + 1) % 2);
}
This function:
- Gets the current board state and player information
- Places the playerâs symbol on the board
- Updates the move count
- Switches to the next playerâs turn
Checking Game Status
One of the most important functions checks if the game has ended in a win or draw:
/**
* Check for a win or draw
* @returns Object with game over status and winner info
*/
export function checkGameStatus(table: Table): {
isGameOver: boolean;
winnerId: string | null;
winningCells: [number, number][] | null;
isDraw: boolean;
} {
const board = table.getAttribute('board');
const players = table.getPlayers();
const symbols = table.getAttribute('symbols');
const moveCount = table.getAttribute('moveCount');
// Check for win - rows, columns, and diagonals
const winPatterns = [
// 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 (const pattern of winPatterns) {
const [a, b, c] = pattern;
if (
board[a[0]][a[1]] !== null &&
board[a[0]][a[1]] === board[b[0]][b[1]] &&
board[a[0]][a[1]] === board[c[0]][c[1]]
) {
// We have a winner!
const winningSymbol = board[a[0]][a[1]];
const winnerIndex = symbols.indexOf(winningSymbol);
if (winnerIndex !== -1 && players.length > winnerIndex) {
return {
isGameOver: true,
winnerId: players[winnerIndex].id,
winningCells: [a, b, c] as [number, number][],
isDraw: false
};
}
}
}
// Check for draw - all cells filled
if (moveCount >= 9) {
return {
isGameOver: true,
winnerId: null,
winningCells: null,
isDraw: true
};
}
// Game is still in progress
return {
isGameOver: false,
winnerId: null,
winningCells: null,
isDraw: false
};
}
This function checks all possible win patterns (rows, columns, and diagonals) to determine if someone has won. It also checks for a draw when all cells are filled.
Helper Functions
Letâs add a few more helper functions to complete our game logic:
/**
* Get the current player
*/
export function getCurrentPlayer(table: Table): Player | null {
const currentPlayerIndex = table.getAttribute('currentPlayerIndex');
const players = table.getPlayers();
if (players.length <= currentPlayerIndex) {
return null;
}
return players[currentPlayerIndex];
}
/**
* Reset the game
*/
export function resetGame(table: Table): void {
// Reset the board
table.setAttribute('board', [
[null, null, null],
[null, null, null],
[null, null, null]
]);
// Reset game state
table.setAttribute('moveCount', 0);
table.setAttribute('winner', null);
table.setAttribute('winningCells', null);
table.setAttribute('isGameOver', false);
table.setAttribute('gameState', GameState.READY_TO_START);
// Randomize the starting player
table.setAttribute('currentPlayerIndex', Math.floor(Math.random() * 2));
}
These functions help us manage the current player and reset the game for a new round.
Complete Game Logic File
The complete src/game-logic.ts
file looks like this:
// src/game-logic.ts
import { Table, Player } from 'shoehive';
import { GameState } from './events';
/**
* Initialize the table with Tic-Tac-Toe game attributes
*/
export function setupTicTacToeTable(table: Table): void {
// Initial game state
table.setAttribute('gameState', GameState.WAITING_FOR_PLAYERS);
// Game board: 3x3 grid, null means empty cell
table.setAttribute('board', [
[null, null, null],
[null, null, null],
[null, null, null]
]);
// Game metadata
table.setAttribute('currentPlayerIndex', 0);
table.setAttribute('moveCount', 0);
table.setAttribute('symbols', ['X', 'O']);
table.setAttribute('winner', null);
table.setAttribute('winningCells', null);
table.setAttribute('isGameOver', false);
}
/**
* Check if a move is valid
*/
export function isValidMove(table: Table, row: number, col: number): boolean {
// Game must be in progress
if (table.getAttribute('gameState') !== GameState.IN_PROGRESS) {
return false;
}
// Check if coordinates are valid
if (row < 0 || row > 2 || col < 0 || col > 2) {
return false;
}
// Check if the cell is empty
const board = table.getAttribute('board');
return board[row][col] === null;
}
/**
* Make a move on the board
*/
export function makeMove(table: Table, playerId: string, row: number, col: number): void {
// Get the current board state
const board = table.getAttribute('board');
const currentPlayerIndex = table.getAttribute('currentPlayerIndex');
const symbols = table.getAttribute('symbols');
// Place the symbol
board[row][col] = symbols[currentPlayerIndex];
// Update the board
table.setAttribute('board', board);
// Increment move count
const moveCount = table.getAttribute('moveCount') + 1;
table.setAttribute('moveCount', moveCount);
// Switch to the next player
table.setAttribute('currentPlayerIndex', (currentPlayerIndex + 1) % 2);
}
/**
* Check for a win or draw
* @returns Object with game over status and winner info
*/
export function checkGameStatus(table: Table): {
isGameOver: boolean;
winnerId: string | null;
winningCells: [number, number][] | null;
isDraw: boolean;
} {
const board = table.getAttribute('board');
const players = table.getPlayers();
const symbols = table.getAttribute('symbols');
const moveCount = table.getAttribute('moveCount');
// Check for win - rows, columns, and diagonals
const winPatterns = [
// 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 (const pattern of winPatterns) {
const [a, b, c] = pattern;
if (
board[a[0]][a[1]] !== null &&
board[a[0]][a[1]] === board[b[0]][b[1]] &&
board[a[0]][a[1]] === board[c[0]][c[1]]
) {
// We have a winner!
const winningSymbol = board[a[0]][a[1]];
const winnerIndex = symbols.indexOf(winningSymbol);
if (winnerIndex !== -1 && players.length > winnerIndex) {
return {
isGameOver: true,
winnerId: players[winnerIndex].id,
winningCells: [a, b, c] as [number, number][],
isDraw: false
};
}
}
}
// Check for draw - all cells filled
if (moveCount >= 9) {
return {
isGameOver: true,
winnerId: null,
winningCells: null,
isDraw: true
};
}
// Game is still in progress
return {
isGameOver: false,
winnerId: null,
winningCells: null,
isDraw: false
};
}
/**
* Get the current player
*/
export function getCurrentPlayer(table: Table): Player | null {
const currentPlayerIndex = table.getAttribute('currentPlayerIndex');
const players = table.getPlayers();
if (players.length <= currentPlayerIndex) {
return null;
}
return players[currentPlayerIndex];
}
/**
* Reset the game
*/
export function resetGame(table: Table): void {
// Reset the board
table.setAttribute('board', [
[null, null, null],
[null, null, null],
[null, null, null]
]);
// Reset game state
table.setAttribute('moveCount', 0);
table.setAttribute('winner', null);
table.setAttribute('winningCells', null);
table.setAttribute('isGameOver', false);
table.setAttribute('gameState', GameState.READY_TO_START);
// Randomize the starting player
table.setAttribute('currentPlayerIndex', Math.floor(Math.random() * 2));
}
Next Steps
Now that we have implemented the core game logic, we need to create handlers for player commands such as making moves, joining games, and forfeiting.