TicTacToe

Minimal and easily extendable tic-tac-toe in a responsive viewport

by Federico Tibaldo

HTML

<div id="announcement"></div>
<div class="game-board-container">
  <svg viewBox="0 0 17 17" version="1.1" xmlns="http://www.w3.org/2000/svg">
		<g class="game-board">
			<line x1="1" y1="6" x2="16" y2="6"/>
			<line x1="1" y1="11" x2="16" y2="11"/>
			<line x1="6" y1="1" x2="6" y2="16"/>
			<line x1="11" y1="1" x2="11" y2="16"/>
		</g>
	</svg>
	<div id="win-stroke"></div>
  <div class="game-board">
    <div class="game-box"></div>
    <div class="game-box"></div>
    <div class="game-box"></div>
    <div class="game-box"></div>
    <div class="game-box"></div>
    <div class="game-box"></div>
    <div class="game-box"></div>
    <div class="game-box"></div>
    <div class="game-box"></div>
  </div>
</div>

SCSS

* {
  box-sizing: inherit;
}

html {
  box-sizing: border-box;
}

body {
  display: flex;
  margin: 0;
  align-content: center;
  justify-content: center;
}

#announcement {
	position: fixed;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	background-color: white;
	z-index: 10;
	opacity: 0;
	visibility: hidden;
	transition: opacity 0.5s ease-in 0.15s;
	&.visible {
		visibility: visible;
		opacity: 0.8;
	}
}

.game-board-container {
  position: relative;
  width: 50%;
}

.game-board-container:after {
  content: "";
  display: block;
  padding-bottom: 100%;
}

.game-board-container>* {
  position: absolute;
  width: 100%;
  height: 100%;
}

.game-board {
  display: flex;
  flex-wrap: wrap;
  padding: 4%;
  .game-box {
    width: 33%;
    height: 33%;
  }
}

/*************************/
/* SVG                   */
/*************************/

/* General */

line {
	stroke-width: 1;
	stroke-linecap: round;
}

circle {
	fill: none;
}

/* Colors */

.x {
	stroke: #f44336;
}

.o {
	stroke: #2196F3;
}

/* Groups */

.game-board {
 	stroke: #607D8B;
}

.x-group {
	stroke-dasharray: 6;
	stroke-dashoffset: 7;
}

.o-group {
	transform: rotate(-150deg);
	transform-origin: center;
	stroke-dasharray: 13;
	stroke-dashoffset: 13.25;
}

.win-stroke {
	stroke-dasharray: 15.5;
	stroke-dashoffset: 15.75;
}

.win-stroke-diagonal {
	stroke-dasharray: 21.5;
	stroke-dashoffset: 21.75;
}

/* Animation */

.dash {
  animation: dash .15s linear forwards;
}

.dash-long {
	animation: dash .25s ease-in forwards;
}

.dash-delay {
  animation: dash .15s linear forwards .15s;
}

.dash-delay-long {
  animation: dash .2s linear forwards .5s;
}

@keyframes dash {
  to {
    stroke-dashoffset: 0;
  }
}

JavaScript

var tictactoe = tictactoe || (function() {
	var turn = 1
	var _winStrokeContainer
	var _announcementContainer
	var _boxes = []
	var gameBoard = []
  /**
   * Displays an X in the given element
   */
	function printX(el) {
		el.innerHTML = '<svg viewBox="0 0 7 7" version="1.1" xmlns="http://www.w3.org/2000/svg"><g class="x-group"><line class="x dash" x1="2" y1="2" x2="5" y2="5"/><line class="x dash-delay" x1="2" y1="5" x2="5" y2="2"/></g></svg>'
	}
  /**
   * Displays an O in the given element
   */
	function printO(el) {
		el.innerHTML = '<svg viewBox="0 0 7 7" version="1.1" xmlns="http://www.w3.org/2000/svg"><g class="o-group"><circle class="o dash-long" cx="3.5" cy="3.5" r="2"/></g></svg>'
	}
	
	function announce(el, msg) {
		let p = document.createElement('p')
		p.textContent = msg
		el.appendChild(p)
		el.classList.toggle('visible')
	}
	
	function strokeRow(index) {
		let y = 3.25 * (index + 1) + 2 * index
		let color = getTurn()
		_winStrokeContainer.innerHTML = `<svg viewBox="0 0 17 17" version="1.1" xmlns="http://www.w3.org/2000/svg"><line class="win-stroke ${color} dash-delay-long" x1="1" y1="${y}" x2="16" y2="${y}"/></svg>`
	}
	
	function strokeColumn(index) {
		let x = 3.25 * (index + 1) + 2 * index
		let color = getTurn()
		_winStrokeContainer.innerHTML = `<svg viewBox="0 0 17 17" version="1.1" xmlns="http://www.w3.org/2000/svg"><g class="win-stroke"><line class="${color} dash-delay-long" x1="${x}" y1="1" x2="${x}" y2="16"/></g></svg>`
	}
	
	function strokeDiagonal(index) {
		let f = (x) => (x - 8.5) * index + 8.5
		let color = getTurn()
		_winStrokeContainer.innerHTML = `<svg viewBox="0 0 17 17" version="1.1" xmlns="http://www.w3.org/2000/svg"><g class="win-stroke-diagonal"><line class="${color} dash-delay-long" x1="1" y1="${f(1)}" x2="16" y2="${f(16)}"/></g></svg>`
	}
  
	function changeTurn() {
		turn ^= 1
	}
	function getTurn() {
		return turn ? 'x' : 'o'
	}
	function update(index) {
		if (legal(index)) {
			gameBoard[index] = getTurn()
			let el =...