mirror of
https://github.com/GeorgeSG/v0.gar.dev
synced 2025-12-28 21:30:29 +00:00
Re-implement with nuxt-ts
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
.nuxt/
|
||||
dist/
|
||||
node_modules/
|
||||
7
.vscode/settings.json
vendored
Normal file
7
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files.exclude": {
|
||||
"**/.nuxt/": true,
|
||||
"**/dist/": true,
|
||||
"**/node_modules/": true
|
||||
}
|
||||
}
|
||||
69
assets/styles/main.scss
Normal file
69
assets/styles/main.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
37
components/TicTacToe/Board/Board.vue
Normal file
37
components/TicTacToe/Board/Board.vue
Normal 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>
|
||||
60
components/TicTacToe/Board/Cell/Cell.vue
Normal file
60
components/TicTacToe/Board/Cell/Cell.vue
Normal 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>
|
||||
276
components/TicTacToe/TicTacToe.vue
Normal file
276
components/TicTacToe/TicTacToe.vue
Normal 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>
|
||||
97
index.html
97
index.html
@@ -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();
|
||||
}
|
||||
};
|
||||
3
layouts/default.vue
Normal file
3
layouts/default.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template lang="pug">
|
||||
nuxt
|
||||
</template>
|
||||
36
nuxt.config.ts
Normal file
36
nuxt.config.ts
Normal 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
30
package.json
Normal 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
51
pages/index.vue
Normal 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
143
plugins/tic-tac-toe/ai.ts
Normal 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';
|
||||
}
|
||||
4
plugins/tic-tac-toe/cell-state.ts
Normal file
4
plugins/tic-tac-toe/cell-state.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
type Token = 'x' | 'o';
|
||||
type CellState = null | Token;
|
||||
|
||||
export { CellState, Token };
|
||||
3
plugins/tic-tac-toe/difficulty.ts
Normal file
3
plugins/tic-tac-toe/difficulty.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
type Difficulty = 'easy' | 'medium' | 'hard';
|
||||
|
||||
export default Difficulty;
|
||||
86
plugins/tic-tac-toe/game.ts
Normal file
86
plugins/tic-tac-toe/game.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
12
plugins/tic-tac-toe/player.ts
Normal file
12
plugins/tic-tac-toe/player.ts
Normal 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
8
shims-svg.d.ts
vendored
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
19
tsconfig.json
Normal file
19
tsconfig.json
Normal 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"
|
||||
],
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user