-
Notifications
You must be signed in to change notification settings - Fork 28
Expand file tree
/
Copy pathboids_console.py
More file actions
179 lines (153 loc) · 5.67 KB
/
Copy pathboids_console.py
File metadata and controls
179 lines (153 loc) · 5.67 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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
"""
Boids flocking with one Behavior shared across many agents.
Build the `Behavior` ONCE, then construct `bt.BT(shared_tree, boid)` for
each of N agents — every BT instance has its own blackboard (a `Boid`
dataclass with `x, y, dx, dy`) but they all share the same tree
definition. Each tick runs all 5 flocking rules:
While(
cond = WhenAll([FlyTowardsCenter, AvoidOthers]),
body = [MatchVelocity, LimitSpeed, KeepWithinBounds],
)
Both cond actions return `Running`, so the BT stays in the body each
tick and all 5 actions fire — updating each boid's velocity and
position. Output is text-only (no graphics window); first and last
boids are logged each tick.
Demonstrates a shared `Behavior` across many `BT` instances,
real-time-loop `dt` integration, `WhenAll` for parallel cond updates,
and `While`-body re-execution per tick.
Run:
python bonsai-py/examples/boids_console.py
"""
from __future__ import annotations
import enum
import math
import random
import time
from dataclasses import dataclass
from typing import Any
import bonsai_bt as bt
NUM_BOIDS = 10
WIDTH = 1280.0
HEIGHT = 720.0
SPEED_LIMIT = 400.0
VISUAL_RANGE = 32.0
MIN_DISTANCE = 16.0
TICKS = 30
DT_SECONDS = 0.1
class Action(enum.Enum):
AVOID_OTHERS = enum.auto()
FLY_TOWARDS_CENTER = enum.auto()
MATCH_VELOCITY = enum.auto()
LIMIT_SPEED = enum.auto()
KEEP_WITHIN_BOUNDS = enum.auto()
@dataclass
class Boid:
x: float
y: float
dx: float
dy: float
def distance(self, other: Boid) -> float:
return math.hypot(self.x - other.x, self.y - other.y)
def build_tree() -> bt.Behavior:
"""One Behavior, shared across all N boid BTs (matches the Rust pattern)."""
avoid_and_fly = bt.WhenAll([
bt.Action(Action.FLY_TOWARDS_CENTER),
bt.Action(Action.AVOID_OTHERS),
])
return bt.While(
avoid_and_fly,
[
bt.Action(Action.MATCH_VELOCITY),
bt.Action(Action.LIMIT_SPEED),
bt.Action(Action.KEEP_WITHIN_BOUNDS),
],
)
def make_callback(idx: int, all_boids: list[Boid]):
"""Build a callback closed over this boid's neighbors (via `all_boids`)."""
def cb(args: Any, boid: Boid) -> tuple[bt.Status, float]:
others = [b for j, b in enumerate(all_boids) if j != idx]
match args.action:
case Action.AVOID_OTHERS:
move_x = move_y = 0.0
for other in others:
dist = boid.distance(other)
if 0.0 < dist < MIN_DISTANCE:
move_x += boid.x - other.x
move_y += boid.y - other.y
boid.dx += move_x * 0.5
boid.dy += move_y * 0.5
return bt.RUNNING
case Action.FLY_TOWARDS_CENTER:
cx = cy = 0.0
n = 0
for other in others:
if boid.distance(other) < VISUAL_RANGE:
cx += other.x
cy += other.y
n += 1
if n > 0:
boid.dx += (cx / n - boid.x) * 0.05
boid.dy += (cy / n - boid.y) * 0.05
return bt.RUNNING
case Action.MATCH_VELOCITY:
avg_dx = avg_dy = 0.0
n = 0
for other in others:
if boid.distance(other) < VISUAL_RANGE:
avg_dx += other.dx
avg_dy += other.dy
n += 1
if n > 0:
boid.dx += (avg_dx / n - boid.dx) * 0.1
boid.dy += (avg_dy / n - boid.dy) * 0.1
return (bt.Status.Success, args.dt)
case Action.LIMIT_SPEED:
speed = math.hypot(boid.dx, boid.dy)
if speed > SPEED_LIMIT:
boid.dx = boid.dx / speed * SPEED_LIMIT
boid.dy = boid.dy / speed * SPEED_LIMIT
return (bt.Status.Success, args.dt)
case Action.KEEP_WITHIN_BOUNDS:
edge = 40.0
turn = 16.0
if boid.x < edge:
boid.dx += turn
if boid.x > WIDTH - edge:
boid.dx -= turn
if boid.y < edge:
boid.dy += turn
if boid.y > HEIGHT - edge:
boid.dy -= turn
return bt.RUNNING
case _:
raise ValueError(f"unknown action: {args.action!r}")
return cb
def main() -> None:
rng = random.Random(0) # deterministic for reproducible console output
boids = [
Boid(
x=rng.uniform(WIDTH / 4, 3 * WIDTH / 4),
y=rng.uniform(HEIGHT / 4, 3 * HEIGHT / 4),
dx=(rng.random() - 0.5) * SPEED_LIMIT,
dy=(rng.random() - 0.5) * SPEED_LIMIT,
)
for _ in range(NUM_BOIDS)
]
shared_tree = build_tree()
bts = [bt.BT(shared_tree, boids[i]) for i in range(NUM_BOIDS)]
print(f"Boids console demo: {NUM_BOIDS} agents sharing one Behavior tree.")
for step in range(TICKS):
for i, tree_bt in enumerate(bts):
tree_bt.tick(DT_SECONDS, make_callback(i, boids))
boid = boids[i]
boid.x += boid.dx * DT_SECONDS
boid.y += boid.dy * DT_SECONDS
print(
f"[boid {i:2d}] step {step:2d}"
f" pos=({boid.x:7.1f}, {boid.y:7.1f})"
f" vel=({boid.dx:7.1f}, {boid.dy:7.1f})"
)
time.sleep(DT_SECONDS / 10.0) # tiny pause so output is readable
print(f"Done after {TICKS} ticks. Each BT instance ticked {bts[0].tick_count()} times.")
if __name__ == "__main__":
main()