mirror of
https://github.com/GeorgeSG/v0.gar.dev
synced 2026-05-05 09:54:24 +00:00
Re-implement with nuxt-ts
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
.nuxt/
|
||||||
|
dist/
|
||||||
|
node_modules/
|
||||||
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files.exclude": {
|
||||||
|
"**/.nuxt/": true,
|
||||||
|
"**/dist/": true,
|
||||||
|
"**/node_modules/": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
-97
@@ -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>
|
|
||||||
@@ -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;
|
|
||||||
};
|
|
||||||
@@ -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;
|
|
||||||
};
|
|
||||||
@@ -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);
|
|
||||||
};
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
var Player = {
|
|
||||||
HUMAN: { token: 'x' },
|
|
||||||
AI: { token: 'o' }
|
|
||||||
};
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<template lang="pug">
|
||||||
|
nuxt
|
||||||
|
</template>
|
||||||
@@ -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']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
};
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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';
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type Token = 'x' | 'o';
|
||||||
|
type CellState = null | Token;
|
||||||
|
|
||||||
|
export { CellState, Token };
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
type Difficulty = 'easy' | 'medium' | 'hard';
|
||||||
|
|
||||||
|
export default Difficulty;
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+8
@@ -0,0 +1,8 @@
|
|||||||
|
declare module '*.svg' {
|
||||||
|
import Vue, { VNode, Component } from 'vue';
|
||||||
|
|
||||||
|
type Svg = Component<Vue>;
|
||||||
|
|
||||||
|
const content: Svg;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user