goapauto 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- goapauto-0.1.0.dist-info/METADATA +291 -0
- goapauto-0.1.0.dist-info/RECORD +9 -0
- goapauto-0.1.0.dist-info/WHEEL +5 -0
- goapauto-0.1.0.dist-info/top_level.txt +1 -0
- models/actions.py +231 -0
- models/goal.py +165 -0
- models/goap_planner.py +297 -0
- models/node.py +213 -0
- models/worldstate.py +273 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: goapauto
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A flexible Goal-Oriented Action Planning (GOAP) system for game AI and planning problems, featuring A* search, efficient state management, and modular design.
|
|
5
|
+
Requires-Python: >=3.13
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
|
|
8
|
+
# GOAP Planner
|
|
9
|
+
|
|
10
|
+
A flexible Goal-Oriented Action Planning (GOAP) system designed for game AI, but applicable to any planning problem. This implementation provides a clean, modular architecture for defining actions, goals, and world states to create intelligent AI behaviors.
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- 🎯 Goal-oriented planning with customizable actions and preconditions
|
|
15
|
+
- 🧠A* search algorithm for optimal plan generation
|
|
16
|
+
- âš¡ Efficient state management and caching
|
|
17
|
+
- 📊 Built-in planning statistics and debugging
|
|
18
|
+
- 🧩 Modular design for easy extension
|
|
19
|
+
- 🎮 Example implementations included
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
### From PyPI
|
|
24
|
+
```bash
|
|
25
|
+
uv add goapauto
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### From Source
|
|
29
|
+
1. Clone the repository:
|
|
30
|
+
```bash
|
|
31
|
+
git clone https://github.com/IAmNo1Special/goapauto.git
|
|
32
|
+
cd goapauto
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from models.goap_planner import Planner, PlanResult
|
|
39
|
+
from models.goal import Goal
|
|
40
|
+
from models.actions import Action, Actions
|
|
41
|
+
from models.worldstate import WorldState
|
|
42
|
+
|
|
43
|
+
# Define your initial state using WorldState for better type safety and validation
|
|
44
|
+
initial_state = WorldState({
|
|
45
|
+
'is_open': False,
|
|
46
|
+
'is_focused': False,
|
|
47
|
+
'has_key': True,
|
|
48
|
+
'energy': 100
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
# Create actions collection
|
|
52
|
+
actions = Actions()
|
|
53
|
+
|
|
54
|
+
# Add actions with proper typing and validation
|
|
55
|
+
try:
|
|
56
|
+
actions.add_action(
|
|
57
|
+
name="open_door",
|
|
58
|
+
preconditions={'is_open': False, 'has_key': True},
|
|
59
|
+
effects={'is_open': True},
|
|
60
|
+
cost=1.0
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
actions.add_action(
|
|
64
|
+
name="consume_energy",
|
|
65
|
+
preconditions={'energy': (lambda x: x > 0)},
|
|
66
|
+
effects={'energy': (lambda x: x - 10)},
|
|
67
|
+
cost=1.0
|
|
68
|
+
)
|
|
69
|
+
except (ValueError, TypeError) as e:
|
|
70
|
+
print(f"Error creating actions: {e}")
|
|
71
|
+
raise
|
|
72
|
+
|
|
73
|
+
# Create a goal with validation
|
|
74
|
+
try:
|
|
75
|
+
goal = Goal(
|
|
76
|
+
name="Open Door",
|
|
77
|
+
priority=1,
|
|
78
|
+
target_state={'is_open': True}
|
|
79
|
+
)
|
|
80
|
+
except ValueError as e:
|
|
81
|
+
print(f"Invalid goal: {e}")
|
|
82
|
+
raise
|
|
83
|
+
|
|
84
|
+
# Create and run the planner
|
|
85
|
+
planner = Planner(actions)
|
|
86
|
+
plan, message = planner.generate_plan(initial_state, goal)
|
|
87
|
+
|
|
88
|
+
# The planner will automatically display the plan and statistics
|
|
89
|
+
# You can access the plan and execute it if needed
|
|
90
|
+
if plan is not None:
|
|
91
|
+
current_state = initial_state.copy()
|
|
92
|
+
for i, action_name in enumerate(plan, 1):
|
|
93
|
+
action = actions.get_action(action_name)
|
|
94
|
+
print(f"\nStep {i}: {action_name}")
|
|
95
|
+
current_state = action.apply(current_state)
|
|
96
|
+
print(f"New state: {current_state}")
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Project Structure
|
|
100
|
+
|
|
101
|
+
- `/models` - Core GOAP implementation
|
|
102
|
+
- `goap_planner.py` - Main planner logic
|
|
103
|
+
- `goal.py` - Goal definition and management
|
|
104
|
+
- `actions.py` - Action definitions
|
|
105
|
+
- `worldstate.py` - World state management
|
|
106
|
+
- `node.py` - Search node implementation
|
|
107
|
+
|
|
108
|
+
- `/examples` - Example implementations
|
|
109
|
+
- `example1.py` - Basic usage example
|
|
110
|
+
- `example2.py` - Advanced scenario
|
|
111
|
+
|
|
112
|
+
## Advanced Usage
|
|
113
|
+
|
|
114
|
+
### Working with WorldState
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from models.worldstate import WorldState
|
|
118
|
+
|
|
119
|
+
# Create a state with validation
|
|
120
|
+
state = WorldState({
|
|
121
|
+
'health': 100,
|
|
122
|
+
'has_weapon': True,
|
|
123
|
+
'enemy_visible': False
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
# Safe state updates
|
|
127
|
+
state = state.merge({'health': 80}) # Returns new state
|
|
128
|
+
state = state.copy() # Create a deep copy
|
|
129
|
+
|
|
130
|
+
# Check conditions
|
|
131
|
+
if state.matches({'health': (lambda x: x > 50)}):
|
|
132
|
+
print("Health is above 50%")
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Creating Custom Actions
|
|
136
|
+
|
|
137
|
+
Define actions using the `Action` class for better type safety and validation:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from models.actions import Action, Actions
|
|
141
|
+
|
|
142
|
+
# Create a single action
|
|
143
|
+
open_door = Action(
|
|
144
|
+
name="open_door",
|
|
145
|
+
preconditions={'door_locked': False},
|
|
146
|
+
effects={'door_open': True},
|
|
147
|
+
cost=1.0
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Or use the Actions collection to manage multiple actions
|
|
151
|
+
actions = Actions()
|
|
152
|
+
actions.add_action(
|
|
153
|
+
name="unlock_door",
|
|
154
|
+
preconditions={'has_key': True},
|
|
155
|
+
effects={'door_locked': False},
|
|
156
|
+
cost=1.5
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Add multiple actions at once
|
|
160
|
+
actions.add_actions([
|
|
161
|
+
("pick_up_item", {"near_item": True}, {"has_item": True}, 1.0),
|
|
162
|
+
("use_item", {"has_item": True}, {"effect_applied": True}, 1.0)
|
|
163
|
+
])
|
|
164
|
+
|
|
165
|
+
# Check if an action is applicable in a given state
|
|
166
|
+
if open_door.is_applicable(current_state):
|
|
167
|
+
new_state = open_door.apply(current_state)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Defininig Goals with target states and priorities
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from models.goal import Goal
|
|
174
|
+
|
|
175
|
+
goal = Goal(
|
|
176
|
+
name="Achieve Objective",
|
|
177
|
+
priority=1,
|
|
178
|
+
target_state={
|
|
179
|
+
'objective_complete': True
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Defining Goals with Conditions
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from models.goal import Goal
|
|
188
|
+
|
|
189
|
+
# Simple goal
|
|
190
|
+
goal = Goal(
|
|
191
|
+
name="Defeat Enemy",
|
|
192
|
+
priority=1,
|
|
193
|
+
target_state={
|
|
194
|
+
'enemy_defeated': True,
|
|
195
|
+
'health': (lambda x: x > 0) # Must have health remaining
|
|
196
|
+
}
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Goal with custom validation
|
|
200
|
+
try:
|
|
201
|
+
goal.validate()
|
|
202
|
+
except ValueError as e:
|
|
203
|
+
print(f"Invalid goal: {e}")
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Working with Plan Results
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
# After generating a plan
|
|
210
|
+
if plan is not None:
|
|
211
|
+
print(f"Plan found with {len(plan)} actions")
|
|
212
|
+
print(f"Total cost: {planner.stats.total_cost}")
|
|
213
|
+
print(f"Nodes expanded: {planner.stats.nodes_expanded}")
|
|
214
|
+
print(f"Planning time: {planner.stats.execution_time:.4f}s")
|
|
215
|
+
|
|
216
|
+
# Access individual plan steps
|
|
217
|
+
for i, action_name in enumerate(plan, 1):
|
|
218
|
+
action = actions.get_action(action_name)
|
|
219
|
+
print(f"{i}. {action_name} (cost: {action.cost})")
|
|
220
|
+
|
|
221
|
+
# Execute the plan and track state changes
|
|
222
|
+
current_state = initial_state.copy()
|
|
223
|
+
for i, action_name in enumerate(plan, 1):
|
|
224
|
+
action = actions.get_action(action_name)
|
|
225
|
+
print(f"\nStep {i}: {action_name}")
|
|
226
|
+
current_state = action.apply(current_state)
|
|
227
|
+
print(f"State after action: {current_state}")
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Custom Heuristics
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
from models.goap_planner import Planner
|
|
234
|
+
|
|
235
|
+
def custom_heuristic(state, goal_state):
|
|
236
|
+
"""Custom heuristic function for A* search."""
|
|
237
|
+
# Implement your heuristic logic here
|
|
238
|
+
# Lower values mean closer to goal
|
|
239
|
+
return 0 # Default to Dijkstra's algorithm
|
|
240
|
+
|
|
241
|
+
planner = Planner(actions, heuristic=custom_heuristic)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Performance Tips
|
|
245
|
+
|
|
246
|
+
1. **State Design**:
|
|
247
|
+
- Keep state keys simple and consistent
|
|
248
|
+
- Use immutable state objects for better caching
|
|
249
|
+
|
|
250
|
+
2. **Action Design**:
|
|
251
|
+
- Make preconditions as specific as possible
|
|
252
|
+
- Use lambda functions for dynamic conditions
|
|
253
|
+
- Keep effects minimal and focused
|
|
254
|
+
|
|
255
|
+
3. **Debugging**:
|
|
256
|
+
- Enable debug logging for detailed planning info
|
|
257
|
+
- Check PlanResult.stats for performance metrics
|
|
258
|
+
- Use state validation to catch issues early
|
|
259
|
+
|
|
260
|
+
## Troubleshooting
|
|
261
|
+
|
|
262
|
+
### Common Issues
|
|
263
|
+
|
|
264
|
+
1. **No Plan Found**
|
|
265
|
+
- Check if actions' preconditions are met
|
|
266
|
+
- Verify the goal is achievable
|
|
267
|
+
- Increase max_iterations if needed
|
|
268
|
+
|
|
269
|
+
2. **Performance Problems**
|
|
270
|
+
- Check for infinite state spaces
|
|
271
|
+
- Add more specific preconditions
|
|
272
|
+
- Consider using a custom heuristic
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
## Running Examples
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
# Run basic example
|
|
279
|
+
python examples/example1.py
|
|
280
|
+
|
|
281
|
+
# Run advanced example
|
|
282
|
+
python examples/example2.py
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Contributing
|
|
286
|
+
|
|
287
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
288
|
+
|
|
289
|
+
## License
|
|
290
|
+
|
|
291
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
models/actions.py,sha256=3dMiKlkwD1wx6b3AulCHk3MJEW4fM52b56jXsvfxvr0,9118
|
|
2
|
+
models/goal.py,sha256=IGrrFZoFSs4Ct0W-pjm8ZGYjPpJly5iis1BDECrJ76k,6275
|
|
3
|
+
models/goap_planner.py,sha256=Gih0yDipsqehwTLb8m9mYm5JRoVLQAr6S5dgXbf_Fgc,10791
|
|
4
|
+
models/node.py,sha256=w3A2Qk9queVNa4Bh9Vv2LsM2z-wx8Jodf8LnGXS-V1s,7558
|
|
5
|
+
models/worldstate.py,sha256=FzTMtLE5Cxa3sLAst4qabehMb7MsEkaELU2vctbhuW8,9052
|
|
6
|
+
goapauto-0.1.0.dist-info/METADATA,sha256=QTj-YkPf5CAwIsY3VrsuDgLFF-7931jhHd3muCAXl5Y,7711
|
|
7
|
+
goapauto-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
+
goapauto-0.1.0.dist-info/top_level.txt,sha256=vH5ByFtkqB2bXlXOuK5zaaM_1GFEE9six0KgGIdPaU0,7
|
|
9
|
+
goapauto-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
models
|
models/actions.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Any, Dict, List, Optional, Type, TypeVar, Union
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from models.worldstate import WorldState
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
T = TypeVar('T', bound='Action')
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Action:
|
|
13
|
+
"""Represents an action the agent can take.
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
name: Unique identifier for the action
|
|
17
|
+
preconditions: Dictionary of state requirements that must be met for the action to be applicable
|
|
18
|
+
effects: Dictionary of state changes that result from applying this action
|
|
19
|
+
cost: The cost of executing this action (used for pathfinding)
|
|
20
|
+
"""
|
|
21
|
+
name: str
|
|
22
|
+
preconditions: Dict[str, Any]
|
|
23
|
+
effects: Dict[str, Any]
|
|
24
|
+
cost: int = 1
|
|
25
|
+
|
|
26
|
+
def __post_init__(self) -> None:
|
|
27
|
+
"""Validate the action after initialization."""
|
|
28
|
+
if not isinstance(self.name, str) or not self.name.strip():
|
|
29
|
+
raise ValueError("Action name must be a non-empty string")
|
|
30
|
+
if not isinstance(self.preconditions, dict):
|
|
31
|
+
raise TypeError("Preconditions must be a dictionary")
|
|
32
|
+
if not isinstance(self.effects, dict):
|
|
33
|
+
raise TypeError("Effects must be a dictionary")
|
|
34
|
+
if not isinstance(self.cost, (int, float)) or self.cost <= 0:
|
|
35
|
+
raise ValueError("Cost must be a positive number")
|
|
36
|
+
|
|
37
|
+
def is_applicable(self, state: Any) -> bool:
|
|
38
|
+
"""Check if this action can be applied to the given state.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
state: The current world state to check against
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
bool: True if all preconditions are met, False otherwise
|
|
45
|
+
"""
|
|
46
|
+
if not hasattr(state, '__dict__'):
|
|
47
|
+
raise TypeError("State must be an object with attributes")
|
|
48
|
+
|
|
49
|
+
logger.debug('Checking applicability of action: %s', self.name)
|
|
50
|
+
try:
|
|
51
|
+
for attr, expected_value in self.preconditions.items():
|
|
52
|
+
if not hasattr(state, attr):
|
|
53
|
+
logger.debug('State missing required attribute: %s', attr)
|
|
54
|
+
return False
|
|
55
|
+
if getattr(state, attr) != expected_value:
|
|
56
|
+
logger.debug('Precondition not met: %s != %s',
|
|
57
|
+
getattr(state, attr), expected_value)
|
|
58
|
+
return False
|
|
59
|
+
return True
|
|
60
|
+
except Exception as e:
|
|
61
|
+
logger.error('Error checking action applicability: %s', str(e), exc_info=True)
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
def apply(self, state: Any) -> Any:
|
|
65
|
+
"""Apply this action to the current state and return a new state.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
state: The current world state to apply the action to
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
A new state with the action's effects applied
|
|
72
|
+
|
|
73
|
+
Raises:
|
|
74
|
+
ValueError: If the action cannot be applied to the current state
|
|
75
|
+
"""
|
|
76
|
+
if not self.is_applicable(state):
|
|
77
|
+
raise ValueError(f"Action {self.name} is not applicable to the current state")
|
|
78
|
+
|
|
79
|
+
logger.info('Applying action: %s', self.name)
|
|
80
|
+
try:
|
|
81
|
+
# Create a deep copy to avoid modifying the original state
|
|
82
|
+
new_state = copy.deepcopy(state)
|
|
83
|
+
|
|
84
|
+
# Apply each effect to the new state
|
|
85
|
+
for attr, value in self.effects.items():
|
|
86
|
+
setattr(new_state, attr, value)
|
|
87
|
+
|
|
88
|
+
logger.debug('New state after %s: %s', self.name, new_state)
|
|
89
|
+
return new_state
|
|
90
|
+
|
|
91
|
+
except Exception as e:
|
|
92
|
+
logger.error('Failed to apply action %s: %s', self.name, str(e), exc_info=True)
|
|
93
|
+
raise
|
|
94
|
+
|
|
95
|
+
def __str__(self) -> str:
|
|
96
|
+
"""Return a string representation of the action."""
|
|
97
|
+
return (f"{self.__class__.__name__}('{self.name}', "
|
|
98
|
+
f"preconditions={self.preconditions}, "
|
|
99
|
+
f"effects={self.effects}, cost={self.cost})")
|
|
100
|
+
|
|
101
|
+
def __repr__(self) -> str:
|
|
102
|
+
"""Return the canonical string representation of the action."""
|
|
103
|
+
return str(self)
|
|
104
|
+
|
|
105
|
+
class Actions:
|
|
106
|
+
"""Manages a collection of available actions for the GOAP planner.
|
|
107
|
+
|
|
108
|
+
This class provides methods to add, retrieve, and manage actions that can be
|
|
109
|
+
used by the planner to achieve goals. It ensures that all actions are valid
|
|
110
|
+
and provides efficient lookup and iteration capabilities.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
def __init__(self) -> None:
|
|
114
|
+
"""Initialize an empty collection of actions."""
|
|
115
|
+
self._actions: List[Action] = []
|
|
116
|
+
|
|
117
|
+
def add_action(self, name: str, preconditions: Dict[str, Any],
|
|
118
|
+
effects: Dict[str, Any], cost: int = 1) -> None:
|
|
119
|
+
"""Add a single action to the collection.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
name: Unique identifier for the action
|
|
123
|
+
preconditions: Dictionary of state requirements for the action
|
|
124
|
+
effects: Dictionary of state changes caused by the action
|
|
125
|
+
cost: The cost of executing this action (default: 1)
|
|
126
|
+
|
|
127
|
+
Raises:
|
|
128
|
+
ValueError: If an action with the same name already exists
|
|
129
|
+
TypeError: If any parameter has an invalid type
|
|
130
|
+
"""
|
|
131
|
+
if not isinstance(name, str) or not name.strip():
|
|
132
|
+
raise ValueError("Action name must be a non-empty string")
|
|
133
|
+
|
|
134
|
+
if self.get_action(name) is not None:
|
|
135
|
+
raise ValueError(f"Action with name '{name}' already exists")
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
action = Action(name, preconditions, effects, cost)
|
|
139
|
+
self._actions.append(action)
|
|
140
|
+
logger.debug('Added action: %s', name)
|
|
141
|
+
except Exception as e:
|
|
142
|
+
logger.error('Failed to add action %s: %s', name, str(e))
|
|
143
|
+
raise
|
|
144
|
+
|
|
145
|
+
def add_actions(self, action_definitions: List[tuple]) -> None:
|
|
146
|
+
"""Add multiple actions to the collection.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
action_definitions: List of action definitions where each definition is a tuple
|
|
150
|
+
in the format (name: str, preconditions: dict, effects: dict, cost: int)
|
|
151
|
+
|
|
152
|
+
Example:
|
|
153
|
+
actions = Actions()
|
|
154
|
+
actions.add_actions([
|
|
155
|
+
("open_door", {"door_locked": False}, {"door_open": True}, 1),
|
|
156
|
+
("unlock_door", {"has_key": True}, {"door_locked": False}, 2)
|
|
157
|
+
])
|
|
158
|
+
"""
|
|
159
|
+
if not isinstance(action_definitions, (list, tuple)):
|
|
160
|
+
raise TypeError("action_definitions must be a list or tuple")
|
|
161
|
+
|
|
162
|
+
for i, action_def in enumerate(action_definitions):
|
|
163
|
+
try:
|
|
164
|
+
if not isinstance(action_def, (list, tuple)) or len(action_def) != 4:
|
|
165
|
+
raise ValueError(f"Action definition at index {i} must be a 4-tuple "
|
|
166
|
+
"(name, preconditions, effects, cost)")
|
|
167
|
+
self.add_action(*action_def)
|
|
168
|
+
except Exception as e:
|
|
169
|
+
logger.error('Error adding action at index %d: %s', i, str(e))
|
|
170
|
+
raise
|
|
171
|
+
|
|
172
|
+
def get_action(self, name: str) -> Optional[Action]:
|
|
173
|
+
"""Retrieve an action by its name.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
name: The name of the action to retrieve
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
The Action object if found, None otherwise
|
|
180
|
+
"""
|
|
181
|
+
if not isinstance(name, str):
|
|
182
|
+
raise TypeError("Action name must be a string")
|
|
183
|
+
|
|
184
|
+
for action in self._actions:
|
|
185
|
+
if action.name == name:
|
|
186
|
+
return action
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
def get_actions(self) -> List[Action]:
|
|
190
|
+
"""Get a list of all actions in the collection.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
A new list containing all Action objects
|
|
194
|
+
"""
|
|
195
|
+
return self._actions.copy()
|
|
196
|
+
|
|
197
|
+
def clear_actions(self) -> None:
|
|
198
|
+
"""Remove all actions from the collection."""
|
|
199
|
+
self._actions.clear()
|
|
200
|
+
logger.info('Cleared all actions')
|
|
201
|
+
|
|
202
|
+
def filter_actions(self, state: Any) -> List[Action]:
|
|
203
|
+
"""Get a list of all actions that can be applied to the given state.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
state: The state to check against action preconditions
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
A list of applicable Action objects
|
|
210
|
+
"""
|
|
211
|
+
return [action for action in self._actions if action.is_applicable(state)]
|
|
212
|
+
|
|
213
|
+
def __iter__(self) -> 'Actions':
|
|
214
|
+
"""Return an iterator over all actions."""
|
|
215
|
+
return iter(self._actions)
|
|
216
|
+
|
|
217
|
+
def __len__(self) -> int:
|
|
218
|
+
"""Return the number of actions in the collection."""
|
|
219
|
+
return len(self._actions)
|
|
220
|
+
|
|
221
|
+
def __contains__(self, name: str) -> bool:
|
|
222
|
+
"""Check if an action with the given name exists."""
|
|
223
|
+
return self.get_action(name) is not None
|
|
224
|
+
|
|
225
|
+
def __str__(self) -> str:
|
|
226
|
+
"""Return a string representation of the actions collection."""
|
|
227
|
+
return f"Actions({len(self._actions)} actions available)"
|
|
228
|
+
|
|
229
|
+
def __repr__(self) -> str:
|
|
230
|
+
"""Return a detailed string representation of the actions collection."""
|
|
231
|
+
return f"<{self.__class__.__name__} with {len(self._actions)} actions>"
|