Skip to content

Instantly share code, notes, and snippets.

@cormullion
Last active August 5, 2026 14:33
Show Gist options
  • Select an option

  • Save cormullion/faf1fe3a822293dc54d16686c17eec72 to your computer and use it in GitHub Desktop.

Select an option

Save cormullion/faf1fe3a822293dc54d16686c17eec72 to your computer and use it in GitHub Desktop.
simple agent script
# this is using Agents.jl, it has nothing to do with AI agents...
using Agents, Random, Luxor, Colors
@agent struct Automaton(GridAgent{2})
heading::Float64
history::Vector{Tuple{Int64,Int64}}
end
# is pos (r, c) in a control zone (ie masked out)(array)?
function in_control_zone(pos, control)
return control[pos...]
end
# set up the model
function build_model(;
dims = (40, 40),
metric = :chebyshev,
seed = 42,
control_zone = BitArray(undef, (dims)))
space = GridSpaceSingle(dims; periodic = false, metric)
properties = Dict()
status = zeros(Int64, dims)
control = control_zone
# We use a `NamedTuple` for the parameter container to avoid type instabilities
properties = (; status, control)
println("seed $(seed)")
model = StandardABM(Automaton, space; properties, agent_step!,
rng = MersenneTwister(seed), container = Vector)
return model
end
dirvec_to_heading(dv) = atan(last(dv), first(dv))
function center(a)
rows, columns = size(a)
# is this biased?
return (rows ÷ 2 + rand(-1:1), columns ÷ 2 + rand(-1:1))
end
# add n agents to model
function add_agents(model, n)
# model.control is a mask, not used in this script
control_zone = model.control
array = model.status
inds = findall(!iszero, array)
for ind in inds
pos = Tuple(ind)
heading = dirvec_to_heading(get_direction(center(array), pos, model))
agent = add_agent!(pos, model, heading, [pos])
array[pos...] = agent.id
nagents(model) > n && break
end
@info "... $(nagents(model)) agents"
na = nagents(model)
# now do the rest
while nagents(model) < n
heading = rand(abmrng(model)) * 2π
pos = random_empty(model)
heading = dirvec_to_heading(pos)
if !in_control_zone(pos, control_zone)
agent = add_agent!(pos, model, heading, [pos])
array[pos...] = agent.id
end
end
@info "... created $(n - na) random agents"
@info "created $(nagents(model)) agents total"
end
function agent_step!(agent, model)
current_pos = agent.pos
# get all empty positions
positions = empty_nearby_positions(agent, model)
array = model.status
control_zone = model.control
# looking for squares that haven't been visited
candidates = []
for (i, nextpos) in enumerate(positions)
possible_new_heading = dirvec_to_heading(get_direction(current_pos, nextpos, model))
xv, yv = get_direction(current_pos, nextpos, model)
if in_control_zone(nextpos, control_zone)
continue
end
# only if that square is unvisited
if !iszero(array[nextpos...]) #|| array[nextpos...] == agent.id # OR if it was there before?
continue
end
# not if the possible new heading is too sharp a turn?
if abs(possible_new_heading - agent.heading) > deg2rad(MAX_TURN_ANGLE_DEGREES)
continue
end
# if on a diagonal, check for crossing another
if all(!iszero, [xv, yv])
# there are four cells of interest
# nextpos, current_pos, and the other two that form a square
# sort them first
p1, p2 = first(nextpos) < first(current_pos) ? (nextpos, current_pos) : (current_pos, nextpos)
# the other two
p3 = (first(p2), last(p1))
p4 = (first(p1), last(p2))
# if they have the same ID, the paths cross!
if array[p3...] == array[p4...] && array[p3...] > 0 && array[p4...] > 0
continue
end
end
push!(candidates, nextpos)
end
# backtrack or skip?
if length(candidates) == 0
# only if there's a bit of history to backtrack to
if length(agent.history) > 2
array[current_pos...] = 0
pop!(agent.history)
newpos = last(agent.history)
prevpos = agent.history[end - 1]
move_agent!(agent, newpos, model)
array[agent.pos...] = agent.id
agent.heading = dirvec_to_heading(get_direction(prevpos, newpos, model))
return
else
#add_one_agent!(model)
return
end
end
# how to choose a new position from the candidate positions
# random or choose from a sorted list?
# sort!(candidates, lt = (a, b) -> begin
# dirvec_to_heading(get_direction(a, current_pos, model)) >
# dirvec_to_heading(get_direction(b, current_pos, model))
# end)
if rand(abmrng(model), 1:20) < 30
newpos = first(Random.shuffle(abmrng(model), candidates))
else
newpos = first(candidates)
end
# move agent to new position
move_agent!(agent, newpos, model)
# update the array and the history
array[newpos...] = agent.id
agent.heading = dirvec_to_heading(get_direction(current_pos, newpos, model))
push!(agent.history, newpos)
end
function run_model(;
dims = (1, 1),
steps = 1,
nagents = 1,
seed = 1,
control_zone = BitArray(undef, (dims)))
model = build_model(dims = dims,
control_zone = control_zone,
seed = seed)
r, c = size(abmspace(model))
add_agents(model, nagents)
step!(model, steps)
return model
end
function lighten(col::Colorant, f)
c = convert(RGB, col)
return RGB(f * c.r, f * c.g, f * c.b)
end
function single(C, R, nagents, nsteps)
julia_colors = [Luxor.julia_red,
Luxor.julia_purple,
Luxor.julia_blue,
Luxor.julia_green,
]
vibrant_julia = RGBA[RGBA{Float64}(0.647, 0.352, 0.735, 1.0),
RGBA{Float64}(0.133, 0.565, 1.1, 1.0),
RGBA{Float64}(0.205, 0.691, 0.272, 1.0),
RGBA{Float64}(0.936, 0.201, 0.1501, 1.0)]
control_zone = BitArray(undef, (R, C))
# do we want a random seed?
S = rand(1:1000)
Random.seed!(S)
@show S
@show R, C
println("█run███████████████████████████████████████\n")
if nagents > R * C
throw("Too many agents or not enough space for them")
end
#=
# a control_zone is a forbidden zone that the agents can't enter
# basically you would create a masking pattern:
control_zone = falses(R, C)
center = (R + 1) / 2
for r in 1:R, c in 1:C
dist = sqrt((r - (R + 1) / 2)^2 + (c - (C + 1) / 2)^2)
control_zone[r, c] = dist <= R/3
end
=#
# run the model
model = run_model(steps = nsteps,
dims = (R, C),
nagents = nagents,
seed = S,
control_zone = control_zone)
println("█draw███████████████████████████████████████\n")
# finally, draw
# convert row column to Luxor x/y by multiplying
# the rows/columns by some number such as 30
# so 50 rows is about 1500 pixels
D = 30
H, W = D .* (R, C)
@show W, H
Drawing(W, H, "/tmp/agent-circuit.svg")
# background("grey10")
origin()
setlinejoin("round")
r, c = size(abmspace(model))
@show r, c
tiles = Tiler(W, H, r, c)
sz = max(tiles.tilewidth, tiles.tileheight)
for (n, agent) in enumerate(allagents(model))
setline(sz / 3)
sethue(vibrant_julia[rand(abmrng(model), 1:end)])
history = agent.history
if length(history) <= 1 # agent didn't go anywhere
continue
end
# draw the agent's history
newpath()
move(tiles[history[1]...])
for i in 1:(length(history) - 1)
y1, x1 = history[i]
y2, x2 = history[i + 1]
line(tiles[y2, x2])
end
strokepath()
# add the circles at each end
for i in [1, length(history)]
y1, x1 = history[i]
circle(tiles[y1, x1], sz / 3, :fill)
end
end
finish()
preview()
end
MAX_TURN_ANGLE_DEGREES = 51 #
# single(Columns, Rows, numberofagents, numbnerofsteps)
single(150, 100, 1600, 2000)
# the trick is to carefully adjust the number of agants and
# the number of steps according to the size of the grid
# single(15, 10, 30, 1000) # v. simple
# single(200, 200, 4000, 6000) # takes 5 seconds on my Mac
@cormullion

Copy link
Copy Markdown
Author
agent-circuit

@cormullion

Copy link
Copy Markdown
Author

@rayegun Well I dug the script out - it still kind of works, but occasionally it will go wrong, so Ctrl-C is as usual your friend.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment