<div id="root">
</div>
@import url('https://fonts.googleapis.com/css2?family=Lato&display=swap');
body,html {
height: 100%;
font-family: 'Lato', sans-serif;
}
#root {
display: grid;
place-content: center;
background-color: ghostwhite;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
}
.main-game {
display: grid;
grid-template-columns: repeat(3, 100px);
opacity: 1;
transition: opacity 1s;
& > * {
height: 100px;
border: 1px solid #666666;
&:nth-child(1), &:nth-child(2), &:nth-child(3) {
border-top: none;
}
&:nth-child(1), &:nth-child(4), &:nth-child(7) {
border-left: none;
}
&:nth-child(7), &:nth-child(8), &:nth-child(9) {
border-bottom: none;
}
&:nth-child(3), &:nth-child(6), &:nth-child(9) {
border-right: none;
}
}
& .quad-icon{
display: grid;
place-content: center;
width: 100%;
height: 100%;
& > img {
width: 90px;
height: 90px;
animation: moveLine .2s;
transition: all ease;
}
}
}
.hidden {
display: none;
visibility: hidden;
}
.fade {
opacity: 0;
}
.modal-wrapper {
position: fixed;
left: 0; right: 0; bottom: 0; top: 0;
display: grid;
place-content: center;
background-color: rgba(255,255,255,.8);
& button {
background-color: ghostwhite;
padding: 5px 10px;
height: 30px;
border: 1px solid #03a9f4;
border-radius: 4px;
color: #03a9f4;
}
& .modal-box {
min-width: 300px;
display: grid;
place-items: center;
& > img {
width: 100px;
height: auto;
}
&.tie {
grid-template-columns: 1fr 1fr;;
}
}
}
@keyframes moveLine {
0% { transform: scale(.3, .3) }
40% { transform: scale(1,1) }
70% { transform: scale(1.3, 1.3) }
100% { transform: scale(1,1) }
}
View Compiled
// images assets for the game
const exImg = 'https://res.cloudinary.com/dvv4qgnka/image/upload/v1612313581/ex.svg';
const circleImg = 'https://res.cloudinary.com/dvv4qgnka/image/upload/v1612313581/circle.svg';
/*------------------------STORAGE--------------------------------------------*/
// Create Storage initial state and handlers
window.storage = new Store({
turn: 'x', // your turn
quads: [0,0,0,0,0,0,0,0,0], // values in quads
winner: false // end game
}, {
'SET_QUAD_VALUE': (action, state) => {
let newQuads = setQuad(action.index, action.turn, state.quads);
state = Object.assign({}, state, {
quads: newQuads,
turn: state.turn === 'x' ? 'o' : 'x',
winner: getWinner(newQuads)
});
return { newState: state }
},
'IA_TURN': (action, state) => {
let newQuads = IATurn(state.quads, state.winner);
state = Object.assign({}, state, {
quads: newQuads,
turn: state.turn === 'x' ? 'o' : 'x',
winner: getWinner(newQuads)
});
return { newState: state }
},
'RESET_GAME': (_, state) => {
state = {
turn: 'x',
quads: [0,0,0,0,0,0,0,0,0],
winner: false
}
return { newState: state }
}
});
/*--------------------Components--------------------------------------------------*/
//Quad function component
const Quad = (props) => {
const state = window.storage.getState();
const quadValue = state.quads[props.index];
const isTaken = quadValue !== 0;
let currentValue = quadValue;
return Div({ className: 'quad-body', onclick: () => {
if(!isTaken) {
window.storage.dispatch({ type: 'SET_QUAD_VALUE', index: props.index, turn: state.turn })
setTimeout(() => {
window.storage.dispatch({ type: 'IA_TURN' })
}, 300);
}
} }, getQuadContent(isTaken, quadValue))
.onStoreEvent('SET_QUAD_VALUE', (state, that) => {
const quadValue = state.quads[props.index];
const isTaken = quadValue !== 0;
if(currentValue !== quadValue) {
that.innerHTML = '';
that.appendChild(getQuadContent(isTaken, quadValue))
currentValue = quadValue;
}
})
.onStoreEvent('IA_TURN', (state, that) => {
const quadValue = state.quads[props.index];
const isTaken = quadValue !== 0;
if(currentValue !== quadValue) {
that.innerHTML = '';
that.appendChild(getQuadContent(isTaken, quadValue))
currentValue = quadValue;
}
})
}
function getQuadContent(isTaken, quadValue) {
return isTaken
? Div({ className: 'quad-icon' }).Img({ src: quadValue === 'x' ? exImg : circleImg }).baseNode()
: ''
}
// winner modal function component
const WinnerModal = () => {
return Div({ className: "hidden"})
.onStoreEvent('SET_QUAD_VALUE', (state, that) => {
if(state.winner) {
that.innerHTML = '';
that.appendChild(getModalContent({ visible: state.winner }))
that.className = 'modal-wrapper';
}
})
.onStoreEvent('IA_TURN', (state, that) => {
if(state.winner) {
that.innerHTML = '';
console.log(state.winner)
that.appendChild(getModalContent({ visible: state.winner }));
that.className = 'modal-wrapper';
}
})
}
function getModalContent(props) {
console.log('symbol', props.visible.symbol);
return Div({className: props.visible.tie ? "modal-box tie" : "modal-box"}, () => {
let content = [];
if (props.visible.tie) content.push(Img({ src: exImg }));
content.push(
Img({src: props.visible.symbol === 'x' ? exImg : circleImg}),
H1({}, props.visible.tie ? 'Tie' : 'Winner'),
Button({ onclick:() => { window.storage.dispatch({type: 'RESET_GAME'}) } }, 'Restart Game')
);
return content;
})
}
/*---------------------DOM-----------------------------------------------------*/
// get Root container
const container = document.querySelector('#root')
container.onStoreEvent('RESET_GAME', () => {
init()
})
init();
// init empty game
function init() {
container.innerHTML=''
// create main game structure
const main = container.Div({ className: 'main-game' }, getQuads())
// create winner Modal
const modal = WinnerModal();
container.appendChild(modal);
}
/*--------------------Game Logic--------------------------------------*/
// get initial quads
function getQuads() {
const resp = [];
for(let i=0; i < 9; i++) {
resp.push(Quad({ index: i }))
}
return resp
}
/*
Check for winner all winner combination
*/
const getWinner = (quads) => {
// vertical stripe win
for(let i=0; i < 3; i++) {
if(quads[i] !== 0 && quads[i] === quads[i + 3] && quads[i + 3] === quads[i + 6]) {
return { cords:[i, i+3, i+6], symbol: quads[i]};
}
}
// horizontal stripe win
for(let i=0; i < 9; i = i + 3) {
if(quads[i] !== 0 && quads[i] === quads[i+1] && quads[i+1] === quads[i+2]) {
return {cords: [i, i+1, i+2], symbol: quads[i]};
}
}
// Diagonal left up
if(quads[0] !== 0 && quads[0] === quads[4] && quads[4] === quads[8]) {
return {cords: [0,4,8], symbol: quads[0]}
}
// Diagonal right up
if(quads[2] !== 0 && quads[2] === quads[4] && quads[4] === quads[6]) {
return {cords: [2, 4, 6], symbol: quads[2]}
}
// check tie
let isFull = true
for (let i = 0; i < 9; i++) {
if(quads[i] === 0) {
isFull = false
}
}
return isFull ? { tie:true } : false;
}
/*
set IA turn
*/
const IATurn = (quads, winner) => {
if(winner) return quads;
const pos = smarPos(quads);
return setQuad(pos, 'o', quads)
}
/*
get a position for the IA player
*/
function smarPos(quads) {
const random = Math.floor(Math.random() * (9 - 0)) + 0;
// map player moves and IA moves
const { playerPos, IAPos} = getPlayedPos(quads);
if(playerPos.length < 2) {
if (quads[random] === 0) {
return random
}
} else {
// get block play and win play
const block = getBlockOrWin(playerPos, quads);
const win = getBlockOrWin(IAPos, quads);
if (win !== undefined) {
// if there is a win route
return win
}
if(block !== undefined) {
// if there is a block route
return block;
}
if (quads[random] === 0) {
// just random
return random
}
}
// in case random is taken
return smarPos(quads);
}
/*
* Evaluate every win route for the human player or the IA player
* returns the coord that is left to win
*/
function getBlockOrWin(playerPos, quads) {
const strPs = playerPos.join();
// vertical stripe win
for(let i=0; i < 3; i++) {
const verticalMatch = strPs.match(new RegExp(`(${i}|${i + 3}|${i + 6})`,'g'));
if(verticalMatch!== null && verticalMatch.length > 1) {
const ret = evaluatePattern(verticalMatch, [i, i + 3,i + 6])
if(quads[ret] === 0) return ret;
}
}
// horizontal stripe win
for(let i=0; i < 9; i = i + 3) {
const horizontalMatch = strPs.match(new RegExp(`(${i}|${i + 1}|${i + 2})`,'g'));
if(horizontalMatch !== null && horizontalMatch.length > 1) {
const ret = evaluatePattern(horizontalMatch, [i, i + 1,i + 2])
if(quads[ret] === 0) return ret;
}
}
// Diagonal left up
const diagLeftMatch = strPs.match(new RegExp(`(0|4|8)`,'g'));
if(diagLeftMatch !== null && diagLeftMatch.length > 1) {
const ret = evaluatePattern(diagLeftMatch, [0,4,8])
if(quads[ret] === 0) return ret;
}
// Diagonal right up
const diagRightMatch = strPs.match(new RegExp(`(2|4|6)`,'g'));
if(diagRightMatch !== null && diagRightMatch.length > 1) {
const ret = evaluatePattern(diagRightMatch, [2,4,6])
if(quads[ret] === 0) return ret;
}
}
/*
* Util Function to evaluate two pattern to get the coord that is missing
*/
function evaluatePattern(pat, win) {
const str = pat.join();
let ret = 0
for (let i = 0; i < 3; i++) {
if(str.match(new RegExp(`(${win[i]})`)) === null) {
ret = win[i]
}
}
return ret
}
/*
* Map all the played positions
*/
function getPlayedPos(quads) {
const playerPos = [];
const IAPos = [];
for (let i = 0; i < 9; i++) {
if(quads[i] === 'x') {
playerPos.push(i);
} else if(quads[i] === 'o') {
IAPos.push(i)
}
}
return { playerPos, IAPos };
}
/*
* set a value to the quad
*/
const setQuad = (index, turn, quads) => {
quads[index] = turn;
return quads;
}
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.