Advertisement
markruff

Predator vs Prey graphical simulator (Ruby and GTK3)

Jan 1st, 2016
214
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Ruby 15.61 KB | None | 0 0
  1. #!/usr/bin/ruby
  2. #
  3. # Graphical simulation of a group of predators and a school of prey
  4. #
  5. # Author: Mark Ruff
  6. #
  7. # PREDATORS: die of old age, chase down prey within their visual range and eat
  8. # them, will reproduce when they have eaten enough prey (at the moment this
  9. # is a random spawn of a new predator)
  10. #
  11. # PREY: run from predators, try not to crash into very close prey, try to
  12. # follow prey close to them (same heading), and if not close enough will move
  13. # towards other prey. reproduction time based (random spawn)
  14. #
  15. # Configuration settings in predator-prey.config as follows:
  16. # (To do: allow comments in the config file)
  17. # # window size
  18. # x_max 300
  19. # y_max 300
  20. # # number of prey and predators
  21. # school_size 20
  22. # predator_size 5
  23. # # individual settings for prey
  24. # rep 10
  25. # fol 20
  26. # att 40
  27. # fear 40
  28. # rep_mag 0.2
  29. # fol_mag 0.4
  30. # att_mag 0.6
  31. # fear_mag 0.95
  32. # # settings for the school
  33. # s_reprod_time 15
  34. # s_reprod_rate 0.1
  35. # s_speed 1.5
  36. # # settings for the predator
  37. # p_speed 2.0
  38. # p_reprod_time 5
  39. # p_vision 40
  40. # p_magnitude 0.2
  41. # p_killzone 7.0
  42. # p_oldage 150
  43. #
  44. # Installing Ruby/GTK3 on Fedora 23 (see: http://pastebin.com/0eWKAwTH)
  45. # sudo dnf install ruby-devel
  46. # sudo dnf install gtk3-devel
  47. # sudo dnf install redhat-rpm-config
  48. # gem install gtk3
  49. #
  50.  
  51. require "gtk3"
  52.  
  53. # 2D catesian Point
  54. class Point
  55.   attr_accessor :x, :y
  56.  
  57.   def initialize(x, y)
  58.     @x = x
  59.     @y = y
  60.   end
  61.  
  62.   # actual distance between this and another point
  63.   def distance(other)
  64.     return Math.sqrt( (@x-other.x)**2 + (@y-other.y)**2 )
  65.   end
  66.  
  67.   # squared Euclidean distance (when ranking only, avoid the costly sqrt)
  68.   def distance_squared(other)
  69.     return (@x-other.x)**2 + (@y-other.y)**2
  70.   end
  71.  
  72.   # angle formed by line between two points and the x axis (heading)
  73.   def heading_to(other)
  74.     return Math.atan2( (other.y - @y), (other.x - @x) )
  75.   end
  76.  
  77.   # a point a certain distance and heading from the current point
  78.   def destination(heading, distance)
  79.     return Point.new( @x + distance*Math.cos(heading), @y + distance*Math.sin(heading))
  80.   end
  81. end
  82.  
  83. # Base class for Creatures in the simulator
  84. # Has a position, direction it is heading, speed and "picutre"
  85. # In the current iteration of the program the picture is just a colour
  86. class Creature
  87.   attr_accessor :position, :heading, :speed, :pic
  88.                
  89.   def initialize(x, y, heading, speed, pic)
  90.     @position = Point.new(x,y)
  91.     @heading = heading
  92.     @speed = speed
  93.     @pic = pic
  94.   end
  95.  
  96.   # Have the creature deviate away from another point
  97.   # This is scaled by the "strength", a factor from 0 - 1
  98.   def deviate_from(p,strength)
  99.     diff = (@heading - @position.heading_to( p )) % (Math::PI*2)
  100.     if diff  < Math::PI then
  101.       @heading += (Math::PI - diff)*strength
  102.     elsif diff > Math::PI then
  103.       @heading -= (diff - Math::PI)*strength
  104.     end
  105.   end
  106.  
  107.   # Have the creature deviate towards another point
  108.   # This is scaled by the "strength", a factor from 0 -1
  109.   def deviate_to(p,strength)
  110.     diff = (@position.heading_to( p ) - @heading) % (Math::PI*2)
  111.     if diff < Math::PI
  112.       @heading += diff*strength
  113.     else
  114.       @heading -= (Math::PI*2 - diff)*strength
  115.     end
  116.   end
  117.  
  118.   # Actually move to a new point based on our current location (point),
  119.   # our heading and our speed plus a small amount of randomness.
  120.   # Movement will need to be constrained by the window in which we exist
  121.   # (0-x_max and 0-y_max).
  122.   def finalise_move(x_max, y_max)
  123.  
  124.     # randomness
  125.     @heading += 0.1 * (rand * 2 - 1)
  126.  
  127.     # new position
  128.     @position.x = @position.x + @speed*Math.cos(@heading)
  129.     @position.y = @position.y + @speed*Math.sin(@heading)
  130.  
  131.     # to do: move this poorly placed wrap variable into the settings file!
  132.     wrap = true
  133.     if wrap then
  134.       # if we have moved out of bounds, bounce back in
  135.       if @position.x < 0 then
  136.       @position.x = @position.x.abs
  137.         @heading = Math::PI - @heading
  138.       elsif @position.x > x_max - 1 then
  139.         @position.x = 2 * x_max - @position.x  - 1
  140.         @heading = Math::PI - @heading
  141.       end
  142.  
  143.       if @position.y < 0 then
  144.         @position.y = @position.y.abs
  145.         @heading = Math::PI*2 - @heading
  146.       elsif @position.y > y_max - 1 then
  147.         @position.y = 2*y_max - @position.y - 1
  148.         @heading = Math::PI*2 - @heading
  149.       end
  150.     else
  151.       # if not wrapping, then pop in on the other side of the window
  152.       if @position.x < 0 then
  153.         @position.x = x_max -1
  154.       elsif @position.x > x_max - 1 then
  155.         @position.x = 0
  156.       end
  157.  
  158.       if @position.y < 0 then
  159.         @position.y = y_max - 1
  160.       elsif @position.y > y_max - 1 then
  161.         @position.y = 0
  162.       end
  163.     end
  164.   end
  165. end
  166.  
  167. # Simulated predator, is a Creature
  168. # Will hunt down Prey:
  169. # - Out of the Prey that are close enough (distance away < vision), find the
  170. #   closest and deviate towards this Prey (by a certain magnitude)
  171. # - If this movement puts the Predator within a certain distance (kill_zone)
  172. #   the Prey is "killed".
  173. # Will eventually die of old age (has a current age and death_age)
  174. class Predator < Creature
  175.   attr_accessor :vision, :magnitude, :kill_zone, :age, :death_age
  176.  
  177.   def initialize(x, y, heading, speed, vision=80, mag=0.4,
  178.                  killzone = 3.0, death_age = 150, pic = [1, 0.1, 0.1])
  179.     super x, y , heading, speed, pic
  180.     @vision = vision
  181.     @magnitude = mag
  182.     @kill_zone = killzone
  183.     @age = 0
  184.     @death_age = death_age
  185.   end
  186.  
  187.   def move(food,x_max,y_max)
  188.     @age += 1
  189.     if @age > @death_age then
  190.       # kill me
  191.       return false
  192.     end
  193.     closest = nil # point
  194.     closest_distance = 100000 # arbitrary big number
  195.     food.each do |f|
  196.       distance = @position.distance(f.position)
  197.       if distance < vision then
  198.         if distance < closest_distance then
  199.           closest_distance = distance
  200.           closest = f.position
  201.         end
  202.       end
  203.     end
  204.    
  205.     if closest != nil then
  206.       deviate_to(closest,@magnitude)  
  207.     end
  208.    
  209.     finalise_move(x_max,y_max)
  210.  
  211.   end
  212. end
  213.  
  214. # Prey - these act like a school of fish:
  215. #  - Die if within the kill_zone of a Predator
  216. #  - Deviate away from other Prey that are very close (dist <= repulsion)
  217. #  - Adjust their heading towards that of Prey that are moderately close
  218. #    (distance < following)
  219. #  - Deviate towards other Prey that are not too far (distance <= attraction)
  220. #  - Deviate away from Predators if they are too close (dist <= fear), and in
  221. #    this case ignore the other Prey... just run
  222. # All of the adjustments are based on individual scaling variables
  223. class Prey < Creature
  224.   attr_accessor :repulsion, :following, :attraction, :fear,
  225.                 :rep_mag, :fol_mag, :att_mag, :fear_mag
  226.  
  227.   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)
  228.     super x, y, heading, speed, pic
  229.     @repulsion = rep
  230.     @following = fol
  231.     @attraction = att
  232.     @fear = fear
  233.     @rep_mag = rep_mag
  234.     @fol_mag = fol_mag
  235.     @att_mag = att_mag
  236.     @fear_mag = fear_mag
  237.   end
  238.  
  239.   # Move the Prey based on factors outlined above, returns true if still
  240.   # alive after the move, false if dead
  241.   def move(x_max, y_max, others, predators)
  242.     afraid = false
  243.  
  244.     # First we handle reaction to any predators
  245.     predators.each do |p|
  246.       dist = @position.distance_squared(p.position)
  247.       # if a predator is within kill range, return false and delete me
  248.       if dist < p.kill_zone**2 then
  249.         #delete me
  250.         others.delete self
  251.         return false
  252.       end
  253.       # if a predator is within my fear range, head away from it
  254.       if dist < @fear**2 then
  255.         afraid = true
  256.         deviate_from(p.position,@fear_mag/Math::sqrt(dist))
  257.       end
  258.     end
  259.     if afraid then
  260.       finalise_move(x_max,y_max)
  261.       return true
  262.     end
  263.  
  264.     # Now move in relation to the other prey.
  265.     # Make an array of prey that lie within each of our ranges (repulsion,
  266.     # follow, attraction). We can ignore any prey outside of the attraction
  267.     # range
  268.  
  269.     @rep_prey = []
  270.     @fol_prey = []
  271.     @att_prey = []
  272.  
  273.     # using this cutoff as an optimisation WILL BREAK any attempts to
  274.     # combine all 3 factors (rep, fol, att), so at the moment we only use
  275.     # one factor (based on the closest prey)
  276.     cutoff = @attraction
  277.  
  278.     # for each prey, except those before me in the array ...
  279.     # this avoids checking A -> B and then also B -> A
  280.     others[others.find_index(self)+1,others.length].each do |o|
  281.      
  282.       # optimise by looking in a SQUARE around me, rather than a circle...
  283.       # don't need to calculate the actual distance if delta x or y out of range
  284.       if (@position.x - o.position.x).abs < cutoff &&
  285.          (@position.y - o.position.y).abs < cutoff then
  286.  
  287.         # optimise by checking the squared distance against the various
  288.         # cut-off settings squared (square root do get distance = more costly)
  289.         dist = @position.distance_squared(o.position)
  290.         if dist < @repulsion**2 then
  291.           @rep_prey.push o
  292.           cutoff = @repulsion
  293.         elsif dist < @following**2 then
  294.           @fol_prey.push o
  295.           cutoff = @following
  296.         elsif dist < @attraction**2 then
  297.           @att_prey.push o
  298.         end
  299.       end
  300.     end
  301.  
  302.     # if other prey are too close, move away
  303.     if !@rep_prey.empty? then
  304.       x_add = 0
  305.       y_add = 0
  306.       @rep_prey.each do |o|
  307.         x_add += o.position.x
  308.         y_add += o.position.y
  309.       end
  310.       x_add = x_add / @rep_prey.length.to_f
  311.       y_add = y_add / @rep_prey.length.to_f
  312.       deviate_from(Point.new(x_add, y_add),rep_mag)
  313.     # OTHERWISE, try to align our heading with prey moderately close
  314.     elsif !@fol_prey.empty? then
  315.       vectoring = position
  316.       @fol_prey.each do |o|
  317.         vectoring = vectoring.destination(o.heading,1)
  318.       end
  319.       deviate_to(vectoring,fol_mag)
  320.     # OTHERWISE, try to get closer to prey within the "attraction" distance
  321.     elsif !@att_prey.empty? then
  322.       x_add = 0
  323.       y_add = 0
  324.       @att_prey.each do |o|
  325.         x_add += o.position.x
  326.         y_add += o.position.y
  327.       end
  328.       x_add = x_add / @att_prey.length.to_f
  329.       y_add = y_add / @att_prey.length.to_f
  330.       deviate_to(Point.new(x_add, y_add),att_mag)
  331.     end
  332.  
  333.     finalise_move(x_max,y_max)
  334.  
  335.   end
  336. end
  337.  
  338. # Our Simulation is a Gtk:Window, so it can be graphically displayed
  339. # Read in various settings from a
  340. class Simulation < Gtk::Window
  341.   attr_accessor :predator, :school, :school_size, :x_max, :y_max, :kills,
  342.                 :rep, :fol, :att, :fear, :rep_mag, :fol_mag, :att_mag, :fear_mag,
  343.                 :reproduced
  344.  
  345.   def initialize(settings)
  346.     # Window settings, including initialisation of the kill counter
  347.     @x_max = settings["x_max"].to_i
  348.     @y_max = settings["y_max"].to_i
  349.     @counter = 0
  350.  
  351.     # a school is an array of prey
  352.     # reproduction rates and speed are kept here at a simulator level
  353.     @school = []
  354.     @school_size = settings["school_size"].to_i
  355.     @s_reprod_time = settings["s_reprod_time"].to_i
  356.     @s_reprod_rate = settings["s_reprod_rate"].to_f
  357.     @s_speed = settings["s_speed"].to_f
  358.  
  359.     # settings for the individual prey (passed to initializer of Prey class)
  360.     @rep = settings["rep"].to_i
  361.     @fol = settings["fol"].to_i
  362.     @att = settings["att"].to_i
  363.     @fear = settings["fear"].to_i
  364.     @rep_mag = settings["rep_mag"].to_f
  365.     @fol_mag = settings["fol_mag"].to_f
  366.     @att_mag = settings["att_mag"].to_f
  367.     @fear_mag = settings["fear_mag"].to_f
  368.  
  369.     # settings for the predator
  370.     @p_speed = settings["p_speed"].to_f
  371.     @p_vision = settings["p_vision"].to_i
  372.     @p_magnitude = settings["p_magnitude"].to_f
  373.     @p_killzone = settings["p_killzone"].to_f
  374.     @p_oldage = settings["p_oldage"].to_i
  375.  
  376.     # settings for the group of predators as a whole (size - starting quantity)
  377.     @predator_size = settings["predator_size"].to_i
  378.     @p_reprod_time = settings["p_reprod_time"].to_i
  379.  
  380.     @reproduced = false
  381.  
  382.     # make our school of prey (up to "school_size", set in settings file)
  383.     0.upto(@school_size - 1) do |x|
  384.       @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)
  385.     end
  386.  
  387.     # similarly, make our predators
  388.     @predator = []
  389.     @predator_size.times do
  390.       @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)
  391.     end
  392.  
  393.     @kills = 0
  394.  
  395.     # set our Gtk:Window up (including call to super() to initialize)
  396.     super()
  397.  
  398.     set_title "Simulator"
  399.     set_window_position :center
  400.  
  401.     signal_connect "destroy" do
  402.       Gtk.main_quit
  403.     end
  404.  
  405.     @darea = Gtk::DrawingArea.new
  406.     @darea.set_size_request(@x_max, @y_max)
  407.     @vbox = Gtk::Box.new :vertical
  408.     @hbox = Gtk::Box.new :horizontal
  409.     @label = Gtk::Label.new "Kill Counter: "
  410.     @kill_counter = Gtk::Label.new "0"
  411.  
  412.     @darea.signal_connect "draw" do
  413.       on_draw
  414.     end
  415.    
  416.     add @vbox
  417.     @vbox.pack_start @darea
  418.     @vbox.pack_start @hbox
  419.    
  420.     @hbox.pack_start @label
  421.     @hbox.pack_start @kill_counter
  422.     show_all
  423.    
  424.   end
  425.  
  426.   # drawing method for our window
  427.   def on_draw
  428.     cr = @darea.window.create_cairo_context
  429.     cr.set_source_rgb 0, 0, 0
  430.     cr.set_line_width 4
  431.     cr.set_line_cap "round"
  432.     cr.paint
  433.     # draw each predator
  434.     @predator.each do |p|
  435.       cr.set_source_rgb p.pic[0], p.pic[1], p.pic[2]
  436.       cr.move_to(p.position.x, p.position.y)
  437.       d = p.position.destination(p.heading,6) # why 7 - should make const
  438.       cr.line_to(d.x, d.y)
  439.       cr.stroke
  440.     end
  441.     cr.set_line_width 4
  442.     # draw each prey
  443.     school.each do |s|
  444.       cr.set_source_rgb s.pic[0], s.pic[1], s.pic[2]
  445.       cr.move_to(s.position.x, s.position.y)
  446.       d = s.position.destination(s.heading,6)
  447.       cr.line_to(d.x, d.y)
  448.       cr.stroke
  449.     end
  450.     # draw the kill counter
  451.     @kill_counter.set_label @kills.to_s
  452.   end
  453.  
  454.   # method to move forward a step in time in the simulator
  455.   def sim_step
  456.     @counter += 1
  457.     # move each predator, if they have died of old age, delete them
  458.     @predator.each do |p|
  459.       if p.move(school, @x_max, @y_max) == false then
  460.         @predator.delete(p)
  461.       end
  462.     end
  463.     # move each prey in the school, if they have been eaten, update kill counter
  464.     self.school.each do |s|
  465.       if s.move(@x_max, @y_max,@school,@predator) == false then
  466.         @kills += 1
  467.       end
  468.     end
  469.    
  470.     # redraw the window
  471.     cr = @darea.window.create_cairo_context
  472.     draw cr
  473.  
  474.     # REPRODUCTION
  475.     # PREY: Add new prey based on the reproduction_time. The amount added
  476.     # Is based on the current population level and the reproduction rate
  477.     # Keep reproducing so long as there are some left
  478.     if @counter % @s_reprod_time == 0 then
  479.       (@school.length * @s_reprod_rate).ceil.times do
  480.         @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)
  481.       end
  482.     end
  483.     # PREDATORS: reprouction time not based on number of steps through the
  484.     # simulator, but rather number of kills. i.e will die out if no prey
  485.     if (@kills+1) % @p_reprod_time == 0 then
  486.       if !reproduced then
  487.         @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)
  488.         @reproduced = true
  489.       end
  490.     else
  491.       @reproduced = false
  492.     end
  493.   end
  494. end
  495.  
  496. # Here we set up and run our Simulation
  497.  
  498. # Pull in all the settings from our configuration file
  499. settings_raw = File.readlines("predator-prey.config")
  500. settings = {}
  501. settings_raw.each do |s|
  502.   values = s.split(/\s+/)
  503.   settings[values[0]] = values[1]
  504. end
  505. puts settings
  506.  
  507. # Create a new simulation using these settings
  508. s = Simulation.new(settings)
  509.  
  510. # Step through the simulation step by step (forever)
  511. # To Do: Add a button to close in a clean fashion
  512. GLib::Timeout.add 50 do
  513.   s.sim_step
  514.   true
  515. end
  516.  
  517. Gtk.main
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement