RRobots (#59)

by Simon Kroeger

RRobots, Inc. is always looking for new talented pilots. Recently they lost so many skilled employees in a show battle against one of their competitors that they decided to try something new.

Don't be afraid, you don't have to lead a robot into the fight personally. RRobots, Inc. is asking you to write an AI for one of their bots. You will be provided with a programming interface and as many shiny new robots as you may need to test your creation.

All bots have a body equipped with a powerful engine and robust plating, a gun capable of firing energy bullets of various strengths, and a radar to scan the battlefield for enemy bots.

Battles take place in an arena, 1600m by 1600m, each robot is placed at a randomly chosen position and powered on simultaneously. For this test all fights are one-on-one, so if you scan something it will be your opponent.

RRobots, Inc. will run a championship competition on 12-27-2005 (wasting even more hardware) matching each participant against all others, three times. (Your bot must be posted to Ruby Talk on or before 6 PM (GMT) the 27th, to compete. A resubmission of the same bot class replaces the original submission, but contestants are allowed to submit multiple distinct bots.) The winner of the competition will be the bot with the most overall wins. The result of these battles will lead to a winner honored with having the next product-line named after him. (James and Simon will provide the winner with a Desktop R/C Mini-Rover from ThinkGeek.com, after the quiz summary is posted. Contestants must provide a valid email address with their solutions to be eligible.)

If you are interested, here are the details:

You have to provide a class including the module 'Robot', defining a method named 'tick' taking an array of events as input. By including the module 'Robot' you gain access to the robot hardware via methods like:

ruby
fire(power) # fires a bullet in the direction of your gun
turn(degrees) # turns the robot (and the gun and the radar)
energy # your remaining energy (if this drops below 0 you are dead)
# ...

You have to define the behavior of the robot for each tick (approximately 20ms). This approach is kind of low level but you are allowed (if not encouraged) to unleash the whole power of Ruby to create higher level functions and interfaces (take a look at OOSittingDuck for an idea).

A word of warning: If your AI tries to cheat (using other ways than those provided by the 'Robot' module to gain information about the battlefield or the other bots) or throws any errors, your submission will be disqualified.

You can get information, sample bots and the arena program on RRobots.

Good luck and may the best bot win!

Disclaimer: Of course there is no 'RRobots, Inc.' (if there is, this is totally unrelated to them) and you will not receive any hardware whatsoever via snail mail. While it is very unlikely that you get hurt in a RRobots battle, I'm not responsible for any harm done to you or your equipment during this quiz.


Quiz Summary

The tournament has been run, so let's begin with the official results. You can find the complete statistics here:

Tournament Results

Briefly though, here are the total wins:

+------------------+-------+
| Bot | Wins |
+------------------+-------+
| Ente | 241.5 |
| RubberDuck | 228.0 |
| RubberDuckLinear | 179.0 |
| WKB | 177.5 |
| Kite2 | 165.0 |
| DuckBill09 | 161.0 |
| CloseTalker | 153.0 |
| GayRush | 115.0 |
| HornRobot | 108.0 |
| Harlock | 67.0 |
| SporkBot | 55.0 |
+------------------+-------+

Looks like our big winner was Ente. I'm told that's German for duck. Congratulations to Jannis Harder, Ente's creator.

Ente is just shy of 500 lines of code, so we're going to get the short version below. As always, I suggest looking at the actual code. Here's the start of the robot:

ruby
require 'robot'

class << Module.new # anonymous container

module RobotMath

def weighted_linear_regression(data)
# ...
end

def zero_fixed_linear_regression(data)
# ...
end

def offset(heading_a,heading_b = 0)
# ...
end

end

# ...

Obviously, we have the required module for the robot interface. Then we see the definition of some simple math helpers.

The interesting part of the above code to me was the second line. Since this code has no need to refer to the defined modules, classes, and methods outside of its own scope, the whole mess added to an anonymous namespace. That seems like a clever way to guard against name collision, among other uses.

One of the larger tasks all the robots had to deal with was turning. You have to ensure your robot, radar, and gun are facing where you need at any given time. Ente has a whole module of helpers for this:

ruby
# ...

module Turnarounder
# ...

def head_to deg # ...
def head_gun_to deg # ...
def head_radar_to deg # ...
def next_delta_heading # ...
alias turn_amount next_delta_heading
def ready? # ...
def next_heading # ...
def turn_gun_amount # ...
def gun_ready? # ...
def next_delta_gun_heading # ...
def next_gun_heading # ...
def turn_radar_amount # ...
def radar_ready? # ...
def next_delta_radar_heading # ...
def next_radar_heading # ...
def final_turn # ...
def turn x # ...
def turn_gun x # ...
def turn_radar x # ...
def mid_radar_heading # ...

end

# ...

The functionality of those methods should be fairly obvious from the names and we will see them put to use in a bit.

Next we have a module for movement:

ruby
# ...

module Pointanizer
include Math
include RobotMath
include Turnarounder

def move_to x,y
@move_x = x
@move_y = y
@move_mode = :to
end

def move mode,x,y
@move_x,@move_y = x,y
@move_mode = mode
end

def halt
@move_mode = false
end

def moving?
@move_mode
end

def on_wall?
xcor <= size*3 or ycor <= size*3 or
battlefield_width - xcor <= size*3 or
battlefield_height - ycor <= size*3
end

def final_point

yc = ycor-@move_y rescue 0
xc = @move_x-xcor rescue 0

if hypot(yc,xc) < size/3
@move_mode = false
end

acc = true

case @move_mode
when :to
head_to atan2(yc,xc).to_deg
when :away
head_to atan2(yc,xc).to_deg+180
when :side_a
head_to atan2(yc,xc).to_deg+60
when :side_b
head_to atan2(yc,xc).to_deg-60
when nil,false
acc = false
else
raise "Unknown move mode!"
end

accelerate(8) if acc

end

def rad_to_xy(r,d)
return xcor + cos(r.to_rad)*d, \
ycor - sin(r.to_rad)*d
end

end

# ...

We're starting to see robot specific knowledge here. This module deals with robot movement. The methods move() and move_to() are used to set a new destination, or halt() can stop the robot.

If you glance through final_point(), you will see the various movement modes this robot uses. For example, :away will turn you 180 degrees so you move "away" from the indicated destination.

Now we're ready for a look at Ente's brain:

ruby
# ...

class Brain
include Math
include RobotMath
include Turnarounder
include Pointanizer

# ...

attr_accessor :predx, :predy

def initialize(robot)
@robot = robot
super()

@points = []

@last_seen_time = -TRACK_TIMEOUT

@radar_speed = 1
@track_mul = 1

@searching =0
@seeking =0

#movement
@move_direction = 1
@lasthitcount = 0
@lasthitcount2 = false
@lastchange = -TIMEOUT
end

# ...

def predict ptime
# ...
end

def predcurrent
@predx,@predy = predict time unless @predx
end

def tick events

fire 0.1

#event processing

# ...

#moving

# ...

#aiming

# ...

#scanning

# ...

end

def method_missing(*args,&block)
@robot.relay(*args,&block)
end

end

# ...

The majority of that is the tick() method, of course. It's quite a beast and I'm not even going to try to predict all that it does. The comments in it will tell you what the following calculations are for, at least. (Jannis should feel free to reply with a detailed breakdown... :D )

One thing that is interesting in the above code is the use of the passed in @robot. Take a look at the first line in the constructor and the definition of method_missing() at the end of this class. In order to understand that, we need to see the last class:

ruby
# ...

class Proxy
include ::Robot

def initialize
@brain = Brain.new(self)
end

EXPORT_MAP = Hash.new{|h,k|k}

EXPORT_MAP['xcor'] = 'x'
EXPORT_MAP['ycor'] = 'y'
EXPORT_MAP['proxy_turn'] = 'turn'
EXPORT_MAP['proxy_turn_gun'] = 'turn_gun'
EXPORT_MAP['proxy_turn_radar'] = 'turn_radar'

def relay(method,*args,&block)
self.send(EXPORT_MAP[method.to_s],*args,&block)
end

def tick events
@brain.tick events
end

end

# ...

As you can see, Proxy ties together the Brain class and the Robot module with a combination of relay() and the Brain.method_missing() we saw earlier. You could swap out brains by changing the assignment in initialize(), or even reassign @brain at runtime to switch behaviors.

Only one issue remains. RRobots is expecting an Ente class to be defined but we haven't seen that yet. That needs to be resolved before we leave this anonymous namespace we're in and lose access to all of these classes. Here's the final chunk of code that handles just that:

ruby
# ...

classname = "Ente"
unless Object.const_defined?(classname)
Object.const_set(classname,Class.new(Proxy))
end
end

A new class is created by subclassing Proxy and that class is assigned to a constant on Object by the name of Ente. That ensures RRobots will find what it expects when the time comes.

My thanks to all the robot coders, especially for the robots I didn't show above. They all wrote some interesting code. Also, a big thank you to Simon Kroeger who helped me setup this quiz, and ran the final tournament.

Tomorrow, Christer Nilsson has a fun little maze of numbers for us...