-
Notifications
You must be signed in to change notification settings - Fork 28
Expand file tree
/
Copy pathsimple_npc_ai.py
More file actions
139 lines (110 loc) · 4.04 KB
/
Copy pathsimple_npc_ai.py
File metadata and controls
139 lines (110 loc) · 4.04 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
"""
Console NPC behavior demo.
An NPC runs and shoots while it has action points. When exhausted, it rests
until fully recovered, then dies. Built from a nested `WhileAll`:
WhileAll(IsDead, [
WhileAll(HasActionPointsLeft, [Run, Shoot]),
Rest,
Die,
])
Demonstrates `WhileAll` looping, blackboard mutation through a `@dataclass`,
and an enum action dispatched via Python's structural-match callback.
Run:
python bonsai-py/examples/simple_npc_ai.py
"""
from __future__ import annotations
import enum
from dataclasses import dataclass
from typing import Any
import bonsai_bt as bt
class EnemyNPC(enum.Enum):
RUN = enum.auto()
SHOOT = enum.auto()
HAS_ACTION_POINTS_LEFT = enum.auto()
REST = enum.auto()
DIE = enum.auto()
IS_DEAD = enum.auto()
@dataclass
class BlackBoard:
times_shot: int = 0
@dataclass
class NPCState:
action_points: int
max_action_points: int
alive: bool
def consume_action_point(self) -> None:
self.action_points = max(0, self.action_points - 1)
def rest(self) -> None:
self.action_points = min(self.action_points + 1, self.max_action_points)
print(f"Rested for a while... Action points: {self.action_points}")
def die(self) -> None:
print("NPC died...")
self.alive = False
def is_alive(self) -> bool:
print("NPC is alive..." if self.alive else "NPC is dead...")
return self.alive
def fully_rested(self) -> bool:
return self.action_points == self.max_action_points
def perform_action(self, action: str) -> None:
if self.action_points > 0:
self.consume_action_point()
print(f"Performing action: {action}. Action points: {self.action_points}")
else:
print(f"Cannot perform action: {action}. Not enough action points.")
def make_callback(state: NPCState):
def cb(args: Any, blackboard: BlackBoard) -> tuple[bt.Status, float]:
match args.action:
case EnemyNPC.RUN:
state.perform_action("run")
return (bt.Status.Success, 0.0)
case EnemyNPC.HAS_ACTION_POINTS_LEFT:
if state.action_points == 0:
print("NPC does not have action points left...")
return (bt.Status.Success, 0.0)
print(f"NPC has action points: {state.action_points}")
return (bt.Status.Running, 0.0)
case EnemyNPC.SHOOT:
state.perform_action("shoot")
blackboard.times_shot += 1
return (bt.Status.Success, 0.0)
case EnemyNPC.REST:
if state.fully_rested():
return (bt.Status.Success, 0.0)
state.rest()
return (bt.Status.Running, 0.0)
case EnemyNPC.DIE:
state.die()
return (bt.Status.Success, 0.0)
case EnemyNPC.IS_DEAD:
if state.is_alive():
return (bt.Status.Running, 0.0)
return (bt.Status.Success, 0.0)
case _:
raise ValueError(f"unknown action: {args.action!r}")
return cb
def build_tree() -> bt.Behavior:
run_and_shoot = bt.WhileAll(
bt.Action(EnemyNPC.HAS_ACTION_POINTS_LEFT),
[bt.Action(EnemyNPC.RUN), bt.Action(EnemyNPC.SHOOT)],
)
return bt.WhileAll(
bt.Action(EnemyNPC.IS_DEAD),
[run_and_shoot, bt.Action(EnemyNPC.REST), bt.Action(EnemyNPC.DIE)],
)
def main() -> None:
max_actions = 3
blackboard = BlackBoard()
state = NPCState(action_points=max_actions, max_action_points=max_actions, alive=True)
tree_bt = bt.BT(build_tree(), blackboard)
callback = make_callback(state)
while True:
print("reached main loop...")
result = tree_bt.tick(0.0, callback)
if result is None:
break
status, _ = result
if status != bt.Status.Running:
break
print(f"NPC shot {blackboard.times_shot} times during the simulation.")
if __name__ == "__main__":
main()