Engee documentation
Notebook

The Sheep and Wolves Game

In this example, we will consider a model, the essence of the simulation of which is to display the "Predator-Prey-Plants" scenario. This is an agent-based model that simulates an ecosystem with three key components:

  1. Sheep (victims) - they feed on plants and reproduce with sufficient energy
  2. Wolves (predators) - hunt sheep, require more energy to survive
  3. Plants - regenerate over time, serve as food for sheep

The key mechanisms here are nutrition cycles, represented as follows Wolves → Sheep → Plants and population dynamics, which are regulated automatically through energy consumption (starvation) and reproduction when energy thresholds are reached, as well as random movement of agents

The model demonstrates the classical principles of ecology:

  • Food chains
  • Competition for resources
  • Dynamic population equilibrium
  • The bottleneck effect of species extinction
In [ ]:
Pkg.add("StatsBase")
using Random
using StatsBase
   Resolving package versions...
  No Changes to `~/.project/Project.toml`
  No Changes to `~/.project/Manifest.toml`

Now we will declare the main parameters and structures of the objects of our model.

  1. World Parameters:

    • WIDTH and HEIGHT - dimensions of the two-dimensional world (in cells)
    • MAX_STEPS - limit of simulation iterations
    • SAVE_EVERY - frequency of saving state for visualization
  2. Agents:

    • All agents have coordinates (x,y) and inherit from the base type Agent
    • Sheep and Wolf contain the parameter energy - a key indicator for:
      • Survival (at energy < 0, the agent dies)
      • Reproduction (requires sufficient energy level)
    • Bush has a flag active - can it be eaten
  3. Model:

    • Contains arrays of all agents (sheep, wolves)
  • The matrix bushes (plants) of the size WIDTH×HEIGHT
  • Step counter step
    • Dictionary params with configurable parameters:
      • Probabilities of events (reproduction, restoration of plants)
  • Energy parameters (consumption, consumption, thresholds)
  • Ecosystem balance (survival difficulty can be adjusted)

These structures form the framework for:

  • Simulation of agent movement
  • Food systems (wolves→sheep→plants)
  • Mechanisms of reproduction and death
  • Visualization of the ecosystem state
In [ ]:
const WIDTH = 50
const HEIGHT = 50
const MAX_STEPS = 2000
const SAVE_EVERY = 100

abstract type Agent end
mutable struct Sheep <: Agent
    x::Int
    y::Int
    energy::Int
end
mutable struct Wolf <: Agent
    x::Int
    y::Int
    energy::Int
end
mutable struct Bush <: Agent
    x::Int
    y::Int
    active::Bool
end
mutable struct Model
    sheep::Vector{Sheep}
    wolves::Vector{Wolf}
    bushes::Matrix{Bush}
    step::Int
    params::Dict{Symbol,Float64}
end

Function init_model creates the initial state of an ecosystem model with three types of agents: sheep, wolves, and plants.

What does the function do:

  1. Initializes the world, creates a matrix of plants measuring WIDTH×HEIGHT (the number is set in n_bushes)

  2. Creates agents: sheep (n_sheep pieces) with starting energy sheep_energy, wolves (n_wolves pieces) with starting energy wolf_energy, randomly places them on the map

  3. Adjusts the model parameters through the dictionary params:

    • Probabilities of plant recovery and animal reproduction
    • Energy consumption during starvation
    • Energy gain during nutrition
    • Energy thresholds for reproduction

Returns: Object type Model With the initial state of the ecosystem, all these function parameters allow you to flexibly adjust the ecosystem balance before launch.

In [ ]:
function init_model(;
    n_sheep = 50,
    n_wolves = 10,
    n_bushes = 500,
    sheep_energy = 10,
    wolf_energy = 50,
    bush_regrowth = 0.01,
    sheep_reprod = 0.04,
    wolf_reprod = 0.05,
    sheep_hunger = 1,
    wolf_hunger = 2,
    sheep_eat_gain = 5,
    wolf_eat_gain = 20,
    sheep_reprod_thresh = 15,
    wolf_reprod_thresh = 30
)

bushes = Matrix{Bush}(undef, WIDTH, HEIGHT)
for x in 1:WIDTH, y in 1:HEIGHT
    bushes[x,y] = Bush(x, y, false)
end
all_positions = [(x, y) for x in 1:WIDTH, y in 1:HEIGHT]
bush_positions = sample(all_positions, n_bushes, replace=false)
for (x, y) in bush_positions
    bushes[x,y].active = true
end
sheep = Sheep[]
for _ in 1:n_sheep
    x, y = rand(1:WIDTH), rand(1:HEIGHT)
    push!(sheep, Sheep(x, y, sheep_energy))
end
wolves = Wolf[]
for _ in 1:n_wolves
    x, y = rand(1:WIDTH), rand(1:HEIGHT)
    push!(wolves, Wolf(x, y, wolf_energy))
end
params = Dict(
    :bush_regrowth => bush_regrowth,
    :sheep_reprod => sheep_reprod,
    :wolf_reprod => wolf_reprod,
    :sheep_hunger => sheep_hunger,
    :wolf_hunger => wolf_hunger,
    :sheep_eat_gain => sheep_eat_gain,
    :wolf_eat_gain => wolf_eat_gain,
    :sheep_reprod_thresh => sheep_reprod_thresh,
    :wolf_reprod_thresh => wolf_reprod_thresh
)
Model(sheep, wolves, bushes, 0, params)
end
Out[0]:
init_model (generic function with 1 method)

Function move_agent! Moves the agent (sheep or wolf) on the map randomly. It generates a random offset dx and dy takes the values -1, 0, or 1 (a step in any direction or in place) and calculates the new coordinates, mod1 provides "wrapping" of borders - if the agent goes beyond the edge of the map, it appears on the opposite side.

In [ ]:
function move_agent!(agent::Agent, model)
    dx, dy = rand(-1:1, 2)
    new_x = mod1(agent.x + dx, WIDTH)
    new_y = mod1(agent.y + dy, HEIGHT)
    agent.x, agent.y = new_x, new_y
end
Out[0]:
move_agent! (generic function with 1 method)

Function eat!(sheep::Sheep, model) allows a sheep to eat a plant in its current cage. She checks the bush under the sheep (model.bushes[sheep.x, sheep.y]), if the bush is active (active=true), makes the bush inactive (eats it) and increases the sheep's energy by sheep_eat_gain from the parameters, returns true (successful feeding), if the bush is inactive, returns false

Function reproduce!(agent::Sheep, model), carries out sheep breeding when conditions are met. It checks two conditions: a random number is less likely to reproduce (sheep_reprod) and the sheep 's energy is above the threshold value (sheep_reprod_thresh), if the conditions are met: divides the sheep's energy in half, creates a new sheep with the same coordinates and half the energy, adds it to the model, returns true (successful reproduction), if the conditions are not met, returns false

In [ ]:
function eat!(sheep::Sheep, model)
    bush = model.bushes[sheep.x, sheep.y]
    if bush.active
        bush.active = false
        sheep.energy += model.params[:sheep_eat_gain]
        return true
    end
    false
end
function reproduce!(agent::Sheep, model)
    if rand() < model.params[:sheep_reprod] && agent.energy > model.params[:sheep_reprod_thresh]
        agent.energy ÷= 2
        new_sheep = Sheep(agent.x, agent.y, agent.energy)
        push!(model.sheep, new_sheep)
        return true
    end
    false
end
Out[0]:
reproduce! (generic function with 2 methods)

Then there are similar functions for wolves, the key differences from sheep:

  • Wolves hunt sheep (not plants)

  • Use their own parameters (wolf_eat_gain, wolf_reprod etc.)

  • The mechanics of reproduction are similar, but with different numerical values

In [ ]:
function eat!(wolf::Wolf, model)
    sheep_idx = findfirst(s -> s.x == wolf.x && s.y == wolf.y, model.sheep)
    if !isnothing(sheep_idx)
        splice!(model.sheep, sheep_idx)
        wolf.energy += model.params[:wolf_eat_gain]
        return true
    end
    false
end
function reproduce!(agent::Wolf, model)
    if rand() < model.params[:wolf_reprod] && agent.energy > model.params[:wolf_reprod_thresh]
        agent.energy ÷= 2
        new_wolf = Wolf(agent.x, agent.y, agent.energy)
        push!(model.wolves, new_wolf)
        return true
    end
    false
end
Out[0]:
reproduce! (generic function with 2 methods)

Function regenerate_bushes!(model), restores eaten plants (bushes) on the map with a certain probability, this function runs through all cells of the playing field (WIDTH × HEIGHT), for each inactive bush (bush.active == false) with probability bush_regrowth (from the parameters of the model) makes the bush active, the probability is checked by comparing a random number with model.params[:bush_regrowth]

In [ ]:
function regenerate_bushes!(model)
    for x in 1:WIDTH, y in 1:HEIGHT
        bush = model.bushes[x,y]
        if !bush.active && rand() < model.params[:bush_regrowth]
            bush.active = true
        end
    end
end
Out[0]:
regenerate_bushes! (generic function with 1 method)

Function step!(model) performs one step of ecosystem simulation, updating the status of all agents and verifying completion conditions.

Main stages of work:

  1. Increment of the step counter model.step += 1

  2. Sheep handling (in a cycle):

    • Moving (move_agent!)
  • Nutrition (if you can 't eat , reduce energy by sheep_hunger)
  • An attempt at reproduction (reproduce!)
  1. Wolf treatment (in a cycle):

    • Moving (move_agent!)
  • Hunting (if not successful , decrease energy by wolf_hunger)
  • An attempt at reproduction (reproduce!)
  1. Filtering of deceased agents:

    • Removing sheep with energy less than 0
  • Removing wolves with energy less than 0
  1. Plant restoration:

    • regenerate_bushes! - renewal of eaten bushes
  2. Checking completion conditions:

    • Extinction of all sheep or all wolves
  • Reaching the maximum number of steps (MAX_STEPS)
  • Output of relevant messages
  1. Progress logging: each SAVE_EVERY steps displays the current statistics

  2. Return value:

  • false - if the simulation is to end
  • true - if the simulation continues
In [ ]:
function step!(model)
    model.step += 1
    for sheep in model.sheep
        move_agent!(sheep, model)
        if !eat!(sheep, model)
            sheep.energy -= model.params[:sheep_hunger]
        end
        reproduce!(sheep, model)
    end
    for wolf in model.wolves
        move_agent!(wolf, model)
        if !eat!(wolf, model)
            wolf.energy -= model.params[:wolf_hunger]
        end
        reproduce!(wolf, model)
    end
    filter!(s -> s.energy > 0, model.sheep)
    filter!(w -> w.energy > 0, model.wolves)
    regenerate_bushes!(model)
    if isempty(model.sheep) || isempty(model.wolves)
        if isempty(model.sheep)
            println("Симуляция остановлена на шаге $(model.step): все овцы вымерли.")
        else
            println("Симуляция остановлена на шаге $(model.step): все волки вымерли.")
        end
        return false
    end
    if model.step >= MAX_STEPS
        println("Симуляция завершена: достигнуто максимальное количество шагов ($MAX_STEPS).")
        return false
    end
    if model.step % SAVE_EVERY == 0
        println("Прогресс: завершен шаг $(model.step). Овец: $(length(model.sheep)), волков: $(length(model.wolves))")
    end
    true
end
Out[0]:
step! (generic function with 285 methods)

Function plot_model(model) вIt visualizes the current state of the ecosystem in the form of a color map, displaying the location of all agents and plants.

  • Each agent occupies exactly one square on the map
  • Display priority: wolves > sheep > plants (if they are in the same cage)
  • Colors are selected for intuitive interpretation:
    • Red - danger (predators)
  • White - neutral (prey)
  • Green - plants
In [ ]:
function plot_model(model)
    grid = zeros(Int, WIDTH, HEIGHT)
    for x in 1:WIDTH, y in 1:HEIGHT
        if model.bushes[x,y].active
            grid[x,y] = 3
        end
    end
    for sheep in model.sheep
        grid[sheep.x, sheep.y] = 2
    end
    for wolf in model.wolves
        grid[wolf.x, wolf.y] = 1
    end
    
    colors = [colorant"black", colorant"red", colorant"white", colorant"green"]
    heatmap(grid, 
        c=colors, 
        clim=(0,3),
        title="Шаг: $(model.step) | Овцы: $(length(model.sheep)) | Волки: $(length(model.wolves))",
        axis=false,
        legend=false
    )
end
Out[0]:
plot_model (generic function with 1 method)

Function run_simulation the main function that runs and manages the entire ecosystem simulation.

It includes the following steps::

  1. Initialization:

  2. The main simulation cycle includes step-by-step visualization of the current state (plot_model), performing the simulation step (step!), saves the current population data and is interrupted when the stop conditions are met.

  3. Create an animation and save the simulation process to a GIF file "wolf_sheep_sim.gif"

  4. Output of the result.

In [ ]:
function run_simulation(;
    n_sheep = 50,
    n_wolves = 10,
    n_bushes = 500,
    sheep_energy = 10,
    wolf_energy = 50,
    bush_regrowth = 0.01,
    sheep_reprod = 0.04,
    wolf_reprod = 0.05,
    sheep_hunger = 1,
    wolf_hunger = 2,
    sheep_eat_gain = 5,
    wolf_eat_gain = 20,
    sheep_reprod_thresh = 15,
    wolf_reprod_thresh = 30
)

model = init_model(
    n_sheep = n_sheep,
    n_wolves = n_wolves,
    n_bushes = n_bushes,
    sheep_energy = sheep_energy,
    wolf_energy = wolf_energy,
    bush_regrowth = bush_regrowth,
    sheep_reprod = sheep_reprod,
    wolf_reprod = wolf_reprod,
    sheep_hunger = sheep_hunger,
    wolf_hunger = wolf_hunger,
    sheep_eat_gain = sheep_eat_gain,
    wolf_eat_gain = wolf_eat_gain,
    sheep_reprod_thresh = sheep_reprod_thresh,
    wolf_reprod_thresh = wolf_reprod_thresh
)
sheep_history = Int[]
wolf_history = Int[]
bush_history = Int[]
step_history = Int[]

anim = @animate for _ in 1:MAX_STEPS
    plot_model(model)
    should_continue = step!(model)
    push!(sheep_history, length(model.sheep))
    push!(wolf_history, length(model.wolves))
    push!(bush_history, sum(b.active for b in model.bushes))
    push!(step_history, model.step)
    
    should_continue || break
end

println("\nРезультаты симуляции:")
println("Финальный шаг: $(model.step)")
println("Количество овец: $(length(model.sheep))")
println("Количество волков: $(length(model.wolves))")
println("Количество растений: $(sum(b.active for b in model.bushes))")

plt = plot(step_history, [sheep_history, wolf_history, bush_history],
     label = ["Овцы" "Волки" "Растения"],
     xlabel = "Шаги симуляции",
     ylabel = "Количество",
     title = "Динамика популяций",
     linewidth = 2)
display(plt)
display(gif(anim, "wolf_sheep_sim.gif", fps=10))
end
Out[0]:
run_simulation (generic function with 1 method)

Next, we run it, for your convenience, masks of code cells have been applied, in accordance with the example below, you can control the entire model using sliders.

run_simulation(
    # Basic population
parameters n_sheep = 100, # Initial number of sheep
    n_wolves = 5, # Initial number of wolves
n_bushes = 70, # Initial amount of grass
    
    # Energy parameters
    sheep_energy = 100, # Initial energy of sheep
    wolf_energy = 1000, # Initial energy of wolves
    
    # Resource Recovery Options
    bush_regrowth = 0.005, # Probability of grass restoration per step
    
    # Breeding Parameters
    sheep_reprod = 0.02, # Probability of sheep reproduction per step
    wolf_reprod = 0.1, # The probability of reproduction of a wolf per step
    
    # Fasting parameters
    sheep_hunger = 1, # Sheep's energy loss per step without food
    wolf_hunger = 3, # Loss of wolf energy per step without food
    
    # Power Parameters
    sheep_eat_gain = 8, # Sheep's energy gain when eating grass
    wolf_eat_gain = 25, # Wolf's energy gain when eating sheep
    
    # Breeding thresholds
    sheep_reprod_thresh = 20, # Minimum energy for sheep reproduction
    wolf_reprod_thresh = 35 # Minimum energy for wolf reproduction
)
In [ ]:
run_simulation(
n_sheep = 100 # @param {type:"slider",min:0,max:1000,step:1}
,
n_wolves = 24 # @param {type:"slider",min:0,max:1000,step:1}
,
n_bushes = 70 # @param {type:"slider",min:0,max:1000,step:1}
,
sheep_energy = 100 # @param {type:"slider",min:0,max:1000,step:1}
,
wolf_energy = 79 # @param {type:"slider",min:0,max:1000,step:1}
,
bush_regrowth = 0.005 # @param {type:"slider",min:0,max:1,step:0.001}
,
sheep_reprod = 0.02 # @param {type:"slider",min:0,max:1,step:0.01}
,
wolf_reprod = 0.01 # @param {type:"slider",min:0,max:1,step:0.01}
,
sheep_hunger = 1 # @param {type:"slider",min:0,max:1000,step:1}
,
wolf_hunger = 3 # @param {type:"slider",min:0,max:1000,step:1}
,
sheep_eat_gain = 8 # @param {type:"slider",min:0,max:1000,step:1}
,
wolf_eat_gain = 25 # @param {type:"slider",min:0,max:1000,step:1}
,
sheep_reprod_thresh = 20 # @param {type:"slider",min:0,max:1000,step:1}
,
wolf_reprod_thresh = 35 # @param {type:"slider",min:0,max:1000,step:1}
)
Прогресс: завершен шаг 100. Овец: 113, волков: 1
Симуляция остановлена на шаге 101: все волки вымерли.

Результаты симуляции:
Финальный шаг: 101
Количество овец: 113
Количество волков: 0
Количество растений: 371
[ Info: Saved animation to /user/my_projects/Demo/Work/wolf_sheep_sim.gif
No description has been provided for this image

Conclusion

To summarize, I would like to consider the positive impact of this model on the community.:

  1. This model is useful for learning – it perfectly demonstrates the basics:

    • Ecological systems (food chains, population dynamics)
  • Agent-based modeling (behavior of autonomous objects)
  • Balancing parameters in simulations
  1. For research – allows you to study:

    • Ecosystem resilience to changes
  • The effect of species extinction
  • The impact of initial conditions on the development of the system
  1. For development, an example of building:

    • Discrete simulations
    • Real-time data visualization
  • Process animations