Book HomeMastering Perl/TkSearch this book

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

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.)

[56] Just like in the message passing example, although this is Tom's idea, not mine.

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 / Figure 20.3.2]

Of course, by now, you are fully capable of computing Figure 20.3.2 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.



Library Navigation Links

Copyright © 2002 O'Reilly & Associates. All rights reserved.