-
-
Notifications
You must be signed in to change notification settings - Fork 554
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: restored GOAP functionality, related to #680
- Loading branch information
Showing
3 changed files
with
277 additions
and
0 deletions.
There are no files selected for viewing
77 changes: 77 additions & 0 deletions
77
fxgl-entity/src/main/kotlin/com/almasb/fxgl/ai/goap/GoapAction.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
/* | ||
* FXGL - JavaFX Game Library. The MIT License (MIT). | ||
* Copyright (c) AlmasB ([email protected]). | ||
* See LICENSE for details. | ||
*/ | ||
|
||
package com.almasb.fxgl.ai.goap | ||
|
||
import com.almasb.fxgl.core.collection.PropertyMap | ||
|
||
/** | ||
* @author Almas Baimagambetov ([email protected]) | ||
*/ | ||
open class GoapAction | ||
@JvmOverloads constructor( | ||
var name: String = "" | ||
) { | ||
|
||
/** | ||
* Predicates that need to be true for this action to run. | ||
*/ | ||
val preconditions = PropertyMap() | ||
|
||
/** | ||
* Results that are true after this action successfully completed. | ||
*/ | ||
val effects = PropertyMap() | ||
|
||
/** | ||
* The cost of performing the action. | ||
* Actions with the total lowest cost are chosen during planning. | ||
*/ | ||
var cost = 1f | ||
|
||
fun addPrecondition(key: String, value: Any) { | ||
preconditions.setValue(key, value) | ||
} | ||
|
||
fun removePrecondition(key: String) { | ||
preconditions.remove(key) | ||
} | ||
|
||
fun addEffect(key: String, value: Any) { | ||
effects.setValue(key, value) | ||
} | ||
|
||
fun removeEffect(key: String) { | ||
effects.remove(key) | ||
} | ||
|
||
override fun toString(): String { | ||
return name | ||
} | ||
} | ||
|
||
// | ||
// /** | ||
// * An action often has to perform on an object. | ||
// * This is that object. Can be null. | ||
// */ | ||
// var target: Entity? = null | ||
// | ||
// /** | ||
// * Check if this action can run. | ||
// * TODO: is available, rather than can run. | ||
// */ | ||
// open fun canRun() = true | ||
// | ||
// override fun onUpdate(tpf: Double) { | ||
// // TODO: perform(tpf) | ||
// perform() | ||
// } | ||
// | ||
// /** | ||
// * Perform the action. | ||
// */ | ||
// abstract fun perform() |
155 changes: 155 additions & 0 deletions
155
fxgl-entity/src/main/kotlin/com/almasb/fxgl/ai/goap/GoapPlanner.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
/* | ||
* FXGL - JavaFX Game Library. The MIT License (MIT). | ||
* Copyright (c) AlmasB ([email protected]). | ||
* See LICENSE for details. | ||
*/ | ||
|
||
package com.almasb.fxgl.ai.goap | ||
|
||
import com.almasb.fxgl.core.collection.PropertyMap | ||
import java.util.* | ||
|
||
/** | ||
* Plans what actions can be completed in order to fulfill a goal state. | ||
* | ||
* Adapted from https://github.com/sploreg/goap | ||
* Original source: C#, author: Brent Anthony Owens. | ||
* | ||
* @author Almas Baimagambetov ([email protected]) | ||
*/ | ||
internal object GoapPlanner { | ||
|
||
/** | ||
* Plan what sequence of actions can fulfill the goal. | ||
* Returns an empty queue if a plan could not be found, | ||
* or a list of the actions that must be performed, in order, to fulfill the goal. | ||
*/ | ||
fun plan(availableActions: Set<GoapAction>, | ||
currentState: PropertyMap, | ||
goalState: PropertyMap): Queue<GoapAction> { | ||
|
||
// reset the actions so we can start fresh with them | ||
// TODO: | ||
//availableActions.forEach { it.cancel() } | ||
|
||
// check what actions can run | ||
// TODO: | ||
//val usableActions = availableActions.filter { it.canRun() }.toSet() | ||
val usableActions = availableActions.toSet() | ||
|
||
// we now have all actions that can run, stored in usableActions | ||
|
||
// build up the tree and record the leaf nodes that provide a solution to the goal | ||
val leaves = ArrayList<Node>() | ||
|
||
// build graph | ||
val start = Node(null, 0f, currentState, null) | ||
val success = buildGraph(start, leaves, usableActions, goalState) | ||
|
||
if (!success) { | ||
return ArrayDeque() | ||
} | ||
|
||
// get the cheapest leaf | ||
val cheapest = leaves.minBy { it.runningCost } | ||
|
||
// get its node and work back through the parents | ||
val result = ArrayList<GoapAction>() | ||
var n: Node? = cheapest | ||
while (n != null) { | ||
if (n.action != null) { | ||
result.add(n.action!!) | ||
} | ||
n = n.parent | ||
} | ||
|
||
// we now have this action list in correct order | ||
return ArrayDeque(result.reversed()) | ||
} | ||
|
||
/** | ||
* Returns true if at least one solution was found. | ||
* The possible paths are stored in the leaves list. | ||
* Each leaf has a 'runningCost' value where the lowest cost will be the best action sequence. | ||
*/ | ||
private fun buildGraph(parent: Node, | ||
leaves: MutableList<Node>, | ||
usableActions: Set<GoapAction>, | ||
goal: PropertyMap): Boolean { | ||
|
||
var foundOne = false | ||
|
||
// prefer low cost actions over high cost | ||
val sortedActions = usableActions.sortedBy { it.cost } | ||
|
||
// go through each action available at this node and see if we can use it here | ||
for (action in sortedActions) { | ||
|
||
// if the parent state has the conditions for this action's preconditions, we can use it here | ||
if (action.preconditions.isIn(parent.state)) { | ||
|
||
// apply the action's effects to the parent state | ||
val currentState = populateState(parent.state, action.effects) | ||
|
||
val node = Node(parent, parent.runningCost + action.cost, currentState, action) | ||
|
||
if (goal.isIn(currentState)) { | ||
// we found a solution! | ||
leaves.add(node) | ||
foundOne = true | ||
} else { | ||
// not at a solution yet, so test all the remaining actions and branch out the tree | ||
val subset = usableActions - action | ||
|
||
val found = buildGraph(node, leaves, subset, goal) | ||
if (found) | ||
foundOne = true | ||
} | ||
} | ||
} | ||
|
||
return foundOne | ||
} | ||
|
||
/** | ||
* @return a new state by applying the [stateChange] to the [currentState] (which does not change) | ||
*/ | ||
private fun populateState(currentState: PropertyMap, stateChange: PropertyMap): PropertyMap { | ||
val newState = currentState.copy() | ||
newState.addAll(stateChange) | ||
return newState | ||
} | ||
|
||
// TODO: currently only supports boolean values | ||
private fun PropertyMap.isIn(other: PropertyMap): Boolean { | ||
var result = true | ||
|
||
this.forEach { key, value -> | ||
|
||
// if doesn't exist in other | ||
if (!other.exists(key)) { | ||
result = false | ||
return@forEach | ||
} | ||
|
||
// or doesn't match the value in other | ||
if (value != other.getValue(key)) { | ||
result = false | ||
return@forEach | ||
} | ||
} | ||
|
||
return result | ||
} | ||
|
||
/** | ||
* An internal type, used for building up the graph and holding the running costs of actions. | ||
*/ | ||
private class Node( | ||
var parent: Node?, | ||
var runningCost: Float, | ||
var state: PropertyMap, | ||
var action: GoapAction? | ||
) | ||
} | ||
|
45 changes: 45 additions & 0 deletions
45
fxgl-entity/src/test/kotlin/com/almasb/fxgl/ai/goap/GoapTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
/* | ||
* FXGL - JavaFX Game Library. The MIT License (MIT). | ||
* Copyright (c) AlmasB ([email protected]). | ||
* See LICENSE for details. | ||
*/ | ||
|
||
package com.almasb.fxgl.ai.goap | ||
|
||
import com.almasb.fxgl.core.collection.PropertyMap | ||
import org.hamcrest.MatcherAssert.assertThat | ||
import org.hamcrest.Matchers.* | ||
import org.junit.jupiter.api.Test | ||
|
||
/** | ||
* @author Almas Baim (https://github.com/AlmasB) | ||
*/ | ||
class GoapTest { | ||
|
||
@Test | ||
fun `plan`() { | ||
val action1 = GoapAction("Equip sword") | ||
val action2 = GoapAction("Attack with sword") | ||
val action3 = GoapAction("Pick up sword") | ||
|
||
action1.addPrecondition("pickUpSword", true) | ||
action1.addEffect("equipSword", true) | ||
|
||
action2.addPrecondition("equipSword", true) | ||
action2.addEffect("attackWithSword", true) | ||
|
||
action3.addEffect("pickUpSword", true) | ||
|
||
val current = PropertyMap() | ||
val goal = PropertyMap() | ||
goal.setValue("attackWithSword", true) | ||
|
||
val actions = GoapPlanner.plan( | ||
setOf(action1, action2, action3), | ||
current, | ||
goal | ||
) | ||
|
||
assertThat(actions, contains(action3, action1, action2)) | ||
} | ||
} |