<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;
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.