diff --git a/.gitignore b/.gitignore index 6a7d6d8..2a8bc67 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +lib/Minimax.md +lib/ProjectExplanation.md + # Logs logs *.log diff --git a/app.js b/app.js deleted file mode 100644 index fe93fbf..0000000 --- a/app.js +++ /dev/null @@ -1,35 +0,0 @@ -import path from 'path'; -import { fileURLToPath } from 'url'; -import { showLocalNetworkIP } from './shownet.js'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -const PORT = process.env.PORT || 8080; -showLocalNetworkIP(PORT); - -import fastify from 'fastify'; - -const app = fastify({ - logger: false, -}); - -import fastifyStatic from '@fastify/static'; - -app.register(fastifyStatic.default, { - root: path.join(__dirname, 'public'), -}); - -app.get('/register', (req, res) => { - res.sendFile('index.html'); -}); - -const start = async () => { - try { - await app.listen({ port: PORT, host: '::' }); - } catch (err) { - app.log.error(err); - process.exit(1); - } -}; -start(); diff --git a/dev.js b/dev.js new file mode 100644 index 0000000..7ed7e12 --- /dev/null +++ b/dev.js @@ -0,0 +1,52 @@ +/** This file is only added to run a development static server. */ + +import path from 'path'; +import { fileURLToPath } from 'url'; +import os from 'os'; +import fastify from 'fastify'; +import fastifyStatic from '@fastify/static'; + +const networkInterfaces = os.networkInterfaces(); +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const PORT = process.env.PORT || 3000; +const app = fastify({ + logger: false, +}); + +app.register(fastifyStatic.default, { + root: path.join(__dirname, 'lib'), +}); + +app.get('/register', (req, res) => { + res.sendFile('index.html'); +}); + +start(); +showlocalNetAddress(PORT); + +function showlocalNetAddress(PORT) { + console.log(`local : localhost:${PORT}/`); + + const localNetAddress = + networkInterfaces?.wlp2s0?.[0]?.address || + networkInterfaces?.enp3s0f1?.[0]?.address || + networkInterfaces?.['Wi-Fi']?.[1]?.address || + networkInterfaces?.Ethernet?.[1]?.address || + null; + + if (localNetAddress) { + console.log(`network : ${localNetAddress}:${PORT}/`); + } else { + console.log('network : Not Available'); + } +} + +async function start () { + try { + await app.listen({ port: PORT, host: '::' }); + } catch (err) { + app.log.error(err); + process.exit(1); + } +}; diff --git a/public/TicTacToe.js b/lib/TicTacToe.js similarity index 77% rename from public/TicTacToe.js rename to lib/TicTacToe.js index 96b4095..24e98aa 100644 --- a/public/TicTacToe.js +++ b/lib/TicTacToe.js @@ -20,8 +20,12 @@ function randomInteger(min, max) { return Math.round(Math.random() * (max - min) + min); } -/** A single thread tic-tac-toe game representation that can uses - * `minimax` algorithm to search for the best possible move. +function intSum(min, max) { + return ((max - min) + 1) * (min + max) / 2; +} + +/** A single thread, blocking, **tic-tac-toe** class game representation that + * can use the **`minimax`** algorithm to search for the best possible move. */ class TicTacToe { constructor(options = { gridLength, winCount, player }) { @@ -51,7 +55,7 @@ class TicTacToe { this.computerAutoPlay = this.computerAutoPlay.bind(this); this.generateMoves = this.generateMoves.bind(this); this.minimax = this.minimax.bind(this); - this.checkWinner = this.checkWinner.bind(this); + this.evaluate = this.evaluate.bind(this); this.isFinish = this.isFinish.bind(this); this.display = this.display.bind(this); @@ -68,7 +72,7 @@ class TicTacToe { * @description **WARNING**: This method will always **make a move** on an **NA square** * regardless **even** if the **game has already ended** with a winning player or a draw. * - * It is advice to use `this.checkWinner()` and/or `this.isFinish()` first before calling this method. + * It is advice to use `this.evaluate()` and/or `this.isFinish()` first before calling this method. * @param {Number} i row index of the board. * @param {Number} j column index of the row. * @returns `MOVE_INVALID = 0`, `MOVE_SUCCESS = 1`. */ @@ -119,7 +123,7 @@ class TicTacToe { const moves = this.generateMoves(); const bestMove = { score: 0, idx_i: null, idx_j: null }; - const winning = this.checkWinner(); + const winning = this.evaluate(); if (winning === P1) { bestMove.score = 1 * (this.turns <= 0 ? 1 : this.turns); return bestMove; @@ -191,7 +195,7 @@ class TicTacToe { * In other board game engines like; chess and GO the evaluation function might be separated. * @returns no winner `0` | player X `1` | player O `2`. */ - checkWinner() { + evaluate() { // check row - for (let i = 0; i < this.grid; ++i) { let samePiece = 1; @@ -231,34 +235,72 @@ class TicTacToe { } // check diag \ - let samePieceSecondLastDiag = 1; - for (let i = 0; i < this.grid - 1; ++i) { - if ( - this.board[i * this.grid + i] === this.board[(i + 1) * this.grid + (i + 1)] && - this.board[i * this.grid + i] !== NA - ) { - samePieceSecondLastDiag++; - if (samePieceSecondLastDiag === this.pieceWinCount) { - this.winner = this.board[i * this.grid + i]; - return this.board[i * this.grid + i]; + for (let i = 0; i < this.grid; ++i) { + let samePiece = 1, prevPiece = null; + for (let j = 0; j < i + 1; ++j) { + const CURRENT_INDEX = (this.grid - i) + ((this.grid + 1) * j) - 1; + if (prevPiece === this.board[CURRENT_INDEX] && this.board[CURRENT_INDEX] !== NA) { + samePiece++; + if (samePiece === this.pieceWinCount) { + this.winner = this.board[CURRENT_INDEX]; + return this.board[CURRENT_INDEX]; + } + } else { + samePiece = 1; } - } else { - samePieceSecondLastDiag = 1; + prevPiece = this.board[CURRENT_INDEX]; + } + } + + for (let i = 1; i < this.grid; ++i) { + let samePiece = 1, prevPiece = null; + for (let j = 0; this.grid - i - j > 0; ++j) { + const CURRENT_INDEX = (i * this.grid) + ((this.grid + 1) * j); + if (prevPiece === this.board[CURRENT_INDEX] && this.board[CURRENT_INDEX] !== NA) { + samePiece++; + if (samePiece === this.pieceWinCount) { + this.winner = this.board[CURRENT_INDEX]; + return this.board[CURRENT_INDEX]; + } + } else { + samePiece = 1; + } + prevPiece = this.board[CURRENT_INDEX]; } } // check diag / - const diagStep = this.grid - 1; - let samePieceLastDiag = 1; + for (let i = 0; i < this.grid; ++i) { + let samePiece = 1, prevPiece = null; + for (let j = 0; j < i + 1; ++j) { + const CURRENT_INDEX = i + (j * this.grid) - j; + if (prevPiece === this.board[CURRENT_INDEX] && this.board[CURRENT_INDEX] !== NA) { + samePiece++; + if (samePiece === this.pieceWinCount) { + this.winner = this.board[CURRENT_INDEX]; + return this.board[CURRENT_INDEX]; + } + } else { + samePiece = 1; + } + prevPiece = this.board[CURRENT_INDEX]; + } + } + for (let i = 1; i < this.grid; ++i) { - if (this.board[i * diagStep] === this.board[(i + 1) * diagStep] && this.board[i * diagStep] !== NA) { - samePieceLastDiag++; - if (samePieceLastDiag === this.pieceWinCount) { - this.winner = this.board[i * diagStep]; - return this.board[i * diagStep]; + let samePiece = 1, prevPiece = null; + for (let j = 0; this.grid - i - j > 0; ++j) { + const CURRENT_INDEX = ((this.grid * (i + 1)) - 1) + ((this.grid - 1) * j); + if (prevPiece === this.board[CURRENT_INDEX] && this.board[CURRENT_INDEX] !== NA) { + samePiece++; + if (samePiece === this.pieceWinCount) { + this.winner = this.board[CURRENT_INDEX]; + return this.board[CURRENT_INDEX]; + } + } else { + samePiece = 1; } - } else { - samePieceLastDiag = 1; + prevPiece = this.board[CURRENT_INDEX]; } } @@ -290,7 +332,7 @@ class TicTacToe { * **This method will also update the `this.winner` member when called**. * @returns `true` if game ended, `false` if not. */ isFinish() { - const hasWinner = this.checkWinner(); + const hasWinner = this.evaluate(); if (hasWinner === P1 || hasWinner === P2) { return true; } @@ -346,7 +388,7 @@ class TicTacToe { } /** This test will only work for a 3x3 initialized board. */ - testCheckWinner(boardStates, winners) { + testEvaluation(boardStates, winners) { let failedTests = 0; for (let i = 0; i < boardStates.length; ++i) { @@ -354,7 +396,7 @@ class TicTacToe { this.board[j] = boardStates[i][j]; } - if (this.checkWinner() === winners[i]) { + if (this.evaluate() === winners[i]) { console.log('test ', i + 1, ' : PASSED'); } else { console.log('test ', i + 1, ' : FAILED'); diff --git a/public/index.html b/lib/index.html similarity index 92% rename from public/index.html rename to lib/index.html index f83c614..cf4b85c 100644 --- a/public/index.html +++ b/lib/index.html @@ -1,3 +1,5 @@ + + @@ -53,7 +55,7 @@

MiniMax - TicTacToe

- source code - licence MIT + source code diff --git a/public/script.js b/lib/script.js similarity index 94% rename from public/script.js rename to lib/script.js index 8f999b2..a31017e 100644 --- a/public/script.js +++ b/lib/script.js @@ -1,3 +1,5 @@ +// this file is only added to show how to use the TicTacToe.js + // =================== html initialization =================== const PIECE = [' ', '✕', '◯']; @@ -77,9 +79,9 @@ SELECT_GRID.addEventListener('change', () => { setNewGame( 'The depth has been optimized in relation to the grid value to ' + - 'improve calculation speed. You can easily adjust the depth setting, ' + - 'but keep in mind that higher values for "depth and grid" will ' + - 'increase the move time of the computer and will use more resources.' + 'improve the calculation speed. You can adjust the depth value, ' + + 'but keep in mind that higher values for "depth and grid" will ' + + 'will take more time to calculate or might even crash the page.' ); }); @@ -125,7 +127,7 @@ function generateCells() { for (let j = 0; j < Game.grid; ++j) { const square = document.createElement('span'); square.className = 'cell'; - square.style.animationDelay = `${i * 0.125 + j * 0.125 + 0.125}s`; + square.style.animationDelay = `${(i * 0.1) + (j * 0.1) + 0.1}s`; square.addEventListener('click', () => makeMove(i, j)); diff --git a/public/style.css b/lib/style.css similarity index 97% rename from public/style.css rename to lib/style.css index 8ab46e9..d7fe980 100644 --- a/public/style.css +++ b/lib/style.css @@ -1,3 +1,5 @@ +/* this file is only added to show the minimax-tic-tac-toe in action */ + * { padding: 0px; margin: 0px; diff --git a/package.json b/package.json index a3a6e78..cdddfd6 100644 --- a/package.json +++ b/package.json @@ -2,8 +2,9 @@ "name": "tic-tac-toe-minimax", "version": "1.0.1", "description": "A tic-tac-toe web application that has a computer player that uses the minimax algorithm to make moves.", - "main": "tests.js", + "main": "lib/TicTacToe.js", "scripts": { + "dev": "node dev", "format": "npx prettier --write \"./**/*.{js,jsx,ts,tsx,json,css,scss,md}\"" }, "keywords": [ diff --git a/readme.md b/readme.md index d826864..7162b56 100644 --- a/readme.md +++ b/readme.md @@ -3,15 +3,25 @@ ![tests](https://github.com/mrdcvlsc/minimax-tic-tac-toe/actions/workflows/tests.yml/badge.svg) ![linter](https://github.com/mrdcvlsc/minimax-tic-tac-toe/actions/workflows/linter.yml/badge.svg) -A simple project showcasing my **naive implementation** of [**minimax**](https://en.wikipedia.org/wiki/Minimax#Pseudocode) algorithm for a **tic-tac-toe** web application game. +A simple project showcasing my **naive implementation** of the [**minimax**](https://en.wikipedia.org/wiki/Minimax#Pseudocode) algorithm for an N x N board, **tic-tac-toe** web application game. -- [SEE DEMO](https://minimax-tic-tac-toe-demo.vercel.app/) +## **Test it over the internet :** -**Run locally** +_If you have an internet connection, you can test my Tic-Tac-Toe web application that uses my `minimax-tic-tac-toe` algorithm implementation_. + +- [Click here to view the DEMO](https://minimax-tic-tac-toe-demo.vercel.app/) +- **Demo raw link** - https://minimax-tic-tac-toe-demo.vercel.app/ + +## **To run it locally :** + +_You need to have `git` and `node` installed in your device as a requirement_. ```shell git clone https://github.com/mrdcvlsc/minimax-tic-tac-toe.git cd minimax-tic-tac-toe npm install -node app +node dev ``` + +Then open `localhost:3000` in your browser. + diff --git a/shownet.js b/shownet.js deleted file mode 100644 index 2a3ab01..0000000 --- a/shownet.js +++ /dev/null @@ -1,20 +0,0 @@ -import os from 'os'; -const networkInterfaces = os.networkInterfaces(); - -/// shows the local-ip where the node app is hosted in the network. -function showLocalNetworkIP(PORT) { - console.log(`(0) | local : localhost:${PORT}/`); - - if (typeof networkInterfaces.wlp2s0 !== 'undefined') { - console.log(`(a) | app-server-ip: ${networkInterfaces.wlp2s0[0].address}:${PORT}/`); - } else if (typeof networkInterfaces.enp3s0f1 !== 'undefined') { - console.log(`(b) | app-server-ip: ${networkInterfaces.enp3s0f1[0].address}:${PORT}/`); - } else if (typeof networkInterfaces['Wi-Fi'] !== 'undefined') { - console.log(`(c) | app-server-ip: ${networkInterfaces['Wi-Fi'][1].address}:${PORT}/`); - } else if (typeof networkInterfaces.Ethernet !== 'undefined') { - console.log(`(d) | app-server-ip: ${networkInterfaces.Ethernet[1].address}:${PORT}/`); - } else { - console.log('no IP found for sharing over the network'); - } -} -export { showLocalNetworkIP }; diff --git a/tests.js b/tests.js index 53bd400..1e8cdc1 100644 --- a/tests.js +++ b/tests.js @@ -1,8 +1,63 @@ -import { TicTacToe, NA, P1, P2 } from './public/TicTacToe.js'; +import { TicTacToe, NA, P1, P2 } from './lib/TicTacToe.js'; const t = new TicTacToe({ gridLength: 3 }); +const fbf = new TicTacToe({ gridLength: 4, winCount: 3 }); let failedTests = 0; +const fbfWinners = [NA, P1, P2, P1, P2, P1, P2, P1, P2]; +const fbfBoardStates = [ + [ + P2, NA, NA, P2, + NA, P1, NA, NA, + NA, NA, P2, NA, + P1, NA, NA, P1 + ], + [ + NA, NA, P1, NA, + NA, P1, NA, NA, + P1, NA, NA, NA, + NA, NA, NA, NA + ],[ + NA, NA, NA, NA, + NA, NA, NA, P2, + NA, NA, P2, NA, + NA, P2, NA, NA + ],[ + NA, P1, NA, NA, + NA, NA, P1, NA, + NA, NA, NA, P1, + NA, NA, NA, NA + ],[ + NA, NA, NA, NA, + P2, NA, NA, NA, + NA, P2, NA, NA, + NA, NA, P2, NA + ],[ + NA, NA, NA, NA, + NA, NA, P1, NA, + NA, P1, NA, NA, + P1, NA, NA, NA + ],[ + NA, NA, NA, NA, + NA, P2, NA, NA, + NA, NA, P2, NA, + NA, NA, NA, P2 + ],[ + NA, NA, NA, P1, + NA, NA, P1, NA, + NA, P1, NA, NA, + NA, NA, NA, NA + ],[ + P2, NA, NA, NA, + NA, P2, NA, NA, + NA, NA, P2, NA, + NA, NA, NA, NA + ], +]; + +console.log('============ 4x4 - piece 3 win state tests ============ '); +failedTests += fbf.testEvaluation(fbfBoardStates, fbfWinners); + const winners = [NA, P1, P1, P1, P1, P1, P1, P2, P2, P2, P2, P2, P2, P1, P1, P2, P2, NA]; const winBoardStates = [ @@ -26,7 +81,8 @@ const winBoardStates = [ [P2, P1, P2, P1, P2, NA, NA, P1, NA], // no winner ]; -failedTests += t.testCheckWinner(winBoardStates, winners); +console.log('============ 3x3 win state tests ============ '); +failedTests += t.testEvaluation(winBoardStates, winners); const correctMoves = [ [ @@ -86,6 +142,7 @@ const moveBoardStates = [ [P1, NA, P1, P2, NA, P2, P1, P2, P1], ]; +console.log('============ 3x3 move generation test ============ '); failedTests += t.testGenerateMoves(moveBoardStates, correctMoves); if (failedTests > 0) {