Skip to content

Commit 9b024ab

Browse files
authored
A* heuristic based on distance to the goal (#3)
Compute the distance to the goal using the current state value, instead of a simple incrementation using the number of steps from the starting point. Heap storage for the nodes, to reduce the number of slices & maps. Reduces the fetch of next node. One call to fetchNodeInHeap instead of two. Hash with XOR incrementation. Simplified hash using a simple hash multiplication. Unit Test coverage > 90%. Code documentation.
1 parent 27a95ab commit 9b024ab

17 files changed

+2936
-279
lines changed

README.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,10 +174,7 @@ GOAP needs to be benchmarked and monitored regularly because of exponential risk
174174
Though if well scoped you can manage hundred of Actions for 200µs per Agent.
175175

176176
## What's next?
177-
- The simulateActionState functions, to check the effect of an action on a node, takes up to 40% of CPU and 40% of memory.
178-
We need to refactorize this part, or find another logical path.
179-
- Heuristic calculation in A* is done poorly, we need a better algorithm to improve performances.
180-
- Benchmark a backward implementation like D*, to improve performances.
177+
- Benchmark a backward implementation like D*. It might improve performances.
181178

182179
## Sources
183180
- https://web.archive.org/web/20230912145018/http://alumni.media.mit.edu/~jorkin/goap.html

action.go

Lines changed: 103 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,30 @@ const (
1515
DIVIDE
1616
)
1717

18+
// Action represents a single action that an agent can perform to modify the world state.
19+
//
20+
// An action has preconditions (conditions) that must be met before it can be executed,
21+
// and postconditions (effects) that describe how it modifies the world state.
22+
// Actions have a cost that is used by the A* algorithm to find the optimal plan.
1823
type Action struct {
1924
name string
2025
cost float32
2126
repeatable bool
2227
conditions Conditions
2328
effects Effects
2429
}
30+
31+
// Actions is a collection of Action pointers.
2532
type Actions []*Action
2633

34+
// AddAction creates a new Action and appends it to the Actions collection.
35+
//
36+
// Parameters:
37+
// - name: unique identifier for the action
38+
// - cost: numeric cost used by pathfinding (lower costs are preferred)
39+
// - repeatable: if false, the action can only be used once per plan
40+
// - conditions: preconditions that must be satisfied before the action can be executed
41+
// - effects: postconditions that describe how the action modifies the world state
2742
func (actions *Actions) AddAction(name string, cost float32, repeatable bool, conditions Conditions, effects Effects) {
2843
action := Action{
2944
name: name,
@@ -36,36 +51,50 @@ func (actions *Actions) AddAction(name string, cost float32, repeatable bool, co
3651
*actions = append(*actions, &action)
3752
}
3853

54+
// GetName returns the action's name identifier.
3955
func (action *Action) GetName() string {
4056
return action.name
4157
}
4258

59+
// GetEffects returns the action's effects (postconditions).
4360
func (action *Action) GetEffects() Effects {
4461
return action.effects
4562
}
4663

64+
// EffectInterface defines the interface that all effect types must implement.
65+
// Effects describe how an action modifies the world state.
4766
type EffectInterface interface {
48-
check(states states) bool
49-
apply(data statesData) error
67+
GetKey() StateKey
68+
check(w world) bool
69+
apply(w *world) error
5070
}
5171

72+
// Effect represents a numeric state modification for types constrained by Numeric.
73+
//
74+
// It supports arithmetic operators (SET, ADD, SUBTRACT, MULTIPLY, DIVIDE) to modify
75+
// numeric state values. The effect is applied when an action is executed during planning.
5276
type Effect[T Numeric] struct {
53-
Key StateKey
54-
Operator arithmetic
55-
Value T
77+
Key StateKey // State key to modify
78+
Operator arithmetic // Arithmetic operation to perform
79+
Value T // Value to use in the operation
80+
}
81+
82+
// GetKey returns the state key that this effect modifies.
83+
func (effect Effect[T]) GetKey() StateKey {
84+
return effect.Key
5685
}
5786

58-
func (effect Effect[T]) check(states states) bool {
59-
// Other operators than '=' mean the effect will have an impact of the states
87+
func (effect Effect[T]) check(w world) bool {
88+
// Other operators than '=' mean the effect will have an impact of the world
6089
if effect.Operator != SET {
6190
return false
6291
}
6392

64-
k := states.data.GetIndex(effect.Key)
93+
k := w.states.GetIndex(effect.Key)
6594
if k < 0 {
6695
return false
6796
}
68-
s := states.data[k]
97+
s := w.states[k]
6998

7099
if _, ok := s.(State[T]); !ok {
71100
return false
@@ -74,23 +103,23 @@ func (effect Effect[T]) check(states states) bool {
74103
return s.(State[T]).Value == effect.Value
75104
}
76105

77-
func (effect Effect[T]) apply(data statesData) error {
78-
k := data.GetIndex(effect.Key)
106+
func (effect Effect[T]) apply(w *world) error {
107+
k := w.states.GetIndex(effect.Key)
79108
if k < 0 {
80109
if slices.Contains([]arithmetic{SET, ADD}, effect.Operator) {
81-
data = append(data, State[T]{Value: effect.Value})
110+
w.states = append(w.states, State[T]{Key: effect.Key, Value: effect.Value})
82111
return nil
83112
} else if slices.Contains([]arithmetic{SUBSTRACT}, effect.Operator) {
84-
data = append(data, State[T]{Value: -effect.Value})
113+
w.states = append(w.states, State[T]{Key: effect.Key, Value: -effect.Value})
85114
return nil
86115
}
87-
return fmt.Errorf("data does not exist")
116+
return fmt.Errorf("w does not exist")
88117
}
89-
if _, ok := data[k].(State[T]); !ok {
118+
if _, ok := w.states[k].(State[T]); !ok {
90119
return fmt.Errorf("type does not match")
91120
}
92121

93-
state := data[k].(State[T])
122+
state := w.states[k].(State[T])
94123
switch effect.Operator {
95124
case SET:
96125
state.Value = effect.Value
@@ -104,129 +133,151 @@ func (effect Effect[T]) apply(data statesData) error {
104133
state.Value /= effect.Value
105134
}
106135

107-
data[k] = state
136+
state.Store(w)
108137

109138
return nil
110139
}
111140

141+
// EffectBool represents a boolean state modification.
142+
//
143+
// Only the SET operator is allowed for boolean effects. Attempting to use other
144+
// operators (ADD, SUBTRACT, etc.) will result in an error when the effect is applied.
112145
type EffectBool struct {
113-
Key StateKey
114-
Value bool
115-
Operator arithmetic
146+
Key StateKey // State key to modify
147+
Value bool // Boolean value to set
148+
Operator arithmetic // Must be SET
116149
}
117150

118-
func (effectBool EffectBool) check(states states) bool {
151+
// GetKey returns the state key that this effect modifies.
152+
func (effectBool EffectBool) GetKey() StateKey {
153+
return effectBool.Key
154+
}
155+
156+
func (effectBool EffectBool) check(w world) bool {
119157
// Other operators than '=' is not allowed
120158
if effectBool.Operator != SET {
121159
return false
122160
}
123161

124-
k := states.data.GetIndex(effectBool.Key)
162+
k := w.states.GetIndex(effectBool.Key)
125163
if k < 0 {
126164
return false
127165
}
128-
if _, ok := states.data[k].(State[bool]); !ok {
166+
if _, ok := w.states[k].(State[bool]); !ok {
129167
return false
130168
}
131169

132-
s := states.data[k].(State[bool])
170+
s := w.states[k].(State[bool])
133171

134172
return s.Value == effectBool.Value
135173
}
136174

137-
func (effectBool EffectBool) apply(data statesData) error {
175+
func (effectBool EffectBool) apply(w *world) error {
138176
if effectBool.Operator != SET {
139177
return fmt.Errorf("operation %v not allowed on bool type", effectBool.Operator)
140178
}
141179

142-
k := data.GetIndex(effectBool.Key)
180+
k := w.states.GetIndex(effectBool.Key)
143181
if k < 0 {
144-
data = append(data, State[bool]{Value: effectBool.Value})
182+
w.states = append(w.states, State[bool]{Key: effectBool.Key, Value: effectBool.Value})
145183
return nil
146184
}
147-
if _, ok := data[k].(State[bool]); !ok {
185+
if _, ok := w.states[k].(State[bool]); !ok {
148186
return fmt.Errorf("type does not match")
149187
}
150188

151-
state := data[k].(State[bool])
189+
state := w.states[k].(State[bool])
152190
state.Value = effectBool.Value
153-
data[k] = state
191+
192+
state.Store(w)
154193

155194
return nil
156195
}
157196

197+
// EffectString represents a string state modification.
198+
//
199+
// Supports SET (replace string) and ADD (concatenate) operators. Other operators
200+
// (SUBTRACT, MULTIPLY, DIVIDE) are not allowed and will result in an error.
158201
type EffectString struct {
159-
Key StateKey
160-
Value string
161-
Operator arithmetic
202+
Key StateKey // State key to modify
203+
Value string // String value to use
204+
Operator arithmetic // Allowed: SET, ADD
162205
}
163206

164-
func (effectString EffectString) check(states states) bool {
165-
k := states.data.GetIndex(effectString.Key)
207+
// GetKey returns the state key that this effect modifies.
208+
func (effectString EffectString) GetKey() StateKey {
209+
return effectString.Key
210+
}
211+
212+
func (effectString EffectString) check(w world) bool {
213+
k := w.states.GetIndex(effectString.Key)
166214
if k < 0 {
167215
return false
168216
}
169-
if _, ok := states.data[k].(State[string]); !ok {
217+
if _, ok := w.states[k].(State[string]); !ok {
170218
return false
171219
}
172220

173-
s := states.data[k].(State[string])
221+
s := w.states[k].(State[string])
174222

175223
return s.Value == effectString.Value
176224
}
177225

178-
func (effectString EffectString) apply(data statesData) error {
226+
func (effectString EffectString) apply(w *world) error {
179227
if !slices.Contains([]arithmetic{SET, ADD}, effectString.Operator) {
180228
return fmt.Errorf("arithmetic operation %v not allowed on string type", effectString.Operator)
181229
}
182230

183-
k := data.GetIndex(effectString.Key)
231+
k := w.states.GetIndex(effectString.Key)
184232
if k < 0 {
185-
data = append(data, State[string]{Value: effectString.Value})
233+
w.states = append(w.states, State[string]{Key: effectString.Key, Value: effectString.Value})
186234
return nil
187235
}
188-
if _, ok := data[k].(State[string]); !ok {
236+
if _, ok := w.states[k].(State[string]); !ok {
189237
return fmt.Errorf("type does not match")
190238
}
191239

192-
state := data[k].(State[string])
240+
state := w.states[k].(State[string])
193241
switch effectString.Operator {
194242
case SET:
195243
state.Value = effectString.Value
196244
case ADD:
197245
state.Value = fmt.Sprint(state.Value, effectString.Value)
198246
}
199-
data[k] = state
247+
248+
state.Store(w)
200249

201250
return nil
202251
}
203252

253+
// EffectFn is a function type for custom procedural effects that directly modify the agent.
254+
// This allows for effects that cannot be expressed through simple state modifications.
204255
type EffectFn func(agent *Agent)
205256

257+
// Effects is a collection of EffectInterface implementations that describe how
258+
// an action modifies the world state.
206259
type Effects []EffectInterface
207260

208-
// If all the effects already exist in states,
261+
// If all the effects already exist in world,
209262
// it is probably not a good path
210-
func (effects Effects) satisfyStates(states states) bool {
263+
func (effects Effects) satisfyStates(w world) bool {
211264
for _, effect := range effects {
212-
if !effect.check(states) {
265+
if !effect.check(w) {
213266
return false
214267
}
215268
}
216269

217270
return true
218271
}
219272

220-
func (effects Effects) apply(states states) (statesData, error) {
221-
data := slices.Clone(states.data)
222-
273+
func (effects Effects) apply(w *world) error {
223274
for _, effect := range effects {
224-
err := effect.apply(data)
275+
err := effect.apply(w)
225276

226277
if err != nil {
227-
return nil, err
278+
return err
228279
}
229280
}
230281

231-
return data, nil
282+
return nil
232283
}

0 commit comments

Comments
 (0)