<div id="tic_tac_toe">
<div class="tiles">
<button v-for="button, index in buttons"
v-bind:class="{
'tile' : true,
'active' : button.active,
'match': button.match,
'disabled' : !play
}"
v-bind:data-tile="index"
v-on:click.self="tileClick">
<div class="turn turn-0"
v-if="button.player === 0"></div>
<svg viewBox="0 0 30 30"
class="turn turn-1"
v-if="button.player === 1">
<circle fill="transparent"
stroke="#d10827"
stroke-width="3"
r="10" cx="15" cy="15" />
</svg>
</button>
</div>
<div class="modal-window"
v-bind:class="{ 'shown' : showDialog }">
<!--STEP 1-->
<div class="dialog-box" v-if="dialogStep === 0">
<div class="dialog-content">
<p class="text-bold">{{ GameTitle }}</p>
<p>Welcome to the example TicTacToe game I made.</p>
<p>You may play this with your friend, or versus the computer.</p>
</div>
<div class="dialog-content">
<button class="btn tile"
v-on:click="proceedToNext()">
Proceed to Game</button>
</div>
</div>
<!--STEP 2-->
<div class="dialog-box" v-if="dialogStep === 1">
<div class="dialog-content">
<p class="text-bold">{{ GameTitle }}</p>
<p>Choose your opponent to play with.</p>
</div>
<div class="dialog-content">
<button class="btn tile"
v-on:click="chooseOpponent(false)">
Friend</button>
<button class="btn tile"
v-on:click="chooseOpponent(true)">
Computer</button>
</div>
</div>
<!--STEP 3-->
<div class="dialog-box" v-if="dialogStep === 2">
<div class="dialog-content">
<p class="text-bold">{{ GameTitle }}</p>
<p>Choose your player.</p>
</div>
<div class="dialog-content">
<button class="btn btn-turn tile"
v-on:click="choosePlayer(0)">
<div class="turn turn-0"></div>
</button>
<button class="btn btn-turn tile"
v-on:click="choosePlayer(1)">
<svg viewBox="0 0 30 30"
class="turn turn-1">
<circle fill="transparent"
stroke="#d10827"
stroke-width="3"
r="10" cx="15" cy="15" />
</svg>
</button>
</div>
</div>
<!--STEP 4-->
<div class="dialog-box" v-if="dialogStep === 3">
<div class="dialog-content">
<p class="text-bold">{{ GameTitle }}</p>
<p>Choose the computer's difficulty.</p>
</div>
<div class="dialog-content">
</div>
<div class="dialog-content">
<button class="btn tile"
v-on:click="chooseDifficulty(0)">
Easy</button>
<button class="btn tile"
v-on:click="chooseDifficulty(1)">
Medium</button>
<button class="btn tile"
v-on:click="chooseDifficulty(2)">
Hard</button>
</div>
</div>
<!--STEP 5/GAME END-->
<div class="dialog-box" v-if="dialogStep === 4">
<div class="dialog-content">
<p class="text-bold">Game ended!</p>
<p v-if="winningPlayer !== null">
Player '{{ winningPlayer }}' won the game! Would you like to play again?
</p>
<p v-else="">
The match is a draw! Would you like to play again?
</p>
</div>
<div class="dialog-content">
<button class="btn tile"
v-on:click="initialize()">
Start New</button>
<button class="btn tile"
v-on:click="playAgain()">
Play Again</button>
</div>
</div>
</div>
</div>
@import url('https://fonts.googleapis.com/css?family=Comfortaa&display=swap');
* {
font-family: 'Comfortaa', cursive;
}
#tic_tac_toe {
background-image: linear-gradient(#1fa5ff 0% 30%, #57f9ff 70% 100%);
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.tiles {
--cell-size: 15vmin;
--shadow-opacity: 0.4;
background-color: rgba(#000000, 0.15);
padding: 10vmin;
border-radius: 5vmin;
display: grid;
grid-template-rows: repeat(3, var(--cell-size));
grid-template-columns: repeat(3, var(--cell-size));
grid-gap: 1.75vmin;
box-shadow: 0 1.5vmin rgba(#000000, var(--shadow-opacity));
}
.tile {
--shadow-size: 0.75vmin;
--transition-time: 150ms;
background-color: #ffc417;
background-image: linear-gradient(to bottom right, rgba(#ffffff, 0.4) 0 30%, rgba(#ffffff, 0) 70% 100%);
min-width: 0;
min-height: 0;
padding: 0;
margin: 0;
border: none;
border-radius: 2vmin;
outline: none;
box-shadow:
0 var(--shadow-size) rgba(#000000, var(--shadow-opacity)),
0 0 0 rgba(#000000, var(--shadow-opacity)) inset;
transition:
background-color var(--transition-time) ease-out,
box-shadow var(--transition-time) ease-out,
transform var(--transition-time) ease-out;
&:active:not(.disabled), &.active, &.disabled.active {
box-shadow:
0 0 rgba(#000000, var(--shadow-opacity)),
0 var(--shadow-size) calc(var(--shadow-size) * 2) rgba(#000000, var(--shadow-opacity)) inset;
transform: translateY(var(--shadow-size));
}
&.disabled {
background-color: #9f9f9f;
}
&.active {
background-color: #e2e2e2;
}
&.match {
background-color: #ffe11f;
}
}
.turn {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
vertical-align: top;
}
.turn-0 {
--draw-time: 250ms;
position: relative;
&:before, &:after {
content: '';
background-color: #0049bf;
width: 12%;
height: 0;
position: absolute;
top: 22%;
transform-origin: top center;
}
&:before {
left: 15.5%;
transform: rotate(-45deg);
animation: turn-0 var(--draw-time) ease-out forwards;
}
&:after {
right: 15.5%;
transform: rotate(45deg);
animation: turn-0 var(--draw-time) ease-out var(--draw-time) forwards;
}
}
@keyframes turn-0 {
0% { height: 0; }
100% { height: 80%; }
}
.turn-1 {
// circumference = 2 * PI * 10 (rounded to 10 thousandths)
transform: rotate(-90deg);
animation: turn-1 500ms ease-out forwards;
}
@keyframes turn-1 {
0% { stroke-dasharray: 0 62.8319; }
100% { stroke-dasharray: 62.8319 62.8319; }
}
.modal-window {
width: 100vw;
height: 100vh;
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
transform: scale(0);
transition: transform 250ms ease-out;
&.shown {
transform: scale(1);
}
}
.dialog-box {
font-size: 16px;
text-align: center;
background-image: linear-gradient(
to bottom right,
rgba(#dfdfdf, 0.90) 0% 30%,
rgba(#ffffff, 0.90) 70% 100%);
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
p {
padding: 0;
margin: 0 0 0.5em 0;
&:last-child {
margin: 0;
}
}
}
.dialog-content {
padding: 0.5em 0;
}
.btn {
--shadow-size: 0.75vmin;
--shadow-opacity: 0.5;
padding: 0.5em 0.8em;
&:active {
--shadow-opacity: 0.25;
}
}
.btn-turn {
width: 3em;
height: 3em;
padding: 0;
vertical-align: middle;
}
.text-bold {
font-weight: bold;
}
View Compiled
enum Player {
X = 0,
O = 1
}
enum Difficulty {
Easy = 0,
Medium = 1,
Hard = 2
}
enum Matches {
Two = 2,
One = 4
}
let app: object = new Vue({
el: "#tic_tac_toe",
data: {
buttons: [],
combinations: [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6]
],
GameTitle: "Tic Tac Toe V.3",
aiPlayer: null,
aiDifficulty: null,
turn: 0,
play: true,
match: null,
showDialog: true,
winningPlayer: null,
dialogStep: 0
},
methods: {
// Analyze the provided combination if the player gets
// 3 consecutives. For AI purposes, +2 of each was added
// if has 2 combinations. +4 if has 1 combination.
analyzeCombination(open: array): number {
open.sort();
let countX: number = 0;
let countO: number = 0;
for (let i: number in open) {
if (open[i] === Player.X) {
countX++;
} else if (open[i] === Player.O) {
countO++;
}
}
if (countX === 3) {
return Player.X;
} else if (countO === 3) {
return Player.O;
} else if (countX === 2) {
return Player.X + Matches.Two;
} else if (countO === 2) {
return Player.O + Matches.Two;
} else if (countX === 1) {
return Player.X + Matches.One;
} else if (countO === 1) {
return Player.O + Matches.One;
} else {
return null;
}
},
createOpened(combination: number): array {
let open: array = [];
for (let i: number = 0; i < combination.length; i++) {
let index: number = combination[i];
open.push(this.buttons[index].player);
}
return open;
},
// Find the combinations of the opened tiles
findCombination(): boolean {
for (let i: number = 0; i < this.combinations.length; i++) {
let combination: array = this.combinations[i];
let open: array = this.createOpened(combination);
let analysis: number = this.analyzeCombination(open);
if (analysis === Player.X || analysis === Player.O) {
this.play = false;
this.match = combination;
return true;
}
}
return false;
},
// Get the buttons that aren't yet opened
getFreeButtons(): array {
let freeButtons: array = [];
for (let i: number = 0; i < this.buttons.length; i++) {
if (this.buttons[i].active === false) {
freeButtons.push(i);
}
}
return freeButtons;
},
// Insert the possible turn of AI
robotInsertTurn(combination: array): number {
let turn: number = null;
for (let j: number = 0; j < combination.length; j++) {
if (this.buttons[combination[j]].player === null) {
turn = combination[j];
break;
}
}
return turn;
},
// Random move by AI
robotRandom(): number {
let freeButtons: array = this.getFreeButtons();
let random: number = Math.round(Math.random() * (freeButtons.length - 1));
if (random >= 0) {
return freeButtons[random];
} else {
return null;
}
},
// Find combination for AI move
robotCombinaton(selectedMatches: number): number {
let turn: number = null;
for (let i: number = 0; i < this.combinations.length; i++) {
let combination: array = this.combinations[i];
let open: array = this.createOpened(combination);
let analysis: number = this.analyzeCombination(open);
// First priority is 2 matches, then 1
if (analysis === selectedMatches) {
turn = this.robotInsertTurn(combination);
break;
}
}
return turn;
},
// Attack move by AI
robotAttack(tileIndex: number): number {
if (tileIndex === null) {
tileIndex = this.robotCombinaton(this.aiPlayer + Matches.Two);
}
if (tileIndex === null) {
tileIndex = this.robotCombinaton(this.aiPlayer + Matches.One);
}
return tileIndex;
},
// Defend move by AI
robotDefend(): number {
let tileIndex = null;
let human = this.aiPlayer === 0 ? 1 : 0;
tileIndex = this.robotCombinaton(human + Matches.Two);
return tileIndex;
},
// Move of the player (AI)
robotMove(): void {
if (this.turn === this.aiPlayer) {
let tileIndex: number = null;
switch (this.aiDifficulty) {
case Difficulty.Easy:
tileIndex = this.robotRandom();
break;
case Difficulty.Medium:
tileIndex = this.robotAttack(tileIndex);
if (tileIndex === null) {
tileIndex = this.robotRandom();
}
break;
case Difficulty.Hard:
tileIndex = this.robotCombinaton(this.aiPlayer + Matches.Two);
if (tileIndex === null) {
tileIndex = this.robotDefend();
}
if (tileIndex === null) {
tileIndex = this.robotCombinaton(this.aiPlayer + Matches.One);
}
if (tileIndex === null) {
tileIndex = this.robotRandom();
}
break;
}
if (tileIndex !== null) {
this.makeDecision(tileIndex);
}
}
},
// Move of the player (human)
humanMove(elm: object): void {
if (!elm.classList.contains("active")) {
let tileIndex: number = elm.dataset.tile;
this.makeDecision(tileIndex);
if (this.aiPlayer !== null) {
this.robotMove();
}
}
},
// Make decision on rendering the board
makeDecision(tileIndex: number): void {
if (tileIndex !== null) {
this.buttons[tileIndex].active = true;
this.buttons[tileIndex].player = this.turn;
}
if (this.findCombination() === true) {
this.decideGameWin();
return;
}
if (this.findAnotherTile() === false) {
this.showDialog = true;
return;
}
this.turn = this.turn === 0 ? 1 : 0;
},
// Changes the buttons color into match
changeButtonsToMatch(): void {
for (let i: number = 0; i < this.match.length; i++) {
let j: number = this.match[i];
this.buttons[j].match = true;
}
},
// Find another available tile
findAnotherTile(): boolean {
let hasTile = false;
for (let i: number = 0; i < this.buttons.length; i++) {
if (this.buttons[i].active === false) {
hasTile = true;
break;
}
}
return hasTile;
},
// Game win event
decideGameWin(): void {
this.changeButtonsToMatch();
this.showDialog = true;
this.winningPlayer = Player[this.turn];
},
// Creates the tic tac toe map
createMap(): void {
for (let i: number = 0; i < 9; i++) {
this.buttons.push({
player: null,
active: false,
match: false
});
}
},
// Event when the tile is clicked
tileClick(e: object): void {
if (this.play === true) {
if (this.turn === this.aiPlayer) {
return false;
}
let elm: object = e.target;
this.humanMove(elm);
}
},
// Proceed to next step of the dialog
proceedToNext(): void {
this.dialogStep += 1;
},
// Choose the opponent to play with
chooseOpponent(isRobot: boolean): void {
if (isRobot === true) {
this.proceedToNext();
} else {
this.dialogStep = 4;
this.showDialog = false;
}
},
// Choose your player (VS AI)
choosePlayer(chosenPlayer: number): void {
this.aiPlayer = chosenPlayer === 0 ? 1 : 0;
this.proceedToNext();
},
// Choose the AI difficulty
chooseDifficulty(difficulty: number): void {
this.aiDifficulty = difficulty;
this.dialogStep = 4;
this.showDialog = false;
if (this.aiPlayer === Player.X) {
this.robotMove();
}
},
// Start the game with same settings
playAgain(): void {
this.buttons = [];
this.turn = 0;
this.play = true;
this.match = null;
this.winningPlayer = null;
this.showDialog = false;
this.dialogStep = 4;
this.createMap();
if (this.aiPlayer === Player.X) {
this.robotMove();
}
},
// Initialize the game
initialize(): void {
this.aiPlayer = null;
this.aiDifficulty = null;
this.playAgain();
this.showDialog = true;
this.dialogStep = 0;
}
},
created(): void {
this.initialize();
}
});
View Compiled
This Pen doesn't use any external CSS resources.