-
Notifications
You must be signed in to change notification settings - Fork 0
/
agents.wren
329 lines (291 loc) · 11.4 KB
/
agents.wren
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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
/// Agent-based modeling (ABM) framework for Wren applications.
///
/// An agent-based (or individual-based) model is a computational simulation of autonomous agents that react to their environment (including other agents) given a predefined set of rules <sup>[[1](http://doi.org/10.1016/j.ecolmodel.2006.04.023)]</sup>.
/// Many real-world emergent behaviors given simple agent interactions can only be captured in an agent-based model.
///
/// ## Prior Art
///
/// [agents.jl](https://juliadynamics.github.io/Agents.jl/stable)
///
/// Authors: Chance Snow <[email protected]>
/// Copyright: Copyright © 2024 Chance Snow
/// License: MIT License
// TODO: Write a Jupyter kernel for Wren: https://jupyter-client.readthedocs.io/en/latest/kernels.html
import "random" for Random
/// An autonomous agent that behave given a set of rules.
class Agent {
/// Aborts: When the world has exhaused its supply of agent IDs.
construct create() {
if (__lastId == Num.maxSafeInteger) Fiber.abort("Error: The world has exhaused its supply of agent IDs.")
_id = (__lastId = __lastId == null ? 0 : __lastId + 1)
_live = true
_location = null
_time = 0
}
/// Returns: Num
id { _id }
/// Whether this agent is living in its world.
/// Returns: Bool
live { _live }
/// @protected
/// Warning: Do *NOT* directly mutate an agent's liveness. It is managed for you by the `World` simulation.
/// See: `Space.kill(agent)`
live=(value) { _live = value }
/// Position of this agent in its `World`.
/// Returns: Pos
location { _location }
/// @protected
/// Warning: Do *NOT* directly mutate an agent's position. It is managed for you by the `World` simulation.
/// See: `Space.move(agent, pos)`
location=(value) { _location = value }
/// The current agent-local time. Akin to a stopwatch counting up from zero.
/// Returns: Num
time { _time }
/// @virtual
/// Returns: Num Current agent-local time.
/// See: `time`
tick() {
_time = _time + 1
}
}
/// @private
var random = Random.new()
/// Represents a speific model, i.e. its agents and the space they share, by mapping unique IDs (integers) to agent
/// instances.
///
/// During simulation, the model evolves in discrete steps.
class World {
/// Params: space: Space
construct create(space) {
_time = 0
_space = space
_agents = {}
space.world = this
}
/// The current global time. Akin to a stopwatch counting up from zero.
/// Returns: Num
time { _time }
/// Returns: Space
space { _space }
/// Map of unique IDs to `Agent`s.
/// Warning: Do *NOT* mutate this map. Use `space.add(agent)`, `space.remove(agent)`, and `space.move(agent, pos)` to
/// modify a world's agents.
/// Returns: Agent[Num]
agents { _agents }
/// Returns: Agent A random agent from the model.
randomAgent { _agents.values.count == 0 ? null : _agents[random.int(_agents.count)] }
/// @virtual
/// Returns: Num Current global time.
/// See: `time`
tick() {
_agents.values.each {|agent| agent.tick() }
_time = _time + 1
}
}
/// A specific area in which `Agent`s occupy. Base class of any `World`-space implementation.
///
/// Provided examples include a `Grid`, and `Graph`.
///
/// When creating custom spaces consider:
/// 1. Type of an agent's position.
/// 2. How agents near each other are represented, such that you can override the `neighbors` property.
/// 3. Potential random values in the space, such that you can override `randomPosition`.
///
/// Then, define a new class and override these members:
/// - `randomPosition` property
/// - `neighbors(agentOrPos)` and `neighbors(agentOrPos, radius)` functions
class Space {
/// Returns: Bool Whether the given `value` is an instance of `Agent` or `Pos`.
/// Aborts: When the given value is _not_ an instance of `Agent` or `Pos`.
static isAgentOrPos(value) {
if (!(value is Agent || value is Pos)) Fiber.abort("Error: Expected a value of type `Agent` or `Pos`.")
return true
}
/// The world this space represents.
/// Returns: World
world { _world }
/// Params: value: World
world=(value) { _world = value }
/// @abstract
/// Returns: Pos
randomPosition { Fiber.abort("Error: `Space.randomPosition` is abstract.") }
/// @virtual
/// Params: agentOrPos: Agent | Pos around which to find neighbors. If an `Agent` is given, it is excluded.
/// Returns: Sequence<Agent>
/// See: Prior art: Agents.jl [`nearby_ids`](https://juliadynamics.github.io/Agents.jl/stable/api/#Agents.nearby_ids)
neighbors(agentOrPos) { Fiber.abort("Error: `Space.neighbors(agentOrPos)` is abstract.") }
/// Params:
/// agentOrPos: Agent | Pos around which to find neighbors. If an `Agent` is given, it is excluded.
/// radius: Num Inclusive distance within which to search
neighbors(agentOrPos, radius) { Fiber.abort("Error: `Space.neighbors(agentOrPos, radius)` is abstract.") }
/// @final
/// Add the given agent to this space.
/// Params: agent: Agent
/// Aborts: When this space does not belong to a `World`.
add(agent) {
this.add(agent, null)
}
/// Add the given agent to this space at the given `pos`ition.
/// Params:
/// agent: Agent
/// location: null | Pos
/// Aborts: When this space does not belong to a `World`.
add(agent, location) {
if (_world == null) Fiber.abort("Error: This space is not attached to a `World`.")
world.agents[agent.id] = agent
move(agent, location)
}
/// @final
/// Remove the given agent from this space.
/// Params: agent: Agent
/// Aborts: When this space does not belong to a `World`.
remove(agent) {
if (_world == null) Fiber.abort("Error: This space is not attached to a `World`.")
if (!world.agents.containsKey(agent.id)) Fiber.abort("Error: The given agent does not exist in this space's world.")
world.agents.remove(agent.id)
}
/// @virtual
/// Move the given `agent` to a new `location`.
/// Params:
/// agent: Agent
/// location: null | Pos
/// Aborts: When this space does not belong to a `World`.
move(agent, location) {
if (_world == null) Fiber.abort("Error: This space is not attached to a `World`.")
agent.location = location
}
/// @final
/// Params: agent: Agent
/// Aborts: When this space does not belong to a `World`.
kill(agent) {
if (_world == null) Fiber.abort("Error: This space is not attached to a `World`.")
if (!world.agents.containsKey(agent.id)) Fiber.abort("Error: The given agent does not exist in this space's world.")
agent.live = false
}
}
import "./wren_modules/wren-vector/vector" for Vector
/// 2D area of a given size. Positions are 2D vectors. Agents _may_ share locations.
class Grid is Space {
/// Params:
/// width: Num
/// height: Num
construct create(width, height) {
_size = Vector.new(width, height)
}
/// Returns: Vector
size { _size }
/// Returns: Num
width { _size.x }
/// Returns: Num
height { _size.y }
/// Returns: Pos
randomPosition { Pos.at(Vector.new(random.int(width), random.int(height))) }
/// Params: agentOrPos: Agent | Pos around which to find neighbors. If an `Agent` is given, it is excluded.
/// Returns: Sequence<Agent>
neighbors(agentOrPos) { neighbors(agentOrPos, width.max(height)) }
/// Params:
/// agentOrPos: Agent | Pos around which to find neighbors. If an `Agent` is given, it is excluded.
/// radius: Num Inclusive distance within which to search.
neighbors(agentOrPos, radius) {
Space.isAgentOrPos(agentOrPos)
return world.agents.values.where {|n|
if (agentOrPos is Agent && n.id == agentOrPos.id) return false
var other = agentOrPos is Agent ? agentOrPos.location.pos : agentOrPos.pos
var distance = (n.location.pos - other).magnitude
return distance <= radius
}
}
/// Move the given `agent` to a new `location`.
/// Params:
/// agent: Agent
/// location: null | Pos When `null`, the agent is placed randomly in this space.
/// Aborts: When this space does not belong to a `World`.
move(agent, location) {
super.move(agent, location == null ? randomPosition : location)
}
}
/// A graph of agents. Agents _may_ share locations.
///
/// Nodes in the graph are collections of agents. Edges are connections to the node's neighbors.
/// Remarks: Represented internally as an [adjacency list](https://en.wikipedia.org/wiki/Adjacency_list) of agent IDs.
class Graph is Space {
construct create() {
/// Adjacency list of agent IDs.
/// Type: (Num[])[Num]
_nodes = {}
}
/// Returns: Pos randomly adjacent to another agent.
randomPosition {
if (_nodes.values.isEmpty) return null
var id = null
while (agent == null || world.agents.containsKey(id) == false) {
id = random.int(lastId)
}
return Pos.at(id)
}
/// Params: agentOrPos: Agent | Pos around which to find neighbors. If an `Agent` is given, it is excluded.
/// Returns: Sequence<Agent>
neighbors(agentOrPos) { neighbors(agentOrPos, Num.maxSafeInteger) }
/// Params:
/// agentOrPos: Agent | Pos around which to find neighbors. If an `Agent` is given, it is excluded.
/// radius: Num Inclusive distance within which to search.
/// Aborts: When the given agent's position or position is not the correct type.
neighbors(agentOrPos, radius) {
Space.isAgentOrPos(agentOrPos)
return world.agents.values.where {|n|
if (agentOrPos is Agent && n.id == agentOrPos.id) return false
var other = agentOrPos is Agent ? agentOrPos.location.pos : agentOrPos.pos
if (other is Num == false) Fiber.abort("Error: The given agent's position or position is not the correct type.")
/// Try to find agent `n` in root's neighbors in a breadth-first search.
/// Short-circuit if the search radius is exceeded or a node is not in the graph
var search = Fn.new {|root, distance|
if (distance > radius || _nodes.containsKey(root) == false) return false
if (_nodes[root].any {|needle| _nodes[root].contains(n.id) }) return true
return _nodes[root].any {|node| search(node, distance + 1) }
}
return search.call(other, 0)
}
}
/// Move the given `agent` to a new `location`.
/// Params:
/// agent: Agent
/// location: null | Pos
/// Aborts: When this space does not belong to a `World`.
move(agent, location) {
location = location == null ? Pos.at(agent.id) : location
super.move(agent, location)
// Disconnect the agent from its old neighbors
var neighbors = _nodes.containsKey(agent.id) ? _nodes[agent.id] : []
neighbors.each {|n| _nodes[n].remove(agent.id) }
if (location.pos == agent.id) _nodes[agent.id] = []
// Connect the agent to its new neighbor and vice-versa
if (location.pos != agent.id) {
_nodes[agent.id] = [location.pos]
var oppositeLinkExists = _nodes.containsKey(location.pos)
oppositeLinkExists ? _nodes[location.pos].add(agent.id) : _nodes[location.pos] = [agent.id]
}
}
}
/// @final
/// A position in a `World`'s space.
/// See: `Space`
class Pos {
/// `List` of valid types to represent an agent's position in a `Space`.
/// Returns: String[] `Class` names.
static validTypes {[
Num.name,
Map.name,
Vector.name
]}
/// Params: pos: Num | Map | Vector
/// Aborts: When the given `pos` is not of a valid type.
/// See: `validTypes`
construct at(pos) {
var valid = pos is Num || pos is Map || pos is Vector
if (!valid) Fiber.abort("Error: `%(pos)` is not a valid position type. See `Pos.validTypes`.")
_pos = pos
}
/// Returns: Num | Map | Vector
/// See: `Agent.location`
pos { _pos }
}