├── 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 | --------------------------------------------------------------------------------