Remote agent/Agent logic

From Rosetta Code
Remote agent/Agent logic is a draft programming task. It is not yet considered ready to be promoted as a complete task, for reasons that should be found in its talk page.
In Remote agent, a game is described where an agent interacts with a simple world of walls, balls and squares, and a component is described that marshals commands between the simulation environment and the logic code behind the agent.

The goal conditions for the game are to get all balls in squares of matching colors, in as few turns as possible.

Using an interface for your language write a program that attempts to reach these goals. The exact agent behavior within the simulated environment is unspecified.

C

See Remote agent/Simulation/C

Go

package agent

import (
    "log"
    "math/rand"
    "time"

    "ra/ifc"
)

// The agent's awareness is quite limited.  It has no representation of the
// maze, which direction it is facing, or what it did last.  It notices and
// remembers just three things:  The color of the sector just entered, the
// presense and color of any ball there, and the presense and color of any
// ball it is holding.
var sectorColor, sectorBall, agentBall byte

// Package level variable to simplify function calls.
var stream ifc.Streamer

func Agent(s ifc.Streamer) {
    stream = s
    // randomness used for movement
    rand.Seed(time.Now().Unix())
    // handshake
    hs := stream.Rec()
    if hs != ifc.Handshake {
        log.Fatal("agent: thats no handshake")
    }
    stream.Send(ifc.Handshake)
    // agent behavior main loop
    for gameOver := false; !gameOver; {
        findMisplaced()
        get()
        findMatching()
        gameOver = drop()
    }
}

// noColor is not part of the interface or the world's representation.
// It is used by the agent as a test for receipt of a color-based event.
const noColor byte = '-'

// Move moves one sector in a random direction.
// It retries on bumps and doesn't return until a forward command succeeds.
// It expects a color event on a successful move and terminates if it doesn't
// get one.
func move() {
    for {
        // Randomness:  50/50 chance of turning or attempting move.
        // For turns, equal chance of turning right or left.
        switch rand.Intn(4) {
        case 0:
            stream.Send(ifc.CmdLeft)
            for stream.Rec() != ifc.EvStop {
            }
            continue
        case 1:
            stream.Send(ifc.CmdRight)
            for stream.Rec() != ifc.EvStop {
            }
            continue
        }
        stream.Send(ifc.CmdForward)
        bump := false
        sectorColor = noColor
        sectorBall = noColor
    events:
        for {
            switch ev := stream.Rec(); ev {
            case ifc.EvBump:
                bump = true
            case ifc.EvColorRed, ifc.EvColorGreen,
                ifc.EvColorYellow, ifc.EvColorBlue:
                sectorColor = ev
            case ifc.EvBallRed, ifc.EvBallGreen,
                ifc.EvBallYellow, ifc.EvBallBlue:
                sectorBall = ev
            case ifc.EvStop:
                break events
            }
        }
        if bump {
            continue
        }
        if sectorColor == noColor {
            log.Fatal("agent: expected color event after move")
        }
        return
    }
}

// FindMisplaced wanders the maze looking for a ball on the wrong sector.
func findMisplaced() {
    for {
        move()
        // get ball from current sector if meaningful
        switch sectorBall {
        case ifc.EvBallRed, ifc.EvBallGreen,
            ifc.EvBallYellow, ifc.EvBallBlue:
            if sectorBall != sectorColor+32 {
                return
            }
        }
    }
}

// Get is only called when get is possible.
func get() {
    stream.Send(ifc.CmdGet)
    for {
        switch stream.Rec() {
        case ifc.EvStop:
            // agent notes ball color, and that sector is now empty
            agentBall = sectorBall
            sectorBall = noColor
            return
        case ifc.EvNoBallInSector, ifc.EvAgentFull:
            log.Fatal("agent: expected get to succeed")
        }
    }
}

// There's a little heuristic built in to findMatching and drop.
// Ideally, findMatching finds an empty sector matching the ball that the
// agent is holding and then drop drops it there.  FindMatching returns
// with partial success however, if it finds a sector matching the ball
// where the sector is not empty, but contains a ball of the wrong color.
// In this case, drop will drop the ball on the nearest empty sector,
// in hopes that it has at least moved the ball near a sector where it
// might ultimately go.

// FindMatching is only called when agent has a ball.
// FindMatching finds a sector where the color matches the ball the agent
// is holding and which does not already contain a matching ball.
// It does not necessarily find an empty matching sector.
func findMatching() {
    for sectorColor+32 != agentBall || agentBall == sectorBall {
        move()
    }
}

// Drop is only called when the agent has a ball.  Unlike get() however,
// drop() can be called whether the sector is empty or not.  drop() means
// drop as soon as possible, so if the sector is full, drop() will wander
// at random looking for an empty sector.
func drop() (gameOver bool) {
    for sectorBall != noColor {
        move()
    }
    // expected to work
    stream.Send(ifc.CmdDrop)
ev:
    for {
        switch stream.Rec() {
        case ifc.EvGameOver:
            gameOver = true
        case ifc.EvStop:
            break ev
        case ifc.EvNoBallInAgent, ifc.EvSectorFull:
            log.Fatal("expected drop to succeed")
        }
    }
    sectorBall = agentBall
    agentBall = noColor
    return
}


Julia

See Remote agent/Agent_logic/Julia


Perl

This is the agent. It talks over tcp. Start it with an argument of "host:port". With no argument it will default to "localhost:3141". It should be run in a terminal that understand ANSI escape sequences, because it shows the world it has found as it looks for all sectors first and then goes around cleaning up the mismatches second. Empty sectors are shown in blue, sectors with a matching ball are shown in green, and sectors with a mismatching ball are shown in red.

#!/usr/bin/perl

use strict; # https://rosettacode.org/wiki/Remote_agent
use warnings;
use IO::Socket;
use List::Util qw( shuffle first );
use Time::HiRes qw( sleep time );
$SIG{__WARN__} = sub { die @_ };
$/ = '.';
$| = 1;
my $delay = 0;
my $show = 1;

my $server = shift // 'localhost:3141';
my $socket = IO::Socket::INET->new($server) or die $@;
getc $socket eq 'A' ? print $socket 'A' : die "no handshake";
my $start = time;

my ($wide, $high) = (3, 3);
my $grid = ('  ' x $wide . " \n") x $high;
my $gap = $wide * 2 + 2;
my %gap = ( N => -$gap, E => 2, S => $gap, W => -2 );
my $dir = 'N';
my $agent = $gap * ($high >> 1) + 2 * ($wide >> 1);
my @wrong = map { my $f = $_;
  map { $f eq $_ ? () : "$f\l$_" } qw(R G B Y) } qw(R G B Y);
my $wrong = qr/(?:@{[ join '|', @wrong ]})/;
my $success = '';
my $turns = 0;
$show and print "\e[H\e[J";

sub show
  {
  $show and print "\e[H$grid\n" =~ s/([RGBY])([rgby])/ $1 eq uc $2 ?
    "\e[92m$1$2\e[m" : "\e[91m$1$2\e[m" /ger =~ s/[RGBY] /\e[94m$&\e[m/gr;
  }

sub out { substr $grid, $agent, 2, shift }

sub at { substr $grid, shift, 2 }

sub command
  {
  $turns += print $socket @_;
  local $_ = <$socket>;
  /\|/ or $turns++;
  $_;
  }

sub set # sends command to rotate from current dir to requested dir
  {
  my $want = shift;
  $want =~ /^[NESW]$/ or die "bad dir $want";
  $want eq $dir and return;
  command $_ for split //,
    'NESWN' =~ /$dir$want/ ? '>' : 'NWSEN' =~ /$dir$want/ ? '<' : '>>';
  $dir = $want;
  }

sub expand # the grid if color sector on edge
  {
  if( $grid =~ /\w.*$/ )
    {
    $grid .= '  ' x $wide . " \n";
    $high += 1;
    }
  elsif( $grid =~ /^.*\w/ )
    {
    $grid = '  ' x $wide . " \n" . $grid;
    $agent += 2 * ($wide + 1);
    $high += 1;
    }
  elsif( $grid =~ /^\w/m )
    {
    my $lines = $` =~ tr/\n//;
    $grid =~ s/^/  /gm;
    $agent += 2 * (1 + $lines);
    $wide++;
    }
  elsif( $grid =~ /\w. \n/ )
    {
    my $lines = $` =~ tr/\n//;
    $grid =~ s/\n/  \n/g;
    $agent += 2 * $lines;
    $wide++;
    }
  $gap = 2 * ($wide + 1); # if changed vertical step
  %gap = ( N => -$gap, E => 2, S => $gap, W => -2 );
  }

sub moveto
  {
  my ($to) = @_;
  $agent == $to and return;
  my $bloc = $agent >> 1;
  local $_ = $grid =~ s/(.).| (\n)/$+/gr;
  tr/RGBY |/    -/;
  substr $_, $to >> 1, 1, 'd';
  my $gap = /\n/ && $-[0];
  substr $_, $bloc, 1, ' ';
  while( ' ' eq substr $_, $bloc, 1 )
    {
    my $west = ((tr/-/ /r =~ s/(.*)./ $1/gr | $_) &
      tr/dnesw /     \xff/r) =~ tr/a-\x7f/w/r;
    my $east = ((tr/-/ /r =~ s/.(.*)/$1 /gr | $_) &
      tr/dnesw /     \xff/r) =~ tr/a-\x7f/ e/r;
    my $south = ((substr($_, $gap + 1) =~ tr/-/ /r | $_) &
      tr/dnesw /     \xff/r) =~ tr/a-\x7f/ s/r;
    my $north = (((' ' x $gap . "\n" . substr($_, 0, -$gap - 1)) =~
      tr/-/ /r | $_) & tr/dnesw /     \xff/r) =~ tr/a-\x7f/ n/r;
    $_ = ($_ & $south =~ tr/ w/\xff\0/r) | $south;
    $_ = ($_ & $north =~ tr/ w/\xff\0/r) | $north;
    $_ = ($_ & $west =~ tr/ w/\xff\0/r) | $west;
    $_ = ($_ & $east =~ tr/ w/\xff\0/r) | $east;
    "$east$west$north$south" =~ /\w/ or die "d not found";
    }
  my $path = '';
  my %gap = (N => -$gap - 1, S => $gap + 1, E => 1, W => -1);
  while( 1 )
    {
    my $dir = uc substr $_, $bloc, 1;
    $dir =~ /[NESW]/ or last;
    $path .= $dir;
    $bloc += $gap{$dir};
    }
  set($_), $_ = command( '^' ), /[|]/ && die "wall during moveto"
    for split //, $path; # walk agent along path
  $agent = $to; # arrived
  }

while( $grid =~ /  / ) ############################################  main
  {
  show;
  $delay and sleep $delay;
  $agent % 2 and die "$agent is odd";
  my $v = qr/(?:..){$wide}/s;
  if( $grid =~ /[RGBY]/ )
    {
    my ($in, $face) =
      at( $agent - 2 ) eq '  ' ? ($agent, 'W') :
      at( $agent + $gap{'N'} ) eq '  ' ? ($agent, 'N') :
      at( $agent + $gap{'S'} ) eq '  ' ? ($agent, 'S') :
      at( $agent + 2 ) eq '  ' ? ($agent, 'E') :
      $grid =~ /  ([RGBY].)/ ? ($-[1], 'W') :
      $grid =~ /([RGBY].)  / ? ($-[1], 'E') :
      $grid =~ /  $v([RGBY].)/ ? ($-[1], 'N') :
      $grid =~ /([RGBY].)$v  / ? ($-[1], 'S') : last;
    moveto($in);
    set($face);
    $_ = command '^';
    if( /\|/ ) { substr $grid, $agent + $gap{$dir}, 2, '||'; }
    else
      {
      $agent += $gap{$dir};
      out tr/RGBY//cdr . (tr/rgby//cdr || ' ');
      expand();
      }
    }
  else
    {
    substr($grid, $agent + $gap{$dir}, 2, '||'),
      set('NESWN' =~ /$dir(.)/ ? $1 : die "bad dir")
      while $_ = command('^'), /\|/;
    $agent += $gap{$dir};
    out tr/RGBY//cdr . (tr/rgby//cdr || ' ');
    expand();
    }
  }
show;
tr/R// >= tr/r// && tr/G// >= tr/g// && tr/B// >= tr/b// && tr/Y// >= tr/y//
  && tr/RGBY// > tr/rgby// or die "invalid ball counts" for $grid;
#$grid =~ /$wrong/ ? print "swapping\n" : print "no swapping needed";

sub any
  {
  my ($qr) = @_;
  my @any;
  push @any, $-[0] while $grid =~ /$qr/g;
  $any[rand @any];
  }

sub dist
  {
  my ($x, $y) = map $_ >> 1, @_;
  my $w = $wide + 1;
  abs($x % $w - $y % $w) + abs(int($x / $w) - int($y / $w));
  }

sub nearest
  {
  my ($qr, $from) = @_;
  my @dist;
  $dist[dist( $from, $-[0] )] = $-[0] while $grid =~ /$qr/g;
  first {defined} @dist;
  }

while( 1 )
  {
  show;
  my $from = nearest( qr/$wrong/, $agent ) or last;
  my $ball = substr $grid, $from + 1, 1;
  $grid =~ /\u$ball / or $from = any( qr/$wrong/ ),
    $ball = substr $grid, $from + 1, 1;
  my $to = nearest( qr/\u$ball /, $from ) || any( qr/[RGBY] / );
# my $to = ( $grid =~ /\u$ball / && $-[0] ) || any( qr/[RGBY] / );
  moveto($from);
  $_ = command '@';
  /[as]/i and die "ERROR $_ on get";
  substr $grid, $from + 1, 1, ' ';
  moveto($to);
  $_ = command '!';
  /[as]/i and die "ERROR $_ on drop";
  substr $grid, $to + 1, 1, $ball;
  /\+/ and $success = "\e[JSUCCESS          ", last;
  $delay and sleep $delay;
  }
show;

print $success, "\n";
printf "\n$turns turns took %.3f seconds  %d usec/turn\n", time - $start,
  (time - $start) / $turns * 1e6;

Phix

--
-- demo\rosetta\Remote_Agent_Agent_Logic.exw
-- =========================================
--
include Remote_Agent_Interface.exw
--
-- Initially just four unknowns in all four directions:
-- slowly poulated with WRGYB (Wall/Red/Green/Yellow/Blue)
-- note the board can be extended in all four directions
-- aside: it should not actually matter if the server 
--        initially points agent in a random direction.
--
sequence board = {"???",
                  "???",
                  "???"},
         balls = {"...", -- (meaningless while the
                  "...", --  equivalent board[y][x]
                  "..."} --  is still set to a '?')

integer x = 2, y = 2,
        face = North,
        ball = '.',
        last_command,   -- (for sanity checks only)
        pickup = 0,     -- (once started keep going)
        putdown = 0     -- (for cases such as RyG Yr,
                        --  force a drop onto G, and
                        --  *not* y/r back onto R/Y.)

function go(integer dn)
    -- common code to get the correct turn/forward command
    -- '<' if dn=face+1 mod 4, '^' if dn==face, else '>':
    -- both face and dn are 1..4 aka NESW, in that order
    integer command = "<>>^<>>"[face+4-dn] -- (tee hee)
    if command!='^' then
        -- 1..4 (ie NESW) ==> first four for '<',
        --                     last four for '>':
        face = {4,1,2,3,4,1}[face+command-'<']
    end if
    return command
end function

bool handshake = true

function get_command()
    if handshake then
        handshake = false
        return 'A'
    end if
    integer command = 0,
            height = length(board),
            width = length(board[1])
    --
    --  Playing strategy:
    --  Use a (marking) breadth-first search to determine the next action.
    --  Candidate actions are explore further, and pick up or drop a ball.
    --  Do not pick up balls until you know somewhere they can be dropped.
    --  Favour "explore first" and forget any "travelling salesman" ideas,
    --  ie: jumping hoops to save a few moves is not the name of the game.
    --  Lastly a case such as RyG Yr, which requires moving either to the
    --  G first must be handled, that is, when no better move is possible.
    --  Another case to watch out for: Gb..gB, initially you find that Gg
    --  and set off to pick up the g, but then find bB first, and turn to
    --  go pick up the b and repeat that forever, so once you decide what
    --  to do, stick with it, using those pickup and putdown variables.
    --
    if find(ball,{lower(board[y][x]),putdown}) -- (right or needed wrong)
    and balls[y][x] = '.' then
        command = '!'               -- drop
        balls[y][x] = ball
        ball = '.'
        putdown = 0
    else
        sequence todo = {{x,y,0}}, next = {}, 
                 seen = repeat(repeat(false,width),height),
                 been = seen, -- already in todo/next
                found = {}, -- balls in wrong sector
                empty = {}  -- empty sectors
        been[y][x] = true
        while command=0 do
            if length(todo)=0 then
                if command=0 and ball='.' then
                    -- look for pair in found/empty (ie rR, gG, yY or bB)
                    for i=1 to length(found) do
                        integer {fb,fy,fx,fd} = found[i]
                        for j=1 to length(empty) do
                            if fb=empty[j][1] then
                                if fd=0 then
                                    if fx!=x or fy!=y 
                                    or balls[y][x]!=fb then
                                        die("d=0 not right?")
                                    end if
                                    command = '@' -- get
                                    ball = fb
                                    balls[y][x] = '.'
                                    pickup = 0
                                    exit
                                end if
                                pickup = fb
                                command = go(fd)
                                exit
                            end if
                        end for
                        if command!=0 then exit end if
                    end for
                end if
                if command!=0 then exit end if
                if length(next)=0 then
                    -- unsolveable or eg RyG Yr case (ie the y blocks
                    -- a r->R move, and the r is blocking a y->Y move):
                    if putdown=0 then
                        -- pick up the nearest wrong'n (which we may be on)
                        integer {fb,fy,fx,fd} = found[1]
                        if fy=y and fx=x then
                            assert(ball=='.')
                            assert(balls[y][x]!='.')
                            command = '@'                           -- get
                            ball = fb
                            balls[y][x] = '.'
                            pickup = 0
                            -- ... and fix the drop cell colour now
                            putdown = empty[1][1]
                        else
                            command = go(fd)
                        end if
                    else
                        integer {eb,ey,ex,ed} = empty[1]
                        assert(eb==putdown) -- (shd only collect such)
                        -- drop, or move to where it should be dropped
                        if ey=y and ex=x then
                            assert(ball!='.')
                            assert(lower(board[y][x])==putdown)
                            assert(balls[y][x]=='.')
                            command = '!'                           -- drop
                            balls[y][x] = ball
                            ball = '.'
                            putdown = 0
                        else
                            command = go(ed)
                        end if
                    end if
                    exit
                end if
                todo = next
                next = {}
            end if
            integer {cx,cy,d0} = todo[1]
            todo = todo[2..$]
            if not seen[cy][cx] then
                integer cell = board[cy][cx],
                        lowc = lower(cell),
                        bcyx = balls[cy][cx]
                if cell!='?' then
                    -- add to found/empty
                    if bcyx='.' then
                        if ball=lowc then
                            -- sanity: imm drop shd have happened
                            assert(cx!=x or cy!=y,"not imm?")
                            command = go(d0)
                            exit
                        end if
                        if putdown=0 or putdown=lowc then
                            empty = append(empty,{lowc,cy,cx,d0})
                        end if
                    elsif bcyx!=lowc then
                        if pickup=0 or pickup=bcyx then
                            found = append(found,{bcyx,cy,cx,d0})
                        end if
                    end if
                    seen[cy][cx] = true
                end if
            end if
            for d=North to West do  -- (1..4)
                integer {dx,dy} = dxy[d],
                        wall = board[cy+dy][cx+dx],
                        {nx,ny} = {cx+dx*2,cy+dy*2},
                        dn = iff(d0=0?d:d0)
                if wall='?' or (wall=' ' and board[ny][nx]='?') then
                    command = go(dn)
                    exit
                elsif wall=' ' and not been[ny][nx] then
                    next = append(next,{nx,ny,dn})
                    been[ny][nx] = true
                end if
            end for
        end while
    end if
    last_command = command -- (for sanity checks only)
    return command
end function

procedure extend_if_needed(integer cell_colour)
    -- (also sets x and y and the new cell colour)
    integer {dx,dy} = dxy[face],
            {fx,fy} = {x+dx*2,y+dy*2},
            height = length(board),
            width = length(board[1])
    board[y+dy][x+dx] = ' ' -- (not a wall)
    if fx=0 then
        -- extend board left (and leave x set to 2)
        for i=1 to height do
            board[i] = "??"&board[i]
            balls[i] = ".."&balls[i]
        end for
    else
        x = fx
        if x>=width then
            -- extend board right
            for i=1 to length(board) do
                board[i] &= "??"
                balls[i] &= ".."
            end for
        end if
    end if
    if fy=0 then
        -- extend board up (and leave y set to 2)
        board = repeat(repeat('?',width),2)&board
        balls = repeat(repeat('.',width),2)&balls
    else
        y = fy
        if y>=height then
            -- extend board down
            board &= repeat(repeat('?',width),2)
            balls &= repeat(repeat('.',width),2)
        end if
    end if
    board[y][x] = cell_colour
end procedure

procedure accept_event(integer event)

    -- Note: the logic above does not attempt to pick up a ball
    --       when it already has one, or when in an empty cell,
    --       or drop one it does not have, or drop one into an 
    --       occupied cell, and hence this does /not/ trigger 
    --       or handle any "SAsa" events, not yet anyway.
    --       (Fairly obviously this would then have to undo a 
    --        few things, or defer updates to the stop event.)

    assert(last_command=='^' or event=='.') -- (optional/deletable)

    integer {fx,fy} = sq_add({x,y},dxy[face]),
            wall = iff(odd(fy)?'-':'|')

    switch event do
        case '!':               board[fy][fx] = wall    -- bump
        case '.':               last_command = '.'      -- stop
        case 'R','G','Y','B':   extend_if_needed(event) -- cell colour
        case 'r','g','y','b':   balls[y][x] = event     -- ball colour
        default:                ?9/0      -- (unknown/unhandled event)
    end switch

end procedure

function get_agent()
    -- (for display only)   
    -- it does not hurt any to do this...
    for i=1 to length(board) by 2 do
        for j=1 to length(board[i]) by 2 do
            board[i][j] = '+'
        end for
    end for
    return {{board,balls},{x,y,face,ball}}
end function

register_agent(get_command, accept_event, get_agent)

For example output see Remote_agent/Simulation#Phix

PicoLisp

Implementation in PicoLisp.

Tcl

Sample agent (not a good or smart player of the game; just to show how to program to the interface).

Works with: Tcl version 8.6
package require Tcl 8.6
package require RC::RemoteAgent

oo::class create Agent {
    superclass AgentAPI
    variable sectorColor ballColor
    forward Behavior my MoveBehavior

    # How to move around
    method MoveBehavior {} {
	set ball ""
	while 1 {
	    try {
		while {rand() < 0.5} {
		    my ForwardStep
		    my BallBehavior
		}
	    } trap bumpedWall {} {}
	    if {rand() < 0.5} {
		my TurnLeft
	    } else {
		my TurnRight
	    }
	}
	set ::wonGame ok
    }

    # How to handle the ball once we've arrived in a square
    method BallBehavior {} {
	upvar 1 ball ball anywhere anywhere
	if {
	    $ball eq ""
	    && $ballColor ne ""
	    && $ballColor ne $sectorColor
	} then {
	    set ball [set ballTarget $ballColor]
	    set anywhere 0
	    my GetBall
	} elseif {
	    $ball ne ""
	    && ($ball eq $sectorColor || $anywhere)
	} {
	    try {
		if {[my DropBall]} {
		    return -code break
		}
		set ball ""
	    } trap sectorFull {} {
		# Target square full; drop this ball anywhere
		set anywhere 1
	    }
	}
    }
}

Agent new "localhost" 12345
vwait wonGame

Wren

Translation of: Go
Library: Wren-str
/* agent.wren */

import "random" for Random
import "./ifc" for Ifc, Log
import "./str" for Char

// The agent's awareness is quite limited.  It has no representation of the
// maze, which direction it is facing, or what it did last.  It notices and
// remembers just three things:  The color of the sector just entered, the
// presence and color of any ball there, and the presence and color of any
// ball it is holding.
var sectorColor = " "
var sectorBall  = " "
var agentBall   = " "
var stream      = null
var noColor     = "-"

var rand = Random.new()

// Move moves one sector in a random direction.
// It retries on bumps and doesn't return until a forward command succeeds.
// It expects a color event on a successful move and terminates if it doesn't
// get one.
var move = Fn.new {
    while (true) {
        // Randomness:  50/50 chance of turning or attempting move.
        // For turns, equal chance of turning right or left.
        var t = rand.int(4)
        if (t == 0) {
            stream.send(Ifc.cmdLeft)
            Fiber.yield()
            while (stream.rec() != Ifc.evStop) { Fiber.yield() }
            continue
        } else if (t == 1) {
            stream.send(Ifc.cmdRight)
            Fiber.yield()
            while (stream.rec() != Ifc.evStop) { Fiber.yield() }
            continue
        }
        stream.send(Ifc.cmdForward)
        var bump = false
        sectorColor = noColor
        sectorBall = noColor
        while (true) {
            Fiber.yield()
            var ev = stream.rec()
            if (ev == Ifc.evBump) {
                bump = true
            } else if ([Ifc.evColorRed, Ifc.evColorGreen, Ifc.evColorYellow, Ifc.evColorBlue].contains(ev)) {
                sectorColor = ev
            } else if ([Ifc.evBallRed, Ifc.evBallGreen, Ifc.evBallYellow, Ifc.evBallBlue].contains(ev)) {
                sectorBall = ev
            } else if (ev == Ifc.evStop) {
                break
            }
        }
        if (bump) continue
        if (sectorColor == noColor) Log.fatal("agent: expected color event after move")
        return
    }
}

// Get is only called when get is possible.
var get = Fn.new {
    stream.send(Ifc.cmdGet)
    while (true) {
        Fiber.yield()
        var ch = stream.rec()
        if (ch == Ifc.evStop) {
            // agent notes ball color, and that sector is now empty
            agentBall = sectorBall
            sectorBall = noColor
            return
        } else if (ch == Ifc.evNoBallInSector || ch == Ifc.evAgentFull) {
            Log.fatal("agent:expected get to succeed")
        }
    }
}

// FindMatching is only called when agent has a ball.
// FindMatching finds a sector where the color matches the ball the agent
// is holding and which does not already contain a matching ball.
// It does not necessarily find an empty matching sector.
var findMatching = Fn.new {
    while (Char.lower(sectorColor) != agentBall || agentBall == sectorBall) move.call()
}

// FindMisplaced wanders the maze looking for a ball on the wrong sector.
var findMisplaced = Fn.new {
    while (true) {
        move.call()

        // get ball from current sector if meaningful
        if ([Ifc.evBallRed, Ifc.evBallGreen, Ifc.evBallYellow, Ifc.evBallBlue].contains(sectorBall)) {
            if (sectorBall != Char.lower(sectorColor)) return
        }
    }
}

// Drop is only called when the agent has a ball.  Unlike get() however,
// drop() can be called whether the sector is empty or not.  drop() means
// drop as soon as possible, so if the sector is full, drop() will wander
// at random looking for an empty sector.
var drop = Fn.new {
    var gameOver = false
    while (sectorBall != noColor) move.call()

    // expected to work
    stream.send(Ifc.cmdDrop)
    while(true) {
        Fiber.yield()
        var ch = stream.rec()
        if (ch == Ifc.evGameOver) {
            gameOver = true
        } else if (ch == Ifc.evStop) {
            break
        } else if (ch == Ifc.evNoBallInAgent || ch == Ifc.evSectorFull) {
            Log.fatal("expected drop to succeed")
        }
    }
    sectorBall = agentBall
    agentBall = noColor
    return gameOver
}

var Agent = Fn.new { |s|
    stream = s

    // handshake
    var hs = stream.rec()
    if (hs != Ifc.handshake) Log.fatal("agent: that's no handshake")
    stream.send(Ifc.handshake)
    Fiber.yield()

    // agent behavior main loop
    var gameOver = false
    while (!gameOver) {
        findMisplaced.call()
        get.call()
        findMatching.call()
        gameOver = drop.call()
    }
}