Re-implement with nuxt-ts

This commit is contained in:
2019-02-20 01:48:54 +02:00
parent 6d29ea6316
commit 6ead5baeec
26 changed files with 8819 additions and 859 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.nuxt/
dist/
node_modules/

7
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
"files.exclude": {
"**/.nuxt/": true,
"**/dist/": true,
"**/node_modules/": true
}
}

View File

@@ -1,4 +0,0 @@
gardev.com
==========
The page sitting at gardev.com

69
assets/styles/main.scss Normal file
View File

@@ -0,0 +1,69 @@
/*
Colors: ;
Light Blue: #19C4FF
Dark Blue: #0085B2
Light Orange: #FF8F19
Dark Orange: #B26009
Light Gray: #f5f5f5
*/
html, body {
margin: 0; padding: 0;
font-family: "Verdana", sans-serif;
font-size: 100%;
background: #f5f5f5;
color: #333;
}
main {
position: relative;
width: 400px;
margin: 0 auto;
text-align: center;
}
h1 {
margin-top: 3em;
margin-bottom: 0;
font-size: 2em;
color: #0085B2;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.75);
}
p {
font-size: 0.8em;
line-height: 1.5em;
}
input {
-webkit-appearance: none;
}
button:focus,
input[type="button"] {
outline: 0 !important;
}
@media only screen
and (min-device-width : 320px)
and (max-device-width : 480px) {
main {
width: 320px;
}
h1 {
margin-top: 0.55em;
}
}
@media (max-width: 799px) {
main {
width: 320px;
}
h1 {
margin-top: 0.55em;
}
}

View File

@@ -0,0 +1,37 @@
<template lang="pug">
.board
Cell(
v-for="(cellState, index) of cellStates"
:key="index"
:state="cellState"
:disabled="finished"
@select="$emit('place', index)")
</template>
<script lang="ts">
import { Component, Vue, Prop } from 'vue-property-decorator';
import Cell from './Cell/Cell.vue';
import { CellState } from '~/plugins/tic-tac-toe/cell-state';
@Component({ components: { Cell } })
export default class Board extends Vue {
@Prop({required: true})
cellStates: CellState[];
@Prop({ required: false, default: false })
finished: boolean;
}
</script>
<style lang="scss">
.board {
display: flex;
flex-wrap: wrap;
margin: 0 auto;
width: 210px;
height: 210px;
background: #fff;
border-left: 1px solid #ccc;
border-top: 1px solid #ccc;
}
</style>

View File

@@ -0,0 +1,60 @@
<template lang="pug">
input.cell(:disabled="disabled" type="button" @click="$emit('select')" :class="classNames")
</template>
<script lang="ts">
import { Component, Vue, Prop } from 'vue-property-decorator'
import { CellState } from '~/plugins/tic-tac-toe/cell-state';
@Component
export default class Cell extends Vue {
@Prop({ required: false, default: null })
state: CellState;
@Prop({ required: false, default: false })
disabled: boolean;
get classNames() {
return {
placed: this.state !== null,
x: this.state === 'x',
o: this.state === 'o'
}
}
}
</script>
<style lang="scss">
.cell {
width: 70px;
height: 70px;
background: #fff;
border-top: 0;
border-left: 0;
border-right: 1px solid #ccc;
border-bottom: 1px solid #ccc;
&:not(.placed):not(:disabled):hover {
cursor: pointer;
background: #0085B2;
}
&:not(.placed):not(:disabled):active {
box-shadow: inset 0px 0px 83px 0px rgba(0,0,0,0.38);
}
&.placed {
background-repeat: no-repeat;
background-position: center center;
}
&.x {
background-image: url(data:image/png;base64,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);
}
&.o {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAtCAYAAADsvzj/AAAABmJLR0QA/wCPABm9JYiOAAAACXBIWXMAAEbJAABGyQGVdoTuAAAAB3RJTUUH3gsdFQ8omYO8AgAABQdJREFUaN7dmltoHFUYx38nqWlj0qatLTqmYgxFW6XipTVoSvIQLxQUH6Kh8UGLCk37ID0KakVBVOrdERRUUESi1ktLRa1WQRQhYou1SmqCumprtMc2TauRZtNtm/Fhv8gynLM7k90Zwe9l4ZyZOfOf813+3/+sIiELfG8GcBqwALgMuAloBk4CxoFvgCeBbUqbbLnrqQq+uALmAxfKiy8FFsrYTGCa5bZR4B3gCWC30ib4z4AEvlcHLAOuBTrk5WtjPuYX4GGgV2kzniqQwPcagBXiMq3y1cuxLPAi8KDSZjhxIIHvTQeuAm4DlgPTLZeNA38C+4FhYETipdXhYv8+HtgMaKXNb4kBCXxvIXAXsBKoL1h8FPgZ6JcgHgD2CogjQA6YAzwE3AzUlFhqK9ATB4yKEchXAo8AFxRMfQq8DXwNZIDDSpuJEvG0BrgbOKXEsm8Ba5U2IxUBEvheFXAD8Li4x6TtArqUNpmYu1otrvkAcHEJN/OB9UqbXKnnVkVY+3rg6RCIg8C9cUEAKG1OKG0+ADqB54C/i3zk1fIRKQtI4HstwKMhNzgueX9bWQVMm73AOsl6ux2X1QHrA99bPGXXkvT6KnB1aGoLsEppM1rBYtoqazU5LnlJ4iU3lR3pEl8utCFgQyVByO70AfcDY45LrgPaYrtW4HuNwFrhRZM2ATyrtPkqIXr2JvC6Y64B6Al8rzbujqwAloTGdgG9SZFMoSZPAT85LukQKhQNSOB7MyVTVYemXlPaGBI0pc2g0BQbeZwNdElNi7Qj54WKHsA+4CPSsTeAH4rsyulRgbQD80JjGaEciZvSZg+wyTHdDLREBbLMMj4OnCA9ew84ZBmvEeIZCcgiy1gjMCtFIP3ATsfcRcLZSgI51cF7grRQKG3GgM8d003SdZYEUm8ZOybUJE370lEg5wJeFCC2xqfW0UAlaRnggGX8ZJvX2IDkHJV1VspAhoFfHR96dhQgo45i1JQykCzwexFWXBLIkMO1LkkThXSaLiYxEQXIgOPmy4Xap2mupisXBUifZClboVyeMpCjlrHjtmJpA7JdZBxbtlgT+F6aQT/NITUdiALke+ALx4OvECkoLauzjI3YYqfKEmRHpZ3NObjOPYHvXZo0AqHrNpaxx+YxrsbqY2CHY+5MwA98b1HCWGaQV/KxtBTZSECUNoeAF8QfbdYCPB/43jkJApkHnGUZ/8MmAhYTH7aQl/xd1g70JuhmZ9s4FTAYS0VR2hwBNrhuLEjJGwPfu6WYMDBFa5NMWWgHXfS+qkR17QfudJC3wph5RlztfFdPHTPQ50iGtDHiwdhAxLYCd5QAUwvcCLwL3Bf4XnOZgNrJn3yFudcrrmO6OGp8J/CYIwDDTVhGdKrNwEAUEbpgrXrRt64JTW0EbpWma2pAChZZSv6Mo4PiBzaTtl86vfelyA5JnSq2Rjd5ibQw5r4FukUuomwgstBcoBvoARZb9C8c/MjIC22X34z0HFmpGY2S1m8P6QbfAatFVqViQAoAnUFekVwpykZNjNtzwGGJu7/Inz82kj/Vqi5w0T5gndJmZ0naX4EM0wCsArRksErYCPAy4Ctt9kXqXyrIi84lf9bRKYCqYz5mQmLqQ4mRHUqbyIJHxf4wIICqpCVukzqwRFymnryyr0LKzJjEzgDwGfAJ8KPS5ljsjjJB9jopEiwQqjFfaHkgDdOw7MAQMDKVl/9f2j8B3XOZyUnYJQAAAABJRU5ErkJggg==);
}
}
</style>

View File

@@ -0,0 +1,276 @@
<template lang="pug">
.tic-tac-toe
fieldset.difficulty
label(:class="{ selected: difficulty === 'easy'}")
input#easy(type="radio" v-model="difficulty" name="difficulty" value="easy")
| easy
label(:class="{ selected: difficulty === 'normal'}")
input#normal(type="radio" v-model="difficulty" name="difficulty" value="normal")
| normal
label(:class="{ selected: difficulty === 'hard'}")
input#hard(type="radio" v-model="difficulty" name="difficulty" value="hard")
| hard
Board(:cellStates="cellStates" :finished="finished" @place="place")
button.new-game(@click="newGame") new game
p.result {{ lastResultString }}
.stats
h2 stats
table
tr
td wins
td.wins.count {{ wins }}
tr
td draws
td.draws.count {{ draws }}
tr
td losses
td.losses.count {{ losses }}
</template>
<script lang="ts">
import { Component, Vue, Watch } from 'vue-property-decorator';
import Board from './Board/Board.vue';
import Game from '~/plugins/tic-tac-toe/game';
import { AI } from '~/plugins/tic-tac-toe/ai';
import Player from '~/plugins/tic-tac-toe/player';
import { CellState } from '~/plugins/tic-tac-toe/cell-state';
@Component({ components: { Board }})
export default class TicTacToe extends Vue {
wins: number = 0;
draws: number = 0;
losses: number = 0;
lastResultString: string = '';
cellStates: CellState[] = [];
finished: boolean = false;
difficulty: AI.Difficulty = 'normal';
private game: Game;
private ai: AI;
@Watch('difficulty', { immediate: true})
onDifficultyChange(newDifficulty: AI.Difficulty) {
if (this.ai) {
this.ai.difficulty = newDifficulty;
}
}
created() {
this.newGame();
}
newGame() {
this.game = new Game(Player.HUMAN);
this.ai = new AI(this.game, this.difficulty);
this.cellStates = this.game.copyBoard();
this.finished = false;
this.lastResultString = '';
}
place(index: number) {
const coords = this.game.toCoords(index);
if (this.game.isFinished() || !this.game.canPlaceAt(coords[0], coords[1])) {
return;
}
this.game.makeMove(coords[0], coords[1]);
if (!this.game.isFinished()) {
this.ai.move();
}
this.cellStates = this.game.copyBoard();
if (this.game.isFinished()) {
this.finishGame();
}
}
private finishGame() {
this.finished = true;
switch(this.game.winner) {
case Player.HUMAN:
this.wins++;
this.lastResultString = 'You win! Congratulations!';
break;
case Player.AI:
this.lastResultString = 'The AI won. Better luck next time!';
this.losses++;
break;
default:
this.lastResultString = 'It\'s a draw. Have another try!';
this.draws++;
}
}
}
</script>
<style lang="scss">
.new-game {
cursor: pointer;
padding: 10px 20px;
margin-top: 10px;
background: #fff;
color: #0085B2;
text-decoration: none;
border: 1px solid #0085B2;
border-radius: 4px;
&:hover {
background: #0085B2;
color: #fff;
}
&:active {
box-shadow: inset 0px 0px 83px 0px rgba(0,0,0,0.38);
}
}
.difficulty {
position: relative;
margin: 40px auto 16px;
padding: 0;
border: 0;
label {
display: inline-block;
padding: 6px 12px;
margin: 0;
text-align: center;
font-size: 0.8em;
cursor: pointer;
background: #0085B2;
color: #fff;
border-bottom: 1px solid #00678a;
border-top: 1px solid #00678a;
&:first-of-type {
margin-left: 0;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
border: 1px solid #00678a;
}
&:last-of-type {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
border: 1px solid #00678a;
}
&:hover {
background: #00678a;
}
&:active {
box-shadow: inset 0px 0px 83px 0px rgba(0,0,0,0.38);
}
input {
display: none;
}
}
> .selected {
cursor: default;
background: #FF8F19;
border-color: #B26009 !important;
&:hover {
background: #FF8F19;
}
&:active {
box-shadow: none;
}
}
}
.stats {
position: absolute;
top: 210px;
left: 350px;
text-align: left;
> h2 {
display: inline-block;
width: 200px;
color: #0085B2;
font-weight: normal;
border-bottom: 1px solid #0085B2;
}
table {
position: relative;
left: 10px;
td:first-child {
text-align: right;
padding-right: 5px;
}
}
.count {
display: inline-block;
min-width: 15px;
padding: 2px 3px;
color: #fff;
text-align: center;
font-size: 0.8em;
border-radius: 2px;
&.wins {
background: #0085B2;
}
&.losses {
background: #FF8F19;
}
&.draws {
background: #666;
}
}
}
@media only screen
and (min-device-width : 320px)
and (max-device-width : 480px) {
.stats {
position: static;
text-align: center;
table {
width: 100px;
margin: 0 auto;
position: static;
td {
text-align: center;
}
}
}
}
@media (max-width: 799px) {
.stats {
position: static;
text-align: center;
table {
width: 100px;
margin: 0 auto;
position: static;
td {
text-align: center;
}
}
}
}
</style>

View File

@@ -1,97 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Georgi Gardev</title>
<link rel="stylesheet" href="stylesheets/style.css" />
<link rel="stylesheet" href="vendor/font-awesome/css/font-awesome.min.css">
<script src="javascripts/player.js"></script>
<script src="javascripts/game.js"></script>
<script src="javascripts/ai.js"></script>
<script src="javascripts/ui.js"></script>
<script src="javascripts/main.js"></script>
<meta name="author" content="Georgi Gardev" />
<meta name="owner" content="Georgi Gardev" />
<meta name="description" content="Personal Homepage of Georgi Gardev" />
<meta name="copyright" content="Georgi Gardev 2014" />
<meta name="robots" content="index, follow" />
<meta name="revisit-after" content="2 days" />
<meta name="GOOGLEBOT" content="index, follow, all" />
<meta name="audience" content="all" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#19C4FF">
</head>
<body>
<main>
<h1>Georgi Gardev</h1>
<p>
Hi! I'm currently working on this page,<br /> but you can play some Tic-Tac-Toe instead!
</p>
<p>
<a href="mailto:georgi@gardev.com" class="social" target="_blank" title="Send me an email!">
<i class="fa fa-envelope-square fa-3x"></i>
</a>
<a href="https://github.com/GeorgeSG" class="social" target="_blank" title="GitHub">
<i class="fa fa-github-square fa-3x"></i>
</a>
<a href="https://bitbucket.org/GeorgeSG" class="social" target="_blank" title="Bitbucket">
<i class="fa fa-bitbucket-square fa-3x"></i>
</a>
<a href="https://www.linkedin.com/profile/view?id=154844036" class="social" target="_blank" title="LinkedIn">
<i class="fa fa-linkedin-square fa-3x"></i>
</a>
<a href="http://steamcommunity.com/id/georgesg/" class="social" target="_blank" title="PC Master Race!">
<i class="fa fa-steam-square fa-3x"></i>
</a>
<a href="https://twitter.com/georgesg92" class="social" target="_blank" title="twitter">
<i class="fa fa-twitter-square fa-3x"></i>
</a>
<a href="http://www.last.fm/user/GeorgeSG" class="social" target="_blank" title="last.fm">
<i class="fa fa-lastfm-square fa-3x"></i>
</a>
</p>
<nav>
<fieldset id="difficulty">
<label for="easy">
<input type="radio" name="difficulty" id="easy">easy
</label><label for="normal" class="checked">
<input type="radio" name="difficulty" id="normal" checked="checked">normal
</label><label for="hard">
<input type="radio" name="difficulty" id="hard">hard
</label>
</fieldset>
</nav>
<div id="board">
<input type="button" data-x="0" data-y="0" class="box unplaced">
<input type="button" data-x="0" data-y="1" class="box unplaced">
<input type="button" data-x="0" data-y="2" class="box unplaced">
<input type="button" data-x="1" data-y="0" class="box unplaced">
<input type="button" data-x="1" data-y="1" class="box unplaced">
<input type="button" data-x="1" data-y="2" class="box unplaced">
<input type="button" data-x="2" data-y="0" class="box unplaced">
<input type="button" data-x="2" data-y="1" class="box unplaced">
<input type="button" data-x="2" data-y="2" class="box unplaced">
</div>
<button id="new-game">new game</button>
<p id="result"></p>
<div id="stats">
<h2>stats</h2>
<table>
<tr>
<td>wins</td><td id="wins-count" class="count">0</td>
</tr>
<tr>
<td>draws</td><td id="draws-count" class="count">0</td>
</tr>
<tr>
<td>losses</td><td id="losses-count" class="count">0</td>
</tr>
</table>
</div>
</main>
</body>
</html>

View File

@@ -1,124 +0,0 @@
var AI = function() {
this.INFINITY = 9;
this.difficulty = 'normal';
};
AI.prototype.randomInt = function(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
};
AI.prototype.chooseMove = function(current_game) {
switch (this.difficulty) {
case 'easy':
// Make a random choice
return this.randomChoice(current_game);
break;
case 'normal':
// Choose either randomly or with minimax alpha-beta pruning
var random = this.randomInt(0, 10);
if (random > 4) {
return this.alphabetaChoice(current_game);
} else {
return this.randomChoice(current_game);
}
break;
case 'hard':
// Choose with minimax alpha-beta pruning
return this.alphabetaChoice(current_game);
break;
}
};
AI.prototype.alphabetaChoice = function(current_game) {
var gameClone = current_game.cloneGame();
var result = this.alphabeta(gameClone, -this.INFINITY, this.INFINITY);
return result[1].lastMove;
};
AI.prototype.randomChoice = function(current_game) {
var gameClone = current_game.cloneGame();
var moves = this.getPossibleMoves(gameClone);
var index = this.randomInt(0, moves.length - 1);
return moves[index].lastMove;
};
AI.prototype.alphabeta = function(current_game, alpha, beta) {
if (current_game.isFinished()) {
var score = 0;
if (current_game.hasWinner()) {
if (current_game.winner == current_game.firstPlayer) {
score = 1;
} else {
score = -1;
}
}
score *= (current_game.remainingMoves() + 1);
return [score, current_game];
}
var moves = this.getPossibleMoves(current_game);
if (current_game.firstPlayer == current_game.currentPlayer) {
return this.maximize(alpha, beta, moves);
} else {
return this.minimize(alpha, beta, moves);
}
};
AI.prototype.maximize = function(alpha, beta, moves) {
var result = [alpha, moves[0]];
for (var i = 0; i < moves.length; i++) {
var move = moves[i];
var alphabeta = this.alphabeta(move, alpha, beta);
if (alpha < alphabeta[0]) {
alpha = alphabeta[0];
result = [alpha, move];
}
if (beta <= alpha) {
break;
}
}
return result;
};
AI.prototype.minimize = function(alpha, beta, moves) {
var result = [beta, moves[0]];
for (var i = 0; i < moves.length; i++) {
var move = moves[i];
var alphabeta = this.alphabeta(move, alpha, beta);
if (beta > alphabeta[0]) {
beta = alphabeta[0];
result = [beta, move];
}
if (beta <= alpha) {
break;
}
}
return result;
};
AI.prototype.getPossibleMoves = function(current_game) {
var moves = [];
for (var i = 0; i < 3; i++) {
for (var j = 0; j < 3; j++) {
if (current_game.canPlaceAt(i, j)) {
var move = current_game.cloneGame();
move.makeMove(i, j);
moves.push(move);
}
}
}
return moves;
};

View File

@@ -1,96 +0,0 @@
var Game = function(firstPlayer) {
this.board = [[0, 0, 0], [0, 0, 0], [0, 0, 0]];
this.firstPlayer = firstPlayer;
this.currentPlayer = firstPlayer;
this.lastMove = null;
this.winner = null;
};
Game.prototype.hasWinner = function() {
return this.winner != null;
};
Game.prototype.canPlaceAt = function(x, y) {
return this.board[x][y] == 0;
};
Game.prototype.makeMove = function(x, y) {
this.lastMove = [x, y];
this.board[x][y] = this.currentPlayer.token;
this.currentPlayer = this.nextPlayer();
};
Game.prototype.nextPlayer = function() {
return this.currentPlayer == Player.HUMAN ? Player.AI : Player.HUMAN;
};
Game.prototype.remainingMoves = function() {
var flattenned = Array.prototype.concat.apply([], this.board);
var filtered = flattenned.filter(function(element) {
return element == 0;
});
return filtered.length;
};
Game.prototype.isFinished = function() {
// Check Rows and Columns
for (var i = 0; i < 3; i++) {
if (this.board[i][0] == this.board[i][1]
&& this.board[i][1] == this.board[i][2]
&& this.board[i][0] != 0) {
this.setWinner();
return true;
}
if (this.board[0][i] == this.board[1][i]
&& this.board[1][i] == this.board[2][i]
&& this.board[0][i] != 0) {
this.setWinner();
return true;
}
}
if (this.board[1][1] == 0) {
return false;
}
// Check Main Diagonal
if (this.board[0][0] == this.board[1][1]
&& this.board[1][1] == this.board[2][2]) {
this.setWinner();
return true;
}
// Check Secondary Diagonal
if (this.board[0][2] == this.board[1][1]
&& this.board[1][1] == this.board[2][0]) {
this.setWinner();
return true;
}
if (this.remainingMoves() > 0) {
return false;
}
return true;
};
Game.prototype.setWinner = function() {
this.winner = this.nextPlayer();
};
Game.prototype.cloneGame = function() {
var clonedGame = new Game();
clonedGame.firstPlayer = this.firstPlayer;
clonedGame.currentPlayer = this.currentPlayer;
clonedGame.lastMove = this.lastMove;
clonedGame.board = this.board.map(function(row) {
return row.slice();
});
return clonedGame;
};

View File

@@ -1,9 +0,0 @@
var ui = new UI();
window.addEventListener("load", function() {
ui.init();
});
// This is needed in order to be able to remove the eventListener
function pickMove(e) {
ui.pickMove(e.target);
};

View File

@@ -1,4 +0,0 @@
var Player = {
HUMAN: { token: 'x' },
AI: { token: 'o' }
};

View File

@@ -1,205 +0,0 @@
var UI = function() {
this.game = new Game(Player.HUMAN);
this.ai = new AI();
this.unplacedStyle = "unplaced";
this.boxes = [];
this.result = null;
this.difficultyLevels = [];
this.difficultyInputs = [];
this.winsCount = null;
this.drawsCount = null;
this.lossesCount = null;
};
UI.prototype.getBoxes = function() {
if (this.boxes.length == 0) {
this.boxes = document.getElementsByClassName("box");
}
return this.boxes;
};
UI.prototype.getResult = function() {
if (this.result == null) {
this.result = document.getElementById("result");
}
return this.result;
};
UI.prototype.getDifficultyLabels = function() {
if (this.difficultyLevels.length == 0) {
this.difficultyLevels = document
.getElementById("difficulty")
.getElementsByTagName("label");
}
return this.difficultyLevels;
};
UI.prototype.getDifficultyInputs = function() {
if (this.difficultyInputs.length == 0) {
this.difficultyInputs = document
.getElementById("difficulty")
.getElementsByTagName("input");
}
return this.difficultyInputs;
};
UI.prototype.getWinsCount = function() {
if (this.winsCount == null) {
this.winsCount = document.getElementById("wins-count");
}
return this.winsCount;
};
UI.prototype.getDrawsCount = function() {
if (this.drawsCount == null) {
this.drawsCount = document.getElementById("draws-count");
}
return this.drawsCount;
};
UI.prototype.getLossesCount = function() {
if (this.lossesCount == null) {
this.lossesCount = document.getElementById("losses-count");
}
return this.lossesCount;
};
UI.prototype.increaseWins = function() {
var wins = this.getWinsCount();
var winsCount = parseInt(wins.innerHTML);
wins.innerHTML = winsCount + 1;
};
UI.prototype.increaseDraws = function() {
var draws = this.getDrawsCount();
var drawsCount = parseInt(draws.innerHTML);
draws.innerHTML = drawsCount + 1;
};
UI.prototype.increaseLosses = function() {
var losses = this.getLossesCount();
var lossesCount = parseInt(losses.innerHTML);
losses.innerHTML = lossesCount + 1;
};
UI.prototype.init = function() {
var self = this;
var boxes = this.getBoxes();
var new_game_button = document.getElementById("new-game");
var difficulties = this.getDifficultyInputs();
new_game_button.addEventListener("click", function() {
self.resetGame();
});
for (var i = 0; i < boxes.length; i++) {
boxes[i].addEventListener("click", pickMove);
}
for (var i = 0; i < difficulties.length; i++) {
difficulties[i].addEventListener("click", function(e) {
self.pickDifficulty(e.target);
});
}
};
UI.prototype.aiMove = function() {
var boxes = this.getBoxes();
var aiMove = this.ai.chooseMove(this.game);
var x = aiMove[0];
var y = aiMove[1];
this.game.makeMove(x, y);
var boxIndex = 3 * x + y;
boxes[boxIndex].classList.remove(this.unplacedStyle);
boxes[boxIndex].classList.add(Player.AI.token);
boxes[boxIndex].removeEventListener("click", pickMove);
};
UI.prototype.finishGame = function() {
var boxes = this.getBoxes();
var result = this.getResult();
for (var i = 0; i < boxes.length; i++) {
boxes[i].classList.remove(this.unplacedStyle);
boxes[i].removeEventListener("click", pickMove);
}
if (this.game.winner == Player.HUMAN) {
result.innerHTML = "You win! Congratulations!";
this.increaseWins();
} else if (this.game.winner == Player.AI) {
result.innerHTML = "The AI won. Better luck next time!";
this.increaseLosses();
} else {
result.innerHTML = "It's a draw. Have another try!";
this.increaseDraws();
}
};
UI.prototype.resetGame = function() {
var boxes = this.getBoxes();
var result = this.getResult();
this.game = new Game(Player.HUMAN);
result.innerHTML = null;
for (var i = 0; i < boxes.length; i++) {
boxes[i].addEventListener("click", pickMove);
boxes[i].classList.remove(Player.HUMAN.token);
boxes[i].classList.remove(Player.AI.token);
boxes[i].classList.add(this.unplacedStyle);
}
};
UI.prototype.pickDifficulty = function(difficulty_object) {
var labels = this.getDifficultyLabels();
for (var i = 0; i < labels.length; i++) {
var label = labels[i];
if (label.getAttribute('for') == difficulty_object.id) {
label.classList.add("checked");
} else {
label.classList.remove("checked");
}
}
this.ai.difficulty = difficulty_object.id;
};
UI.prototype.pickMove = function(box_object) {
if (this.game.isFinished()) {
return;
}
var x = parseInt(box_object.getAttribute('data-x'));
var y = parseInt(box_object.getAttribute('data-y'));
box_object.classList.add(this.game.currentPlayer.token);
this.game.makeMove(x, y);
box_object.classList.remove(this.unplacedStyle);
box_object.removeEventListener("click", pickMove);
if (!this.game.isFinished()) {
this.aiMove();
}
if (this.game.isFinished()) {
this.finishGame();
}
};

3
layouts/default.vue Normal file
View File

@@ -0,0 +1,3 @@
<template lang="pug">
nuxt
</template>

36
nuxt.config.ts Normal file
View File

@@ -0,0 +1,36 @@
export default {
head: {
title: 'Georgi Gardev',
meta: [
{ charset: 'utf-8' },
{ name: 'author', content: 'Georgi Gardev' },
{ name: 'owner', content: 'Georgi Gardev' },
{ name: 'description', content: 'Personal Homepage of Georgi Gardev' },
{ name: 'copyright', content: 'Georgi Gardev 2014' },
{ name: 'robots', content: 'index, follow' },
{ name: 'revisit-after', content: '2 days' },
{ name: 'GOOGLEBOT', content: 'index, follow, all' },
{ name: 'audience', content: 'all' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' }
]
},
css: [{ lang: 'scss', src: '@/assets/styles/main.scss' }],
modules: [
[
'nuxt-fontawesome',
{
component: 'fa',
imports: [
{
set: '@fortawesome/free-solid-svg-icons',
icons: ['fas']
},
{
set: '@fortawesome/free-brands-svg-icons',
icons: ['fab']
}
]
}
]
]
};

30
package.json Normal file
View File

@@ -0,0 +1,30 @@
{
"name": "gardev.com",
"description": "Georgi Gardev's personal website. Hosted at gardev.com",
"version": "1.0.0",
"author": {
"name": "Georgi Gardev",
"email": "georgi@gardev.com",
"url": "http://gardev.com"
},
"devDependencies": {
"node-sass": "^4.11.0",
"pug": "^2.0.3",
"pug-plain-loader": "^1.0.0",
"sass-loader": "^7.1.0"
},
"dependencies": {
"@fortawesome/free-brands-svg-icons": "^5.7.2",
"@fortawesome/free-solid-svg-icons": "^5.7.2",
"nuxt-fontawesome": "^0.4.0",
"nuxt-ts": "latest",
"vue-property-decorator": "^7.3.0"
},
"license": "UNLICENSED",
"scripts": {
"dev": "nuxt-ts",
"build": "nuxt-ts build",
"start": "nuxt-ts start",
"generate": "nuxt-ts generate"
}
}

51
pages/index.vue Normal file
View File

@@ -0,0 +1,51 @@
<template lang="pug">
main
h1 Georgi Gardev
p
| Hi! I'm currently working on this page,
br
| but you can play some Tic-Tac-Toe instead!
p
a.social(href="mailto:georgi@gardev.com" target="_blank" title="Send me an email!")
fa.fa-3x(icon="envelope")
a.social(href="https://github.com/GeorgeSG" target="_blank" title="GitHub")
fa.fa-3x(:icon="['fab', 'github']")
a.social(href="https://bitbucket.org/GeorgeSG" target="_blank" title="Bitbucket")
fa.fa-3x(:icon="['fab', 'bitbucket']")
a.social(href="https://www.linkedin.com/profile/view?id=154844036" target="_blank" title="LinkedIn")
fa.fa-3x(:icon="['fab', 'linkedin']")
a.social(href="http://steamcommunity.com/id/georgesg/" target="_blank" title="PC Master Race!")
fa.fa-3x(:icon="['fab', 'steam']")
a.social(href="https://twitter.com/georgesg92" target="_blank" title="twitter")
fa.fa-3x(:icon="['fab', 'twitter']")
a.social(href="http://www.last.fm/user/GeorgeSG" target="_blank" title="last.fm")
fa.fa-3x(:icon="['fab', 'lastfm']")
TicTacToe
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import TicTacToe from '~/components/TicTacToe/TicTacToe.vue';
@Component({ components: { TicTacToe } })
export default class Home extends Vue {
}
</script>
<style lang="scss">
.social {
color: #bbb;
text-decoration: none;
padding: 0 4px;
border-radius: 5px;
&:hover {
color: #0085B2;
}
&:active {
color: #006385;
}
}
</style>

143
plugins/tic-tac-toe/ai.ts Normal file
View File

@@ -0,0 +1,143 @@
import Game from './game';
export class AI {
private static readonly INFINITY = 9;
constructor(private game: Game, public difficulty: AI.Difficulty = 'normal') {}
move(): void {
const move = this.chooseMove();
this.game.makeMove(move[0], move[1]);
}
private chooseMove(): number[] {
switch (this.difficulty) {
case 'easy':
return this.randomChoice(this.game);
case 'normal':
if (this.randomInt(0, 10) > 4) {
return this.alphabetaChoice(this.game);
} else {
return this.randomChoice(this.game);
}
case 'hard':
return this.alphabetaChoice(this.game);
}
}
private randomChoice(currentGame: Game) {
const gameClone = currentGame.clone();
const moves = this.getPossibleMoves(gameClone);
const index = this.randomInt(0, moves.length - 1);
return moves[index].lastMove;
}
private getPossibleMoves(game: Game): Game[] {
const moves = [];
let move: Game;
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (game.canPlaceAt(i, j)) {
move = game.clone();
move.makeMove(i, j);
moves.push(move);
}
}
}
return moves;
}
private alphabetaChoice(game: Game) {
const gameClone = game.clone();
const result = this.alphabeta(gameClone, -AI.INFINITY, AI.INFINITY);
return result.game.lastMove;
}
private alphabeta(game: Game, alpha: number, beta: number) {
if (game.isFinished()) {
let score = 0;
if (game.hasWinner()) {
if (game.winner === game.firstPlayer) {
score = 1;
} else {
score = -1;
}
}
score *= game.remainingMoves() + 1;
return {
score,
game
};
}
const moves = this.getPossibleMoves(game);
if (game.firstPlayer === game.currentPlayer) {
return this.maximize(alpha, beta, moves);
} else {
return this.minimize(alpha, beta, moves);
}
}
private maximize(alpha: number, beta: number, moves: Game[]) {
let result = {
score: alpha,
game: moves[0]
};
for (let i = 0; i < moves.length; i++) {
const move = moves[i];
const alphabeta = this.alphabeta(move, alpha, beta);
if (alpha < alphabeta.score) {
alpha = alphabeta.score;
result.score = alphabeta.score;
result.game = move;
}
if (beta <= alpha) {
break;
}
}
return result;
}
private minimize(alpha: number, beta: number, moves: Game[]) {
let result = {
score: beta,
game: moves[0]
};
for (let i = 0; i < moves.length; i++) {
const move = moves[i];
const alphabeta = this.alphabeta(move, alpha, beta);
if (beta > alphabeta.score) {
beta = alphabeta.score;
result.score = alphabeta.score;
result.game = move;
}
if (beta <= alpha) {
break;
}
}
return result;
}
private randomInt(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
}
export namespace AI {
export type Difficulty = 'easy' | 'normal' | 'hard';
}

View File

@@ -0,0 +1,4 @@
type Token = 'x' | 'o';
type CellState = null | Token;
export { CellState, Token };

View File

@@ -0,0 +1,3 @@
type Difficulty = 'easy' | 'medium' | 'hard';
export default Difficulty;

View File

@@ -0,0 +1,86 @@
import { CellState } from './cell-state';
import Player from './player';
export default class Game {
private static readonly WINNING_STATES = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6]
];
private board: CellState[] = [null, null, null, null, null, null, null, null, null];
public currentPlayer: Player;
public winner: Player | null = null;
public lastMove: number[];
constructor(public firstPlayer: Player, public players: Player[] = [Player.HUMAN, Player.AI]) {
this.currentPlayer = firstPlayer;
}
copyBoard(): CellState[] {
return this.board.slice();
}
toCoords(index: number): number[] {
return [Math.floor(index / 3), index % 3];
}
toIndex(x: number, y: number): number {
return x * 3 + y;
}
canPlaceAt(x: number, y: number): boolean {
return this.board[this.toIndex(x, y)] === null;
}
makeMove(x: number, y: number): void {
this.lastMove = [x, y];
this.board[this.toIndex(x, y)] = this.currentPlayer.token;
this.currentPlayer = this.nextPlayer();
}
nextPlayer(): Player {
return this.players.find((player) => player !== this.currentPlayer) as Player;
}
remainingMoves(): number {
return this.board.filter((e) => e === null).length;
}
isInWinningState(): boolean {
return Game.WINNING_STATES.some((winningState) => {
return (
this.board[winningState[0]] !== null &&
this.board[winningState[0]] === this.board[winningState[1]] &&
this.board[winningState[0]] === this.board[winningState[2]]
);
});
}
isFinished(): boolean {
if (this.isInWinningState()) {
this.winner = this.nextPlayer();
return true;
}
return this.remainingMoves() === 0;
}
hasWinner(): boolean {
return this.winner !== null;
}
clone(): Game {
const clonedGame = new Game(this.firstPlayer, this.players);
clonedGame.currentPlayer = this.currentPlayer;
clonedGame.lastMove = this.lastMove;
clonedGame.board = this.board.slice();
return clonedGame;
}
}

View File

@@ -0,0 +1,12 @@
import { Token } from './cell-state';
export default class Player {
public static readonly HUMAN = new Player('x');
public static readonly AI = new Player('o');
constructor(private _token: Token) {}
get token(): Token {
return this._token;
}
}

8
shims-svg.d.ts vendored Normal file
View File

@@ -0,0 +1,8 @@
declare module '*.svg' {
import Vue, { VNode, Component } from 'vue';
type Svg = Component<Vue>;
const content: Svg;
export default content;
}

View File

@@ -1,320 +0,0 @@
/*
Colors: ;
Light Blue: #19C4FF
Dark Blue: #0085B2
Light Orange: #FF8F19
Dark Orange: #B26009
Light Gray: #f5f5f5
*/
html, body {
margin: 0; padding: 0;
font-family: "Verdana", sans-serif;
font-size: 100%;
background: #f5f5f5;
color: #333;
}
main {
position: relative;
width: 400px;
margin: 0 auto;
text-align: center;
}
h1 {
margin-top: 3em;
margin-bottom: 0;
font-size: 2em;
color: #0085B2;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.75);
}
p {
font-size: 0.8em;
line-height: 1.5em;
}
nav {
margin: 2.5em 0 1em;
}
input {
-webkit-appearance: none;
}
button:focus,
input[type="button"] {
outline: 0 !important;
}
nav button,
#new-game {
cursor: pointer;
padding: 10px 20px;
background: #fff;
color: #0085B2;
text-decoration: none;
border: 1px solid #0085B2;
border-radius: 4px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
}
nav button:hover,
#new-game:hover {
background: #0085B2;
color: #fff;
}
nav button:active,
.box.unplaced:active,
#difficulty > label:active,
#new-game:active {
-webkit-box-shadow: inset 0px 0px 83px 0px rgba(0,0,0,0.38);
-moz-box-shadow: inset 0px 0px 83px 0px rgba(0,0,0,0.38);
box-shadow: inset 0px 0px 83px 0px rgba(0,0,0,0.38);
}
#new-game {
margin-top: 1em;
}
#difficulty {
position: relative;
margin: 1em auto 0;
padding: 0;
border: 0;
}
#difficulty input {
position: absolute;
clip: rect(0,0,0,0);
pointer-events: none;
}
#difficulty > label {
display: inline-block;
padding: 6px 12px;
margin: 0;
text-align: center;
font-size: 0.8em;
cursor: pointer;
background: #0085B2;
color: #fff;
border-bottom: 1px solid #00678a;
border-top: 1px solid #00678a;
}
#difficulty > label:first-of-type {
margin-left: 0;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
border: 1px solid #00678a;
}
#difficulty > label:last-of-type {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
border: 1px solid #00678a;
}
#difficulty > label:hover {
background: #00678a;
}
#difficulty > .checked {
cursor: default;
background: #FF8F19;
border-color: #B26009 !important;
}
#difficulty > .checked:hover {
background: #FF8F19;
}
#difficulty > .checked:active {
-webkit-box-shadow: none;
-moz-box-shadow: none
box-shadow: none
}
.social {
color: #bbb;
text-decoration: none;
}
.social:hover {
color: #0085B2;
}
.social:active {
color: #006385;
}
#board {
margin: 0 auto;
width: 210px;
height: 210px;
background: #fff;
border: 1px solid #ccc;
}
.box {
float: left;
width: 70px;
height: 70px;
background: #fff;
border-color: #ccc;
border-style: solid;
border-width: 0;
}
.box:nth-child(1),
.box:nth-child(2),
.box:nth-child(4),
.box:nth-child(5) {
border-right-width: 1px;
border-bottom-width: 1px;
}
.box:nth-child(3),
.box:nth-child(6) {
border-bottom-width: 1px;
}
.box:nth-child(7),
.box:nth-child(8) {
border-right-width: 1px;
}
.box.unplaced:hover {
cursor: pointer;
background: #0085B2;
}
.box.x,
.box.o {
background-repeat: no-repeat;
background-position: center center;
}
.box.x {
background-image: url(data:image/png;base64,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);
}
.box.o {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAtCAYAAADsvzj/AAAABmJLR0QA/wCPABm9JYiOAAAACXBIWXMAAEbJAABGyQGVdoTuAAAAB3RJTUUH3gsdFQ8omYO8AgAABQdJREFUaN7dmltoHFUYx38nqWlj0qatLTqmYgxFW6XipTVoSvIQLxQUH6Kh8UGLCk37ID0KakVBVOrdERRUUESi1ktLRa1WQRQhYou1SmqCumprtMc2TauRZtNtm/Fhv8gynLM7k90Zwe9l4ZyZOfOf813+3/+sIiELfG8GcBqwALgMuAloBk4CxoFvgCeBbUqbbLnrqQq+uALmAxfKiy8FFsrYTGCa5bZR4B3gCWC30ib4z4AEvlcHLAOuBTrk5WtjPuYX4GGgV2kzniqQwPcagBXiMq3y1cuxLPAi8KDSZjhxIIHvTQeuAm4DlgPTLZeNA38C+4FhYETipdXhYv8+HtgMaKXNb4kBCXxvIXAXsBKoL1h8FPgZ6JcgHgD2CogjQA6YAzwE3AzUlFhqK9ATB4yKEchXAo8AFxRMfQq8DXwNZIDDSpuJEvG0BrgbOKXEsm8Ba5U2IxUBEvheFXAD8Li4x6TtArqUNpmYu1otrvkAcHEJN/OB9UqbXKnnVkVY+3rg6RCIg8C9cUEAKG1OKG0+ADqB54C/i3zk1fIRKQtI4HstwKMhNzgueX9bWQVMm73AOsl6ux2X1QHrA99bPGXXkvT6KnB1aGoLsEppM1rBYtoqazU5LnlJ4iU3lR3pEl8utCFgQyVByO70AfcDY45LrgPaYrtW4HuNwFrhRZM2ATyrtPkqIXr2JvC6Y64B6Al8rzbujqwAloTGdgG9SZFMoSZPAT85LukQKhQNSOB7MyVTVYemXlPaGBI0pc2g0BQbeZwNdElNi7Qj54WKHsA+4CPSsTeAH4rsyulRgbQD80JjGaEciZvSZg+wyTHdDLREBbLMMj4OnCA9ew84ZBmvEeIZCcgiy1gjMCtFIP3ATsfcRcLZSgI51cF7grRQKG3GgM8d003SdZYEUm8ZOybUJE370lEg5wJeFCC2xqfW0UAlaRnggGX8ZJvX2IDkHJV1VspAhoFfHR96dhQgo45i1JQykCzwexFWXBLIkMO1LkkThXSaLiYxEQXIgOPmy4Xap2mupisXBUifZClboVyeMpCjlrHjtmJpA7JdZBxbtlgT+F6aQT/NITUdiALke+ALx4OvECkoLauzjI3YYqfKEmRHpZ3NObjOPYHvXZo0AqHrNpaxx+YxrsbqY2CHY+5MwA98b1HCWGaQV/KxtBTZSECUNoeAF8QfbdYCPB/43jkJApkHnGUZ/8MmAhYTH7aQl/xd1g70JuhmZ9s4FTAYS0VR2hwBNrhuLEjJGwPfu6WYMDBFa5NMWWgHXfS+qkR17QfudJC3wph5RlztfFdPHTPQ50iGtDHiwdhAxLYCd5QAUwvcCLwL3Bf4XnOZgNrJn3yFudcrrmO6OGp8J/CYIwDDTVhGdKrNwEAUEbpgrXrRt64JTW0EbpWma2pAChZZSv6Mo4PiBzaTtl86vfelyA5JnSq2Rjd5ibQw5r4FukUuomwgstBcoBvoARZb9C8c/MjIC22X34z0HFmpGY2S1m8P6QbfAatFVqViQAoAnUFekVwpykZNjNtzwGGJu7/Inz82kj/Vqi5w0T5gndJmZ0naX4EM0wCsArRksErYCPAy4Ctt9kXqXyrIi84lf9bRKYCqYz5mQmLqQ4mRHUqbyIJHxf4wIICqpCVukzqwRFymnryyr0LKzJjEzgDwGfAJ8KPS5ljsjjJB9jopEiwQqjFfaHkgDdOw7MAQMDKVl/9f2j8B3XOZyUnYJQAAAABJRU5ErkJggg==);
}
#stats {
position: absolute;
top: 210px;
left: 350px;
text-align: left;
}
#stats > h2 {
display: inline-block;
width: 200px;
color: #0085B2;
font-weight: normal;
border-bottom: 1px solid #0085B2;
}
#stats > table {
position: relative;
left: 10px;
}
#stats td:first-child {
text-align: right;
padding-right: 5px;
}
#stats .count {
display: inline-block;
min-width: 15px;
padding: 2px 3px;
color: #fff;
text-align: center;
font-size: 0.8em;
border-radius: 2px;
-webkit-border-radius: 2px;
-moz-border-radius: 2px;
}
#stats #wins-count {
background: #0085B2;
}
#stats #losses-count {
background: #FF8F19;
}
#stats #draws-count {
background: #666;
}
@media only screen
and (min-device-width : 320px)
and (max-device-width : 480px) {
main {
width: 320px;
}
h1 {
margin-top: 0.55em;
}
nav {
margin: 1.25em 0 0.5em;
}
#stats {
position: static;
text-align: center;
}
#stats table {
width: 100px;
margin: 0 auto;
position: static;
}
#stats td {
text-align: center;
}
}
@media (max-width: 799px) {
main {
width: 320px;
}
h1 {
margin-top: 0.55em;
}
nav {
margin: 1.25em 0 0.5em;
}
#stats {
position: static;
text-align: center;
}
#stats table {
width: 100px;
margin: 0 auto;
position: static;
}
#stats td {
text-align: center;
}
}

19
tsconfig.json Normal file
View File

@@ -0,0 +1,19 @@
{
"extends": "@nuxt/typescript",
"compilerOptions": {
"baseUrl": ".",
"experimentalDecorators": true,
"paths": {
"~/plugins/*": ["./plugins/*"],
"~/components/*": ["./components/*"],
"~/pages/*": ["./pages/*"],
"~/assets/*": ["./assets/*"],
},
"strictPropertyInitialization": false,
"strict": true,
"types": [
"@types/node",
"@nuxt/vue-app"
],
},
}

7972
yarn.lock Normal file

File diff suppressed because it is too large Load Diff