Debugging and Testing
In this section, weâll learn how to debug and test our Tic-Tac-Toe game. Proper testing ensures that our game works correctly and provides a good user experience.
Setting Up for Development
First, letâs update our package.json
to include convenient scripts for development:
{
"scripts": {
"start": "ts-node src/index.ts",
"dev": "NODE_ENV=development ts-node src/index.ts",
"build": "tsc",
"serve": "node dist/index.js"
}
}
Use npm run dev
to run the game in development mode, which enables our debug monitoring.
Debug Monitoring
In our main application, weâve set up debug monitoring for development mode:
// Enable debug monitoring for development
if (process.env.NODE_ENV !== 'production') {
eventBus.debugMonitor(
true,
(eventName) => eventName.startsWith('tictactoe:'),
(event, ...args) => {
console.log(`[TicTacToe Event] ${event}`, JSON.stringify(args, null, 2));
}
);
}
This logs all Tic-Tac-Toe events to the console, which helps us understand the flow of events during gameplay.
Creating a Test Client
To test our game, letâs create a simple WebSocket client. Create a file called client.js
in the project root:
const WebSocket = require('ws');
const readline = require('readline');
const ws = new WebSocket('ws://localhost:3000');
let tableId = null;
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
ws.on('open', () => {
console.log('Connected to server');
rl.question('Enter your player ID: ', (playerId) => {
console.log('Available commands:');
console.log('1. create - Create a new game');
console.log('2. join [tableId] - Join a game');
console.log('3. start - Start the game');
console.log('4. move [row] [col] - Make a move');
console.log('5. forfeit - Forfeit the game');
console.log('6. reset - Reset the game');
rl.on('line', (input) => {
const args = input.split(' ');
const command = args[0];
if (command === 'create') {
ws.send(JSON.stringify({
type: 'tictactoe:create'
}));
} else if (command === 'join') {
const joinTableId = args[1] || tableId;
ws.send(JSON.stringify({
type: 'tictactoe:join',
tableId: joinTableId
}));
} else if (command === 'start') {
ws.send(JSON.stringify({
type: 'tictactoe:start'
}));
} else if (command === 'move') {
const row = parseInt(args[1], 10);
const col = parseInt(args[2], 10);
ws.send(JSON.stringify({
type: 'tictactoe:makeMove',
row,
col
}));
} else if (command === 'forfeit') {
ws.send(JSON.stringify({
type: 'tictactoe:forfeit'
}));
} else if (command === 'reset') {
ws.send(JSON.stringify({
type: 'tictactoe:reset'
}));
} else {
console.log('Unknown command');
}
});
});
});
ws.on('message', (data) => {
const message = JSON.parse(data);
console.log('Received:', message);
if (message.type === 'gameCreated') {
tableId = message.tableId;
console.log(`Game created with ID: ${tableId}`);
}
});
ws.on('close', () => {
console.log('Disconnected from server');
rl.close();
});
To use this client, first install the WebSocket library:
npm install ws
Then run the client:
node client.js
Testing the Game
To test the game, you need to run two instances of the client to simulate two players.
Test Case 1: Creating and Joining a Game
- In the first client:
- Run
create
to create a new game - Note the table ID that is returned
- Run
- In the second client:
- Run
join [tableId]
using the table ID from the first client - Both clients should receive a âgameReadyâ message
- Run
Test Case 2: Starting the Game
- In either client:
- Run
start
to start the game - Both clients should receive a âgameStartedâ message
- Run
Test Case 3: Making Moves
- The player whose turn it is:
- Run
move 0 0
to place their symbol in the top-left corner - Both clients should receive a âmoveMadeâ message
- Run
- The other player:
- Run
move 1 1
to place their symbol in the center - Continue alternating moves
- Run
Test Case 4: Winning the Game
- Try to get three symbols in a row:
- If player 1 has X, they could play moves at (0,0), (0,1), and (0,2)
- Both clients should receive a âgameOverâ message with the winner information
Test Case 5: Drawing the Game
- Fill the board without a winner:
- Make moves until all cells are filled but no player has three in a row
- Both clients should receive a âgameOverâ message with draw=true
Test Case 6: Resetting the Game
- After the game is over:
- Run
reset
to reset the game - Both clients should receive a âgameResetâ message
- Run
Test Case 7: Forfeiting the Game
- During an active game:
- Run
forfeit
to forfeit the game - Both clients should receive a âgameOverâ message with the forfeit information
- Run
Common Issues and Debugging Tips
Issue: Player canât join a game
Debugging steps:
- Check that the table ID is correct
- Verify that the table isnât already full (2 players)
- Look for error messages in the server logs
Issue: Moves arenât being accepted
Debugging steps:
- Check if itâs the playerâs turn
- Verify that the cell coordinates are valid (0-2)
- Ensure the cell isnât already occupied
- Check that the game is in the IN_PROGRESS state
Issue: Game state isnât updating correctly
Debugging steps:
- Look at the event logs to see what events are being fired
- Check the table attributes using console.log statements
- Verify that the appropriate handler is being called
Advanced Testing Techniques
For more robust testing, consider implementing:
- Unit Tests: Test individual functions in isolation
- Integration Tests: Test the interaction between components
- End-to-End Tests: Test the entire game flow from start to finish
- Stress Tests: Test the game with many concurrent players
Next Steps
Now that weâve learned how to debug and test our game, letâs review the complete implementation and discuss best practices.
Next: Complete Implementation