export class ConnectBoard { static DRAW = 3; static RED_PLAYER = 1; static BLUE_PLAYER = 2; static EMPTY_VALUE= 0; constructor(rows, columns, solver){ if (!solver){ throw new Error("solver required to be provided"); } if (typeof solver.SolveTable !== "function"){ throw new Error("solver.SolveTable function required from Solver"); } this.table = []; this.rows = rows; this.columns = columns; this.winner = -1; this.redMoves = 0; this.blueMoves = 0; this.solver = solver; this.totalMoves = rows * columns; this.resetTable(); } isPlayable(){ return this.winner === -1; } // isDraw returns true/false if giving table was a draw. isDraw(){ return (this.blueMoves + this.redMoves) === this.totalMoves; } // movesMade returns giving total moves made by both players. movesMade(){ return this.blueMoves + this.redMoves; } // resetTable resets the underline this.table table. resetTable(){ const table = []; for (let i = 0; i < this.rows; i++){ const columns = []; for (let j = 0; j < this.columns; j++){ columns.push(ConnectBoard.EMPTY_VALUE); } table.push(columns); } this.winner = -1; this.redMoves = 0; this.blueMoves = 0; this.table = table; } // Play runs a brute-force method where we check if a giving // set of moves by either player reaches a draw or win. // // Callback will be called with the following arguments: // // 1. An error object if any // 2. A integer indicating state: -1 for no-win, 0 for draw and 1 or 2 // for a winner of game. // // Whatever solver is used is expected to follow the underline argument // format. Play(row, column, player, callback){ if (player !== ConnectBoard.RED_PLAYER && player !== ConnectBoard.BLUE_PLAYER){ callback(new Error("invalid player provided")); return null; } if (row >= this.table.length || row < 0) { callback(new Error("invalid row")); return null; } const targetRow = this.table[row]; if (column >= targetRow.length || column < 0) { callback(new Error("invalid column")); return null; } if (targetRow[column]) { callback(new Error("unplayable column")); return null; } if (!this.isPlayable()){ callback(new Error("game already completed, please reset")); return null; } if (player === ConnectBoard.RED_PLAYER){ this.redMoves++; } if (player === ConnectBoard.BLUE_PLAYER){ this.blueMoves++; } // set giving column point. targetRow[column] = player; // if neither players have made at least 4 moves, then // there isn't a need to check,just skip. if (this.redMoves < 4 && this.blueMoves < 4){ callback(null, -1); return null; } this.solver.SolveTable(this.table, function (err, state) { if (err){ callback(err, state); return null; } // if it's an ongoing game, then pass back to callback. if (state === -1){ callback(null, state); return null; } // set winner and pass to callback. this.winner = state; callback(null, state) }.bind(this)); } } export class BruteForceSolver { constructor(rows, columns, emptyValue){ this.columns = columns; this.rows = rows; this.emptyValue = emptyValue; } // SolveTable solves the provided table matching // expected rows and columns range. // // Callback is provided with two arguments: // // 1. An error if any // 2. A integer value indicating a draw (0), win (1 or 2) or // on going game -1. // // A second argument providing the value in this case // 1 or 2 which won the game, or a -1 if it was a draw. // If the game is still ongoing then a 0 is giving as // second value, indicating no win or draw yet. SolveTable(table, callback) { if (!table){ callback(new Error("invalid table argument")); return null; } if (table.length < this.rows){ callback(new Error("table with unmatched row length")); return null; } const section = table[0]; if (section.length < this.columns){ callback(new Error("table with unmatched column length")); return null; } const vertResult = this.checkVertical(table); if (vertResult){ callback(null, vertResult); return null; } const horizResult = this.checkHorizontal(table); if (horizResult){ callback(null, horizResult); return null; } const dLeft = this.checkDiagonalLeft(table); if (dLeft){ callback(null, dLeft); return null; } const dRight = this.checkDiagonalRight(table); if (dRight){ callback(null, dRight); return null; } if (this.checkDraw(table)) { callback(null, 0); return null; } return callback(null, -1); } checkDraw(table) { for (let r = 0; r < this.rows; r++) { for (let c = 0; c < this.columns; c++) { if (table[r][c] === this.emptyValue) { return false; } } } return true; } checkVertical(table) { // Check only if row is 3 or greater for (let r = 3; r < this.rows; r++) { for (let c = 0; c < this.columns; c++) { if (table[r][c]) { if (table[r][c] === table[r - 1][c] && table[r][c] === table[r - 2][c] && table[r][c] === table[r - 3][c]) { return table[r][c]; } } } } return null; } checkDiagonalLeft(table) { // Check only if row is 3 or greater AND column is 3 or greater for (let r = 3; r < this.rows; r++) { for (let c = 3; c < this.columns; c++) { if (table[r][c]) { if (table[r][c] === table[r - 1][c - 1] && table[r][c] === table[r - 2][c - 2] && table[r][c] === table[r - 3][c - 3]) { return table[r][c]; } } } } return null; } checkHorizontal(table) { // Check only if column is 3 or less for (let r = 0; r < this.rows; r++) { for (let c = 0; c < 4; c++) { if (table[r][c]) { if (table[r][c] === table[r][c + 1] && table[r][c] === table[r][c + 2] && table[r][c] === table[r][c + 3]) { return table[r][c]; } } } } } checkDiagonalRight(table) { // Check only if row is 3 or greater AND column is 3 or less for (let r = 3; r < this.rows; r++) { for (let c = 0; c < 4; c++) { if (table[r][c]) { if (table[r][c] === table[r - 1][c + 1] && table[r][c] === table[r - 2][c + 2] && table[r][c] === table[r - 3][c + 3]) { return table[r][c]; } } } } } // doVerticalCheck checks if giving table matches a // possible vertical win. // // Vertical checks can only make sense from row 3 and // above. static doVerticalCheck(table, r, c) { if (r < 3){ return null; } if (table[r][c]) { if (table[r][c] === table[r - 1][c] && table[r][c] === table[r - 2][c] && table[r][c] === table[r - 3][c]) { return table[r][c]; } } return null; } // doDiagonalLeftCheck checks if giving diagonal positions // from row 3 and column 3 and above can be matched, since // we can't do diagonal checks when in row one and two. static doDiagonalLeftCheck(table, r, c) { if (r < 3){ return null; } if (c < 3){ return null; } if (table[r][c]) { if (table[r][c] === table[r - 1][c - 1] && table[r][c] === table[r - 2][c - 2] && table[r][c] === table[r - 3][c - 3]) { return table[r][c]; } } return null; } // doHorizontalCheck checks giving row 4 length column for // a winning match. static doHorizontalCheck(table, r, c) { if (c > 4){ return null; } if (table[r][c]) { if (table[r][c] === table[r][c + 1] && table[r][c] === table[r][c + 2] && table[r][c] === table[r][c + 3]) { return table[r][c]; } } return null; } // doDiagonalRightCheck checks giving diagonal right row and column areas for // a winning match. static doDiagonalRightCheck(table, r, c) { if (r < 3){ return null; } if (c > 4){ return null; } if (table[r][c]) { if (table[r][c] === table[r - 1][c + 1] && table[r][c] === table[r - 2][c + 2] && table[r][c] === table[r - 3][c + 3]) { return table[r][c]; } } return null; } }