├── README.md
├── .gitignore
├── src
├── GameObject.js
└── engine.js
├── package.json
├── index.html
├── main.js
├── styles.css
├── minimaxAILow
└── minimaxAILow.js
└── minimaxAI
└── minimaxAI.js
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/src/GameObject.js:
--------------------------------------------------------------------------------
1 | (function(window) {
2 | /**
3 | * Creates a new game object.
4 | *
5 | * @class GameObject
6 | * @param {*} type The type of game object.
7 | */
8 | function GameObject(type) {
9 | this.type = type;
10 | }
11 |
12 | GameObject.prototype = {
13 | /**
14 | * The type of object.
15 | *
16 | * Number represents players. 0 = first player, 1 = second player, etc.
17 | *
18 | * In the future we may have other types of objects such as "wall".
19 | */
20 | type: null
21 | };
22 |
23 | window.GameObject = GameObject;
24 | }(window));
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gomoku-ai",
3 | "version": "1.0.0",
4 | "description": "A gomoku game, gui, and minimax-based AI.",
5 | "main": "main.js",
6 | "scripts": {
7 | "start": "electron main.js"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/charleslai/gomoku-ai.git"
12 | },
13 | "keywords": [
14 | "gomoku",
15 | "ai",
16 | "minimax",
17 | "pruning"
18 | ],
19 | "author": "charleslai",
20 | "license": "CC0-1.0",
21 | "bugs": {
22 | "url": "https://github.com/charleslai/gomoku-ai/issues"
23 | },
24 | "homepage": "https://github.com/charleslai/gomoku-ai#readme",
25 | "devDependencies": {
26 | "electron-prebuilt": "^0.36.0"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Omok
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
Omok!
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const electron = require('electron');
4 | // Module to control application life.
5 | const app = electron.app;
6 | // Module to create native browser window.
7 | const BrowserWindow = electron.BrowserWindow;
8 |
9 | // Keep a global reference of the window object, if you don't, the window will
10 | // be closed automatically when the JavaScript object is garbage collected.
11 | let mainWindow;
12 |
13 | function createWindow () {
14 | // Create the browser window.
15 | mainWindow = new BrowserWindow({width: 800, height: 600});
16 |
17 | // and load the index.html of the app.
18 | mainWindow.loadURL('file://' + __dirname + '/index.html');
19 |
20 | // Emitted when the window is closed.
21 | mainWindow.on('closed', function() {
22 | // Dereference the window object, usually you would store windows
23 | // in an array if your app supports multi windows, this is the time
24 | // when you should delete the corresponding element.
25 | mainWindow = null;
26 | });
27 | }
28 |
29 | // This method will be called when Electron has finished
30 | // initialization and is ready to create browser windows.
31 | app.on('ready', createWindow);
32 |
33 | // Quit when all windows are closed.
34 | app.on('window-all-closed', function () {
35 | // On OS X it is common for applications and their menu bar
36 | // to stay active until the user quits explicitly with Cmd + Q
37 | if (process.platform !== 'darwin') {
38 | app.quit();
39 | }
40 | });
41 |
42 | app.on('activate', function () {
43 | // On OS X it's common to re-create a window in the app when the
44 | // dock icon is clicked and there are no other windows open.
45 | if (mainWindow === null) {
46 | createWindow();
47 | }
48 | });
49 |
--------------------------------------------------------------------------------
/styles.css:
--------------------------------------------------------------------------------
1 | /*------------------------------------------------------------
2 |
3 | base.css
4 |
5 | Created by: Dave Rupert ... augmented by Charles Lai for
6 | CS 2300
7 | Contact: http://github.com/davatron5000/foldy960
8 |
9 | Copyright 2012
10 | License: WTFPL + "Not going to maintain this because
11 | the rent is too damn high licence."
12 |
13 | =================================================================== */
14 |
15 | /* Responsive Resets
16 | =================================================================== */
17 | @-o-viewport {
18 | width: device-width;
19 | }
20 | @-ms-viewport {
21 | width: device-width;
22 | }
23 | @viewport {
24 | width: device-width;
25 | }
26 |
27 | html {
28 | overflow-y: auto;
29 | overflow-x: hidden;
30 | }
31 |
32 | img,
33 | audio,
34 | video,
35 | canvas {
36 | max-width: 100%;
37 | }
38 |
39 | body {
40 | font-family: 'Open Sans', sans-serif !important;
41 | }
42 |
43 | /* Grid > 6 Column Mobile First
44 | =================================================================== */
45 |
46 | .container {
47 | /*
48 | The `max-width` property is the width governer. I dare you to experiment
49 | with setting this larger, something like 1280px.
50 | */
51 | width:100% !important;
52 | height: 100vh;
53 | position: relative;
54 | }
55 |
56 | .container-big {
57 | /*
58 | The `max-width` property is the width governer. I dare you to experiment
59 | with setting this larger, something like 1280px.
60 | */
61 | width:100% !important;
62 | height: 125vh;
63 | position: relative;
64 | }
65 |
66 | .container-half {
67 | width:100%;
68 | height: 50vh;
69 | position: relative;
70 | }
71 |
72 | .row {
73 | clear: both;
74 | }
75 |
76 | @media screen and (min-width: 0px) {
77 | .container {
78 | width: 98%;
79 | }
80 |
81 | .grid-1,
82 | .grid-2,
83 | .grid-3,
84 | .grid-4,
85 | .grid-5,
86 | .grid-6,
87 | .grid-half,
88 | .grid-full,
89 | .grid-unit {
90 | float: left;
91 | width:96.969696969697%;
92 | margin:0 1.515151515152% 1em;
93 | }
94 |
95 | .gallery .grid-unit,
96 | .grid-half {
97 | width: 30%;
98 | height: 20vh;
99 | background-size: cover;
100 | }
101 |
102 | .grid-flow-opposite{
103 | float:right
104 | }
105 |
106 | }
107 |
108 | @media screen and (min-width: 640px) {
109 | .grid-1 { width: 13.636363636364%; }
110 | .grid-2 { width: 30.30303030303%; }
111 | .grid-3,
112 | .grid-half { width: 46.969696969697%; }
113 | .grid-4 { width: 63.636363636364%; }
114 | .grid-5 { width: 80.30303030303%; }
115 | .grid-6,
116 | .grid-full { width: 96.969696969697%; }
117 |
118 | .gallery .grid-unit {
119 | width: 30%;
120 | height: 20vh;
121 | background-size: cover;
122 | }
123 |
124 | .grid-unit:focus,
125 | .grid-unit:hover{
126 | background-color:#ebebeb;
127 | }
128 |
129 | .content-pad-right {
130 | padding-right: 4%; /* Use (or don't) as necessary. */
131 | }
132 |
133 | .content-pad-left {
134 | padding-left: 8%;
135 | }
136 | }
137 |
138 | /* Colors and Typography
139 | =================================================================== */
140 | /*Colors*/
141 | .white {
142 | background-color: #FFFFFF;
143 | }
144 |
145 | .black {
146 | background-color: #000000;
147 | }
148 |
149 | .light-gray {
150 | background-color: #BBBBBB;
151 | }
152 |
153 | .gray {
154 | background-color: #888888;
155 | }
156 |
157 | .dark-gray {
158 | background-color: #555555;
159 | }
160 |
161 | .almost-black {
162 | background-color: #181a1a;
163 | }
164 |
165 | .beige {
166 | background-color: #ebe1c3;
167 | }
168 |
169 | .green{
170 | background-color: #5cb85c !important;
171 | }
172 |
173 | .magenta {
174 | background-color:#ff3452;
175 | }
176 |
177 | .black-gradient {
178 | background: #1c1e20;
179 | background: -moz-radial-gradient(center, circle cover, #555a5f 0%, #1c1e20 100%);
180 | background: -webkit-gradient(radial, center center, 0px, center center, 100%, color-stop(0%, #555a5f), color-stop(100%, #1c1e20));
181 | background: -webkit-radial-gradient(center, circle cover, #555a5f 0%, #1c1e20 100%);
182 | background: -o-radial-gradient(center, circle cover, #555a5f 0%, #1c1e20 100%);
183 | background: -ms-radial-gradient(center, circle cover, #555a5f 0%, #1c1e20 100%);
184 | background: radial-gradient(center, circle cover, #555a5f 0%, #1c1e20 100%);
185 | background-color: #2b2b2b;
186 | }
187 |
188 | .navy-blue {
189 | background-image: url("../img/navy_blue.png");
190 | }
191 |
192 | .white-fabric {
193 | background-image: url("../img/whitefabric.png");
194 | }
195 |
196 | .centered {
197 | text-align: center;
198 | }
199 |
200 | /*Typesetting*/
201 | .justified {
202 | text-align: justify;
203 | }
204 |
205 | .right {
206 | text-align: right;
207 | }
208 |
209 | .left{
210 | text-align: left;
211 | }
212 |
213 | /*Font Colors*/
214 | .white-font{
215 | color:white;
216 | }
217 |
218 | .red-font, .error {
219 | color:red;
220 | }
221 |
222 | .magenta-font {
223 | color:#ff3452;
224 | }
225 |
226 | .gray-font {
227 | color:#888888;
228 | }
229 | .black-font {
230 | color:black;
231 | }
232 |
233 | .green-font {
234 | color:#5cb85c;
235 | }
236 |
237 | .beige-font {
238 | color:#ebe1c3;
239 | }
240 |
241 | /* Headings and Styling */
242 | .large-heading {
243 | font-size: 4.5em;
244 | margin: 0;
245 | }
246 |
247 | .heading {
248 | font-size: 3em;
249 | margin: 0;
250 | }
251 |
252 | .sub-heading {
253 | font-size: 2em;
254 | margin-top: 0em;
255 | margin-bottom: 0.5em;
256 | }
257 |
258 | .sub-sub-heading {
259 | font-size: 1.5em;
260 | margin: 0;
261 | }
262 |
263 | .description {
264 | font-size: 1.1em;
265 | text-align: justify;
266 | }
267 |
268 | .shadow {
269 | text-shadow:0px 1.5px 4px #000000;
270 | }
271 |
272 | .box-shadow {
273 | box-shadow:0px 1.5px 4px #000000;
274 | }
275 |
276 | /* Link Styling */
277 | a {
278 | color: inherit;
279 | text-decoration: none;
280 | font-size: 1em;
281 | }
282 |
283 | a:focus,
284 | a:hover {
285 | color: gray;
286 | }
287 |
288 | .border {
289 |
290 | }
291 |
292 | /* Layout
293 | =================================================================== */
294 | body {
295 | font: 100%/1.5 'Open Sans';
296 | font-weight: 300;
297 | width: 100vw;
298 | height: 100vh;
299 | min-height: 100%;
300 | padding: 0;
301 | margin: 0 0 0 0;
302 | }
303 |
304 | footer {
305 | font-size: 0.9em;
306 | padding: 0.5em 0 2.5em;
307 | height: 9vh;
308 | width: 100vw;
309 | }
310 |
311 | .header-bar{
312 | z-index: 10;
313 | height: 7vh;
314 | margin: 0 0 0 0;
315 | width: 100vw;
316 | position: fixed;
317 | display: inline;
318 | box-shadow: 0px 3px 10px 3px #000000;
319 | }
320 |
321 | /* Buttons
322 | -------------------------------------------------------------- */
323 | .button {
324 | display: inline-block;
325 | padding: 6px 12px;
326 | margin: 0;
327 | font-size: 1em;
328 | font-weight: normal;
329 | line-height: 1.428571429;
330 | text-align: center;
331 | white-space: nowrap;
332 | vertical-align: middle;
333 | cursor: pointer;
334 | -webkit-user-select: none;
335 | -moz-user-select: none;
336 | -ms-user-select: none;
337 | -o-user-select: none;
338 | user-select: none;
339 | border: 1px solid transparent;
340 | border-radius: 4px;
341 | }
342 |
343 | .button-white {
344 | color: #333;
345 | background-color: #fff;
346 | border-color: #ccc;
347 | }
348 |
349 | .button-blue {
350 | color: #ffffff;
351 | background-color: #428bca;
352 | border-color: #357ebd;
353 | }
354 |
355 | .button-white:hover,
356 | .button-white:focus,
357 | .button-white:active {
358 | color: #333;
359 | background-color: #ebebeb;
360 | border-color: #adadad;
361 | }
362 |
363 | .button-blue:hover,
364 | .button-blue:focus,
365 | .button-blue:active,
366 | .button-blue.active {
367 | color: #fff;
368 | background-color: #3276b1;
369 | border-color: #285e8e;
370 | }
371 |
372 | .button-green {
373 | color: #fff;
374 | background-color: #5cb85c;
375 | border-color: #4cae4c;
376 | }
377 |
378 | .button-green:hover,
379 | .button-green:focus,
380 | .button-green:active,
381 | .button-green.active {
382 | color: #fff;
383 | background-color: #47a447;
384 | border-color: #398439;
385 | }
386 |
387 | .button-large {
388 | padding: 10px 16px;
389 | font-size: 18px;
390 | line-height: 1.33;
391 | border-radius: 6px;
392 | }
393 |
394 | /* Section Separation
395 | -------------------------------------------------------------- */
396 | .divider {
397 | position: relative;
398 | width: 100vw;
399 | padding: 0 0 0 0;
400 | margin: 0 0 0 0;
401 | border-color: #555555;
402 | }
403 |
404 | .padding {
405 | height: 9vh;
406 | }
407 |
408 | .center-of-element {
409 | padding-top: 28vh;
410 | }
411 |
412 | /* Forms
413 | -------------------------------------------------------------- */
414 | .search {
415 | width:29vw;
416 | }
417 |
418 | .searchform {
419 | padding-top:14vh;
420 | }
421 |
422 | .rounded {
423 | border-radius: 4px;
424 | }
425 |
426 | .longtext{
427 | height:150px;
428 | width: 300px;
429 | }
430 |
431 | /* Navigation
432 | -------------------------------------------------------------- */
433 | .scroll_arrow {
434 | position: absolute;
435 | bottom: 2vh;
436 | left: 0;
437 | text-align: center;
438 | cursor: pointer;
439 | }
440 |
441 | .sidebar {
442 | float: left;
443 | background-color:#000;
444 | width:17vw;
445 | height:100vh;
446 | overflow-y:scroll;
447 | }
448 |
449 | .sidebar-category {
450 | background-color: #222 ;
451 | width: 100%;
452 | height:5%;
453 | text-align: center;
454 | font-size: 1em;
455 | border-style: solid;
456 | border-color: black;
457 | border-width: 1px 0 0 0;
458 | }
459 |
460 | .sidebar-item {
461 | background-color: #333;
462 | width: 100%;
463 | height: 4%;
464 | text-align: center;
465 | font-size: .85em;
466 | border-style: solid;
467 | border-color: black;
468 | border-width: 1px 0 0 0;
469 | }
470 |
471 |
472 | input.sidebar-item {
473 | color: white !important;
474 | font-family: 'Open Sans' !important;
475 | font-size: .85 em;
476 | font-weight: 300;
477 | }
478 |
479 | input.sidebar-item:focus {
480 | outline: 0;
481 | box-shadow: none;
482 | cursor: pointer;
483 | }
484 |
485 | #search:focus{
486 | cursor: default;
487 | }
488 |
489 | .sidebar-item:hover {
490 | cursor: pointer;
491 | background-color: #444;
492 | }
493 |
494 | .sidebar-main {
495 | overflow-y: scroll;
496 | overflow-x: hidden;
497 | position: relative;
498 | float: right;
499 | width: 80vw;
500 | height: 100vh;
501 | text-align: center;
502 | }
503 |
504 | .return {
505 | position: absolute;
506 | bottom: 50px;
507 | }
508 |
509 | /*.sticky {
510 | position: fixed relative;
511 | }*/
512 |
513 | /* Logo and Images
514 | -------------------------------------------------------------- */
515 | .logo-container {
516 | margin-right: auto;
517 | margin-left: auto;
518 | text-align: center;
519 | margin-top: 15vh;
520 | }
521 |
522 | .logo{
523 | margin-right: auto;
524 | margin-left: auto;
525 | width: 25vw;
526 | height: auto;
527 | }
528 |
529 | .mini-logo{
530 |
531 | }
532 |
533 | #search-icon {
534 | max-width: 10%;
535 | }
536 |
537 | .photo {
538 | background-repeat: repeat-x;
539 | }
540 | .photo:hover {
541 | cursor: pointer;
542 | }
543 |
544 | .guestbook-map {
545 |
546 | }
--------------------------------------------------------------------------------
/src/engine.js:
--------------------------------------------------------------------------------
1 | (function(window) {
2 |
3 | // =======================================================================
4 | //
5 | // Game Engine Constructor Function
6 | //
7 | // ========================================================================
8 | function Engine() {
9 | var me = this;
10 | var gameBoard = window.document.getElementById('game-board');
11 |
12 | // Set properties.
13 | this.ctx = gameBoard.getContext('2d');
14 | this.boardHeight = parseInt(gameBoard.getAttribute('height'));
15 | this.boardWidth = parseInt(gameBoard.getAttribute('width'));
16 |
17 | // Listen to "start" button clicks.
18 | window.document.getElementById('start').addEventListener('click', function() {
19 | me.run();
20 | }, false);
21 |
22 | // Listen to user clicks.
23 | gameBoard.addEventListener('click', this.onGameBoardClick.bind(this), false);
24 | }
25 |
26 | // =======================================================================
27 | //
28 | // Main Game Engine Class
29 | //
30 | // ========================================================================
31 | Engine.prototype = {
32 | /**
33 | * @type CanvasContext
34 | */
35 | ctx: null,
36 |
37 | /**
38 | * Determines the board cell size. Should not be modified on the fly.
39 | */
40 | boardCellSize: 64,
41 | boardWidth: 0,
42 | boardHeight: 0,
43 |
44 | /**
45 | * An array consisting of [fromX, fromY, toX, toY] winner line. False if no winner yet.
46 | */
47 | winnerLine: false,
48 |
49 | /**
50 | * Contains every object in the board.
51 | *
52 | * This is a 2d "array", e.g. gameObjects[0][1] equals to X = 0, Y = 1.
53 | */
54 | gameObjects: {},
55 |
56 | /**
57 | * Determines which side has the turn.
58 | *
59 | * 0 = X
60 | * 1 = O
61 | */
62 | turn: 0,
63 |
64 | /**
65 | * The players who are playing. This array should always contain 2 entries.
66 | *
67 | * If it's a string "player", then it's human, otherwise it's an AI of the specified name.
68 | */
69 | players: ['player', 'minimaxAI'],
70 |
71 | /**
72 | * List of all possible AI players.
73 | */
74 | AIs: {},
75 |
76 | /**
77 | * List of all scopes for AI players.
78 | */
79 | AIScopes: {},
80 |
81 | /**
82 | * X and Y coordinates of the last successful move.
83 | */
84 | lastMoveCoordinates: {x: -1, y: -1},
85 |
86 | /**
87 | * Push functions to register after rendering callbacks.
88 | */
89 | afterRenderCallbacks: [],
90 |
91 | /**
92 | * Adds the given AI to the list of AIs.
93 | *
94 | * @param {String} name The name of the AI.
95 | * @param {Function} getter The function to call to retrieve the move.
96 | * @param {Object} scope
97 | */
98 | addAI: function(name, getter, scope) {
99 | this.AIs[name] = getter;
100 | this.AIScopes[name] = scope;
101 | },
102 |
103 | /**
104 | * Returns the game object at the given location.
105 | *
106 | * @param {Number} x
107 | * @param {Number} y
108 | * @return {GameObject|null}
109 | */
110 | getGameObject: function(x, y) {
111 | var column = this.gameObjects[x];
112 |
113 | if (!column) {
114 | return null;
115 | }
116 |
117 | return column[y] || null;
118 | },
119 |
120 | getGameObjects: function() {
121 | var copy_board = {};
122 | // Copy the game board and retun the copy
123 | for (var x in this.gameObjects) {
124 | if (!this.gameObjects.hasOwnProperty(x)) {
125 | continue;
126 | }
127 | copy_board[x] = this.gameObjects[x];
128 | }
129 | return copy_board;
130 | },
131 |
132 | /**
133 | * Returns the canvas context.
134 | *
135 | * @return {CanvasContext}
136 | */
137 | getCanvasContext: function() {
138 | return this.ctx;
139 | },
140 |
141 | /**
142 | * Check if there is a winner.
143 | */
144 | checkWinner: function() {
145 | var c = this.ctx;
146 |
147 | for (var x in this.gameObjects) {
148 | if (!this.gameObjects.hasOwnProperty(x)) {
149 | continue;
150 | }
151 |
152 | var objects = this.gameObjects[x];
153 | for (var y in objects) {
154 | if (!objects.hasOwnProperty(y)) {
155 | continue;
156 | }
157 |
158 | /** @type GameObject object */
159 | var object = objects[y];
160 |
161 | // XXXXX
162 | var found = true;
163 | for (var i = 1; i < 5; i++) {
164 | var gameObject = this.getGameObject(parseInt(x, 10) + i, y);
165 | if (!gameObject || gameObject.type !== object.type) {
166 | found = false;
167 | }
168 | }
169 |
170 | if (found) {
171 | this.winnerLine = [x, y, parseInt(x, 10) + i, y];
172 | return true;
173 | }
174 |
175 | // X
176 | // X
177 | // X
178 | // X
179 | // X
180 | found = true;
181 | for (i = 1; i < 5; i++) {
182 | gameObject = this.getGameObject(x, parseInt(y, 10) + i);
183 | if (!gameObject || gameObject.type !== object.type) {
184 | found = false;
185 | }
186 | }
187 |
188 | if (found) {
189 | this.winnerLine = [x, y, x, parseInt(y, 10) + i];
190 | return true;
191 | }
192 |
193 | // X
194 | // X
195 | // X
196 | // X
197 | // X
198 | found = true;
199 | for (i = 1; i < 5; i++) {
200 | gameObject = this.getGameObject(parseInt(x, 10) + i, parseInt(y, 10) + i);
201 | if (!gameObject || gameObject.type !== object.type) {
202 | found = false;
203 | }
204 | }
205 |
206 | if (found) {
207 | this.winnerLine = [x, y, parseInt(x, 10) + i, parseInt(y, 10) + i];
208 | return true;
209 | }
210 |
211 | // X
212 | // X
213 | // X
214 | // X
215 | // X
216 | found = true;
217 | for (i = 1; i < 5; i++) {
218 | gameObject = this.getGameObject(parseInt(x, 10) + i, parseInt(y, 10) - i);
219 | if (!gameObject || gameObject.type !== object.type) {
220 | found = false;
221 | }
222 | }
223 |
224 | if (found) {
225 | this.winnerLine = [x, y, parseInt(x, 10) + i, parseInt(y, 10) - i];
226 | return true;
227 | }
228 | }
229 | }
230 | },
231 |
232 | /**
233 | * Draws the game board. Main render function - called on each turn.
234 | * And after the start button is clicked!
235 | */
236 | draw: function() {
237 | var c = this.ctx;
238 |
239 | // Clear.
240 | c.clearRect(0, 0, this.boardWidth, this.boardHeight);
241 | // Draw the cells of the game board
242 | this.drawCells();
243 | // Draw all of the current pieces on the board
244 | this.drawObjects();
245 | // Draw a red square over the last move
246 | this.drawLastMove();
247 | // Draw a red line along the winning line
248 | this.drawWinnerLine();
249 |
250 | this.afterRenderCallbacks.forEach(function(func) {func();});
251 | },
252 |
253 | /**
254 | * Draws the given text into the given cell.
255 | *
256 | * @param text
257 | * @param x
258 | * @param y
259 | */
260 | drawInfoOnCell: function(text, x, y) {
261 | x = x * this.boardCellSize;
262 | y = y * this.boardCellSize;
263 |
264 | this.ctx.strokeStyle = 'black';
265 | this.ctx.font = 'normal 11pt Courier';
266 | this.ctx.strokeText(text, x + 1, y + 13);
267 | },
268 |
269 | /**
270 | * Draws the cells.
271 | */
272 | drawCells: function() {
273 | var c = this.ctx;
274 |
275 | var numberOfCellsHor = Math.floor(this.boardWidth / this.boardCellSize);
276 | var numberOfCellsVer = Math.floor(this.boardHeight / this.boardCellSize);
277 |
278 | // Draw squares/cells.
279 | c.strokeStyle = 'rgb(128,128,128)';
280 |
281 | // Iterate through the number of cells and draw columns
282 | for (var x = 0; x <= numberOfCellsHor; x++) {
283 | c.beginPath();
284 | c.moveTo(x * this.boardCellSize + 0.5, 0);
285 | c.lineTo(x * this.boardCellSize + 0.5, this.boardHeight);
286 | c.stroke();
287 | c.closePath();
288 | }
289 |
290 | // Iterate through the number of vertical cells and draw rows
291 | for (var y = 0; y <= numberOfCellsVer; y++) {
292 | c.beginPath();
293 | c.moveTo(0, y * this.boardCellSize + 0.5);
294 | c.lineTo(this.boardWidth, y * this.boardCellSize + 0.5);
295 | c.stroke();
296 | c.closePath();
297 | }
298 | },
299 |
300 | /**
301 | * Draws the winner line.
302 | */
303 | drawWinnerLine: function() {
304 | var c = this.ctx;
305 |
306 | if (this.winnerLine !== false) {
307 | c.strokeStyle = 'rgb(255, 0, 0)';
308 | c.lineWidth = 2;
309 |
310 | c.beginPath();
311 | c.moveTo(this.winnerLine[0] * this.boardCellSize, this.winnerLine[1] * this.boardCellSize + this.boardCellSize / 2);
312 | c.lineTo(this.winnerLine[2] * this.boardCellSize, this.winnerLine[3] * this.boardCellSize + this.boardCellSize / 2);
313 | c.stroke();
314 | c.closePath();
315 | console.log(this.winnerLine);
316 | }
317 | },
318 |
319 | /**
320 | * Draws all game objects.
321 | */
322 | drawObjects: function() {
323 | var c = this.ctx;
324 |
325 | for (var x in this.gameObjects) {
326 | if (!this.gameObjects.hasOwnProperty(x)) {
327 | continue;
328 | }
329 |
330 | var column = this.gameObjects[x];
331 | for (var y in column) {
332 | if (!column.hasOwnProperty(y)) {
333 | continue;
334 | }
335 |
336 | /** @type GameObject object */
337 | var object = column[y];
338 |
339 | switch (object.type) {
340 | // X
341 | case 0:
342 | c.beginPath();
343 | c.strokeStyle = 'rgb(207,91,30)';
344 | c.moveTo(x * this.boardCellSize + 2, y * this.boardCellSize + 2.5);
345 | c.lineTo(x * this.boardCellSize + this.boardCellSize - 2, y * this.boardCellSize + 0.5 + this.boardCellSize - 2);
346 | c.moveTo(x * this.boardCellSize + this.boardCellSize - 2, y * this.boardCellSize + 2.5);
347 | c.lineTo(x * this.boardCellSize + 2, y * this.boardCellSize + 0.5 + this.boardCellSize - 2);
348 | c.stroke();
349 | c.closePath();
350 | break;
351 |
352 | // O
353 | case 1:
354 | c.beginPath();
355 | c.strokeStyle = 'rgb(10,148,207)';
356 | c.arc(x * this.boardCellSize + 0.5 + this.boardCellSize / 2, y * this.boardCellSize + 0.5 + this.boardCellSize / 2, this.boardCellSize / 2 - 2 , 0, 360, false);
357 | c.stroke();
358 | c.closePath();
359 | break;
360 |
361 | default:
362 | throw new Error('Not implemented');
363 | break;
364 | }
365 | }
366 | }
367 | },
368 |
369 | /**
370 | * Draws the last move with a red rectangle.
371 | */
372 | drawLastMove: function() {
373 | var ctx = this.ctx;
374 |
375 | ctx.strokeStyle = 'rgb(255, 0, 0)';
376 | ctx.lineWidth = 2;
377 |
378 | ctx.beginPath();
379 | ctx.strokeRect(
380 | this.lastMoveCoordinates.x * this.boardCellSize,
381 | this.lastMoveCoordinates.y * this.boardCellSize,
382 | this.boardCellSize + 1,
383 | this.boardCellSize + 1
384 | );
385 | ctx.closePath();
386 |
387 | ctx.lineWidth = 1;
388 | },
389 |
390 | /**
391 | * Processes the next move.
392 | */
393 | processTurn: function() {
394 | var me = this;
395 | var currentPlayer = this.getCurrentPlayer();
396 |
397 | // Process AI logic.
398 | if (currentPlayer !== 'player' && this.winnerLine === false) {
399 | this.AIs[currentPlayer].call(this.AIScopes[currentPlayer], function(position) {
400 | this.addGameObject(position[0], position[1], this.turn);
401 | me.lastMoveCoordinates = {x: position[0], y: position[1]};
402 |
403 | this.turn = 1 - this.turn;
404 |
405 | this.checkWinner();
406 | this.draw();
407 |
408 | if (me.getCurrentPlayer() !== 'player') {
409 | setTimeout(function() {
410 | me.processTurn();
411 | }, 0);
412 | }
413 | }.bind(this));
414 | }
415 | },
416 |
417 | /**
418 | * Adds the given game object.
419 | *
420 | * @param x
421 | * @param y
422 | * @param type
423 | */
424 | addGameObject: function(x, y, type) {
425 | // Create new game object.
426 | var object = new GameObject();
427 | object.type = type;
428 |
429 | // Add it to the list.
430 | if (this.gameObjects[x] === undefined) {
431 | this.gameObjects[x] = {};
432 | }
433 |
434 | // TODO: Prevent overwritting other player's pieces??
435 | this.gameObjects[x][y] = object;
436 | },
437 |
438 | /**
439 | * Returns the current player.
440 | */
441 | getCurrentPlayer: function() {
442 | return this.players[this.turn];
443 | },
444 |
445 | /**
446 | * Fired upon game board click.
447 | *
448 | * @param {Event} e
449 | */
450 | onGameBoardClick: function(e) {
451 | // Ignore the click if it's not human player's turn.
452 | if (this.getCurrentPlayer() === 'player' && this.winnerLine === false) {
453 | var cellX = Math.floor((e.offsetX || (e.clientX - e.target.offsetLeft + window.scrollX)) / this.boardCellSize);
454 | var cellY = Math.floor((e.offsetY || (e.clientY - e.target.offsetTop + window.scrollY)) / this.boardCellSize);
455 |
456 | this.addGameObject(cellX, cellY, this.turn);
457 | this.lastMoveCoordinates = {x: cellX, y: cellY};
458 |
459 | // Continue processing the turn.
460 | this.turn = 1 - this.turn;
461 | this.checkWinner();
462 | this.draw();
463 | this.processTurn();
464 | }
465 | },
466 |
467 | /**
468 | * Runs the game engine - Called when the start button is pressed.
469 | */
470 | run: function() {
471 | // Reset the current game
472 | this.reset();
473 | // Draw all the default objects
474 | this.draw();
475 | // Get ready for the next turn
476 | this.processTurn();
477 | },
478 |
479 | /**
480 | * Resets the game - clear gameObjects, winnerline, and the board.
481 | */
482 | reset: function() {
483 | this.gameObjects = {};
484 | this.winnerLine = false;
485 | this.ctx.clearRect(0, 0, this.boardWidth, this.boardHeight);
486 | }
487 | };
488 |
489 | window.addEventListener('load', function() {
490 | // Expose the engine instance to global scope.
491 | window.TTTEngine = new Engine();
492 | }, false);
493 | }(window));
--------------------------------------------------------------------------------
/minimaxAILow/minimaxAILow.js:
--------------------------------------------------------------------------------
1 | window.addEventListener('load', function() {
2 | var engine = window.TTTEngine;
3 |
4 | /*
5 |
6 | x = opponent / self tic or toe
7 | * = empty, placeable
8 | - = empty, not placeable
9 |
10 | */
11 |
12 | // Define this AI.
13 | var ai = {
14 | /**
15 | * Run the main AI logic function.
16 | *
17 | * @param {Function} callback
18 | */
19 | run: function(callback) {
20 | var copyBoard = engine.getGameObjects();
21 | var myType = engine.turn;
22 | var bestMove = this.minimax(copyBoard, 1, Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER, myType);
23 | console.log(bestMove[0]);
24 | var x = bestMove[1][0];
25 | var y = bestMove[1][1];
26 | callback([x,y]);
27 | },
28 |
29 | /**
30 | * Find all empty cells in a given board
31 | * @param {[type]} board [description]
32 | * @return {[type]} [description]
33 | */
34 | findEmptyCells: function(board) {
35 | var copyBoard = this.makeBoardCopy(board);
36 | var emptyCells = [];
37 | var maxX = Math.floor(engine.boardWidth/engine.boardCellSize);
38 | var maxY = Math.floor(engine.boardHeight/engine.boardCellSize);
39 | for (var x = 0; x < maxX; x++ ) {
40 | // Add all cells of an empty column
41 | if (!copyBoard.hasOwnProperty(x)) {
42 | for (var i = 0; i < maxY; i++) {
43 | emptyCells.push([x,i]);
44 | }
45 | // Go to the next column
46 | continue;
47 | }
48 |
49 | // Add all empty cells of an existing column
50 | var column = copyBoard[x];
51 | for (var y = 0; y < maxY; y++) {
52 | if (!column.hasOwnProperty(y)) {
53 | emptyCells.push([x,y]);
54 | }
55 | }
56 | }
57 | return emptyCells;
58 | },
59 |
60 | /**
61 | * Find all cells in a given board with a
62 | * given type.
63 | * @param {[type]} board [description]
64 | * @param {[type]} type [description]
65 | * @return {[type]} [description]
66 | */
67 |
68 | findTypeCells: function(board, type) {
69 | var copyBoard = this.makeBoardCopy(board);
70 | var typeCells = [];
71 | var maxX = Math.floor(engine.boardWidth/engine.boardCellSize);
72 | var maxY = Math.floor(engine.boardHeight/engine.boardCellSize);
73 | for (var x = 0; x < maxX; x++ ) {
74 | if (!copyBoard.hasOwnProperty(x)) {
75 | // Go to the next column
76 | continue;
77 | }
78 | // Loop through the column
79 | var column = copyBoard[x];
80 | for (var y = 0; y < maxY; y++) {
81 | if (!column.hasOwnProperty(y)) {
82 | continue;
83 | }
84 | // Add any cells that match the type
85 | if (column[y].type === type) {
86 | typeCells.push([x,y]);
87 | }
88 | }
89 | }
90 | return typeCells;
91 | },
92 |
93 | /**
94 | * Find all empty cells adjacent to an
95 | * x,y position on the evaluated board.
96 | *
97 | * @param {[type]} board [description]
98 | * @param {[type]} x [description]
99 | * @param {[type]} y [description]
100 | * @return {[type]} [description]
101 | */
102 | findEmptyAdjacent: function(board, x, y){
103 | var emptyCells = [];
104 | var maxX = Math.floor(engine.boardWidth/engine.boardCellSize)-1;
105 | var maxY = Math.floor(engine.boardHeight/engine.boardCellSize)-1;
106 | // If the point doesn't exist, return an empty array
107 | if (!board.hasOwnProperty(x)) {
108 | return [];
109 | }
110 | var column = board[x];
111 | if (!column.hasOwnProperty(y)) {
112 | return [];
113 | }
114 | var topLeft = [x-1,y-1];
115 | var topRight = [x+1,y-1];
116 | var bottomLeft = [x-1,y+1];
117 | var bottomRight = [x+1,y+1];
118 | var top = [x,y-1];
119 | var bottom = [x,y+1];
120 | var left = [x-1,y];
121 | var right = [x+1,y];
122 | // Check boundaries
123 | if (x+1 > maxX) {
124 | topRight = [];
125 | right = [];
126 | bottomRight = [];
127 | }
128 | if (x-1 < 0) {
129 | topLeft = [];
130 | left = [];
131 | bottomLeft = [];
132 | }
133 | if (y+1 > maxY) {
134 | bottomRight = [];
135 | bottom = [];
136 | bottomLeft = [];
137 | }
138 | if (y-1 < 0) {
139 | topRight = [];
140 | top = [];
141 | topLeft = [];
142 | }
143 | // If any of these exist already, then unset them
144 | if (board.hasOwnProperty(x-1)) {
145 | if (board[x-1].hasOwnProperty(y)) {
146 | left = [];
147 | }
148 | if (board[x-1].hasOwnProperty(y+1)){
149 | bottomLeft = [];
150 | }
151 | if (board[x-1].hasOwnProperty(y-1)) {
152 | topLeft = [];
153 | }
154 | }
155 | if (board.hasOwnProperty(x+1)){
156 | if (board[x+1].hasOwnProperty(y)) {
157 | right = [];
158 | }
159 | if (board[x+1].hasOwnProperty(y+1)){
160 | bottomRight = [];
161 | }
162 | if (board[x+1].hasOwnProperty(y-1)) {
163 | topRight = [];
164 | }
165 | }
166 | if (board.hasOwnProperty(x)) {
167 | if (board[x].hasOwnProperty(y+1)){
168 | bottom = [];
169 | }
170 | if (board[x].hasOwnProperty(y-1)) {
171 | top = [];
172 | }
173 | }
174 | // Push the cells to the empty cell array and return
175 | if (topLeft.length !== 0) emptyCells.push(topLeft);
176 | if (topRight.length !== 0) emptyCells.push(topRight);
177 | if (bottomLeft.length !== 0) emptyCells.push(bottomLeft);
178 | if (bottomRight.length !== 0) emptyCells.push(bottomRight);
179 | if (top.length !== 0) emptyCells.push(top);
180 | if (bottom.length !== 0) emptyCells.push(bottom);
181 | if (left.length !== 0) emptyCells.push(left);
182 | if (right.length !== 0) emptyCells.push(right);
183 | return emptyCells;
184 | },
185 |
186 | /**
187 | * Find the number of i-length chains
188 | * found in the column of the board
189 | *
190 | * @param {[type]} board [description]
191 | * @param {[type]} type [description]
192 | * @param {[type]} i [description]
193 | */
194 | findIColChain: function(board,type,i,isFive) {
195 | var copyBoard = this.makeBoardCopy(board);
196 | var maxX = Math.floor(engine.boardWidth/engine.boardCellSize);
197 | var maxY = Math.floor(engine.boardHeight/engine.boardCellSize);
198 | var count = 0;
199 | // Iterate through the columns of the game board
200 | for (var x = 0; x < maxX; x++) {
201 | // Check that the column exists first
202 | if (!copyBoard.hasOwnProperty(x)) {
203 | continue;
204 | }
205 | var column = copyBoard[x];
206 | // spaceFront is a flag indicating whether a space in the front of a chain has been seen
207 | // sfChainNum is the length of current chain with a space in the front
208 | // sbChainNum is the length of current chain with a space in the back
209 | var spaceFront = false;
210 | var sfChainNum = 0;
211 | var sbChainNum = 0;
212 | for (var y = 0; y < maxY; y++){
213 | // If we see an empty space, check if sbChainNum is i-length.
214 | // Otherwise, check if we've seen a space before so we can start checking for chains with space in front
215 | if (!column.hasOwnProperty(y)){
216 | if (sbChainNum === i){
217 | count++;
218 | }
219 | sbChainNum = 0;
220 | if (!spaceFront){
221 | spaceFront = true;
222 | }
223 | continue;
224 | }
225 | // Increment sfChainNum if flag has been set and type matches, otherwise reset
226 | if (spaceFront) {
227 | if(column[y].type === type){
228 | sfChainNum++;
229 | // If we see an i-length chain, increment count and reset stats
230 | if (sfChainNum === i){
231 | count++;
232 | spaceFront = false;
233 | sfChainNum = 0;
234 | }
235 | }
236 | else{
237 | spaceFront = false;
238 | sfChainNum = 0;
239 | }
240 | }
241 | else {
242 | // Increment sbChainNum if it matches the type and reset to 0 if it doesn't
243 | if(column[y].type === type){
244 | sbChainNum++;
245 | if (isFive && sbChainNum === i) {
246 | count++;
247 | sbChainNum = 0;
248 | }
249 | }
250 | else {
251 | sbChainNum = 0;
252 | }
253 | }
254 | }
255 | }
256 | return count;
257 | },
258 |
259 | /**
260 | * Find the number of i-length chains
261 | * found in the rows of the board
262 | *
263 | * @param {[type]} board [description]
264 | * @param {[type]} type [description]
265 | * @param {[type]} i [description]
266 | */
267 | findIRowChain: function(board,type,i,isFive) {
268 | var copyBoard = this.makeBoardCopy(board);
269 | var transposeBoard = {};
270 | var maxX = Math.floor(engine.boardWidth/engine.boardCellSize);
271 | var maxY = Math.floor(engine.boardHeight/engine.boardCellSize);
272 | var count;
273 | // Get transpose of original board
274 | // Iterate through the columns of the game board
275 | for (var x = 0; x < maxX; x++) {
276 | // Check that the column exists
277 | if (!copyBoard.hasOwnProperty(x)) {
278 | continue;
279 | }
280 | for (var y = 0; y < maxY; y++){
281 | // Check that the cell exists
282 | if (copyBoard[x].hasOwnProperty(y)){
283 | if (!transposeBoard.hasOwnProperty(y)) {
284 | transposeBoard[y] = {};
285 | }
286 | transposeBoard[y][x] = copyBoard[x][y];
287 | }
288 | }
289 | }
290 | count = this.findIColChain(transposeBoard,type,i,isFive);
291 | return count;
292 | },
293 |
294 | /**
295 | * Find the number of i-length diagonal chains
296 | * going in a downwards direction
297 | *
298 | * @param {[type]} board [description]
299 | * @param {[type]} type [description]
300 | * @param {[type]} i [description]
301 | */
302 | findDIDiagChain: function(board,type,i) {
303 | // Vars to iterate through the boundaries of the board
304 | var copyBoard = this.makeBoardCopy(board);
305 | var maxX = Math.floor(engine.boardWidth/engine.boardCellSize);
306 | var maxY = Math.floor(engine.boardHeight/engine.boardCellSize);
307 | // Output variable, number of diagonal chains of length i
308 | var count = 0;
309 | // Length of potential chains
310 | // every potential chain will have its own index
311 | // and every broken chain will have undefined in its own index
312 | var chainNums = [];
313 | // Points to check with potential chains
314 | // pointsToCheck[i] should be the next point to check for chain with chainNums[i] length
315 | // pointsToCheck[i] will be undefined if chainNums[i] is undefined
316 | var pointsToCheck = [];
317 | // Iterate through the columns of the game board
318 | for (var x = 0; x < maxX; x++) {
319 | // Check that the column does not exist
320 | if (!copyBoard.hasOwnProperty(x)) {
321 | // Reset all diagonal chain statistics if there are any
322 | // because all your diagonal chains are broken now
323 | if (chainNums.length !== 0){
324 | // This sets all indices to undefined
325 | for (var j = 0; j < pointsToCheck.length; j++){
326 | delete chainNums[j];
327 | delete pointsToCheck[j];
328 | }
329 | }
330 | continue;
331 | }
332 | var column = copyBoard[x];
333 | for (var y = 0; y < maxY; y++){
334 | var pointFound = false;
335 | // check if the point is a point we're looking for
336 | for (var k = 0; k < pointsToCheck.length; k++){
337 | // ignore indicies that have been reset
338 | if (pointsToCheck[k] === undefined){
339 | continue;
340 | }
341 | var ptcX = pointsToCheck[k][0];
342 | var ptcY = pointsToCheck[k][1];
343 | if (x === ptcX && y === ptcY){
344 | pointFound = true;
345 | // If it is a point we're looking for and the cell doesn't exist
346 | // that means the chain is broken and we reset the stats for it
347 | if (!column.hasOwnProperty(y)){
348 | delete chainNums[k];
349 | delete pointsToCheck[k];
350 | continue;
351 | }
352 | // Otherwise, add to the appropriate chainNum,
353 | // check if chainNum is i and increment count accordingly,
354 | // change the next point to check to the next point in the diagonal chain
355 | else {
356 | if (column[y].type !== type) {
357 | delete chainNums[k];
358 | delete pointsToCheck[k];
359 | continue;
360 | }
361 | chainNums[k]++;
362 | if (chainNums[k] === i){
363 | count++;
364 | delete chainNums[k];
365 | delete pointsToCheck[k];
366 | }
367 | else{
368 | pointsToCheck[k] = [ptcX + 1,ptcY + 1];
369 | }
370 | }
371 | }
372 | }
373 | // if it's not a point we were looking for
374 | if (!pointFound){
375 | // if it doesn't exist, ignore it
376 | if (!column.hasOwnProperty(y)){
377 | continue;
378 | }
379 | // otherwise, add it as the start of a potential chain
380 | else{
381 | if (column[y].type === type) {
382 | chainNums.push(1);
383 | pointsToCheck.push([x+1,y+1]);
384 | }
385 | }
386 | }
387 | }
388 | }
389 | return count;
390 | },
391 |
392 | /**
393 | * Find the number of i-length diagonal chains
394 | * going in an upwards direction
395 | *
396 | * @param {[type]} board [description]
397 | * @param {[type]} type [description]
398 | * @param {[type]} i [description]
399 | */
400 | findUIDiagChain: function(board,type,i) {
401 | // Vars to iterate through the boundaries of the board
402 | var copyBoard = this.makeBoardCopy(board);
403 | var maxX = Math.floor(engine.boardWidth/engine.boardCellSize);
404 | var maxY = Math.floor(engine.boardHeight/engine.boardCellSize);
405 | // Output variable, number of diagonal chains of length i
406 | var count = 0;
407 | // Horizonally flipped board
408 | var flipBoard = {};
409 | for (var x = 0; x < maxX; x++) {
410 | // Check that the column exists
411 | if (!copyBoard.hasOwnProperty(x)) {
412 | continue;
413 | }
414 | for (var y = 0; y < maxY; y++){
415 | // Check that the cell exists
416 | if (copyBoard[x].hasOwnProperty(y)){
417 | if (!flipBoard.hasOwnProperty(maxX-x-1)) {
418 | flipBoard[maxX-x-1] = {};
419 | }
420 | flipBoard[maxX-x-1][y] = copyBoard[x][y];
421 | }
422 | }
423 | }
424 | count = this.findDIDiagChain(flipBoard,type,i);
425 | return count;
426 | },
427 |
428 | /**
429 | * Evaluate board function. Determines score using
430 | * the number of rows and columns that are 1,2,and
431 | * 3 pieces away from a win state.
432 | *
433 | * @param {Array} Game board to evaluate
434 | * @return {Integer} Score
435 | */
436 | evaluateBoard: function(board, type) {
437 | var numZeros = this.findIColChain(board,type,5,true) + this.findIRowChain(board,type,5,true) + this.findDIDiagChain(board,type,5) + this.findUIDiagChain(board,type,5);
438 | var numOnes = this.findIColChain(board,type,4,true)+ this.findIRowChain(board,type,4,true) + this.findDIDiagChain(board,type,4) + this.findUIDiagChain(board,type,4);
439 | var numTwos = this.findIColChain(board,type,3,false) + this.findIRowChain(board,type,3,false) + this.findDIDiagChain(board,type,3) + this.findUIDiagChain(board,type,3);
440 | var numThrees = this.findIColChain(board,type,2,false) + this.findIRowChain(board,type,2,false) + this.findDIDiagChain(board,type,2) + this.findUIDiagChain(board,type,2);
441 | var numFours = this.findIColChain(board,type,1,false) + this.findIRowChain(board,type,1,false) + this.findDIDiagChain(board,type,1) + this.findUIDiagChain(board,type,1);
442 | var score = numZeros * 100000.0 + numOnes * 2500.0 + numTwos * 50.0 + numThrees * 5.0 + numFours * 1.0;
443 | return score;
444 | },
445 |
446 | // ======================================
447 | //
448 | // Utility Functions
449 | //
450 | // ======================================
451 | uniq: function(items, key) {
452 | var set = {};
453 | return items.filter(function(item) {
454 | var k = key ? key.apply(item) : item;
455 | return k in set ? false : set[k] = true;
456 | });
457 | },
458 |
459 | makeBoardCopy: function(board) {
460 | var copy_board = {};
461 | // Copy the game board and retun the copy
462 | for (var x in board) {
463 | if (!board.hasOwnProperty(x)) {
464 | continue;
465 | }
466 | copy_board[x] = {};
467 | for (var y in board[x]) {
468 | copy_board[x][y] = board[x][y];
469 | }
470 | }
471 | return copy_board;
472 | },
473 |
474 | /**
475 | * Minimax functon for finding the best move on
476 | * a given game board
477 | *
478 | * @param {[type]} board [description]
479 | * @param {[type]} depth [description]
480 | * @param {[type]} alpha [description]
481 | * @param {[type]} beta [description]
482 | * @param { } [varname] [description]
483 | * @return {[type]} [description]
484 | */
485 | minimax: function(board, depth, alpha, beta, type) {
486 | var playerPieces = this.findTypeCells(board,type);
487 | playerPieces = playerPieces.concat(this.findTypeCells(board,1-type));
488 | var numPieces = playerPieces.length;
489 | var moves = [];
490 | if (playerPieces.length !== 0) {
491 | for (var i = 0; i < numPieces; i++) {
492 | // Get the x and y values of a player piece
493 | var x = playerPieces[i][0];
494 | var y = playerPieces[i][1];
495 | // Concatenate the empty cells around the piece into the move list
496 | moves = moves.concat(this.findEmptyAdjacent(board,x,y));
497 | }
498 | }
499 | // If no good spaces were found, use all empty spaces.
500 | if (moves.length === 0) {
501 | moves = this.findEmptyCells(board);
502 | }
503 | // Make sure the list of moves is unique
504 | moves = this.uniq(moves, [].join);
505 |
506 | var bestMove = moves[0];
507 | var result = [alpha,bestMove];
508 | //========================
509 | // Minimax Algorithm
510 | //========================
511 | // Base Case:
512 | if (depth === 0) {
513 | result = [(this.evaluateBoard(board,type)-this.evaluateBoard(board,1-type)),moves.pop()];
514 | return result;
515 | }
516 | // Recursive Case:
517 | //var temp = [];
518 | var currentAlpha = alpha;
519 | while (moves.length > 0) {
520 | var freshBoard = this.makeBoardCopy(board);
521 | var testMove = moves.pop();
522 | // Get the x,y coordinates from the testMove
523 | var testX = testMove[0];
524 | var testY = testMove[1];
525 |
526 | // Create new game object.
527 | var object = new GameObject();
528 | object.type = type;
529 |
530 | // Add it to the fresh game board
531 | if (freshBoard[testX] === undefined) {
532 | freshBoard[testX] = {};
533 | }
534 | freshBoard[testX][testY] = object;
535 | // Go down the minimax tree and get the results
536 | var temp = this.minimax(freshBoard, depth-1, -beta, -currentAlpha, 1-type);
537 | var tempScore = -temp[0];
538 |
539 | // Update the alpha values
540 | if (tempScore > currentAlpha) {
541 | currentAlpha = tempScore;
542 | bestMove = testMove;
543 | }
544 | // Alpha-Beta Pruning
545 | if (currentAlpha > beta) {
546 | result = [currentAlpha,bestMove];
547 | return result;
548 | }
549 | }
550 | result = [currentAlpha,bestMove];
551 | return result;
552 | }
553 | };
554 |
555 | engine.addAI('minimaxAILow', ai.run, ai);
556 | }, false);
--------------------------------------------------------------------------------
/minimaxAI/minimaxAI.js:
--------------------------------------------------------------------------------
1 | window.addEventListener('load', function() {
2 | var engine = window.TTTEngine;
3 | // AI Class:
4 | var ai = {
5 | /**
6 | * Run the main AI logic function.
7 | *
8 | * @param {Function} callback
9 | */
10 | run: function(callback) {
11 | var copyBoard = engine.getGameObjects();
12 | var myType = engine.turn;
13 | var bestMove = this.minimax(copyBoard, 2, Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER, 1 - myType);
14 | console.log(bestMove[0]);
15 | var x = bestMove[1][0];
16 | var y = bestMove[1][1];
17 | callback([x,y]);
18 | },
19 |
20 | /**
21 | * Find all empty cells in a given board
22 | * @param {[type]} board [description]
23 | * @return {[type]} [description]
24 | */
25 | findEmptyCells: function(board) {
26 | var copyBoard = this.makeBoardCopy(board);
27 | var emptyCells = [];
28 | var maxX = Math.floor(engine.boardWidth/engine.boardCellSize);
29 | var maxY = Math.floor(engine.boardHeight/engine.boardCellSize);
30 | for (var x = 0; x < maxX; x++ ) {
31 | // Add all cells of an empty column
32 | if (!copyBoard.hasOwnProperty(x)) {
33 | for (var i = 0; i < maxY; i++) {
34 | emptyCells.push([x,i]);
35 | }
36 | // Go to the next column
37 | continue;
38 | }
39 |
40 | // Add all empty cells of an existing column
41 | var column = copyBoard[x];
42 | for (var y = 0; y < maxY; y++) {
43 | if (!column.hasOwnProperty(y)) {
44 | emptyCells.push([x,y]);
45 | }
46 | }
47 | }
48 | return emptyCells;
49 | },
50 |
51 | /**
52 | * Find all cells in a given board with a
53 | * given type.
54 | * @param {[type]} board [description]
55 | * @param {[type]} type [description]
56 | * @return {[type]} [description]
57 | */
58 |
59 | findTypeCells: function(board, type) {
60 | var copyBoard = this.makeBoardCopy(board);
61 | var typeCells = [];
62 | var maxX = Math.floor(engine.boardWidth/engine.boardCellSize);
63 | var maxY = Math.floor(engine.boardHeight/engine.boardCellSize);
64 | for (var x = 0; x < maxX; x++ ) {
65 | if (!copyBoard.hasOwnProperty(x)) {
66 | // Go to the next column
67 | continue;
68 | }
69 | // Loop through the column
70 | var column = copyBoard[x];
71 | for (var y = 0; y < maxY; y++) {
72 | if (!column.hasOwnProperty(y)) {
73 | continue;
74 | }
75 | // Add any cells that match the type
76 | if (column[y].type === type) {
77 | typeCells.push([x,y]);
78 | }
79 | }
80 | }
81 | return typeCells;
82 | },
83 |
84 | /**
85 | * Find all empty cells adjacent to an
86 | * x,y position on the evaluated board.
87 | *
88 | * @param {[type]} board [description]
89 | * @param {[type]} x [description]
90 | * @param {[type]} y [description]
91 | * @return {[type]} [description]
92 | */
93 | findEmptyAdjacent: function(board, x, y){
94 | var emptyCells = [];
95 | var maxX = Math.floor(engine.boardWidth/engine.boardCellSize)-1;
96 | var maxY = Math.floor(engine.boardHeight/engine.boardCellSize)-1;
97 | // If the point doesn't exist, return an empty array
98 | if (!board.hasOwnProperty(x)) {
99 | return [];
100 | }
101 | var column = board[x];
102 | if (!column.hasOwnProperty(y)) {
103 | return [];
104 | }
105 | var topLeft = [x-1,y-1];
106 | var topRight = [x+1,y-1];
107 | var bottomLeft = [x-1,y+1];
108 | var bottomRight = [x+1,y+1];
109 | var top = [x,y-1];
110 | var bottom = [x,y+1];
111 | var left = [x-1,y];
112 | var right = [x+1,y];
113 | // Check boundaries
114 | if (x+1 > maxX) {
115 | topRight = [];
116 | right = [];
117 | bottomRight = [];
118 | }
119 | if (x-1 < 0) {
120 | topLeft = [];
121 | left = [];
122 | bottomLeft = [];
123 | }
124 | if (y+1 > maxY) {
125 | bottomRight = [];
126 | bottom = [];
127 | bottomLeft = [];
128 | }
129 | if (y-1 < 0) {
130 | topRight = [];
131 | top = [];
132 | topLeft = [];
133 | }
134 | // If any of these exist already, then unset them
135 | if (board.hasOwnProperty(x-1)) {
136 | if (board[x-1].hasOwnProperty(y)) {
137 | left = [];
138 | }
139 | if (board[x-1].hasOwnProperty(y+1)){
140 | bottomLeft = [];
141 | }
142 | if (board[x-1].hasOwnProperty(y-1)) {
143 | topLeft = [];
144 | }
145 | }
146 | if (board.hasOwnProperty(x+1)){
147 | if (board[x+1].hasOwnProperty(y)) {
148 | right = [];
149 | }
150 | if (board[x+1].hasOwnProperty(y+1)){
151 | bottomRight = [];
152 | }
153 | if (board[x+1].hasOwnProperty(y-1)) {
154 | topRight = [];
155 | }
156 | }
157 | if (board.hasOwnProperty(x)) {
158 | if (board[x].hasOwnProperty(y+1)){
159 | bottom = [];
160 | }
161 | if (board[x].hasOwnProperty(y-1)) {
162 | top = [];
163 | }
164 | }
165 | // Push the cells to the empty cell array and return
166 | if (topLeft.length !== 0) emptyCells.push(topLeft);
167 | if (topRight.length !== 0) emptyCells.push(topRight);
168 | if (bottomLeft.length !== 0) emptyCells.push(bottomLeft);
169 | if (bottomRight.length !== 0) emptyCells.push(bottomRight);
170 | if (top.length !== 0) emptyCells.push(top);
171 | if (bottom.length !== 0) emptyCells.push(bottom);
172 | if (left.length !== 0) emptyCells.push(left);
173 | if (right.length !== 0) emptyCells.push(right);
174 | return emptyCells;
175 | },
176 |
177 | /**
178 | * Find the number of i-length chains
179 | * found in the column of the board
180 | *
181 | * @param {[type]} board [description]
182 | * @param {[type]} type [description]
183 | * @param {[type]} i [description]
184 | */
185 | findIColChain: function(board,type,i,isFive) {
186 | var copyBoard = this.makeBoardCopy(board);
187 | var maxX = Math.floor(engine.boardWidth/engine.boardCellSize);
188 | var maxY = Math.floor(engine.boardHeight/engine.boardCellSize);
189 | var count = 0;
190 | // Iterate through the columns of the game board
191 | for (var x = 0; x < maxX; x++) {
192 | // Check that the column exists first
193 | if (!copyBoard.hasOwnProperty(x)) {
194 | continue;
195 | }
196 | var column = copyBoard[x];
197 | // spaceFront is a flag indicating whether a space in the front of a chain has been seen
198 | // sfChainNum is the length of current chain with a space in the front
199 | // sbChainNum is the length of current chain with a space in the back
200 | var spaceFront = false;
201 | var sfChainNum = 0;
202 | var sbChainNum = 0;
203 | for (var y = 0; y < maxY; y++){
204 | // If we see an empty space, check if sbChainNum is i-length.
205 | // Otherwise, check if we've seen a space before so we can start checking for chains with space in front
206 | if (!column.hasOwnProperty(y)){
207 | if (sbChainNum === i){
208 | count++;
209 | }
210 | sbChainNum = 0;
211 | if (!spaceFront){
212 | spaceFront = true;
213 | }
214 | continue;
215 | }
216 | // Increment sfChainNum if flag has been set and type matches, otherwise reset
217 | if (spaceFront) {
218 | if(column[y].type === type){
219 | sfChainNum++;
220 | // If we see an i-length chain, increment count and reset stats
221 | if (sfChainNum === i){
222 | count++;
223 | spaceFront = false;
224 | sfChainNum = 0;
225 | }
226 | }
227 | else{
228 | spaceFront = false;
229 | sfChainNum = 0;
230 | }
231 | }
232 | else {
233 | // Increment sbChainNum if it matches the type and reset to 0 if it doesn't
234 | if(column[y].type === type){
235 | sbChainNum++;
236 | if (isFive && sbChainNum === i) {
237 | count++;
238 | sbChainNum = 0;
239 | }
240 | }
241 | else {
242 | sbChainNum = 0;
243 | }
244 | }
245 | }
246 | }
247 | return count;
248 | },
249 |
250 | /**
251 | * Find the number of i-length chains
252 | * found in the rows of the board
253 | *
254 | * @param {[type]} board [description]
255 | * @param {[type]} type [description]
256 | * @param {[type]} i [description]
257 | */
258 | findIRowChain: function(board,type,i,isFive) {
259 | var copyBoard = this.makeBoardCopy(board);
260 | var transposeBoard = {};
261 | var maxX = Math.floor(engine.boardWidth/engine.boardCellSize);
262 | var maxY = Math.floor(engine.boardHeight/engine.boardCellSize);
263 | var count;
264 | // Get transpose of original board
265 | // Iterate through the columns of the game board
266 | for (var x = 0; x < maxX; x++) {
267 | // Check that the column exists
268 | if (!copyBoard.hasOwnProperty(x)) {
269 | continue;
270 | }
271 | for (var y = 0; y < maxY; y++){
272 | // Check that the cell exists
273 | if (copyBoard[x].hasOwnProperty(y)){
274 | if (!transposeBoard.hasOwnProperty(y)) {
275 | transposeBoard[y] = {};
276 | }
277 | transposeBoard[y][x] = copyBoard[x][y];
278 | }
279 | }
280 | }
281 | count = this.findIColChain(transposeBoard,type,i,isFive);
282 | return count;
283 | },
284 |
285 | /**
286 | * Find the number of i-length diagonal chains
287 | * going in a downwards direction
288 | *
289 | * @param {[type]} board [description]
290 | * @param {[type]} type [description]
291 | * @param {[type]} i [description]
292 | */
293 | findDIDiagChain: function(board,type,i) {
294 | // Vars to iterate through the boundaries of the board
295 | var copyBoard = this.makeBoardCopy(board);
296 | var maxX = Math.floor(engine.boardWidth/engine.boardCellSize);
297 | var maxY = Math.floor(engine.boardHeight/engine.boardCellSize);
298 | // Output variable, number of diagonal chains of length i
299 | var count = 0;
300 | // Length of potential chains
301 | // every potential chain will have its own index
302 | // and every broken chain will have undefined in its own index
303 | var chainNums = [];
304 | // Points to check with potential chains
305 | // pointsToCheck[i] should be the next point to check for chain with chainNums[i] length
306 | // pointsToCheck[i] will be undefined if chainNums[i] is undefined
307 | var pointsToCheck = [];
308 | // Iterate through the columns of the game board
309 | for (var x = 0; x < maxX; x++) {
310 | // Check that the column does not exist
311 | if (!copyBoard.hasOwnProperty(x)) {
312 | // Reset all diagonal chain statistics if there are any
313 | // because all your diagonal chains are broken now
314 | if (chainNums.length !== 0){
315 | // This sets all indices to undefined
316 | for (var j = 0; j < pointsToCheck.length; j++){
317 | delete chainNums[j];
318 | delete pointsToCheck[j];
319 | }
320 | }
321 | continue;
322 | }
323 | var column = copyBoard[x];
324 | for (var y = 0; y < maxY; y++){
325 | var pointFound = false;
326 | // check if the point is a point we're looking for
327 | for (var k = 0; k < pointsToCheck.length; k++){
328 | // ignore indicies that have been reset
329 | if (pointsToCheck[k] === undefined){
330 | continue;
331 | }
332 | var ptcX = pointsToCheck[k][0];
333 | var ptcY = pointsToCheck[k][1];
334 | if (x === ptcX && y === ptcY){
335 | pointFound = true;
336 | // If it is a point we're looking for and the cell doesn't exist
337 | // that means the chain is broken and we reset the stats for it
338 | if (!column.hasOwnProperty(y)){
339 | delete chainNums[k];
340 | delete pointsToCheck[k];
341 | continue;
342 | }
343 | // Otherwise, add to the appropriate chainNum,
344 | // check if chainNum is i and increment count accordingly,
345 | // change the next point to check to the next point in the diagonal chain
346 | else {
347 | if (column[y].type !== type) {
348 | delete chainNums[k];
349 | delete pointsToCheck[k];
350 | continue;
351 | }
352 | chainNums[k]++;
353 | if (chainNums[k] === i){
354 | count++;
355 | delete chainNums[k];
356 | delete pointsToCheck[k];
357 | }
358 | else{
359 | pointsToCheck[k] = [ptcX + 1,ptcY + 1];
360 | }
361 | }
362 | }
363 | }
364 | // if it's not a point we were looking for
365 | if (!pointFound){
366 | // if it doesn't exist, ignore it
367 | if (!column.hasOwnProperty(y)){
368 | continue;
369 | }
370 | // otherwise, add it as the start of a potential chain
371 | else{
372 | if (column[y].type === type) {
373 | chainNums.push(1);
374 | pointsToCheck.push([x+1,y+1]);
375 | }
376 | }
377 | }
378 | }
379 | }
380 | return count;
381 | },
382 |
383 | /**
384 | * Find the number of i-length diagonal chains
385 | * going in an upwards direction
386 | *
387 | * @param {[type]} board [description]
388 | * @param {[type]} type [description]
389 | * @param {[type]} i [description]
390 | */
391 | findUIDiagChain: function(board,type,i) {
392 | // Vars to iterate through the boundaries of the board
393 | var copyBoard = this.makeBoardCopy(board);
394 | var maxX = Math.floor(engine.boardWidth/engine.boardCellSize);
395 | var maxY = Math.floor(engine.boardHeight/engine.boardCellSize);
396 | // Output variable, number of diagonal chains of length i
397 | var count = 0;
398 | // Horizonally flipped board
399 | var flipBoard = {};
400 | for (var x = 0; x < maxX; x++) {
401 | // Check that the column exists
402 | if (!copyBoard.hasOwnProperty(x)) {
403 | continue;
404 | }
405 | for (var y = 0; y < maxY; y++){
406 | // Check that the cell exists
407 | if (copyBoard[x].hasOwnProperty(y)){
408 | if (!flipBoard.hasOwnProperty(maxX-x-1)) {
409 | flipBoard[maxX-x-1] = {};
410 | }
411 | flipBoard[maxX-x-1][y] = copyBoard[x][y];
412 | }
413 | }
414 | }
415 | count = this.findDIDiagChain(flipBoard,type,i);
416 | return count;
417 | },
418 |
419 | /**
420 | * Evaluate board function. Determines score using
421 | * the number of rows and columns that are 1,2,and
422 | * 3 pieces away from a win state.
423 | *
424 | * @param {Array} Game board to evaluate
425 | * @return {Integer} Score
426 | */
427 | evaluateBoard: function(board, type) {
428 | var numZeros = this.findIColChain(board,type,5,true) + this.findIRowChain(board,type,5,true) + this.findDIDiagChain(board,type,5) + this.findUIDiagChain(board,type,5);
429 | var numOnes = this.findIColChain(board,type,4,true)+ this.findIRowChain(board,type,4,true) + this.findDIDiagChain(board,type,4) + this.findUIDiagChain(board,type,4);
430 | var numTwos = this.findIColChain(board,type,3,false) + this.findIRowChain(board,type,3,false) + this.findDIDiagChain(board,type,3) + this.findUIDiagChain(board,type,3);
431 | var numThrees = this.findIColChain(board,type,2,false) + this.findIRowChain(board,type,2,false) + this.findDIDiagChain(board,type,2) + this.findUIDiagChain(board,type,2);
432 | var numFours = this.findIColChain(board,type,1,false) + this.findIRowChain(board,type,1,false) + this.findDIDiagChain(board,type,1) + this.findUIDiagChain(board,type,1);
433 | var score = numZeros * 1000000 + numOnes * 5000.0 + numTwos * 50.0 + numThrees * 5.0 + numFours * 1.0;
434 | return score;
435 | },
436 |
437 | // ======================================
438 | //
439 | // Utility Functions
440 | //
441 | // ======================================
442 | // Function to make a list of items unique
443 | uniq: function(items, key) {
444 | var set = {};
445 | return items.filter(function(item) {
446 | var k = key ? key.apply(item) : item;
447 | return k in set ? false : set[k] = true;
448 | });
449 | },
450 |
451 | // Function to make a hard copy of a given board
452 | makeBoardCopy: function(board) {
453 | var copy_board = {};
454 | // Copy the game board and retun the copy
455 | for (var x in board) {
456 | if (!board.hasOwnProperty(x)) {
457 | continue;
458 | }
459 | copy_board[x] = {};
460 | for (var y in board[x]) {
461 | copy_board[x][y] = board[x][y];
462 | }
463 | }
464 | return copy_board;
465 | },
466 |
467 | // Function to shuffle an array:
468 | // "http://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array"
469 | shuffleArray: function(array) {
470 | for (var i = array.length - 1; i > 0; i--) {
471 | var j = Math.floor(Math.random() * (i + 1));
472 | var temp = array[i];
473 | array[i] = array[j];
474 | array[j] = temp;
475 | }
476 | return array;
477 | },
478 |
479 | /**
480 | * Minimax functon for finding the best move on
481 | * a given game board
482 | *
483 | * @param {[type]} board [description]
484 | * @param {[type]} depth [description]
485 | * @param {[type]} alpha [description]
486 | * @param {[type]} beta [description]
487 | * @param { } [varname] [description]
488 | * @return {[type]} [description]
489 | */
490 | minimax: function(board, depth, alpha, beta, type) {
491 | var playerPieces = this.findTypeCells(board,type);
492 | playerPieces = playerPieces.concat(this.findTypeCells(board,1-type));
493 | var numPieces = playerPieces.length;
494 | var moves = [];
495 | if (playerPieces.length !== 0) {
496 | for (var i = 0; i < numPieces; i++) {
497 | // Get the x and y values of a player piece
498 | var x = playerPieces[i][0];
499 | var y = playerPieces[i][1];
500 | // Concatenate the empty cells around the piece into the move list
501 | moves = moves.concat(this.findEmptyAdjacent(board,x,y));
502 | }
503 | }
504 | // If no good spaces were found, use all empty spaces.
505 | if (moves.length === 0) {
506 | moves = this.findEmptyCells(board);
507 | }
508 | // Make sure the list of moves is unique and shuffle them
509 | moves = this.uniq(moves, [].join);
510 | moves = this.shuffleArray(moves);
511 |
512 | var bestMove = moves[0];
513 | var result = [alpha,bestMove];
514 | //========================
515 | // Minimax Algorithm
516 | //========================
517 | // Base Case:
518 | if (depth === 0) {
519 | result = [(this.evaluateBoard(board,type)-this.evaluateBoard(board,1-type)),moves.pop()];
520 | return result;
521 | }
522 | // Recursive Case:
523 | //var temp = [];
524 | var currentAlpha = alpha;
525 | while (moves.length > 0) {
526 | var freshBoard = this.makeBoardCopy(board);
527 | var testMove = moves.pop();
528 | // Get the x,y coordinates from the testMove
529 | var testX = testMove[0];
530 | var testY = testMove[1];
531 |
532 | // Create new game object.
533 | var object = new GameObject();
534 | object.type = type;
535 |
536 | // Add it to the fresh game board
537 | if (freshBoard[testX] === undefined) {
538 | freshBoard[testX] = {};
539 | }
540 | freshBoard[testX][testY] = object;
541 | // Go down the minimax tree and get the results
542 | var temp = this.minimax(freshBoard, depth-1, -beta, -currentAlpha, 1-type);
543 | var tempScore = -temp[0];
544 |
545 | // Update the alpha values
546 | if (tempScore > currentAlpha) {
547 | currentAlpha = tempScore;
548 | bestMove = testMove;
549 | }
550 | // Alpha-Beta Pruning
551 | if (currentAlpha > beta) {
552 | result = [currentAlpha,bestMove];
553 | return result;
554 | }
555 | }
556 | result = [currentAlpha,bestMove];
557 | return result;
558 | }
559 | };
560 |
561 | engine.addAI('minimaxAI', ai.run, ai);
562 | }, false);
563 |
--------------------------------------------------------------------------------