⚠️ Warning: This is a draft ⚠️

This means it might contain formatting issues, incorrect code, conceptual problems, or other severe issues.

If you want to help to improve and eventually enable this page, please fork RosettaGit's repository and open a merge request on GitHub.

using Gtk.ShortNames, Colors, Cairo, Graphics

const fontpointsize = 10
const mapwidth = 1000
const mapheight = 500
const windowmaxx = div(mapwidth, Int(round(fontpointsize * 0.92)))
const windowmaxy = div(mapheight, fontpointsize)
const basebuffer = fill(' ', windowmaxy, windowmaxx)

win = Window("Boids", mapwidth, mapheight) |> (can = Canvas())
set_gtk_property!(can, :expand, true)

@guarded Gtk.draw(can) do widget
    ctx = Gtk.getgc(can)
    select_font_face(ctx, "Courier", Cairo.FONT_SLANT_NORMAL, Cairo.FONT_WEIGHT_BOLD)
    set_font_size(ctx, fontpointsize)
    workcolor = colorant"black"
    set_source_rgb(ctx, 0.2, 0.2, 0.2)
    rectangle(ctx, 0, 0, mapwidth, mapheight)
    fill(ctx)
    color = colorant"white"
    set_source(ctx, color)
    linelen = size(basebuffer)[2]
    workbuf = Char[]
    for i in 1:size(basebuffer)[1]
        move_to(ctx, 0, i * fontpointsize)
        lastcharprinted = '\x01'
        for j in 1:linelen
            ch = basebuffer[i, j]
            if j == 1
                lastcharprinted = ch
            elseif ch != lastcharprinted
                show_text(ctx, String(workbuf))
                empty!(workbuf)
            end
            if haskey(itemcolors, ch) && itemcolors[ch] != color
                color = itemcolors[ch]
                set_source(ctx, color)
            end
            push!(workbuf, ch)
            if j == linelen
                show_text(ctx, String(workbuf))
                empty!(workbuf)
            end
        end
    end
end


@enum Directions NW N NE E SE S SW W Here

const defaultdirection = E

boidmoves = Dict{Directions, Vector{Int}}(Here => [0, 0], NW => [-1, -1], N => [0, -1], NE => [1, -1],
    E => [1, 0], SE => [1, 1], S => [0, 1], SW => [-1, 1], W => [-1, 0])

struct Point
    x::Int
    y::Int
end

mutable struct Obstacle
    occupied::Vector{Point}
end

mutable struct Walls
    occupied::Vector{Point}
end

mutable struct Environment
    width::Int
    height::Int
    walls::Walls
    obstacles::Vector{Obstacle}
    buffer::Matrix{Char}
end

ebuf(e, p::Point) = e.buffer[p.x, p.y]

mutable struct Boid
    pos::Point
    flock::Vector{Boid}
end

aschar(b::Boid) = 'o'
aschar(o::Obstacle) = '*'
aschar(w::Walls) = '\u2593'

function buildwalls(environ)
    for i in 1:environ.height
        push!(environ.walls.occupied, Point(1, i), Point(environ.width, i))
    end
    for i in 1:environ.width
        push!(environ.walls.occupied, Point(i, 1), Point(i, environ.height))
    end
    for p in environ.walls.occupied
        if 0 < p.x <= environ.width && 0 < p.y <= environ.height
            environ.buffer[p.y, p.x] = aschar(environ.walls)
        end
    end
end

inellipse(dx, dy, a, b) = (dx / a)^2 + (dy / b)^2 < 1.0
inellipseat(p, x, y, a, b) = inellipse(x - p.x, y - p.y, a, b)

function buildobstacles(environ, n=5)
    widthoptions = collect(3:max(5, div(environ.height, 10)))
    heightoptions = collect(7:max(10, div(environ.height, 2)))
    for i in 1:n
        obst = Obstacle(Point[])
        push!(environ.obstacles, obst)
        w, h = rand(widthoptions), rand(heightoptions)
        w, h = (w > h) ? (h, w) : (w, h)
        center = Point(Int(round(environ.width * rand())), Int(round(environ.height * rand())))
        for y in center.y-h:center.y+h, x in center.x-w:center.x+w
            if inellipseat(center, x, y, w, h) && 1 < x < environ.width && 1 < y < environ.height
                push!(obst.occupied, Point(x, y))
                environ.buffer[y, x] = aschar(obst)
            end
        end
    end
end

function buildenvironment()
    environ = Environment(windowmaxx, windowmaxy, Walls(Point[]), Obstacle[], basebuffer)
    buildwalls(environ)
    buildobstacles(environ)
    environ
end

function addflock(allflocks, numboids, environ)
    f = Vector{Boid}()
    center = Point(12, div(environ.height, 2))
    varpick = collect(-10:10)
    while length(f) < numboids
        while true
            newboid = Boid(Point(center.x + rand(varpick), center.y + rand(varpick)), f)
            if all(x -> x.pos != newboid.pos, f) && environ.buffer[newboid.pos.y, newboid.pos.x] == ' '
                push!(f, newboid)
                break
            end
        end
    end
    push!(allflocks, f)
end

function availablemoves(boid, environ)
    avail = Vector{Directions}()
    for (direc, v) in boidmoves
        if environ.buffer[boid.pos.y + v[2], boid.pos.x + v[1]] == ' '
            push!(avail, direc)
        end
    end
    avail
end

function center(flock)
    xs, ys, n = 0, 0, length(flock)
    for b in flock
        xs += b.pos.x
        ys += b.pos.y
    end
    Point(Int(round(xs / n)), Int(round(ys / n)))
end

isobstacle(x, y, environ) = (c = environ.buffer[y, x]; c != '*' && c != '\u2593')
nextobstaclex(pos, e) = (x = pos.x + 1; while e.buffer[pos.y, x] == ' ' x += 1 end; x)

nearobs(b, e, delt=8) = b.pos.x - nextobstaclex(b.pos, e) < delt
atobs(b, e) = e.buffer[b.pos.y, b.pos.x + 1] != ' '

function nearbyopen(b, e, opendist = e.width)
    dist, y = findmax([nextobstaclex(Point(b.pos.x, y), e) for y in 1:e.height])
    return y
end

function move(boid, d::Directions)
    m = boidmoves[d]
    boid.pos = Point(boid.pos.x + m[1], boid.pos.y + m[2])
end

showboid(b, environ) = begin x, y = b.pos.x, b.pos.y; environ.buffer[y, x] = aschar(b) end
hideboid(b, environ) = begin x, y = b.pos.x, b.pos.y; environ.buffer[y, x] = ' ' end
showmove(b, e) = begin hideboid(b, e); move(b, e); showboid(b, e) end

function move(boid, environ::Environment)
    possmoves = availablemoves(boid, environ)
    fcenter = center(boid.flock)
    wantsouth, wantnorth = false, false
    if atobs(boid, environ)
        wanty = nearbyopen(boid, environ, 8)
        d = wanty > boid.pos.y && S in possmoves ? S : N in possmoves ? N : rand(possmoves)
        move(boid, d)
        return
    end
    if rand() > 0.5 && nearobs(boid, environ)
        wanty = nearbyopen(boid, environ, 8)
        d = wanty > boid.pos.y && SE in possmoves ? SE : NE in possmoves ? NE : E
        move(boid, d)
        return
    end
    if rand() > 0.5 && fcenter.x < boid.pos.x - 2 && W in possmoves
        move(boid, W)
        return
    end
    if fcenter.y > boid.pos.y + 1
        wantsouth = true
    elseif fcenter.y < boid.pos.y - 1
        wantnorth = true
    end
    if wantsouth
        if SE in possmoves
            move(boid, SE)
        elseif S in possmoves
            move(boid, S)
        end
    elseif wantnorth
        if NE in possmoves
            move(boid, NE)
        elseif N in possmoves
            move(boid, N)
        end
    elseif E in possmoves
        move(boid, E)
    end
end

const itemcolors = Dict{Char, Colorant}('o' => colorant"white", ' ' => colorant"black", '*' => colorant"gold",
                                       '\u2593' => colorant"silver")

environ = buildenvironment()
const allflocks = Vector{Vector{Boid}}()
addflock(allflocks, 5, environ)

draw(can)
show(can)

while true 
    sleep(0.5)
    for flock in allflocks, boid in flock
        showmove(boid, environ)
    end
    draw(can)
end