Skip to content

Instantly share code, notes, and snippets.

@ajesler
Created July 20, 2015 07:53
Show Gist options
  • Save ajesler/3903075e820d1c1f8727 to your computer and use it in GitHub Desktop.
Save ajesler/3903075e820d1c1f8727 to your computer and use it in GitHub Desktop.
wator simulator using ncurses

WaTor

Wa-Tor is a population dynamics simulation devised by Alexander Keewatin Dewdney

See http://home.cc.gatech.edu/biocs1/uploads/2/wator_dewdney.pdf and https://en.wikipedia.org/wiki/Wa-Tor.

Installation

gem install curses

Usage

To start the simulation, run ruby wator-vis.rb

The available key commands are:

  • r to reset the simulation
  • n to step the simulation
  • q to quit the simulation
  • c to continue to step the simulation once a species dies out.

Customising

See the create_sim_options method in wator-vis.rb to see what can be changed. A full list of options is available in the DEFAULTS constant of WaTor::Simulation in wator-engine.rb

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
require './wator-engine'
require 'curses'
class SimVis
def initialize(sim)
@sim = sim
end
def next
@sim.next_chronon
end
def finished?
return [email protected]_both_species?
end
def build_screen
out = []
out << [
"cycle:#{@sim.cycles.to_s.rjust(7)}",
" | nfish:#{@sim.board.fish.size.to_s.rjust(5)}",
" | nsharks:#{@sim.board.sharks.size.to_s.rjust(5)}",
" | width:#{@sim.width.to_s.rjust(5)}",
" | height:#{@sim.height.to_s.rjust(5)}"
].join
if finished?
out << "\nSimulation has finished after #{@sim.cycles} cycles.\n"
else
out << "\n\n"
end
@sim.height.times do |y|
line = []
@sim.width.times do |x|
creature = @sim.board[x,y]
char = if creature.nil?
" "
elsif creature.type == :shark
"S"
elsif creature.type == :fish
"."
end
line << char
end
out << line.join
end
return out.join("\n")
end
def draw
screen = build_screen
Curses.setpos(0,0)
Curses.addstr(screen)
Curses.refresh
end
end
def create_sim_options
width = Curses.cols
height = Curses.lines - 11
area = width * height
pop_percent = 0.05
opts = {
:width => width,
:height => height,
:nfish => (area*pop_percent).round,
:nsharks => (area*(pop_percent/10)).round,
:fish_reproduction => 5
}
end
Curses.init_screen
begin
Curses.crmode
Curses.noecho
sim = WaTor::Simulation.new(create_sim_options)
simvis = SimVis.new(sim)
simvis.draw
loop do
case Curses.getch
when ?Q, ?q
break
when ?N, ?n
if !simvis.finished?
simvis.next
simvis.draw
end
when ?C, ?c
simvis.next
simvis.draw
when ?R, ?r
sim = WaTor::Simulation.new(create_sim_options)
simvis = SimVis.new(sim)
simvis.draw
end
end
ensure
Curses.close_screen
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment