Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Dependencies
node_modules/

# Coverage reports
coverage/

# Environment
.env
.envrc

# IDE
.vscode/
.idea/

# OS
.DS_Store
Thumbs.db
225 changes: 225 additions & 0 deletions game-logic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
/**
* GameLogic - Pure game logic for Tic-Tac-Toe
* This class handles all game state and logic without any DOM dependencies,
* making it fully testable.
*/
class GameLogic {
constructor() {
this.reset();
}

/**
* Reset the game to initial state
*/
reset() {
this.currentPlayer = 'X';
this.gameBoard = ['', '', '', '', '', '', '', '', ''];
this.gameActive = true;
this.winner = null;
this.winningCombination = [];
this.moveHistory = [];
}

/**
* Check if a move is valid
* @param {number} cellIndex - The cell index (0-8)
* @returns {boolean} - True if the move is valid
*/
isValidMove(cellIndex) {
// Check if cell index is valid
if (typeof cellIndex !== 'number' || isNaN(cellIndex)) {
return false;
}

// Check if cell index is in range
if (cellIndex < 0 || cellIndex > 8) {
return false;
}

// Check if game is still active
if (!this.gameActive) {
return false;
}

// Check if cell is empty
if (this.gameBoard[cellIndex] !== '') {
return false;
}

return true;
}

/**
* Get the reason why a move is invalid
* @param {number} cellIndex - The cell index (0-8)
* @returns {string|null} - Error message or null if valid
*/
getInvalidMoveReason(cellIndex) {
if (typeof cellIndex !== 'number' || isNaN(cellIndex)) {
return 'Invalid cell index: must be a number';
}

if (cellIndex < 0 || cellIndex > 8) {
return `Invalid cell index: ${cellIndex} is out of range (0-8)`;
}

if (!this.gameActive) {
return 'Game is already over';
}

if (this.gameBoard[cellIndex] !== '') {
return `Cell ${cellIndex} is already occupied by ${this.gameBoard[cellIndex]}`;
}

return null;
}

/**
* Make a move on the board
* @param {number} cellIndex - The cell index (0-8)
* @returns {Object} - Result of the move
*/
makeMove(cellIndex) {
// Validate the move
if (!this.isValidMove(cellIndex)) {
const reason = this.getInvalidMoveReason(cellIndex);
return {
success: false,
error: reason,
gameState: this.getGameState()
};
}

// Place the mark
this.gameBoard[cellIndex] = this.currentPlayer;
this.moveHistory.push({
player: this.currentPlayer,
cellIndex: cellIndex,
moveNumber: this.moveHistory.length + 1
});

// Check for win or draw
const result = this.checkResult();

return {
success: true,
player: this.currentPlayer,
cellIndex: cellIndex,
...result,
gameState: this.getGameState()
};
}

/**
* Check the game result after a move
* @returns {Object} - Game result information
*/
checkResult() {
const winningConditions = [
[0, 1, 2], // Top row
[3, 4, 5], // Middle row
[6, 7, 8], // Bottom row
[0, 3, 6], // Left column
[1, 4, 7], // Middle column
[2, 5, 8], // Right column
[0, 4, 8], // Diagonal top-left to bottom-right
[2, 4, 6] // Diagonal top-right to bottom-left
];

// Check for win
for (const combination of winningConditions) {
const [a, b, c] = combination;

if (this.gameBoard[a] === '' || this.gameBoard[b] === '' || this.gameBoard[c] === '') {
continue;
}

if (this.gameBoard[a] === this.gameBoard[b] && this.gameBoard[b] === this.gameBoard[c]) {
this.gameActive = false;
this.winner = this.currentPlayer;
this.winningCombination = combination;
return {
gameOver: true,
winner: this.currentPlayer,
winningCombination: combination,
isDraw: false
};
}
}

// Check for draw
if (!this.gameBoard.includes('')) {
this.gameActive = false;
this.winner = null;
return {
gameOver: true,
winner: null,
winningCombination: [],
isDraw: true
};
}

// Game continues - switch player
this.switchPlayer();
return {
gameOver: false,
winner: null,
winningCombination: [],
isDraw: false
};
}

/**
* Switch the current player
*/
switchPlayer() {
this.currentPlayer = this.currentPlayer === 'X' ? 'O' : 'X';
}

/**
* Get the current game state
* @returns {Object} - Current game state
*/
getGameState() {
return {
currentPlayer: this.currentPlayer,
gameBoard: [...this.gameBoard],
gameActive: this.gameActive,
winner: this.winner,
winningCombination: [...this.winningCombination],
isDraw: this.winner === null && !this.gameActive,
moveCount: this.moveHistory.length,
moveHistory: [...this.moveHistory]
};
}

/**
* Get all winning combinations for testing
* @returns {Array} - Array of winning combinations
*/
static getWinningCombinations() {
return [
[0, 1, 2], // Top row
[3, 4, 5], // Middle row
[6, 7, 8], // Bottom row
[0, 3, 6], // Left column
[1, 4, 7], // Middle column
[2, 5, 8], // Right column
[0, 4, 8], // Diagonal top-left to bottom-right
[2, 4, 6] // Diagonal top-right to bottom-left
];
}

/**
* Get all possible cell indices
* @returns {Array} - Array of cell indices 0-8
*/
static getAllCellIndices() {
return [0, 1, 2, 3, 4, 5, 6, 7, 8];
}
}

// Export for testing (Node.js) and browser compatibility
if (typeof module !== 'undefined' && module.exports) {
module.exports = GameLogic;
}
Loading