R
Ruby Quiz
The tournament has been run, so let's begin with the official results. You can
find the complete statistics here:
http://www.rubyquiz.com/rrobots/tournament.html
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:
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:
# ...
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:
# ...
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:
# ...
class Brain
include Math
include RobotMath
include Turnarounder
include Pointanizer
# ...
attr_accessor
redx,
redy
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...
)
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:
# ...
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:
# ...
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...
find the complete statistics here:
http://www.rubyquiz.com/rrobots/tournament.html
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:
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:
# ...
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:
# ...
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:
# ...
class Brain
include Math
include RobotMath
include Turnarounder
include Pointanizer
# ...
attr_accessor
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...
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:
# ...
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:
# ...
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...