Last active
August 5, 2026 14:33
-
-
Save cormullion/faf1fe3a822293dc54d16686c17eec72 to your computer and use it in GitHub Desktop.
simple agent script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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
commented
Aug 4, 2026
Author
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