Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/ruby
- #
- # Graphical simulation of a group of predators and a school of prey
- #
- # Author: Mark Ruff
- #
- # PREDATORS: die of old age, chase down prey within their visual range and eat
- # them, will reproduce when they have eaten enough prey (at the moment this
- # is a random spawn of a new predator)
- #
- # PREY: run from predators, try not to crash into very close prey, try to
- # follow prey close to them (same heading), and if not close enough will move
- # towards other prey. reproduction time based (random spawn)
- #
- # Configuration settings in predator-prey.config as follows:
- # (To do: allow comments in the config file)
- # # window size
- # x_max 300
- # y_max 300
- # # number of prey and predators
- # school_size 20
- # predator_size 5
- # # individual settings for prey
- # rep 10
- # fol 20
- # att 40
- # fear 40
- # rep_mag 0.2
- # fol_mag 0.4
- # att_mag 0.6
- # fear_mag 0.95
- # # settings for the school
- # s_reprod_time 15
- # s_reprod_rate 0.1
- # s_speed 1.5
- # # settings for the predator
- # p_speed 2.0
- # p_reprod_time 5
- # p_vision 40
- # p_magnitude 0.2
- # p_killzone 7.0
- # p_oldage 150
- #
- # Installing Ruby/GTK3 on Fedora 23 (see: http://pastebin.com/0eWKAwTH)
- # sudo dnf install ruby-devel
- # sudo dnf install gtk3-devel
- # sudo dnf install redhat-rpm-config
- # gem install gtk3
- #
- require "gtk3"
- # 2D catesian Point
- class Point
- attr_accessor :x, :y
- def initialize(x, y)
- @x = x
- @y = y
- end
- # actual distance between this and another point
- def distance(other)
- return Math.sqrt( (@x-other.x)**2 + (@y-other.y)**2 )
- end
- # squared Euclidean distance (when ranking only, avoid the costly sqrt)
- def distance_squared(other)
- return (@x-other.x)**2 + (@y-other.y)**2
- end
- # angle formed by line between two points and the x axis (heading)
- def heading_to(other)
- return Math.atan2( (other.y - @y), (other.x - @x) )
- end
- # a point a certain distance and heading from the current point
- def destination(heading, distance)
- return Point.new( @x + distance*Math.cos(heading), @y + distance*Math.sin(heading))
- end
- end
- # Base class for Creatures in the simulator
- # Has a position, direction it is heading, speed and "picutre"
- # In the current iteration of the program the picture is just a colour
- class Creature
- attr_accessor :position, :heading, :speed, :pic
- def initialize(x, y, heading, speed, pic)
- @position = Point.new(x,y)
- @heading = heading
- @speed = speed
- @pic = pic
- end
- # Have the creature deviate away from another point
- # This is scaled by the "strength", a factor from 0 - 1
- def deviate_from(p,strength)
- diff = (@heading - @position.heading_to( p )) % (Math::PI*2)
- if diff < Math::PI then
- @heading += (Math::PI - diff)*strength
- elsif diff > Math::PI then
- @heading -= (diff - Math::PI)*strength
- end
- end
- # Have the creature deviate towards another point
- # This is scaled by the "strength", a factor from 0 -1
- def deviate_to(p,strength)
- diff = (@position.heading_to( p ) - @heading) % (Math::PI*2)
- if diff < Math::PI
- @heading += diff*strength
- else
- @heading -= (Math::PI*2 - diff)*strength
- end
- end
- # Actually move to a new point based on our current location (point),
- # our heading and our speed plus a small amount of randomness.
- # Movement will need to be constrained by the window in which we exist
- # (0-x_max and 0-y_max).
- def finalise_move(x_max, y_max)
- # randomness
- @heading += 0.1 * (rand * 2 - 1)
- # new position
- @position.x = @position.x + @speed*Math.cos(@heading)
- @position.y = @position.y + @speed*Math.sin(@heading)
- # to do: move this poorly placed wrap variable into the settings file!
- wrap = true
- if wrap then
- # if we have moved out of bounds, bounce back in
- if @position.x < 0 then
- @position.x = @position.x.abs
- @heading = Math::PI - @heading
- elsif @position.x > x_max - 1 then
- @position.x = 2 * x_max - @position.x - 1
- @heading = Math::PI - @heading
- end
- if @position.y < 0 then
- @position.y = @position.y.abs
- @heading = Math::PI*2 - @heading
- elsif @position.y > y_max - 1 then
- @position.y = 2*y_max - @position.y - 1
- @heading = Math::PI*2 - @heading
- end
- else
- # if not wrapping, then pop in on the other side of the window
- if @position.x < 0 then
- @position.x = x_max -1
- elsif @position.x > x_max - 1 then
- @position.x = 0
- end
- if @position.y < 0 then
- @position.y = y_max - 1
- elsif @position.y > y_max - 1 then
- @position.y = 0
- end
- end
- end
- end
- # Simulated predator, is a Creature
- # Will hunt down Prey:
- # - Out of the Prey that are close enough (distance away < vision), find the
- # closest and deviate towards this Prey (by a certain magnitude)
- # - If this movement puts the Predator within a certain distance (kill_zone)
- # the Prey is "killed".
- # Will eventually die of old age (has a current age and death_age)
- class Predator < Creature
- attr_accessor :vision, :magnitude, :kill_zone, :age, :death_age
- def initialize(x, y, heading, speed, vision=80, mag=0.4,
- killzone = 3.0, death_age = 150, pic = [1, 0.1, 0.1])
- super x, y , heading, speed, pic
- @vision = vision
- @magnitude = mag
- @kill_zone = killzone
- @age = 0
- @death_age = death_age
- end
- def move(food,x_max,y_max)
- @age += 1
- if @age > @death_age then
- # kill me
- return false
- end
- closest = nil # point
- closest_distance = 100000 # arbitrary big number
- food.each do |f|
- distance = @position.distance(f.position)
- if distance < vision then
- if distance < closest_distance then
- closest_distance = distance
- closest = f.position
- end
- end
- end
- if closest != nil then
- deviate_to(closest,@magnitude)
- end
- finalise_move(x_max,y_max)
- end
- end
- # Prey - these act like a school of fish:
- # - Die if within the kill_zone of a Predator
- # - Deviate away from other Prey that are very close (dist <= repulsion)
- # - Adjust their heading towards that of Prey that are moderately close
- # (distance < following)
- # - Deviate towards other Prey that are not too far (distance <= attraction)
- # - Deviate away from Predators if they are too close (dist <= fear), and in
- # this case ignore the other Prey... just run
- # All of the adjustments are based on individual scaling variables
- class Prey < Creature
- attr_accessor :repulsion, :following, :attraction, :fear,
- :rep_mag, :fol_mag, :att_mag, :fear_mag
- def initialize(x, y, heading, speed, pic = [0.5,0.5,0.5], rep = 10, fol = 20, att = 40, fear = 40, rep_mag = 0.4, fol_mag = 0.4, att_mag = 0.4, fear_mag = 0.5)
- super x, y, heading, speed, pic
- @repulsion = rep
- @following = fol
- @attraction = att
- @fear = fear
- @rep_mag = rep_mag
- @fol_mag = fol_mag
- @att_mag = att_mag
- @fear_mag = fear_mag
- end
- # Move the Prey based on factors outlined above, returns true if still
- # alive after the move, false if dead
- def move(x_max, y_max, others, predators)
- afraid = false
- # First we handle reaction to any predators
- predators.each do |p|
- dist = @position.distance_squared(p.position)
- # if a predator is within kill range, return false and delete me
- if dist < p.kill_zone**2 then
- #delete me
- others.delete self
- return false
- end
- # if a predator is within my fear range, head away from it
- if dist < @fear**2 then
- afraid = true
- deviate_from(p.position,@fear_mag/Math::sqrt(dist))
- end
- end
- if afraid then
- finalise_move(x_max,y_max)
- return true
- end
- # Now move in relation to the other prey.
- # Make an array of prey that lie within each of our ranges (repulsion,
- # follow, attraction). We can ignore any prey outside of the attraction
- # range
- @rep_prey = []
- @fol_prey = []
- @att_prey = []
- # using this cutoff as an optimisation WILL BREAK any attempts to
- # combine all 3 factors (rep, fol, att), so at the moment we only use
- # one factor (based on the closest prey)
- cutoff = @attraction
- # for each prey, except those before me in the array ...
- # this avoids checking A -> B and then also B -> A
- others[others.find_index(self)+1,others.length].each do |o|
- # optimise by looking in a SQUARE around me, rather than a circle...
- # don't need to calculate the actual distance if delta x or y out of range
- if (@position.x - o.position.x).abs < cutoff &&
- (@position.y - o.position.y).abs < cutoff then
- # optimise by checking the squared distance against the various
- # cut-off settings squared (square root do get distance = more costly)
- dist = @position.distance_squared(o.position)
- if dist < @repulsion**2 then
- @rep_prey.push o
- cutoff = @repulsion
- elsif dist < @following**2 then
- @fol_prey.push o
- cutoff = @following
- elsif dist < @attraction**2 then
- @att_prey.push o
- end
- end
- end
- # if other prey are too close, move away
- if !@rep_prey.empty? then
- x_add = 0
- y_add = 0
- @rep_prey.each do |o|
- x_add += o.position.x
- y_add += o.position.y
- end
- x_add = x_add / @rep_prey.length.to_f
- y_add = y_add / @rep_prey.length.to_f
- deviate_from(Point.new(x_add, y_add),rep_mag)
- # OTHERWISE, try to align our heading with prey moderately close
- elsif !@fol_prey.empty? then
- vectoring = position
- @fol_prey.each do |o|
- vectoring = vectoring.destination(o.heading,1)
- end
- deviate_to(vectoring,fol_mag)
- # OTHERWISE, try to get closer to prey within the "attraction" distance
- elsif !@att_prey.empty? then
- x_add = 0
- y_add = 0
- @att_prey.each do |o|
- x_add += o.position.x
- y_add += o.position.y
- end
- x_add = x_add / @att_prey.length.to_f
- y_add = y_add / @att_prey.length.to_f
- deviate_to(Point.new(x_add, y_add),att_mag)
- end
- finalise_move(x_max,y_max)
- end
- end
- # Our Simulation is a Gtk:Window, so it can be graphically displayed
- # Read in various settings from a
- class Simulation < Gtk::Window
- attr_accessor :predator, :school, :school_size, :x_max, :y_max, :kills,
- :rep, :fol, :att, :fear, :rep_mag, :fol_mag, :att_mag, :fear_mag,
- :reproduced
- def initialize(settings)
- # Window settings, including initialisation of the kill counter
- @x_max = settings["x_max"].to_i
- @y_max = settings["y_max"].to_i
- @counter = 0
- # a school is an array of prey
- # reproduction rates and speed are kept here at a simulator level
- @school = []
- @school_size = settings["school_size"].to_i
- @s_reprod_time = settings["s_reprod_time"].to_i
- @s_reprod_rate = settings["s_reprod_rate"].to_f
- @s_speed = settings["s_speed"].to_f
- # settings for the individual prey (passed to initializer of Prey class)
- @rep = settings["rep"].to_i
- @fol = settings["fol"].to_i
- @att = settings["att"].to_i
- @fear = settings["fear"].to_i
- @rep_mag = settings["rep_mag"].to_f
- @fol_mag = settings["fol_mag"].to_f
- @att_mag = settings["att_mag"].to_f
- @fear_mag = settings["fear_mag"].to_f
- # settings for the predator
- @p_speed = settings["p_speed"].to_f
- @p_vision = settings["p_vision"].to_i
- @p_magnitude = settings["p_magnitude"].to_f
- @p_killzone = settings["p_killzone"].to_f
- @p_oldage = settings["p_oldage"].to_i
- # settings for the group of predators as a whole (size - starting quantity)
- @predator_size = settings["predator_size"].to_i
- @p_reprod_time = settings["p_reprod_time"].to_i
- @reproduced = false
- # make our school of prey (up to "school_size", set in settings file)
- 0.upto(@school_size - 1) do |x|
- @school.push Prey.new(rand(0..@x_max-1), rand(0..@y_max - 1), rand*Math::PI*2, @s_speed, [1,1, rand/2+0.5], @rep, @fol, @att, @fear, @rep_mag, @fol_mag, @att_mag, @fear_mag)
- end
- # similarly, make our predators
- @predator = []
- @predator_size.times do
- @predator.push Predator.new(rand(0..@x_max-1), rand(0..@y_max -1), rand*Math::PI*2, @p_speed, @p_vision, @p_magnitude, @p_killzone, @p_oldage)
- end
- @kills = 0
- # set our Gtk:Window up (including call to super() to initialize)
- super()
- set_title "Simulator"
- set_window_position :center
- signal_connect "destroy" do
- Gtk.main_quit
- end
- @darea = Gtk::DrawingArea.new
- @darea.set_size_request(@x_max, @y_max)
- @vbox = Gtk::Box.new :vertical
- @hbox = Gtk::Box.new :horizontal
- @label = Gtk::Label.new "Kill Counter: "
- @kill_counter = Gtk::Label.new "0"
- @darea.signal_connect "draw" do
- on_draw
- end
- add @vbox
- @vbox.pack_start @darea
- @vbox.pack_start @hbox
- @hbox.pack_start @label
- @hbox.pack_start @kill_counter
- show_all
- end
- # drawing method for our window
- def on_draw
- cr = @darea.window.create_cairo_context
- cr.set_source_rgb 0, 0, 0
- cr.set_line_width 4
- cr.set_line_cap "round"
- cr.paint
- # draw each predator
- @predator.each do |p|
- cr.set_source_rgb p.pic[0], p.pic[1], p.pic[2]
- cr.move_to(p.position.x, p.position.y)
- d = p.position.destination(p.heading,6) # why 7 - should make const
- cr.line_to(d.x, d.y)
- cr.stroke
- end
- cr.set_line_width 4
- # draw each prey
- school.each do |s|
- cr.set_source_rgb s.pic[0], s.pic[1], s.pic[2]
- cr.move_to(s.position.x, s.position.y)
- d = s.position.destination(s.heading,6)
- cr.line_to(d.x, d.y)
- cr.stroke
- end
- # draw the kill counter
- @kill_counter.set_label @kills.to_s
- end
- # method to move forward a step in time in the simulator
- def sim_step
- @counter += 1
- # move each predator, if they have died of old age, delete them
- @predator.each do |p|
- if p.move(school, @x_max, @y_max) == false then
- @predator.delete(p)
- end
- end
- # move each prey in the school, if they have been eaten, update kill counter
- self.school.each do |s|
- if s.move(@x_max, @y_max,@school,@predator) == false then
- @kills += 1
- end
- end
- # redraw the window
- cr = @darea.window.create_cairo_context
- draw cr
- # REPRODUCTION
- # PREY: Add new prey based on the reproduction_time. The amount added
- # Is based on the current population level and the reproduction rate
- # Keep reproducing so long as there are some left
- if @counter % @s_reprod_time == 0 then
- (@school.length * @s_reprod_rate).ceil.times do
- @school.push Prey.new(rand(0..@x_max-1), rand(0..@y_max - 1), rand*Math::PI*2, @s_speed, [1,1, rand/2+0.5], @rep, @fol, @att, @fear, @rep_mag, @fol_mag, @att_mag, @fear_mag)
- end
- end
- # PREDATORS: reprouction time not based on number of steps through the
- # simulator, but rather number of kills. i.e will die out if no prey
- if (@kills+1) % @p_reprod_time == 0 then
- if !reproduced then
- @predator.push Predator.new(rand(0..@x_max-1), rand(0..@y_max -1), rand*Math::PI*2, @p_speed, @p_vision, @p_magnitude, @p_killzone, @p_oldage)
- @reproduced = true
- end
- else
- @reproduced = false
- end
- end
- end
- # Here we set up and run our Simulation
- # Pull in all the settings from our configuration file
- settings_raw = File.readlines("predator-prey.config")
- settings = {}
- settings_raw.each do |s|
- values = s.split(/\s+/)
- settings[values[0]] = values[1]
- end
- puts settings
- # Create a new simulation using these settings
- s = Simulation.new(settings)
- # Step through the simulation step by step (forever)
- # To Do: Add a button to close in a clean fashion
- GLib::Timeout.add 50 do
- s.sim_step
- true
- end
- Gtk.main
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement