Did anyone work on a Ruby entry to the ICFP contest this year?
This proved too great a temptation to ignore. Below is my new Ruby
simulator. Feel free to play with it and/or enhance it.
Interface is practically non-existant so far, but I'm thinking of
hooking it into OpenGL when I have time.
You would have to make changes if you wanted to use it with "Ant Wars"
but it is fully compliant with the original contest.
Enjoy.
James Edward Gray II
#!/usr/bin/ruby -w
# a simulator for ICFP 2004 ant game
# copyright 2004 James Edward Gray II <
[email protected]>
# permission given to modify and use
# reads ant files from "ants" directory and world files from "worlds"
# a simple ant data structure
class Ant
@@brains = { } # store brains at class level to save on memory
attr_reader :id, :color
attr_writer :alive, :state, :resting, :has_food
attr_accessor :direction, :x, :y
def initialize(id, color, brain_file, x, y)
@alive = true
@id = id
@color = color
unless @@brains.include? color
unless brain_file =~ /^ants/
brain_file = File.join("ants", brain_file)
end
@@brains[color] = [ ]
IO.foreach brain_file do |line| # ant file parser
if line =~ / ^\s*(Sense)\s+
(Here|Ahead|(?:Left|Right)Ahead)\s+
(\d+)\s+(\d+)\s+
(Friend(?:WithFood)?|Foe(?:WithFood|Marker|Home)?|
Food|Rock|Marker\s+[0-5]|Home)\s*$ /ix
@@brains[color].push [ $1.downcase, $2.downcase,
$3.to_i, $4.to_i,
$5.downcase ]
elsif line =~ /^\s*((?:Un)?mark)\s+([0-5])\s+(\d+)\s*$/i or
line =~ /^\s*(PickUp|Move)\s+(\d+)\s+(\d+)\s*$/i
@@brains[color].push [ $1.downcase, $2.to_i, $3.to_i ]
elsif line =~ /^\s*(Turn)\s+(Left|Right)\s+(\d+)\s*$/i
@@brains[color].push [ $1.downcase, $2.downcase, $3.to_i ]
elsif line =~ /^\s*(Drop)\s+(\d+)\s*$/i
@@brains[color].push [ $1.downcase, $2.to_i ]
elsif line =~ /^\s*(Flip)\s+(\d+)\s+(\d+)\s+(\d+)\s*$/i
@@brains[color].push [ $1.downcase, $2.to_i,
$3.to_i, $4.to_i ]
elsif line =~ /\S/
raise "Corrupt ant brain. State #{$. - 1}: #{line}."
end
end
end
@brain = @@brains[color]
@state = 0
@resting = 0
@direction = 0
@has_food = 0
@x = x
@y = y
end
def alive?
return @alive
end
def enemy
if color == "red"
return "black"
else
return "red"
end
end
def has_food?
if @has_food == 1
return true
else
return false
end
end
def resting?
if @resting > 0
@resting -= 1
return true
else
return false
end
end
def state
return @brain[@state]
end
def inspect # for test dumps
return "#@color ant of id #@id, dir #@direction, " +
"food #@has_food, state #@state, resting #@resting"
end
end
# a simple data structure for representing world spaces
class Cell
attr_reader :x, :y
attr_writer :ant
attr_accessor :food
def initialize(x, y, rocky = false, hill = nil, food = 0, ant = nil)
@x = x
@y = y
@rocky = rocky
@hill = if hill.kind_of? String then hill.downcase else hill end
@food = food
@ant = ant
@red_markers = [ 0, 0, 0, 0, 0, 0 ]
@black_markers = [ 0, 0, 0, 0, 0, 0 ]
end
def adjacent(direction)
case direction
when 0
return @x + 1, @y
when 1
if @y % 2 == 0
return @x, @y + 1
else
return @x + 1, @y + 1
end
when 2
if @y % 2 == 0
return @x - 1, @y + 1
else
return @x, @y + 1
end
when 3
return @x - 1, @y
when 4
if @y % 2 == 0
return @x - 1, @y - 1
else
return @x, @y - 1
end
when 5
if @y % 2 == 0
return @x, @y - 1
else
return @x + 1, @y - 1
end
end
end
def ant?(color = nil)
color.downcase! if color.kind_of? String
if not color.nil? and not @ant.nil? and @ant.color == color
return @ant
elsif color.nil? and not @ant.nil?
return @ant
else
return false
end
end
def rocky?
return
@rocky
end
def hill?(color = nil)
color.downcase! if color.kind_of? String
if not color.nil? and @hill == color
return true
elsif color.nil? and not hill.nil?
return true
else
return false
end
end
def mark(color, i)
if color == "red"
@red_markers
= 1
else
@black_markers = 1
end
end
def mark?(color, i = nil)
if i.nil?
if color == "red"
@red_markers.each { |e| return true if e == 1 }
return false
else
@black_markers.each { |e| return true if e == 1 }
return false
end
else
if color == "red"
if @red_markers == 1
return true
else
return false
end
else
if @black_markers == 1
return true
else
return false
end
end
end
end
def unmark(color, i)
if color == "red"
@red_markers = 0
else
@black_markers = 0
end
end
def inspect # for test dumps
dump = "cell (#@x, #@y): "
if @rocky
dump += "rock; "
end
if @food > 0
dump += "#@food food; "
end
if not @hill.nil?
dump += "#@hill hill; "
end
if @red_markers.include? 1
dump += "red marks: "
@red_markers.each_with_index do |mark, i|
dump += i.to_s if mark == 1
end
dump += "; "
end
if @black_markers.include? 1
dump += "black marks: "
@black_markers.each_with_index do |mark, i|
dump += i.to_s if mark == 1
end
dump += "; "
end
if not @ant.nil?
dump += @ant.inspect
end
dump.sub!(/(rock);\s*$/, '\1') # ugly hack to match their dump format
return dump
end
end
# primary game logic object
class Simulator
def initialize( red_brain_file, black_brain_file, world_file = nil,
final_round = 100000, test_mode = false )
@world = [ ]
@ants = [ ]
if world_file.nil?
worlds = [ ]
Dir.foreach "worlds" do |file_name|
worlds.push file_name unless file_name[0, 1] == "."
end
world_file = worlds[rand(worlds.size)]
end
unless world_file =~ /^worlds/
world_file = File.join("worlds", world_file)
end
IO.foreach world_file do |line| # world file parser
next if $. < 3
row = [ ]
line.split(" ").each do |e|
case e
when "#"
row.push Cell.new(row.length, @world.length, true)
when "."
row.push Cell.new(row.length, @world.length)
when "1".."9"
row.push Cell.new( row.length, @world.length,
false, nil, e.to_i )
when "+", "-"
color = if e == "+" then "red" else "black" end
brain = if e == "+"
red_brain_file
else
black_brain_file
end
@ants.push Ant.new( @ants.length, color, brain,
row.length, @world.length )
row.push Cell.new( row.length, @world.length,
false, color, 0, @ants[-1] )
else
raise "Corrupt world. Unknown symbol: #{e}."
end
end
@world.push row
end
@final_round = final_round.to_i
# the following was just for testing the sim against the spec
@test_mode = test_mode
@random_seed = 12345
3.times { @random_seed = (@random_seed * 22695477 + 1) % 1073741824 }
end
# main event loop - not broken down on purpose, for speed
# extended interface calls should be interleaved in here
def run
test_dump 0 if @test_mode
1.upto @final_round do |round|
deaths = [ ] # for removing ants after each() iteration
@ants.each do |ant|
next if ant.resting? or not ant.alive?
action = ant.state
cell = @world[ant.y][ant.x]
case action[0]
when "sense"
check = cell
case action[1]
when "ahead"
look_x, look_y = cell.adjacent ant.direction
check = @world[look_y][look_x]
when "leftahead"
look_x, look_y =
cell.adjacent((ant.direction + 5) % 6)
check = @world[look_y][look_x]
when "rightahead"
look_x, look_y =
cell.adjacent((ant.direction + 1) % 6)
check = @world[look_y][look_x]
end
case action[4]
when "friend"
if check.ant? ant.color
ant.state = action[2]
else
ant.state = action[3]
end
when "foe"
if check.ant? ant.enemy
ant.state = action[2]
else
ant.state = action[3]
end
when "friendwithfood"
if check.ant? ant.color and check.ant?.has_food?
ant.state = action[2]
else
ant.state = action[3]
end
when "foewithfood"
if check.ant? ant.enemy and check.ant?.has_food?
ant.state = action[2]
else
ant.state = action[3]
end
when "food"
if check.food > 0
ant.state = action[2]
else
ant.state = action[3]
end
when "rock"
if check.rocky?
ant.state = action[2]
else
ant.state = action[3]
end
when "foemarker"
if check.mark? ant.enemy
ant.state = action[2]
else
ant.state = action[3]
end
when "home"
if check.hill? ant.color
ant.state = action[2]
else
ant.state = action[3]
end
when "foehome"
if check.hill? ant.enemy
ant.state = action[2]
else
ant.state = action[3]
end
else # marker 0-5 - trick to avoid regex
if check.mark? ant.color, action[4][-1, 1].to_i
ant.state = action[2]
else
ant.state = action[3]
end
end
when "mark"
cell.mark(ant.color, action[1])
ant.state = action[2]
when "unmark"
cell.unmark(ant.color, action[1])
ant.state = action[2]
when "pickup"
if ant.has_food? or cell.food == 0
ant.state = action[2]
else
cell.food = cell.food - 1
ant.has_food = 1
ant.state = action[1]
end
when "drop"
if ant.has_food?
ant.has_food = 0
cell.food = cell.food + 1
end
ant.state = action[1]
when "move"
new_x, new_y = cell.adjacent ant.direction
to = @world[new_y][new_x]
if to.rocky? or to.ant?
ant.state = action[2]
else
cell.ant = nil
to.ant = ant
ant.x = to.x
ant.y = to.y
ant.resting = 14
ant.state = action[1]
check_surround = [ to ]
0.upto 5 do |direction|
x, y = to.adjacent direction
check_surround.push @world[y][x]
end
check_surround.each do |e|
next unless e.ant?
enemy_count = 0
0.upto 5 do |direction|
test_x, test_y = e.adjacent direction
next if test_x < 0 or test_y < 0 or
test_x >= @world[0].size or
test_y >= @world.size
if @world[test_y][test_x].ant? e.ant?.enemy
enemy_count += 1
end
break if enemy_count == 5
break if enemy_count < direction
end
if enemy_count == 5
e.ant?.alive = false
deaths.push e.ant?.id
e.ant = nil
e.food += 3
end
end
end
when "turn"
if action[1] == "left"
ant.direction = (ant.direction + 5) % 6
else
ant.direction = (ant.direction + 1) % 6
end
ant.state = action[2]
when "flip"
if @test_mode
if test_random(action[1]) == 0
ant.state = action[2]
else
ant.state = action[3]
end
else # for speed
if rand(action[1]) == 0
ant.state = action[2]
else
ant.state = action[3]
end
end
end
end
deaths.each do |e| # remove ants that died this turn
@ants.delete(@ants.find { |ant| ant.id == e })
end
test_dump round if @test_mode
end
print score unless @test_mode
end
# super basic results printout
def score
red_score = black_score = 0
@world.each do |row|
row.each do |cell|
if cell.hill? "red"
red_score += cell.food
elsif cell.hill? "black"
black_score += cell.food
end
end
end
score = "Final Score:\n\n" +
"\tRed: #{red_score}\n\tBlack: #{black_score}\n\n"
if red_score > black_score
score += "Red wins!\n"
elsif red_score == black_score
score += "Draw.\n"
else
score += "Black wins!\n"
end
end
# these final two method were just for validation - done to 10,000
turns
def test_dump(round)
print "random seed: 12345\n\n" if round == 0
puts "After round #{round}..."
@world.each do |row|
row.each { |cell| p cell }
end
puts
end
def test_random(limit)
@random_seed = (@random_seed * 22695477 + 1) % 1073741824
return ((@random_seed / 65536) % 16384) % limit
end
end
unless ARGV.size >= 2
puts "Usage: ant_sim.rb RED_ANT_FILE BLACK_ANT_FILE [ TURNS TEST_MODE
]"
exit
end
game = Simulator.new(*ARGV)
game.run