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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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>"