|
require 'set' |
|
|
|
module WaTor |
|
class Simulation |
|
|
|
attr_reader :width, :height, :num_fish, :num_sharks, :board, |
|
:fish_reproduction, :shark_reproduction, :shark_starvation, |
|
:cycles |
|
|
|
DEFAULTS = { |
|
:height => 14, |
|
:width => 32, |
|
:nfish => 200, |
|
:nsharks => 20, |
|
:fish_reproduction => 3, |
|
:shark_reproduction => 10, |
|
:shark_starvation => 3 |
|
} |
|
|
|
def initialize(opts) |
|
|
|
opts = DEFAULTS.merge(opts) |
|
|
|
@width, @height = opts[:width], opts[:height] |
|
@num_fish, @num_sharks = opts[:nfish], opts[:nsharks] |
|
@fish_reproduction = opts[:fish_reproduction] |
|
@shark_reproduction = opts[:shark_reproduction] |
|
@shark_starvation = opts[:shark_starvation] |
|
@cycles = 0 |
|
|
|
@board = Board.new(@width, @height) |
|
@board.seed(@num_fish, @num_sharks) |
|
end |
|
|
|
def next_chronon |
|
@board.fish.to_a.each do |pos| |
|
fish = @board[pos.x,pos.y] |
|
fish.update |
|
move_fish(fish, pos) |
|
end |
|
|
|
@board.sharks.to_a.each do |pos| |
|
shark = @board[pos.x, pos.y] |
|
shark.update |
|
|
|
if shark.turns_without_eating > @shark_starvation |
|
@board.remove_creature(pos) |
|
else |
|
move_shark(shark, pos) |
|
end |
|
end |
|
|
|
@cycles += 1 |
|
end |
|
|
|
def has_both_species? |
|
@board.fish.size > 0 && @board.sharks.size > 0 |
|
end |
|
|
|
def move_fish(fish, start_pos) |
|
# move to an unoccupied adjacent square if possible, else stay in start pos |
|
unoccupied = board.unoccupied_adjacent_to(start_pos) |
|
|
|
if unoccupied.size > 0 |
|
new_pos = unoccupied.sample |
|
@board.move_creature(start_pos, new_pos) |
|
|
|
if fish.can_reproduce? @fish_reproduction |
|
@board.new_creature(Fish.new, start_pos) |
|
end |
|
end |
|
end |
|
|
|
def move_shark(shark, start_pos) |
|
has_moved = false |
|
adjacent_fish = @board.adjacent_of_type(start_pos, :fish) |
|
|
|
if adjacent_fish.size > 0 |
|
new_pos = adjacent_fish.sample |
|
|
|
@board.remove_creature(new_pos) |
|
@board.move_creature(start_pos, new_pos) |
|
shark.reset_starvation |
|
|
|
has_moved = true |
|
else |
|
unoccupied = @board.unoccupied_adjacent_to(start_pos) |
|
if unoccupied.size > 0 |
|
new_pos = unoccupied.sample |
|
# move |
|
@board.move_creature(start_pos, new_pos) |
|
has_moved = true |
|
end |
|
end |
|
|
|
if has_moved && shark.can_reproduce?(@shark_reproduction) |
|
@board.new_creature(Shark.new, start_pos) |
|
end |
|
end |
|
end |
|
|
|
Position = Struct.new(:x, :y) |
|
|
|
class Board |
|
|
|
attr_reader :fish, :sharks |
|
|
|
def initialize(width, height) |
|
@width, @height = width, height |
|
@board = Array.new(@width) { Array.new(@height, nil) } |
|
@random = Random.new |
|
@fish, @sharks = Set.new, Set.new |
|
end |
|
|
|
def seed(nfish, nsharks) |
|
nfish.times do |i| |
|
p = random_unoccupied_square |
|
@board[p.x][p.y] = Fish.new |
|
@fish << p |
|
end |
|
nsharks.times do |i| |
|
p = random_unoccupied_square |
|
@board[p.x][p.y] = Shark.new |
|
@sharks << p |
|
end |
|
end |
|
|
|
def [](x, y) |
|
@board[x][y] |
|
end |
|
|
|
def random_unoccupied_square |
|
p = Position.new(@random.rand(@width), @random.rand(@height)) |
|
while is_occupied? p |
|
p = Position.new(@random.rand(@width), @random.rand(@height)) |
|
end |
|
|
|
return p |
|
end |
|
|
|
def is_unoccupied? position |
|
return @board[position.x][position.y].nil? |
|
end |
|
|
|
def is_occupied? position |
|
return !@board[position.x][position.y].nil? |
|
end |
|
|
|
def adjacent_to pos |
|
adjacent = [ |
|
Position.new(pos.x - 1, pos.y - 1), |
|
Position.new(pos.x + 1, pos.y + 1), |
|
Position.new(pos.x - 1, pos.y + 1), |
|
Position.new(pos.x + 1, pos.y - 1), |
|
].map! { |p| adjust_position_bounds p } |
|
end |
|
|
|
def adjacent_of_type(pos, ctype) |
|
adjacent_to(pos).select do |p| |
|
creature = @board[p.x][p.y] |
|
creature && creature.is_of_type?(ctype) |
|
end |
|
end |
|
|
|
def unoccupied_adjacent_to pos |
|
adjacent_to(pos).select { |p| is_unoccupied? p } |
|
end |
|
|
|
def adjust_position_bounds p |
|
if p.x >= @width |
|
p.x = 0 |
|
end |
|
if p.x < 0 |
|
p.x = @width - 1 |
|
end |
|
if p.y >= @height |
|
p.y = 0 |
|
end |
|
if p.y < 0 |
|
p.y = @height - 1 |
|
end |
|
p |
|
end |
|
|
|
def new_creature(creature, pos) |
|
@board[pos.x][pos.y] = creature |
|
case creature.type |
|
when :fish |
|
@fish << pos |
|
when :shark |
|
@sharks << pos |
|
end |
|
end |
|
|
|
def remove_creature(pos) |
|
creature = @board[pos.x][pos.y] |
|
@board[pos.x][pos.y] = nil |
|
|
|
case creature.type |
|
when :fish |
|
@fish.delete pos |
|
when :shark |
|
@sharks.delete pos |
|
end |
|
end |
|
|
|
def move_creature from, to |
|
return unless is_occupied? from |
|
return if from == to |
|
|
|
@board[to.x][to.y] = @board[from.x][from.y] |
|
@board[from.x][from.y] = nil |
|
creature_set = case @board[to.x][to.y].type |
|
when :fish; @fish |
|
when :shark; @sharks |
|
end |
|
creature_set.delete(from) |
|
creature_set.add(to) |
|
end |
|
end |
|
|
|
class Creature |
|
attr_reader :type, :age |
|
|
|
def initialize(type) |
|
@type = type |
|
@age = 0 |
|
end |
|
|
|
def can_reproduce? repro_age |
|
@age % repro_age == 0 |
|
end |
|
|
|
def is_of_type? t |
|
@type == t |
|
end |
|
end |
|
|
|
class Fish < Creature |
|
def initialize |
|
super(:fish) |
|
end |
|
|
|
def update |
|
@age += 1 |
|
end |
|
end |
|
|
|
class Shark < Creature |
|
attr_reader :turns_without_eating |
|
|
|
def initialize |
|
super(:shark) |
|
@turns_without_eating = 0 |
|
end |
|
|
|
def update |
|
@age += 1 |
|
@turns_without_eating += 1 |
|
end |
|
|
|
def reset_starvation |
|
@turns_without_eating = 0 |
|
end |
|
end |
|
end |