<link href="https://fonts.googleapis.com/css?family=Montserrat" rel="stylesheet">
<div class="drawer">
<header>
<button class="play-btn"></button>
<p class="message"></p>
<div class="dice-roll">
<div class="opponent"></div>
<div class="dice-rolling"></div>
<div class="dice-score"></div>
<div class="dice-result"></div>
</div>
</header>
<main class="board">
<div class="cell circle"></div>
<div class="cell cross"></div>
<div class="cell circle"></div>
<div class="cell cross"></div>
<div class="cell circle"></div>
<div class="cell"></div>
<div class="cell cross"></div>
<div class="cell cross"></div>
<div class="cell"></div>
</main>
<footer class="scores hide">
<div>
<span></span>
<ul>
<li></li>
<li></li>
<li></li>
</ul>
</div>
<div>
<span></span>
<ul>
<li></li>
<li></li>
<li></li>
</ul>
</div>
</footer>
</div>
*, *::after, *::before {
box-sizing: border-box;
}
html {
font-size:62.5%;
}
body {
font-size:1.6rem;
margin:0;
height:100vh;
background:hsl(300, 15%, 36%);
font-family: 'Montserrat', 'Arial', sans-serif;
letter-spacing: 1px;
}
.drawer {
width: 80%;
margin:0 auto;
padding-top:60px;
}
.board {
display:flex;
flex-wrap: wrap;
width: 320px;
height: 320px;
margin: 0 auto;
}
.cell {
position: relative;
width:90px;
height:90px;
margin:5px;
border-radius: 0.3em;
background:hsl(300, 15%, 33%);
}
.cell.circle,
.cell.cross {
background:transparent;
}
.circle::after,
.cross::before,
.cross::after {
content:'';
position:absolute;
top:50%;
left:50%;
}
.cross::before,
.cross::after {
width:5px;
height:75px;
background:hsl(300, 15%, 33%);
}
.playing .cross::before,
.playing .cross::after {
background:hsl(194, 100%, 73%);
}
.cross::before {
transform:translate(-50%, -50%) rotate(45deg);
}
.cross::after {
transform:translate(-50%, -50%) rotate(-45deg);
}
.circle::after {
width:70px;
height:70px;
border-radius:50%;
transform:translate(-50%, -50%);
border:5px solid hsl(300, 15%, 33%);
}
.playing .circle::after {
border-color:hsl(7, 63%, 78%);
}
.playing .cell:not(.cross):not(.circle){
cursor:pointer;
}
.playing .cell:not(.cross):not(.circle):hover{
background:hsl(300, 15%, 34%);
}
#instructions {
display: none;
}
.message {
text-align: center;
color: hsla(300, 15%, 20%, 1);
font-size: 2rem;
}
.play-btn {
position:absolute;
top:0;
left:50%;
outline:none;
border:none;
cursor:pointer;
background: hsl(300, 3%, 18%);
padding: 1rem 1.5rem;
font-size: 2.4rem;
color:hsla(300, 15%, 44%, 1);
border-radius: 0 0 0.2rem 0.2rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
border:1px solid hsla(300, 3%, 17%, 1);
transform:translate(-50%, 0);
transition: transform 200ms ease-out;
}
.play-btn:hover {
background: hsl(300, 3%, 20%);
}
.play-btn.hide {
display:inline-block;
transform:translate(-50%, -100%);
}
header {
max-width: 320px;
margin:0 auto 20px;
}
.scores {
display:flex;
justify-content: space-between;
position:relative;
max-width: 320px;
margin:2rem auto 0 ;
border-top: 2px solid hsla(300, 15%, 20%, 1);
padding-top: 2rem;
opacity: 1;
transform: translate(0, 0);
transition: all 200ms 75ms ease-out;
}
.scores.hide {
display: flex;
opacity:0;
transform: translate(0, 20%);
}
.scores div {
flex:1;
}
.scores span {
display:block;
color:hsla(300, 15%, 20%, 1);
}
.scores ul {
list-style: none;
margin:0;
padding: 0;
display: inline-block;
}
.scores li {
width: 10px;
height:10px;
border: 2px solid hsla(300, 15%, 20%, 1);
border-radius:50%;
display: inline-block;
}
.scores li.won {
background: hsla(300, 15%, 53%, 1);
animation: win 300ms;
}
@keyframes win {
0% {transform: scale(1);}
40% {transform: scale(3); }
100% {transform: scale(1);}
}
.scores::after{
display: none;
content: 'vs';
position: absolute;
left:50%;
top:50%;
font-size: 2.4rem;
transform:translate(-50%, -50%);
color:#bdbdbd;
}
.scores >div:last-child{
text-align: right;
}
.hide { display:none;}
.dice-roll {
text-align: center;
padding:1.5rem;
font-size:1.4rem;
}
View Compiled
const EMPTY = -1;
const PLAYER = 0;
const OPPONENT = 1;
// DOM
const $board = document.querySelector('.board');
const $cells = Array.from($board.children);
const $diceRoll = document.querySelector('.dice-roll');
const $scores = document.querySelector('.scores');
const $message = document.querySelector('.message');
const $playBtn = document.querySelector('.play-btn');
let board = emptyBoard();
let winPatterns = [
0b111000000, 0b000111000, 0b000000111, // rows
0b100100100, 0b010010010, 0b001001001, // cols
0b100010001, 0b001010100 // diags
];
// Minimax (see http://www.geeksforgeeks.org/minimax-algorithm-in-game-theory-set-1-introduction/)
class AI {
constructor(difficulty = 1) {
this.difficulty = difficulty;
}
findBestMove() {
return this.minimax(this.difficulty, OPPONENT).position;
}
minimax(depth, minmaxer) {
let nextMoves = getAvailableMoves();
let bestMove = { score: (minmaxer === OPPONENT) ? -10000 : 10000, position: -1};
// Collect every available move
let randomizedMoves = [];
if (!nextMoves.length || depth === 0) {
bestMove.score = this.evaluate();
} else {
for (let i = 0; i < nextMoves.length; ++i) {
let moveSimulation = nextMoves[i];
board[moveSimulation] = minmaxer;
let score = this.minimax(depth - 1, (minmaxer === OPPONENT) ? PLAYER : OPPONENT).score;
randomizedMoves.push({score:score, position:moveSimulation});
if ((minmaxer === OPPONENT && score > bestMove.score) ||
(minmaxer === PLAYER && score < bestMove.score)) {
bestMove = {score: score, position: moveSimulation };
}
board[moveSimulation] = EMPTY;
}
}
// Take one random move if several moves with the same score are available.
if(randomizedMoves.length){
// First AI move
if(randomizedMoves.length === board.length){
bestMove = randomizedMoves[Math.floor(Math.random() * randomizedMoves.length)];
} else {
randomizedMoves = randomizedMoves.filter( m => m.score === bestMove.score);
bestMove = randomizedMoves[Math.floor(Math.random() * randomizedMoves.length)];
}
}
return bestMove;
}
// Score Heuristic Evaluation
evaluate() {
let score = 0;
score += this.evaluateLine(0, 1, 2); // row 1
score += this.evaluateLine(3, 4, 5); // row 2
score += this.evaluateLine(6, 7, 8); // row 3
score += this.evaluateLine(0, 3, 6); // col 1
score += this.evaluateLine(1, 4, 7); // col 2
score += this.evaluateLine(2, 5, 8); // col 3
score += this.evaluateLine(0, 4, 8); // diag.
score += this.evaluateLine(2, 4, 6); // alt. diag.
return score;
}
evaluateLine(a, b, c) {
let score = 0;
let cA = board[a];
let cB = board[b];
let cC = board[c];
// first cell
if (cA == OPPONENT) {
score = 1;
} else if (cA == PLAYER) {
score = -1;
}
// second cell
if (cB == OPPONENT) {
if (score == 1) {
score = 10;
} else if (score == -1) {
return 0;
} else {
score = 1;
}
} else if (cB == PLAYER) {
if (score == -1) {
score = -10;
} else if (score == 1) {
return 0;
} else {
score = -1;
}
}
// third cell
if (cC == OPPONENT) {
if (score > 0) {
score *= 10;
} else if (score < 0) {
return 0;
} else {
score = 1;
}
} else if (cC == PLAYER) {
if (score < 0) {
score *= 10;
} else if (score > 1) {
return 0;
} else {
score = -1;
}
}
return score;
}
}
class HumanPlayer {
constructor() {
this.name = 'You';
this.win = 0;
}
play() {
$message.textContent = 'Your turn!';
return new Promise((resolve) => {
let disposeFn = event($board, 'click', e => {
let target = e.target;
if (target.classList.contains('cell')) { // If we hit a cell
let idx = $cells.indexOf(target); // get the cell index.
if (getAvailableMoves().indexOf(idx) !== -1) { // must be available
disposeFn();
resolve(idx);
}
}
});
})
}
}
class AIPlayer {
constructor(difficulty = 2) {
this.difficulty = difficulty;
this.name = `${this._getRandomName()}(AI)`;
this.win = 0;
}
_getRandomName() {
return AIPlayer.names[Math.floor(Math.random() * (AIPlayer.names.length - 1))];
}
setBoard() {
this.ai = new AI(1);
}
play() {
$message.textContent = `${this.name}'s turn`;
return new Promise((res) => {
let randomTimer = Math.floor(Math.random() * 1000 + 500);
let move = this.ai.findBestMove();
setTimeout(() => res(move), randomTimer);
})
}
}
AIPlayer.names = ['Leanne', 'Ervin', 'Clementine', 'Patricia', 'Chelsey', 'Dennis', 'Kurtis',
'Nicholas', 'Alphonse', 'Marie', 'Edouard', 'Lucille', 'Julie', 'Bernard'
];
let player = null;
let opponent = null;
let startingPlayer = null;
let currentPlayer = null;
/**
* Game utils
*/
function emptyBoard() {
return [EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY];
}
function hasAvailableMove() {
return board.some(cell => cell === EMPTY);
}
function getAvailableMoves() {
return board.reduce((acc, current, idx) => {
current === EMPTY && acc.push(idx);
return acc;
}, []);
}
function hasWon(player) {
let pattern = board.reduce((acc, curr, i) => {
curr === player.symbol && (acc |= (1 << i));
return acc;
}, 0b000000000);
return winPatterns.some(winPattern => {
return (pattern & winPattern) == winPattern;
});
}
function getWinner() {
if (hasWon(player)) return player;
if (hasWon(opponent)) return opponent;
return null;
}
function clearBoard() {
board = emptyBoard();
$cells.forEach(cell => {
cell.classList.remove('cross');
cell.classList.remove('circle');
});
}
function updateBoard(idx, symbol) {
board[idx] = symbol;
$board.children[idx].classList.add(symbol === PLAYER ? 'cross' : 'circle');
}
function isOver() {
return hasWon(player) || hasWon(opponent) || !hasAvailableMove();
}
function declareTurnWinner() {
let winner = getWinner();
if (winner) {
winner.win++;
$message.textContent = `${winner.name} win!`;
$scores.children[winner.symbol].querySelectorAll('li')[winner.win - 1].classList.add('won');
if (player.win == 3) {
endState(player);
} else if (opponent.win == 3) {
endState(opponent);
} else {
nextTurn();
}
} else {
$message.textContent = `Draw!`;
nextTurn();
}
}
function nextTurn() {
$playBtn.textContent = 'Next turn';
$playBtn.classList.remove('hide');
let disposeEvent = event($playBtn, 'click', () => {
currentPlayer = startingPlayer;
$playBtn.classList.add('hide');
clearBoard();
disposeEvent();
takeTurn();
});
}
function getOpponent(which) {
return which === player ? opponent : player;
}
function takeTurn() {
return currentPlayer.play()
.then(move => {
updateBoard(move, currentPlayer.symbol);
currentPlayer = getOpponent(currentPlayer);
return isOver() ? declareTurnWinner() : takeTurn();
})
}
/**
* Events handling
*/
let events = [];
function event(target, type, handler) {
target.addEventListener(type, handler);
return function disposeEvent() {
target.removeEventListener(type, handler);
}
}
function removeEvents() {
events.forEach(disposeFn => disposeFn());
events = [];
}
/**
* Game States
*/
function initState() {
removeEvents();
$scores.classList.add('hide');
$diceRoll.classList.add('hide');
$playBtn.classList.remove('hide');
$playBtn.textContent = 'Click to start';
$message.textContent = 'Tic Tac Toe';
events.push(event($playBtn, 'click', playerSetup));
}
function dice() {
$playBtn.classList.add('hide');
document.body.classList.remove('playing');
setTimeout(() => {
$playBtn.textContent = 'Click to throw the dice';
$playBtn.classList.remove('hide');
}, 500);
let disposeEvent = event($playBtn, 'click', onDiceRoll);
function onDiceRoll() {
$playBtn.classList.add('hide');
$diceRoll.querySelector('.dice-rolling').textContent = 'The dices are rolling!';
let scoreA = Math.floor(Math.random() * 5) + 1;
let scoreB = Math.floor(Math.random() * 3) + 1; // Yes...cheating here, so player has more chance to start... :)
while (scoreA === scoreB) {
scoreA = Math.floor(Math.random() * 5) + 1;
scoreB = Math.floor(Math.random() * 3) + 1;
}
startingPlayer = scoreA > scoreB ? player : opponent;
currentPlayer = startingPlayer;
disposeEvent();
setTimeout(() => {
$diceRoll.querySelector('.dice-score').textContent = `You: ${scoreA} - ${opponent.name}: ${scoreB}.`;
$diceRoll.querySelector('.dice-result').textContent = `${startingPlayer.name} start!`;
$playBtn.textContent = 'Start';
$playBtn.classList.remove('hide');
events.push(event($playBtn, 'click', playingState));
}, 1000);
}
}
function playerSetup() {
removeEvents();
$scores.classList.add('hide');
$message.classList.add('hide');
$playBtn.classList.add('hide');
$board.classList.add('hide');
$diceRoll.classList.remove('hide');
$diceRoll.querySelector('.dice-rolling').textContent = '';
$diceRoll.querySelector('.dice-score').textContent = '';
$diceRoll.querySelector('.dice-result').textContent = '';
player = new HumanPlayer();
player.symbol = PLAYER;
opponent = new AIPlayer();
opponent.symbol = OPPONENT;
opponent.setBoard(board);
$diceRoll.querySelector('.opponent').textContent = `You are playing against ${opponent.name}`;
dice();
}
function playingState() {
removeEvents();
clearBoard();
Array.from($scores.querySelectorAll('li')).forEach(li => li.classList.remove('won'));
$board.classList.remove('hide');
$scores.classList.remove('hide');
$playBtn.classList.add('hide');
$diceRoll.classList.add('hide');
$message.classList.remove('hide');
$scores.children[PLAYER].querySelector('span').textContent = player.name;
$scores.children[OPPONENT].querySelector('span').textContent = opponent.name;
document.body.classList.add('playing');
takeTurn();
}
function endState(winner) {
removeEvents();
$message.textContent = `${winner.name} wins the game!`;
document.body.classList.remove('playing');
$playBtn.classList.remove('hide');
$playBtn.textContent = 'Try again!';
events.push(event($playBtn, 'click', playerSetup));
}
initState();
View Compiled
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.