From: James Edward Gray II Date: 2004-09-16T11:08:19+09:00 Subject: Re: Ant Wars (Warning: long) On Sep 12, 2004, at 11:39 AM, Phil Tomson wrote: > 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 # 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[i] = 1 else @black_markers[i] = 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[i] == 1 return true else return false end else if @black_markers[i] == 1 return true else return false end end end end def unmark(color, i) if color == "red" @red_markers[i] = 0 else @black_markers[i] = 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