20.3. TclRobots
TclRobots, written by Tom Poindexter, is a
test of programming prowess. The goal is to program a robot that
battles other robots and hopefully, survives to become the victor.
How well a robot performs depends mostly on the Robot Control Program
(RCP) you write. As in real life, there is randomness built into the
contest, so even the best RCP is occasionally defeated.
The TclRobots arena is a square, 1,000 meters on each side. Robots,
identical other than their controlling RCPs, have drive mechanisms
that move them each in a given direction at a given velocity within
the arena, scanners for seeking out enemy robots, and cannons for
firing at the enemy. A contest involves two, three, or four robots,
each trying to outlast the others. In Figure 20-4,
two robot scanners have targeted enemies, complex.tr
has taken a severe hit, and a cannon shot is in
mid-flight.
Figure 20-4. A TclRobots contest
tclrobots starts a robot instance by
exec ing a new wish
interpreter,[56] then uses send to customize it. First,
it transmits specially crafted subroutines that implement the RCP
command set (commands such as drive,
scan, and cannon). These
commands define the RCP application programming interface (API), and
they use send to invoke subroutines in
tclrobots. Next, tclrobots
sends the Tcl RCP source code to the robot and starts it running.
(There's actually a lot more detail behind
the scenes. Feel free to visit the tclrobots
source code.)
Until
now, the only RCP programming language available was Tcl, but my
desire to write RCPs in Perl changed that, as we are about to see.
Several things are required before a Perl RCP is possible. First,
tclrobots needs to distinguish between a Tcl and
Perl RCP. Since the convention of using the extension
.tr, for TclRobot, was already in place, I
selected .ptr, for PerlTclRobot. Thus,
tclrobots checks for those extensions and
differentially loads perl or
wish. Second, incoming Tcl messages must be
translated into proper Perl actions, and results returned in Tcl
format. Lastly, the RCP API needs to be re-implemented in Perl so it
sends Tcl commands and returns Perl results.
20.3.1. TclRobots.pm, the Perl Interface to TclRobots
This module implements a thin API so you
can write RCPs in your favorite language—Perl—and do
battle with all the existing Tcl RCPs. It's one of the
strangest modules around, mixing Exporter subroutines; Perl, Tk, and
Tcl code; and inter-language, bidirectional communications. You never
knowingly use this module; rather, it's
included when tclrobots runs an instance of
perl -MTclRobots, at which time
this module is loaded and begins execution on behalf of your RCP. It
creates the main window of the required dimensions and at the proper
location on the display, and adds all the widgets, text, and images.
When instructed by tclrobots, the module then
loads your Perl RCP via require, so be sure the
code returns a true value. From that point on, incoming TclRobots
messages are dispatched to Perl emulation handlers, and Perl RCP
commands are converted to Tcl syntax and sent to
tclrobots. The communication is handled
transparently via send and
Tk::Receive.
20.3.1.1. Tk::Receive handles tclrobots to Perl communications
To figure out the TclRobots protocol, the
first version of Tk::Receive simply dumped
messages to a file for study. A little experimentation determined
what tclrobots sent and what it expected to
receive. Some messages were easy to dechiper, like expr
1+1, which obviously must return 2. Other messages create
and modify the Tk interface or start and stop the RCP. The final
version of Tk::Receive follows; note that all global symbols are
prefixed with an underscore, indicating they are private to
TclRobots.pm. The CASE
statement simply uses several regular expressions to pattern match
the incoming Tcl commands and call a Perl handler.
sub Tk::Receive {
# Accept Tcl strings from tclrobots and invoke
# Perl/Tk emulation code.
my($mw) = shift; # main window
$_ = shift; # Tcl command
return 2 if /expr 1\+1/;
return if /(Knuth|^rename)/m;
CASE:
{
/setup window/m and do {_setup_window_; last CASE};
/create|configure/ and do {_customize_window_ $_; last CASE};
/set _start_ 0/ and do {_load_rcp_; last CASE};
/set _start_ 1/ and do {_start_rcp_; last CASE};
/^proc after/ and do {_disable_rcp_; last CASE};
/\.d\.l insert/ and do {_insult_rcp_ $_; last CASE};
/^_a_\d+ 0 _e_\d+/ and do {_destroy_rcp_; last CASE};
/^set/ and do {_set_variables_ $_; last CASE};
print STDERR "UNHANDLED cmd=$_!\n";
} # casend
} # end Tk::Receive
As an example, _setup_window_ creates the small
RCP MainWindow shown in Figure 20-4, containing a
tiny Canvas and two Labels. These display the RCP's icon,
filename, and damage percentage, respectively. Below these widgets is
a scrolled Listbox for debug and status information. (The following
section describes when and how the MainWindow,
$_mw_, is created.)
sub _setup_window_ {
# Setup the RCP's debug and damage window.
my $f = $_mw_->Frame;
$f->pack(qw/-side top -fill x -ipady 5/);
$_fc_ = $f->Canvas(qw/-width 20 -height 16/);
$_fl_ = $f->Label(qw/-relief sunken -width 30 -text/ =>
"(loading robot code..)");
$_fs_ = $f->Label(qw/-relief sunken -width 5 -text/ => "0%");
$_fc_->pack(qw/-side left/);
$_fs_->pack(qw/-side right/);
$_fl_->pack(qw/-side left -expand 1 -fill both/);
$_dl_ = $_mw_->Scrolled('Listbox', qw/-relief sunken -scrollbars se/);
$_dl_->pack(qw/-side left -expand 1 -fill both/);
$_mw_->minsize(100, 70);
$_mw_->update;
}
20.3.1.2. The RCP API handles Perl to tclrobots communications
TclRobots.pm is a plain old module that exports
subroutines. Specifically, it exports the RCP commands that control
the robot. Unlike other Perl modules, however, once loaded, this one
begins executing, creates a MainWindow, enters the Tk event loop, and
never returns. I've distilled the salient portions:
$TclRobots::VERSION = '2.1';
package TclRobots;
# This module implements a thin API that interfaces ...
use Exporter;
@ISA = qw/Exporter/;
@EXPORT = qw/after alert cannon damage dputs drive dsp
heat loc_x loc_y scanner speed team_declare team_get
team_send tick update/;
use Tk;
use Tk::widgets qw/Dialog/;
use vars qw/$_mw_/;
use strict;
$ENV{'HOME'} = '/tmp';
$_mw_ = MainWindow->new;
MainLoop;
# Robot Control Program commands ...
sub drive {
}
sub scan {
}
sub cannon {
}
In Tcl, the RCP command to move a robot is drive:
drive $heading $velocity
The heading is given in degrees, 0 to 359, with 0 degrees due east,
rotating counter-clockwise. The velocity is a percentage, from 0 to
100. Here's the Perl drive implementation;
all RCP subroutines follow this format:
sub drive {
my($deg, $speed) = @_;
$_mw_->after(100);
$_mw_->update;
my $val = Tk::catch {$_mw_->send($_tclrobots_,
"do_drive $_robot_ $deg $speed")};
$_mw_->waitVariable(\$_resume_) if $_debug and $_step_;
&_ping_check_;
$_mw_->update;
return $val;
}
The subroutine first delays for 100 milliseconds (reflecting the fact
that hardware commands take a finite amount of time), invokes
tclrobots' do_drive
subroutine, and returns the result. For debugging purposes, the RCP
can be single-stepped, which is what the
waitVariable statement is for. An RCP can also
define a callback alerting it when an enemy robot scans it;
_ping_check_ invokes any such callback.
20.3.2. Robot Control Programs
"Do
I flee when scanned, or do I turn and attack? How do I know where I
am? I've kept a list of enemy robots and their locations, which
do I shoot at first? How do I keep from running into an arena wall?
How do I steer? How do I get from point A to point B? How do I
compensate for my motion when computing a firing solution?" We
certainly won't answer these questions, but just in case you
think writing an RCP is easy, think again!
A good way to start writing your own
RCP is to examine the samples, and the tournament entries at
http://www.neosoft.com/tcl/ftparchive/sorted/misc/TclRobots/.
Or search the Web; I've found Computer Science courses that
teach RCP programming! And, of course, read the manpage.
Let's take a quick look at the Perl version of a sample RCP,
charger.ptr. It's one of the shortest RCPs
I've seen, yet it's surprisingly effective. Its strategy
is simple: scan with a wide resolution until an enemy robot is found,
charge the opponent, pinpoint the target with a narrow scan, and fire
the cannon. Notice the code is strictly Perl, no Tcl or Tk required.
The $status callback, executed every 10 seconds,
demonstrates how to emulate repeat if it's
not part of the language. It posts the robot's current position
and the hardware tick count in the debug window. The
scanner command expects the direction and
resolution of the scan and returns the distance to the first target
found. The cannon command fires a shell in the
specified direction for the specified distance.
use vars qw/$dir $closest $limit $nothing $rng $start $status/;
use strict;
$dir = 90;
$nothing = 0;
$closest = 0;
$status = sub {
dputs "tick count=", tick, "location=(", loc_x, ",", loc_y, ")";
after 10_000 => \&$status;
};
after 0 => \&$status; # Tcl idiom for repeat( )
while (1) {
$rng = scanner $dir, 10; # look for an opponent
if ($rng > 0 and $rng < 700) { # if found and in range
$start = ($dir+20)%360; # begin narrow scan
for ($limit = 1; $limit <= 40; $limit++) {
$dir = ($start - $limit + 360) % 360;
$rng = scanner $dir, 1;
if ($rng > 0 and $rng < 700) {
$nothing = 0; # charge! fire a shell, and backup
cannon $dir, $rng; # scan so not to miss a moving target
drive $dir, 70;
$limit -= 4;
}
}
} else {
$nothing++;
$closest = $dir if $rng > 700;
}
drive 0, 0;
if ($nothing >= 30) { # check for nothing found in reasonable time
$nothing = 0;
drive $closest, 100;
after 10000 => sub {drive 0, 0};
}
$dir = ($dir - 20 + 360) % 360;
}
1;
Unsurprisingly, trigonometry plays an important part in any
nontrivial RCP. Here are two formulas I've found useful. The
first computes the distance between two points:
[expr hypot( ($x-$x0),($y-$y0) )]
You might use this to check if the robot has reached a particular
coordinate or to calculate the range to a target. The second equation
computes a heading between two points:
[expr (round($R2D * atan2(($y-$y0),($x-$x0))) + 360) % 360]
This is useful for steering the robot. The variable
$R2D converts radians to degrees and is given by:
set R2D [expr 180.0 / ]
Of course, by now, you are fully capable of computing and
translating these simple Tcl statements to Perl.
Turn to Appendix C, "Complete Program Listings" for a listing of
complex.ptr, a sophisticated RCP written in Perl.
This RCP is completely state driven and uses clock ticks to schedule
internal events. It moves in a path described by an
n-sided polygon that approximates a
circle. The direction of movement is randomly chosen during preset,
eliminating "wall" code (although the RCP may perform a
"crazy Ivan" if it cannot find an enemy). The RCP also
attempts to move as fast as possible, thus tracks its cooling rate to
coordinate turns, eliminating "flee" code. There is also
some watchdog code that periodically checks the RCP's health.
Finally, the robot recognizes team members and targets only real
enemies.
 |  |  | 20.2. Computing with Parallel Message Passing |  | 21. C Widget Internals |
Copyright © 2002 O'Reilly & Associates. All rights reserved.
|
|