Tic-Tac-Toe Game w/ Minimax Algorithm
Build a Tic Tac Toe Game - freecodecamp A Tic-Tac-Toe application with artificial intelligence based on a modified version of the minimax algorithm.
A Pen by HARUN PEHLİVAN on CodePen.
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
<link href="https://codepen.io/U-ways/pen/qNLZrg" rel="stylesheet" />
/* Green Lab template overrides/additions
==============================================================================*/
#header {
height: 75px;
margin: 0 0 0 0;
} #proj-subheading {
height: 25px;
font-size: 1em;
font-style: italic;
margin: 0 0 0 1vh;
} #details-body {
margin-top: 15px;
} #proj-details summary {
margin-top: 15px;
}
/* Color Scheme
================================================================================
Main Colors:
#c5dec2 - #95a893
#3e5050 - #3a4949
#263331 - #333
Matching R&G:
#549042 - #8e0000
*//* App
==============================================================================*/
#app {
margin-top: 15px;
justify-content: flex-start;
}
#blocks-con {
margin: auto;
flex-wrap: wrap;
width: 430px;
padding: 20px 15px;
background-color: #739595;
border-radius: 4%;
border: 11px solid #c5dec2;
box-shadow: 0px 0px 6px 1px rgba(0, 0, 0, 0.5)
} #blocks-con > div {
height: 120px;
width: 120px;
padding: 5px;
color: #3e5050;
font-size: 100px;
font-weight: normal;
text-align: center;
border: 2.5px solid #c5dec2;
border-radius: 2%;
cursor: pointer;
user-select: none;
}
/*Uncomment the colors so you can understand what I did*/
/*(-n+3) Selects Only The First 3*/
#blocks-con > div:nth-child(-n+3) {
border-top: none;
/* color: red; */
} #blocks-con > div:nth-child(n+7) {
border-bottom: none;
/* color: yellow; */
} #blocks-con > div:nth-child(3n) {
border-right: none;
/* color: blue; */
} #blocks-con > div:nth-child(3n+1) {
border-left: none;
/* color: green; */
}
/* Symbol selection & Misc
============================================*/
#selection {
width: inherit;
margin-top: 0;
margin-bottom: 3vh;
padding: 0 0 2px 0;
text-align: center;
background-color: #c5dec2;
color: #3b4c4c;
}
.xo {
font-weight: 300;
cursor: pointer;
user-select: none;
font-size: 2em;
vertical-align: middle;
} .xo:hover {
color: #739595;
text-decoration: underline;
} .xo_active {
color: #739595;
text-decoration: underline;
font-weight: 300;
}
#draw, #won, #lost {
font-weight: 600;
}
.win {
color: #c5dec2 !important;
animation: 1.2s blinker .6s linear;
animation-iteration-count: 2;
transition: 1.2s;
} @keyframes blinker {
50% { color: #374848;}
}
/* Reset button
============================================*/
#reset {
width: 200px;
margin: auto;
margin-top: 10px;
color: #3b4c4c;
font-size: 1.3em;
font-weight: 600;
background-color: #c5dec2;
border-radius: 4%;
border: 11px solid #c5dec2;
cursor: pointer;
} #reset:active {
position: relative;
top: 4px;
}
<script src="https://codepen.io/U-ways/pen/qNLZrg"></script>
/*jshint esversion: 6*/
/* Initial selector/variable assignments
==============================================================================*/
const blocks = document.querySelectorAll('#blocks-con > div');
let board = new Array(9).fill(0);
let computer = [1,"O"]; // [Player id, Symbol]
let user = [2,"X"];
let firstChoice = true;
const winCombos = [
[0,4,8], [2,4,6], // Diagonal
[0,1,2], [3,4,5], [6,7,8], // Horizontal
[0,3,6], [1,4,7], [2,5,8] // Vertical
];
const x = document.querySelector("#x");
const o = document.querySelector("#o");
x.addEventListener("click", x_or_o);
o.addEventListener("click", x_or_o);
/* O & X Selection function
==============================================================================*/
function x_or_o() {
if (this.id === "x") {
user[1] = "X";
computer[1] = "O";
o.classList.remove("xo_active");
this.classList.add("xo_active");
reset();
} else {
user[1] = "O";
computer[1] = "X";
x.classList.remove("xo_active");
this.classList.add("xo_active");
reset();
aiIntialChoice();
}
}
/* Blocks listeners assignment function (Used for initial assignment & board reset)
==============================================================================*/
function addBlocksEventsHandler() {
blocks.forEach(
block => block.addEventListener("click", blockEvents)
);
}
function blockEvents() {
let blockNumber = this.getAttribute("data-blockNumber");
update(this, blockNumber);
}
addBlocksEventsHandler();
/* Blocks listeners removal function (Used for board reset)
==============================================================================*/
function removeBlocksEventsHandler() {
blocks.forEach(
block => block.removeEventListener("click", blockEvents)
);
}
/* Update board function
==============================================================================*/
function update(block, number) {
block.removeEventListener("click", blockEvents);
board[number] = user[0];
block.innerHTML = user[1];
boardResult(user[0]);
return (firstChoice) ? aiIntialChoice() : ai(board);
}
/* Board Result Checker
==============================================================================*/
/** @function boardResult(player)
* Take a @param player as an argument and loop through @const winCombos
* to determine if a winning combination occurred.
*
* If a @const winCombos element had a full house (counter === 3), remove blocks events
* to prevent further gameplay and trigger the winning 'ceremony'.
*/
function boardResult(player){
for (let i = 0; i < winCombos.length; i++) {
let counter = 0;
for (let j = 0; j < winCombos[i].length; j++) {
if (board[winCombos[i][j]] !== player) {
break; // Early break as a mismatch found
} else {
counter++;
}
}
if (counter === 3) {
removeBlocksEventsHandler();
return ceremony(winCombos[i]);
}
}
}
const ceremony = combo => {
combo.forEach((winningBlock) => blocks[winningBlock].classList.add("win"));
};
/* Board Reset (Returns everything to its original state)
==============================================================================*/
function reset() {
firstChoice = true;
board = new Array(9).fill(0);
blocks.forEach(
block => {
block.classList.remove("win");
block.innerHTML = "";
});
removeBlocksEventsHandler();
addBlocksEventsHandler();
if (computer[1] === "X") {aiIntialChoice();}
}
const resetBtn = document.querySelector("#reset");
resetBtn.addEventListener("click", reset);
/* Initial AI choice function: (performance boost compared to using minimax first)
==============================================================================*/
function aiIntialChoice() {
if (computer[1] === "O") {
const choice = (board[4] === 0) ? 4 : 2;
board[choice] = computer[0];
blocks[choice].innerHTML = computer[1];
blocks[choice].removeEventListener("click", blockEvents);
} else {
board[6] = computer[0];
blocks[6].innerHTML = computer[1];
blocks[6].removeEventListener("click", blockEvents);
}
firstChoice = false;
}
/* Artificial Intelligence functionality
==============================================================================*/
function ai(currentBoard) {
let choicesLeft = choiceFinder(board).length;
if (choicesLeft === 0) {return false;}
function choiceFinder(board) {
let choicesLeft = [];
board.forEach((block, index) => {if (block === 0){choicesLeft.push(index);}});
return choicesLeft;
}
/** @function boardState(board)
* Returns an integer:
* +10 = user lost
* -10 = user won
* 0 = tie or neutral board state
*/
function boardState(board) {
for (let i = 0; i < winCombos.length; i++) {
let score = [0,0]; // [computer,user]
for (let j = 0; j < winCombos[i].length; j++) {
if (board[winCombos[i][j]] === 0) {
break; // Out of loop as a mismatch found
} else if (board[winCombos[i][j]] === computer[0]) {
score[0]++;
} else {
score[1]++;
}
} // End of [j]
if (score[0] === 3) {
return 10;
} else if (score[1] === 3) {
return -10;
}
} // End of [i]
return 0; // No winning combinations found.
}
/* U-minimax (see comment below) algorithm to find the best choice for computer
==============================================================================*/
/** @function [u-]minimax(currentBoard,turn)
* My minimax explanation took about 60 lines (~100 columns each).
* Try reading this as a starter: https://en.wikipedia.org/wiki/Minimax
*
* NOTE:
* The minimax algorithm I implemented is not the same as the standard one. It is an algorithm
* I created to make a 'realistic' use of the stack. It climbs down from the stack like a
* "real" human instead of creating external values to find the best move.
*/
function minimax(currentBoard, turn) {
let depth = choiceFinder(currentBoard).length; // Track stack depth
function recursion(board, turn, choiceMade) {
let choicesLeft = choiceFinder(board);
let nextTurn = (turn === computer[0]) ? user[0] : computer[0];
let state = boardState(board);
if (choicesLeft.length === 0 || state !== 0) {
return state;
} else {
let childValues = choicesLeft.map(
(choice) => {
let nextBoard = [...board];
nextBoard[choice] = turn;
let nextBoardState = recursion(nextBoard, nextTurn);
return [choice, nextBoardState];
}
).sort((a,b) => b[1] - a[1]);
if (depth !== choicesLeft.length) {
return (turn === computer[0]) ? childValues[0][1] : childValues[childValues.length-1][1];
} else {
return childValues[0][0];
}
} // else
} // recursion
return recursion(currentBoard, turn);
} // minimax
let aiChoice = minimax(currentBoard, computer[0]);
board[aiChoice] = computer[0];
blocks[aiChoice].innerHTML = computer[1];
blocks[aiChoice].removeEventListener("click", blockEvents);
return boardResult(computer[0]);
}
/* Adding project Details
==============================================================================*/
addProjDetails(0, 'Project Overview', 'This is a Tic-Tac-Toe application with an integrated artificial intelligence based on a modified version of the popular game theory algorithm for decision making; minimax.');
addProjDetails(0, 'Techniques/Technologies', 'Nothing special, vanilla HTML, CSS and JavaScript. Flexbox layout module and a bunch of event listeners to track user progress and interactivity. <br><br>I implemented a customized version of the minimax algorithm. My objective was to make a \'realistic\' use of the stack. It climbs down from the stack like a \'real\' human, instead of creating external values to find the best move. I will not go into detail about that as this section was not made for documentation purposes.');
addProjDetails(0, 'Future implementations', 'I am planning to Implement the Alpha-Beta helping search algorithm, add extra rows and columns to the application, improve the board result algothrim, and then build on top of the AI to create a chess engine. A long-term vision, but will get there eventually.');
addProjDetails(1, 'Hurdles encountered', 'The minimax algothrim was definitely a big challenge as I did not have any prerequisite knowledge about game theory, how the stack works in JavaScript, recursion, and the like...');
addProjDetails(1, 'Reflection', 'The Tic-Tac-Toe application took me less than 10 hours to build. However, It took me about 80-120 working hours in a span of 2 months to obtain the prerequisite knowledge and understand the minimax algothrim to get the AI to work. On a positive note, lots of stuff started to make sense as I filled lots of gaps within my understanding. <br><br>If I decided to give up at any time but the 120th hour, I would\'ve <b>never</b> made this application to work as expected. Just a small reminder for everyone out there.');
#container
header#header.grid.cols-6
section#proj-heading
h1 Tic-Tac-Toe Application
h3#proj-subheading w/ Minimax Algorithm
details#proj-details
summary Project Info
section#details-body.grid.cols-2.rows-2
.details-column
.details-column
main#app.flex-c
#app-con.flex-c
section#selection
h2
| - Choose
span#x.xo.xo_active X
| or
span#o.xo O
span#draw Win
| ,
span#won Draw
| or
span#lost Lose
| . -
section#blocks-con.flex-r
.block data-blocknumber="0"
.block data-blocknumber="1"
.block data-blocknumber="2"
.block data-blocknumber="3"
.block data-blocknumber="4"
.block data-blocknumber="5"
.block data-blocknumber="6"
.block data-blocknumber="7"
.block data-blocknumber="8"
button#reset.flex-c Reset
footer#footer
p
| Last Updated:
time datetime="2017-05-03" 03/05/2017
|   |  
a href="https://codepen.io/harunpehlivan" target="_blank"
i.fa.fa-codepen aria-hidden="true"
| HARUN PEHLİVAN