diff --git a/src/checkers/checkers_game.js b/src/checkers/checkers_game.js index 4ff8657..eb0215c 100644 --- a/src/checkers/checkers_game.js +++ b/src/checkers/checkers_game.js @@ -9,9 +9,11 @@ function CheckersGame(board) { this.moveEvaluationType = TableTop.Constants.moveEvalationTypeGameEvaluator; this.possibleNumPlayers = [2]; this.showNextPlayerScreen = false; + this.AIDifficulty = TableTop.Constants.AIDifficultyHard; this.hasMadeGame = false; this.firstMove = true; }; + inherits(CheckersGame, TableTop.Game); CheckersGame.prototype.createPlayer = function(name) { @@ -166,15 +168,14 @@ CheckersGame.prototype.isValidMove = function(token, oldTile, newTile) { it's not a valid move! */ - var p = this.getPlayerForToken(token); - + var p = this.getPlayerForToken(token); + if (this.getPlayerForToken(token) != player || newTile.color != TableTop.Constants.redColor || newTile.tokens[0]) { - return false; } - + return this.validNormalMove(token, oldPos, newPos, 1) || this.validJumpMove(token, oldPos, newPos); }; @@ -209,4 +210,43 @@ CheckersGame.prototype.playerDidWin = function(player) { }; +// returns all possible valid moves +CheckersGame.prototype.getValidMoves = function() { + + var validMoves = []; + + this.board.tokens.forEach(function(token) { + + var tile = this.board.findTileForToken(token); + + // for each possible destination tile... + this.board.tiles.forEach(function(destinationRow) { + destinationRow.forEach(function(destination) { + + if (this.isValidMove(token, tile, destination)) { + validMoves.push({ + token: token, + tile: tile, + destination: destination + }); + } + + }, this); + }, this); + }, this); + + + return validMoves; +}; + + +// takes in a player +// returns the score for the board based on the player passed in (ie. if the player +// has won it should return 10, if he loses should return -10) +CheckersGame.prototype.scoreBoard = function() { + var otherPlayer = this.players[this.getNextPlayer()]; + return 12 - otherPlayer.tokens.length; +}; + + module.exports = CheckersGame; diff --git a/src/checkers/style.css b/src/checkers/style.css index 712e58d..7823bc5 100644 --- a/src/checkers/style.css +++ b/src/checkers/style.css @@ -17,34 +17,34 @@ form { } select { - background: #FFE064; - width: 5%; - padding: 20px; - font-size: 4vw; - line-height: 1; - border: 0; - border-radius: 0; - height: 15%; - text-align: center; + background: #FFE064; + width: 5%; + padding: 20px; + font-size: 4vw; + line-height: 1; + border: 0; + border-radius: 0; + height: 15%; + text-align: center; } input { - background: #FFE064; - width: 15%; - padding: 10px; - font-size: 3vw; - line-height: 1; - border: 0; - border-radius: 0; - height: 15%; - text-align: center; - border-radius: 20px; + background: #FFE064; + width: 15%; + padding: 10px; + font-size: 3vw; + line-height: 1; + border: 0; + border-radius: 0; + height: 15%; + text-align: center; + border-radius: 20px; } canvas { - padding-left: 0; - padding-right: 0; - margin-left: auto; - margin-right: auto; - display: block; + padding-left: 0; + padding-right: 0; + margin-left: auto; + margin-right: auto; + display: block; }*/ \ No newline at end of file diff --git a/tabletop/core/aiplayer.js b/tabletop/core/aiplayer.js new file mode 100644 index 0000000..efe2a21 --- /dev/null +++ b/tabletop/core/aiplayer.js @@ -0,0 +1,70 @@ +var inherits = require('util').inherits; +var Player = require("./Player.js"); +var Gridboard = require("./grid_board.js"); +var c = require("./ttConstants"); + +/** + * AI Player + * @constructor + * @extends {Player} + * @param {string} difficulty - difficulty of AI. Should be one of the values of TableTop.Constants.validAIDifficulties. +*/ + +function AIPlayer(name, color, id, difficulty) { + Player.call(this, name, color, id); + + // safety check + if (c.validAIDifficulties.indexOf(difficulty) == -1) + difficulty = c.AIDifficultyEasy; + + this.difficulty = difficulty; +} + +inherits(AIPlayer, Player); + + +AIPlayer.prototype.isAI = function() { + return true; +}; + + +AIPlayer.prototype.generateMove = function(game) { + + var moves = game.getValidMoves(); + var results = []; + + moves.forEach(function(move) { + + var gameCopy = game.copyGameStatus(game); + gameCopy.proposedMove = game.copyMoveForGame(move, gameCopy); + gameCopy.executeMove(); + results.push({ + move: move, + score: gameCopy.scoreBoard() + }); + }); + + var result = this.pickMove(results); + return result.move; +}; + +AIPlayer.prototype.pickMove = function(results) { + + // sort descending based on score + var resultsSorted = results.sort(function(a, b) { + return b.score - a.score; + }); + + var range; + if (this.difficulty == c.AIDifficultyHard) { + range = 1; + } else if (this.difficulty == c.AIDifficultyMedium) { + range = Math.min(results.length, 3); + } else if (this.difficulty == c.AIDifficultyEasy) { + range = Math.min(results.length, 5); + } + + return resultsSorted[Math.floor(Math.random()*range)]; +}; + +module.exports = AIPlayer; diff --git a/tabletop/core/component.js b/tabletop/core/component.js index cf35e9e..c67a495 100644 --- a/tabletop/core/component.js +++ b/tabletop/core/component.js @@ -53,10 +53,10 @@ Component.prototype.subscribe = function(callback) { * @return {void} */ Component.prototype.propagate = function(child) { - var context = this; - child.subscribe(function(messageObj) { - context.sendMessage(messageObj.text, messageObj.type, messageObj.sender, messageObj.clientID); - }); -} + var context = this; + child.subscribe(function(messageObj) { + context.sendMessage(messageObj.text, messageObj.type, messageObj.sender, messageObj.clientID); + }); +}; -module.exports = Component; \ No newline at end of file +module.exports = Component; diff --git a/tabletop/core/game.js b/tabletop/core/game.js index 4bb8143..e8d1195 100644 --- a/tabletop/core/game.js +++ b/tabletop/core/game.js @@ -3,6 +3,10 @@ var ManualTurn = require("./manual_turn.js"); var Component = require("./component.js"); var inherits = require('util').inherits; var _ = require('lodash'); +var Tile = require('./tile.js'); +var Token = require('./token.js'); +var Player = require('./player.js'); +var Gridboard = require('./grid_board.js'); /** * The Game class @@ -23,6 +27,7 @@ function Game(board) { this.showNextPlayerScreen = true; this.playerColors = [0xFF0000, 0x000000, 0x00FF00, 0x0000FF, 0xFF00FF]; this.currentPlayer = 0; + this.AIDifficulty = c.AIDifficultyEasy; this.gameID = null; this.clientPlayerID = -1; // id of the player of THIS client }; @@ -268,6 +273,15 @@ Game.prototype.nextPlayer = function() { this.currentPlayer = (this.currentPlayer + 1) % this.players.length; }; +/** + * Returns the next player, but does not switch like nextPlayer does + * Override to provide more logic on determining the next player + * @returns {int} The index of the next player. +*/ +Game.prototype.getNextPlayer = function() { + return (this.currentPlayer + 1) % this.players.length; +}; + /** * Set the destination for a proposed move * @param {Tile} tile - the tile to move to @@ -403,4 +417,101 @@ Game.prototype.destroyToken = function(token) { this.board.destroyToken(token); }; -module.exports = Game; \ No newline at end of file + +// Create copy of game and manually copy over +// tiles/tokens to remove references +Game.prototype.copyGameStatus = function(game) { + + //var newGame = $.extend(true, {}, game); + var newGame = Object.create(this); + newGame.board = Object.create(this.board); + newGame.board.tokens = []; + newGame.board.tiles = []; + newGame.players = []; + var i, j = 0; + + // copy players, clear token arrays + for (i = 0; i < game.players.length; i++) { + newGame.players.push(new Player()); + Object.assign(newGame.players[i], game.players[i]); + newGame.players[i].tokens = []; + } + + // copy tiles, clear token arrays + if (game.board instanceof Gridboard) { + for (i = 0; i < game.board.tiles.length; i++) { + newGame.board.tiles.push([]); + for (j = 0; j < game.board.tiles[i].length; j++) { + newGame.board.tiles[i].push(new Tile({})); + Object.assign(newGame.board.tiles[i][j], game.board.tiles[i][j]); + newGame.board.tiles[i][j].clearTokens(); + } + } + } else { + for (i = 0; i < game.board.tiles.length; i++) { + newGame.board.tiles.push(new Tile({})); + Object.assign(newGame.board.tiles[i], game.board.tiles[i]); + newGame.board.tiles[i].clearTokens(); + } + } + + // copy tokens, and assign to proper tile and player + for (i = 0; i < game.board.tokens.length; i++) { + var token = game.board.tokens[i]; + newGame.board.tokens.push(new Token()); + var newToken = newGame.board.tokens[i]; + Object.assign(newToken, token); + + // assign tile to proper player if it's owned, or do nothing + for (j = 0; j < game.players.length; j++) { + if (game.players[j].tokens.indexOf(token) >= 0) { + newGame.players[j].tokens.push(newToken); + } + } + + // if a token is on a tile, add it to the tile's list of tokens + var tile = game.board.findTileForToken(token); + var tilePos = game.board.getTilePosition(tile); + if (!tile || !tilePos) continue; + + if (game.board instanceof Gridboard) + newGame.board.tiles[tilePos.x][tilePos.y].addToken(newGame.board.tokens[i]); + else + newGame.board.tiles[tilePos].addToken(newGame.board.tokens[i]); + + } + + return newGame; +}; + +Game.prototype.copyMoveForGame = function(move, gameCopy) { + + var newMove = {}; + Object.keys(move).forEach(function(key) { + + var obj = move[key]; + if (obj instanceof Token) { + var tokenIdx = this.board.tokens.indexOf(obj); + var tokenCp = gameCopy.board.tokens[tokenIdx]; + newMove[key] = tokenCp; + } else if (obj instanceof Tile) { + var tilePos = this.board.getTilePosition(obj); + var tileCp; + if (this.board instanceof Gridboard) + tileCp = gameCopy.board.tiles[tilePos.x][tilePos.y]; + else + tileCp = gameCopy.board.tiles[tilePos]; + + newMove[key] = tileCp; + } + }, this); + + return newMove; +}; + +Game.prototype.getValidMoves = function() { + return []; +}; + + +module.exports = Game; diff --git a/tabletop/core/grid_board.js b/tabletop/core/grid_board.js index d293f3a..da9e69b 100644 --- a/tabletop/core/grid_board.js +++ b/tabletop/core/grid_board.js @@ -13,7 +13,7 @@ function GridBoard(width, height) { Board.call(this); for (var i = 0; i < width; i++) { this.tiles[i] = Array(this.height); - } + } this.width = width; this.height = height; @@ -69,6 +69,7 @@ GridBoard.prototype.moveTokenToTile = function(token, tile) { GridBoard.prototype.destroyToken = function(token) { var tile = this.findTileForToken(token); tile.removeToken(token); + token.isDead = true; }; module.exports = GridBoard; diff --git a/tabletop/core/index.js b/tabletop/core/index.js index 9f56563..39fe011 100644 --- a/tabletop/core/index.js +++ b/tabletop/core/index.js @@ -15,7 +15,7 @@ var core = Object.assign({ NextPlayerView: require('./next_player_view'), Player: require('./player'), StartView: require('./start_view'), - StyleSheets: require('../../src/checkers/style.css'), +// StyleSheets: require('../../src/checkers/style.css'), Tile: require('./tile'), Token: require('./token'), Trade: require('./trade'), diff --git a/tabletop/core/manual_turn.js b/tabletop/core/manual_turn.js index 295adad..ec4f81a 100644 --- a/tabletop/core/manual_turn.js +++ b/tabletop/core/manual_turn.js @@ -20,7 +20,6 @@ function ManualTurn(game) { uninitialized: { start : function() { this.transition("waitingForMove"); - } }, @@ -50,20 +49,30 @@ function ManualTurn(game) { // 2 waitingForMove: { _onEnter: function() { - - }, + if (this.game.getCurrentPlayer().isAI()) { + var AIMove = this.game.getCurrentPlayer().generateMove(this.game); + game.proposedMove = AIMove; + this.handle("makeMove"); + } + }, makeMove : function() { - if (game.hasValidMove()) { - game.executeMove(); - this.transition("postTurn"); + + if (game.hasValidMove()) { + if (this.game.getCurrentPlayer().isAI()) { + var turnMap = this; + setTimeout(function() { game.executeMove(); turnMap.transition("postTurn"); }, 500); + } else { + game.executeMove(); + this.transition("postTurn"); + } } else { alert("Invalid move. Try again."); console.log("Invalid move. Try again."); } } }, - + // 3 postTurn: { _onEnter : function() { diff --git a/tabletop/core/player.js b/tabletop/core/player.js index b190354..65333a5 100644 --- a/tabletop/core/player.js +++ b/tabletop/core/player.js @@ -42,6 +42,10 @@ Player.prototype.destroyToken = function(token) { token.isDead = true; }; +Player.prototype.isAI = function() { + return false; +}; + Player.prototype.getJSONString = function() { var tokenArray = []; @@ -55,7 +59,7 @@ Player.prototype.getJSONString = function() { color: this.color, id: this.id, tokens: tokenArray - } + }; }; Player.prototype.createFromJSONString = function(data) { diff --git a/tabletop/core/start_view.js b/tabletop/core/start_view.js index 4f35f2f..bae00a2 100644 --- a/tabletop/core/start_view.js +++ b/tabletop/core/start_view.js @@ -1,5 +1,6 @@ var c = require("./ttConstants.js"); var Player = require("./player.js"); +var AIPlayer = require("./aiplayer.js"); var Component = require("./component"); var inherits = require('util').inherits; @@ -118,9 +119,13 @@ StartView.prototype.handleButtonClick = function() { } else { playerName = document.getElementById('player' + (i+1) + 'Name').value; } - players[i] = new Player(playerName, this.game.playerColors[i]); + + if (playerName.toUpperCase() === "AI") + players[i] = new AIPlayer(playerName, this.game.playerColors[i], null, this.game.AIDifficulty); + else + players[i] = new Player(playerName, this.game.playerColors[i]); } - + this.game.setPlayers(players); this.game.updateState("play"); }; @@ -137,4 +142,4 @@ StartView.prototype.drawMessage = function() { }; -module.exports = StartView; \ No newline at end of file +module.exports = StartView; diff --git a/tabletop/core/tile.js b/tabletop/core/tile.js index bc5f2b2..e904332 100644 --- a/tabletop/core/tile.js +++ b/tabletop/core/tile.js @@ -24,7 +24,7 @@ inherits(Tile, Component); * @returns {void} */ Tile.prototype.clearTokens = function() { - this.tokens = null; + this.tokens = []; }; /** diff --git a/tabletop/core/ttConstants.js b/tabletop/core/ttConstants.js index ac64b28..b431d0d 100644 --- a/tabletop/core/ttConstants.js +++ b/tabletop/core/ttConstants.js @@ -50,5 +50,17 @@ ttConstants.moveEvaluationTypeLandingAction = 1; // after game.isValidMove() verfies move is legal ttConstants.moveEvaluationTypeGameEvaluator = 2; +// ai difficulties +ttConstants.AIDifficultyEasy = 1; +ttConstants.AIDifficultyMedium = 2; +ttConstants.AIDifficultyHard = 3; +ttConstants.validAIDifficulties = + [ + ttConstants.AIDifficultyEasy, + ttConstants.AIDifficultyMedium, + ttConstants.AIDifficultyHard + ]; + + module.exports = ttConstants; diff --git a/tutorials/markdown/ai.md b/tutorials/markdown/ai.md new file mode 100644 index 0000000..4571e29 --- /dev/null +++ b/tutorials/markdown/ai.md @@ -0,0 +1,71 @@ +# Using AI + +## How to use AI Player + +Simply give one or more of the players the name "ai" (case insensitive) + +## How to implement AI in your game + +### Optional + +Set the difficulty using this.AIDifficulty. Defaults to AIDifficultyEasy. + +### Required + +You need to override two methods. + +First, override ```getValidMoves()```. This function should return all the possible moves in the same format used for game.proposedMove. For moveTypeManual games, such as Checkers, the proposed move takes the format +``` +{ + token: token, + tile: tile, + destination: destination +} +``` + +Here's our implemetation for Checkers: + +``` +CheckersGame.prototype.getValidMoves = function() { + + var validMoves = []; + + this.board.tokens.forEach(function(token) { + + var tile = this.board.findTileForToken(token); + + // for each possible destination tile... + this.board.tiles.forEach(function(destinationRow) { + destinationRow.forEach(function(destination) { + + if (this.isValidMove(token, tile, destination)) + validMoves.push({ + token: token, + tile: tile, + destination: destination + }); + + + }, this); + }, this); + }, this); + + + return validMoves; +}; +``` + +Second, you need to override scoreBoard. This can be as simple or as advanced as you'd like. For a game like Checkers, you could value the board based on how many tokens are remaining (shown below). If you wanted more advanced AI, you could play higher value on defending pieces. + +``` +CheckersGame.prototype.scoreBoard = function(player) { + var otherPlayer = this.players[this.getNextPlayer()]; + return 12 - otherPlayer.tokens.length; +}; +``` + + +### Advanced + +For the AI to properly work, it needs to be able to understand the objects in your proposedMove object in order to properly convert into objects that the game copies can understand. By default, the AI system copies all tokens and tiles over using their appropriate keys. If your proposed move contains objects that aren't tiles or tokens, then you'll need to override ```game.copyMoveForGame(move, gameCopy)``` +