├── .gitignore ├── CNAME ├── README.md ├── app.js ├── assets ├── blacksmith.png ├── forge.png ├── grass5.png ├── material_depot2.png ├── miner.png ├── ore2.png └── tool_deposit2.png ├── gameplay ├── actions │ ├── crafttool.js │ ├── deliverore.js │ ├── delivertool.js │ ├── getore.js │ ├── gettool.js │ └── mineore.js ├── ai │ ├── action.js │ ├── agent.js │ ├── node.js │ └── planner.js ├── data │ ├── material_storage.js │ └── tools_deposit.js ├── entities │ ├── blacksmith.js │ └── miner.js ├── game_state.js └── states │ ├── action.js │ ├── idle.js │ └── moving.js ├── index.html ├── libs └── phaser.min.js └── utils ├── positions.js └── state_machine.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | ### Windows ### 4 | # Windows image file caches 5 | Thumbs.db 6 | ehthumbs.db 7 | 8 | # Folder config file 9 | Desktop.ini 10 | 11 | # Recycle Bin used on file shares 12 | $RECYCLE.BIN/ 13 | 14 | # Windows Installer files 15 | *.cab 16 | *.msi 17 | *.msm 18 | *.msp 19 | 20 | # Windows shortcuts 21 | *.lnk 22 | 23 | 24 | ### OSX ### 25 | .DS_Store 26 | .AppleDouble 27 | .LSOverride 28 | 29 | # Icon must end with two \r 30 | Icon 31 | 32 | 33 | # Thumbnails 34 | ._* 35 | 36 | # Files that might appear in the root of a volume 37 | .DocumentRevisions-V100 38 | .fseventsd 39 | .Spotlight-V100 40 | .TemporaryItems 41 | .Trashes 42 | .VolumeIcon.icns 43 | 44 | # Directories potentially created on remote AFP share 45 | .AppleDB 46 | .AppleDesktop 47 | Network Trash Folder 48 | Temporary Items 49 | .apdisk 50 | 51 | 52 | ### Linux ### 53 | *~ 54 | 55 | # KDE directory preferences 56 | .directory 57 | 58 | # Linux trash folder which might appear on any partition or disk 59 | .Trash-* 60 | 61 | 62 | ### Node ### 63 | # Logs 64 | logs 65 | *.log 66 | 67 | # Runtime data 68 | pids 69 | *.pid 70 | *.seed 71 | 72 | # Directory for instrumented libs generated by jscoverage/JSCover 73 | lib-cov 74 | 75 | # Coverage directory used by tools like istanbul 76 | coverage 77 | 78 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 79 | .grunt 80 | 81 | # node-waf configuration 82 | .lock-wscript 83 | 84 | # Compiled binary addons (http://nodejs.org/api/addons.html) 85 | build/Release 86 | 87 | # Dependency directory 88 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 89 | node_modules 90 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | goap.kasoki.de 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GOAP 2 | 3 | Simple Goal Oriented Action Planning demo written in Javascript with Phaser for studies. 4 | 5 | ## Demo 6 | 7 | [Click here for an demo](http://goap.kasoki.de) 8 | 9 | ## Asset Attributions 10 | 11 | All assets are made by Daniel Cook from the "Planet Cute" asset pack. 12 | 13 | ## License 14 | 15 | MIT 16 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | game = new Phaser.Game(1024, 800, Phaser.AUTO, 'game'); 2 | game.state.add('game', GameState, true); 3 | -------------------------------------------------------------------------------- /assets/blacksmith.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomicptr/goap/b7d1b322ec8c1d5ec5fa7acdd261c4cefdec1b05/assets/blacksmith.png -------------------------------------------------------------------------------- /assets/forge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomicptr/goap/b7d1b322ec8c1d5ec5fa7acdd261c4cefdec1b05/assets/forge.png -------------------------------------------------------------------------------- /assets/grass5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomicptr/goap/b7d1b322ec8c1d5ec5fa7acdd261c4cefdec1b05/assets/grass5.png -------------------------------------------------------------------------------- /assets/material_depot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomicptr/goap/b7d1b322ec8c1d5ec5fa7acdd261c4cefdec1b05/assets/material_depot2.png -------------------------------------------------------------------------------- /assets/miner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomicptr/goap/b7d1b322ec8c1d5ec5fa7acdd261c4cefdec1b05/assets/miner.png -------------------------------------------------------------------------------- /assets/ore2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomicptr/goap/b7d1b322ec8c1d5ec5fa7acdd261c4cefdec1b05/assets/ore2.png -------------------------------------------------------------------------------- /assets/tool_deposit2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomicptr/goap/b7d1b322ec8c1d5ec5fa7acdd261c4cefdec1b05/assets/tool_deposit2.png -------------------------------------------------------------------------------- /gameplay/actions/crafttool.js: -------------------------------------------------------------------------------- 1 | CraftToolAction = function() { 2 | Action.call(this, "CraftTool", 4); 3 | 4 | this._position = positions.forge; 5 | 6 | this.addPrecondition("HasTool", false); 7 | this.addPrecondition("HasOre", true); 8 | this.addEffect("HasTool", true); 9 | }; 10 | 11 | CraftToolAction.prototype = Object.create(Action.prototype); 12 | -------------------------------------------------------------------------------- /gameplay/actions/deliverore.js: -------------------------------------------------------------------------------- 1 | DeliverOreAction = function() { 2 | Action.call(this, "DeliverOre", 1); 3 | 4 | this._position = positions.material_depot; 5 | 6 | this.addPrecondition("HasOre", true); 7 | this.addEffect("HasOre", false); 8 | } 9 | 10 | DeliverOreAction.prototype = Object.create(Action.prototype); 11 | 12 | DeliverOreAction.prototype.execute = function() { 13 | MaterialStorage.Ore++; 14 | } 15 | -------------------------------------------------------------------------------- /gameplay/actions/delivertool.js: -------------------------------------------------------------------------------- 1 | DeliverToolAction = function() { 2 | Action.call(this, "DeliverTool", 1); 3 | 4 | this._position = positions.tool_deposit; 5 | 6 | this.addPrecondition("HasTool", true); 7 | this.addEffect("HasTool", false); 8 | } 9 | 10 | DeliverToolAction.prototype = Object.create(Action.prototype); 11 | 12 | DeliverToolAction.prototype.execute = function() { 13 | ToolsDeposit.PickAxe++ 14 | } 15 | -------------------------------------------------------------------------------- /gameplay/actions/getore.js: -------------------------------------------------------------------------------- 1 | GetOreAction = function() { 2 | Action.call(this, "GetOre", 1); 3 | 4 | this._position = positions.material_depot; 5 | 6 | this.addEffect("HasOre", true); 7 | } 8 | 9 | GetOreAction.prototype = Object.create(Action.prototype); 10 | 11 | GetOreAction.prototype.canExecute = function() { 12 | return MaterialStorage.Ore > 0; 13 | } 14 | 15 | GetOreAction.prototype.execute = function() { 16 | MaterialStorage.Ore--; 17 | } 18 | -------------------------------------------------------------------------------- /gameplay/actions/gettool.js: -------------------------------------------------------------------------------- 1 | GetToolAction = function() { 2 | Action.call(this, "GetTool", 1); 3 | 4 | this._position = positions.tool_deposit; 5 | 6 | this.addPrecondition("HasTool", false); 7 | this.addEffect("HasTool", true); 8 | } 9 | 10 | GetToolAction.prototype = Object.create(Action.prototype); 11 | 12 | GetToolAction.prototype.canExecute = function() { 13 | return ToolsDeposit.PickAxe > 0; 14 | } 15 | 16 | GetToolAction.prototype.execute = function() { 17 | ToolsDeposit.PickAxe--; 18 | } 19 | -------------------------------------------------------------------------------- /gameplay/actions/mineore.js: -------------------------------------------------------------------------------- 1 | MineOreAction = function() { 2 | Action.call(this, "MineOre", 4); 3 | 4 | this._mineCounter = 0; 5 | 6 | this._position = positions.ore; 7 | 8 | this.addPrecondition("HasTool", true); 9 | this.addPrecondition("HasOre", false); 10 | this.addEffect("HasOre", true); 11 | }; 12 | 13 | MineOreAction.prototype = Object.create(Action.prototype); 14 | 15 | MineOreAction.prototype.execute = function() { 16 | this._mineCounter++; 17 | 18 | if(this._mineCounter >= 4) { 19 | console.log("Tool broke while mining :("); 20 | this.agent.setState("HasTool", false); 21 | this._mineCounter = 0; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /gameplay/ai/action.js: -------------------------------------------------------------------------------- 1 | var Action = function(name, cost) { 2 | this.name = name; 3 | this.agent = undefined; 4 | this.effects = {}; 5 | this.preconditions = {}; 6 | 7 | this.cost = cost; 8 | }; 9 | 10 | Action.prototype.addEffect = function(name, value) { 11 | this.effects[name] = value; 12 | }; 13 | 14 | Action.prototype.addPrecondition = function(name, value) { 15 | this.preconditions[name] = value; 16 | }; 17 | 18 | Action.prototype.execute = function() { 19 | console.warn(this.name + ": You might want to override execute for me :P"); 20 | }; 21 | 22 | Action.prototype.canExecute = function() { 23 | return true; 24 | } 25 | -------------------------------------------------------------------------------- /gameplay/ai/agent.js: -------------------------------------------------------------------------------- 1 | var Agent = function(name) { 2 | this.name = name; 3 | this.actions = []; 4 | this.currentActions = []; 5 | 6 | this.state = {}; 7 | 8 | this.sm = new StateMachine(); 9 | 10 | this.sm.add("idle", new IdleState(this)); 11 | this.sm.add("moving", new MovingState(this)); 12 | this.sm.add("action", new ActionState(this)); 13 | 14 | this.sm.enter("idle"); 15 | }; 16 | 17 | Agent.prototype.update = function() { 18 | this.sm.update(); 19 | }; 20 | 21 | Agent.prototype.addAction = function(action) { 22 | action.agent = this; 23 | 24 | this.actions.push(action); 25 | }; 26 | 27 | Agent.prototype.applyAction = function(action) { 28 | for(var effect in action.effects) { 29 | this.setState(effect, action.effects[effect]); 30 | } 31 | } 32 | 33 | Agent.prototype.setState = function(name, value) { 34 | this.state[name] = value; 35 | }; 36 | 37 | Agent.prototype.is = function(name, value) { 38 | return this.state[name] == value; 39 | }; 40 | 41 | Agent.prototype.getUsableActions = function() { 42 | // get all actions with cleared preconditions 43 | return this.actions.filter(function(action) { 44 | return action.canExecute(); 45 | }); 46 | }; 47 | -------------------------------------------------------------------------------- /gameplay/ai/node.js: -------------------------------------------------------------------------------- 1 | var Node = function(parent, action, cost, state) { 2 | this.parent = parent; 3 | this.action = action; 4 | this.cost = cost; 5 | this.state = state; 6 | }; 7 | -------------------------------------------------------------------------------- /gameplay/ai/planner.js: -------------------------------------------------------------------------------- 1 | var Planner = function() { 2 | 3 | }; 4 | 5 | Planner.prototype.plan = function(agent, goal) { 6 | var root = new Node(null, null, 0, agent.state); 7 | 8 | var leaves = []; 9 | 10 | var found = this._buildGraph(root, leaves, agent.actions, goal); 11 | 12 | var cheapest = leaves.sort(function(a, b) { 13 | return a.cost < b.cost; 14 | })[0]; 15 | 16 | var plan = []; 17 | 18 | var node = cheapest; 19 | 20 | while(node) { 21 | if(node.action) { 22 | plan.unshift(node.action); 23 | } 24 | 25 | node = node.parent; 26 | } 27 | 28 | console.log(plan); 29 | 30 | return plan; 31 | }; 32 | 33 | Planner.prototype._buildGraph = function(parent, leaves, actions, goal) { 34 | var foundOne = false; 35 | 36 | var that = this; 37 | 38 | actions.forEach(function(action) { 39 | if(that._inState(parent.state, action.preconditions)) { 40 | var currentState = that._applyState(parent.state, action.effects); 41 | var node = new Node(parent, action, parent.cost + action.cost, currentState); 42 | 43 | if(currentState[goal.name] == goal.value) { 44 | leaves.push(node); 45 | foundOne = true; 46 | } else { 47 | var index = actions.indexOf(action); 48 | 49 | var subset = actions.slice(0, index).concat(actions.slice(index + 1, actions.length)); 50 | 51 | var found = that._buildGraph(node, leaves, subset, goal); 52 | 53 | if(found) { 54 | foundOne = true; 55 | } 56 | } 57 | } 58 | }); 59 | 60 | return foundOne; 61 | }; 62 | 63 | Planner.prototype._inState = function(state, preconditions) { 64 | var clear = true; 65 | for(var cond in preconditions) { 66 | clear = clear && (state[cond] == preconditions[cond]); 67 | } 68 | 69 | return clear; 70 | }; 71 | 72 | Planner.prototype._applyState = function(old, newState) { 73 | var result = []; 74 | 75 | for(var val in old) { 76 | result[val] = old[val]; 77 | } 78 | 79 | for(var effect in newState) { 80 | result[effect] = newState[effect]; 81 | } 82 | 83 | return result; 84 | }; 85 | -------------------------------------------------------------------------------- /gameplay/data/material_storage.js: -------------------------------------------------------------------------------- 1 | var MaterialStorage = { 2 | Ore: 1 3 | }; 4 | -------------------------------------------------------------------------------- /gameplay/data/tools_deposit.js: -------------------------------------------------------------------------------- 1 | ToolsDeposit = { 2 | PickAxe: 0 3 | }; 4 | -------------------------------------------------------------------------------- /gameplay/entities/blacksmith.js: -------------------------------------------------------------------------------- 1 | Blacksmith = function(game, options) { 2 | Agent.call(this, "Blacksmith"); 3 | 4 | this._game = game; 5 | this._sprite = this._game.add.sprite(options.x, options.y, "blacksmith"); 6 | this._game.physics.arcade.enable(this._sprite); 7 | 8 | this._current_plan = []; 9 | this._target = null; 10 | 11 | this.setState("HasOre", false); 12 | this.setState("HasTool", false); 13 | 14 | this.addAction(new CraftToolAction(this)); 15 | this.addAction(new DeliverToolAction(this)); 16 | this.addAction(new GetOreAction(this)); 17 | }; 18 | 19 | Blacksmith.prototype = Object.create(Agent.prototype); 20 | 21 | Blacksmith.prototype.plan = function() { 22 | var planner = new Planner(); 23 | 24 | var plan = planner.plan(this, { 25 | name: "HasTool", 26 | value: true 27 | }); 28 | 29 | return plan; 30 | } 31 | -------------------------------------------------------------------------------- /gameplay/entities/miner.js: -------------------------------------------------------------------------------- 1 | Miner = function(game, options) { 2 | Agent.call(this, "Miner"); 3 | 4 | this._game = game; 5 | this._sprite = this._game.add.sprite(options.x, options.y, "miner"); 6 | this._game.physics.arcade.enable(this._sprite); 7 | 8 | this._current_plan = []; 9 | this._target = null; 10 | 11 | this.setState("HasOre", false); 12 | this.setState("HasTool", false); 13 | 14 | this.addAction(new MineOreAction()); 15 | this.addAction(new DeliverOreAction()); 16 | this.addAction(new GetToolAction()); 17 | }; 18 | 19 | Miner.prototype = Object.create(Agent.prototype); 20 | 21 | Miner.prototype.plan = function() { 22 | var planner = new Planner(); 23 | 24 | var plan = planner.plan(this, { 25 | name: "HasOre", 26 | value: true 27 | }); 28 | 29 | return plan; 30 | } 31 | -------------------------------------------------------------------------------- /gameplay/game_state.js: -------------------------------------------------------------------------------- 1 | var GameState = function(game) { 2 | }; 3 | 4 | GameState.prototype.preload = function() { 5 | this.game.load.image("miner", "assets/miner.png"); 6 | this.game.load.image("blacksmith", "assets/blacksmith.png"); 7 | 8 | this.game.load.image("grass", "assets/grass5.png"); 9 | this.game.load.image("ore", "assets/ore2.png"); 10 | this.game.load.image("tool_deposit", "assets/tool_deposit2.png"); 11 | this.game.load.image("material_depot", "assets/material_depot2.png"); 12 | this.game.load.image("forge", "assets/forge.png"); 13 | }; 14 | 15 | GameState.prototype.create = function() { 16 | for(var i = 0; i < 20; i++) { 17 | for(var j = 0; j < 15; j++) { 18 | this.game.add.sprite(i * 63, j * 64, "grass"); 19 | } 20 | } 21 | 22 | this.game.add.sprite(positions.ore.x, positions.ore.y, "ore"); 23 | this.game.add.sprite(positions.tool_deposit.x, positions.tool_deposit.y, "tool_deposit"); 24 | this.game.add.sprite(positions.material_depot.x, positions.material_depot.y, "material_depot"); 25 | this.game.add.sprite(positions.forge.x, positions.forge.y, "forge"); 26 | 27 | this.miner = new Miner(this.game, positions.miner_spawn); 28 | this.blacksmith = new Blacksmith(this.game, positions.blacksmith_spawn); 29 | 30 | this.oreText = game.add.text(5, 5, "Ore: " + MaterialStorage.Ore, { 31 | font: "14pt Helvetica", 32 | fill: "white" 33 | }); 34 | 35 | this.pickAxeText = game.add.text(5, 25, "Pick Axes: " + ToolsDeposit.PickAxe, { 36 | font: "14pt Helvetica", 37 | fill: "white" 38 | }); 39 | }; 40 | 41 | GameState.prototype.update = function() { 42 | this.miner.update(); 43 | this.blacksmith.update(); 44 | 45 | this.oreText.setText("Ore: " + MaterialStorage.Ore); 46 | this.pickAxeText.setText("Pick Axes: " + ToolsDeposit.PickAxe); 47 | }; 48 | -------------------------------------------------------------------------------- /gameplay/states/action.js: -------------------------------------------------------------------------------- 1 | var ActionState = function(entity) { 2 | this._entity = entity; 3 | this._waiting = false; 4 | this._last_action = null; 5 | this._timeout_set = false; 6 | }; 7 | 8 | ActionState.prototype.enter = function() { 9 | console.log(this._entity.name + " enters Action state"); 10 | }; 11 | 12 | ActionState.prototype.leave = function() { 13 | console.log(this._entity.name + " leaves Action state"); 14 | }; 15 | 16 | ActionState.prototype.update = function() { 17 | var action = this._waiting ? null : this._entity._current_plan.shift(); 18 | 19 | if(action || this._last_action) { 20 | this._waiting = true; 21 | this._last_action = action ? action : this._last_action; 22 | 23 | action = this._last_action; 24 | 25 | if(action.canExecute()) { 26 | var cost = action.cost; 27 | var that = this; 28 | 29 | if(!this._timeout_set) { 30 | this._timeout_set = true; 31 | // wait, apply and move to the next one (if there is one) 32 | setTimeout(function() { 33 | action.execute(); // execute action, might break tools or something like this 34 | that._entity.applyAction(action); 35 | 36 | that._waiting = false; 37 | that._last_action = null; 38 | that._timeout_set = false; 39 | 40 | if(that._entity._current_plan.length > 0) { 41 | that._entity.sm.enter("moving"); 42 | } else { 43 | that._entity.sm.enter("idle"); 44 | } 45 | }, 500 * cost); // 1 cost = 0.5s 46 | } 47 | } 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /gameplay/states/idle.js: -------------------------------------------------------------------------------- 1 | var IdleState = function(entity) { 2 | this._entity = entity; 3 | }; 4 | 5 | IdleState.prototype.enter = function() { 6 | console.log(this._entity.name + " enters Idle state"); 7 | }; 8 | 9 | IdleState.prototype.leave = function() { 10 | console.log(this._entity.name + " leaves Idle state"); 11 | }; 12 | 13 | IdleState.prototype.update = function() { 14 | var plan = this._entity.plan(); 15 | 16 | if(plan.length > 0) { 17 | this._entity._current_plan = plan; 18 | this._entity.sm.enter("moving"); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /gameplay/states/moving.js: -------------------------------------------------------------------------------- 1 | var MovingState = function(entity) { 2 | this._entity = entity; 3 | }; 4 | 5 | MovingState.prototype.enter = function() { 6 | console.log(this._entity.name + " enters Moveing state"); 7 | 8 | this._entity._target = this._entity._current_plan[0]._position; 9 | this._entity._sprite.body.moves = true; 10 | }; 11 | 12 | MovingState.prototype.leave = function() { 13 | console.log(this._entity.name + " leaves Moveing state"); 14 | }; 15 | 16 | MovingState.prototype.update = function() { 17 | if(this._entity._target) { 18 | var t = this._entity._target; 19 | this._entity._game.physics.arcade.moveToXY(this._entity._sprite, t.x, t.y, 240); 20 | 21 | var p = this._entity._sprite.position; 22 | 23 | var dist = Phaser.Math.distance(p.x, p.y, t.x, t.y); 24 | 25 | if(dist <= 10) { 26 | this._entity._sprite.body.moves = false; 27 | this._entity._target = null; 28 | this._entity.sm.enter("action"); 29 | } 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 |