I'm working on modernizing Rosetta Code's infrastructure. Starting with communications. Please accept this time-limited open invite to RC's Slack.. --Michael Mol (talk) 20:59, 30 May 2020 (UTC)

Remote agent/Agent interface

From Rosetta Code
Remote agent/Agent interface 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 component is described that marshals commands and events between a stream and a program that issues commands and processes the resulting events.


Task

Using the protocol definition described there, build this component in a fashion idiomatic and natural to your language.

C[edit]

See Remote agent/Simulation/C

Go[edit]

package ifc
 
// Streamer interface type defines how agent and world talk to each other.
//
// Send and Rec may be implemented as synchronous blocking operations.
// They can be considered perfectly reliable as there is no acknowledgement
// or timeout mechanism.
type Streamer interface {
Send(byte)
Rec() byte
}
 
// The agent and world send and receive single bytes, out of the set of
// constants defined here.
const Handshake = 'A'
 
const (
CmdForward = '^'
CmdRight = '>'
CmdLeft = '<'
CmdGet = '@'
CmdDrop = '!'
EvGameOver = '+'
EvStop = '.'
EvColorRed = 'R'
EvColorGreen = 'G'
EvColorYellow = 'Y'
EvColorBlue = 'B'
EvBallRed = 'r'
EvBallGreen = 'g'
EvBallYellow = 'y'
EvBallBlue = 'b'
EvBump = '|'
EvSectorFull = 'S'
EvAgentFull = 'A'
EvNoBallInSector = 's'
EvNoBallInAgent = 'a'
)

Above is the interface package defining the abstract interface. Below is an example of a driver program that supplies a concrete implementation of the interface. This implementation uses Go channels to run a simulation in a single process. A different implementation might produce two executables that would run on different processes or different machines. It would use the same interface however.

package main
 
import (
"log"
"os"
 
"ra/agent"
"ra/world"
)
 
// Concrete type chanStreamer implements interface ifc.Streamer.
// Implementaion is with channels, a simple way to demonstrate concurrency.
type chanStreamer struct {
name string
in <-chan byte
out chan<- byte
}
 
// Send satisfies ifc.Streamer.Send method.
func (c chanStreamer) Send(b byte) {
log.Print(c.name, " sends ", text[b])
c.out <- b
}
 
// Rec satisfies ifc.Streamer.Rec method.
func (c chanStreamer) Rec() byte {
b := <-c.in
log.Print(c.name, " recieves ", text[b])
return b
}
 
func main() {
// Logging is done with the log package default logger, so this is
// technically something that might be shared by the agent and the world.
// Currently though, the agent doesn't do anything with the logger, only
// the world does.
log.SetFlags(0)
log.SetOutput(os.Stdout)
// Create channels for chanStreamer. Only two channels are needed,
// cmd going from agent to world, and ev going from world to agent.
cmd := make(chan byte)
ev := make(chan byte)
// Instantiate chanStreamer for agent and start agent as a goroutine.
// This allows it to run concurrently with the world.
go agent.Agent(chanStreamer{"agent", ev, cmd})
// World doesn't need another goroutine, it just takes over main at this
// point. The program terminates immediately when World returns.
world.World(chanStreamer{"world", cmd, ev})
}

A separate file, but still part of the driver, contains human readable text for commands and events. Alternatively, this file could be included with the interface package for maintainability, but technically it is not a required part of the abstract interface used by the agent and world simulator.

package main
 
import "ra/ifc"
 
// Human readable text for commands and events.
var text = map[byte]string{
ifc.CmdForward: "command forward",
ifc.CmdRight: "command turn right",
ifc.CmdLeft: "command turn left",
ifc.CmdGet: "command get",
ifc.CmdDrop: "command drop",
ifc.EvGameOver: "event game over",
ifc.EvStop: "event stop",
ifc.EvColorRed: "event color red",
ifc.EvColorGreen: "event color green",
ifc.EvColorYellow: "event color yellow",
ifc.EvColorBlue: "event color blue",
ifc.EvBallRed: "event ball red",
ifc.EvBallGreen: "event ball green",
ifc.EvBallYellow: "event ball yellow",
ifc.EvBallBlue: "event ball blue",
ifc.EvBump: "event bump",
ifc.EvSectorFull: "event sector full",
ifc.EvAgentFull: "event agent full",
ifc.EvNoBallInSector: "event no ball in sector",
ifc.EvNoBallInAgent: "event no ball in agent",
}


Julia[edit]

See Remote agent/Agent_interface/Julia

Phix[edit]

abstract interface[edit]

Modify this to include one of the two concrete implementations below (but not both!)

--
-- demo\rosetta\Remote_Agent_Interface.exw
-- =======================================
--
-- First, a handful of common constants, just to avoid the duplication:
global enum North, East, South, West
global constant dxy = {{0,-1},{1,0},{0,1},{-1,0}}

-- and some display/progress routines:
global procedure show_at(integer l,c, string msg, sequence args={})
    position(l,c)
    printf(1,msg,args)
end procedure

constant pause = false
global procedure show(string what, integer event, yoffset, fn)
    if fn then
        {sequence {board, balls}, integer {x, y, d, ball}} = fn()
        show_at(yoffset,1,sprintf("%s  -- x:%2d, y:%2d, ball:%c, d:%d  ",{what,x,y,ball,d}))
        balls[y+1,x] = "^>v<"[d]    
        for i=1 to length(board) do
            show_at(i+yoffset,1,board[i])
            show_at(i+yoffset,40,balls[i])
        end for
        if event then
            show_at(20,1,"(after "&event&" accepted by %s)",{what})
            if pause then {} = wait_key() end if
        end if
    end if
end procedure

global procedure die(string msg)
    -- (to avoid clobbering the above display)
    position(35,1)
    crash(msg)
end procedure

--
--  Only one of the following should be included, the other(s) commented out:
--
include Remote_Agent_Interface_Direct.e
--include Remote_Agent_Interface_IPC.e
--include Remote_Agent_Interface_Xxx.e -- (no such file, see below)
--
-- each of the above must implement
--
--  global procedure register_world(integer accept_command, get_event, get_world=0)
--  global procedure register_agent(integer get_command, accept_event, get_agent=0)
--
--  The four mandatory and two optional routines to be provided are described below.
--  Note that Remote_Agent_Interface_Direct returns control immediately,
--  to one, whereas Remote_Agent_Interface_IPC does not, see below.
--
-- Remote_Agent_Interface_Direct.e includes both
-- Remote_Agent_Simulator.exw and
-- Remote_Agent_Agent_Logic.exw, and operates as a fully complete and
--  standalone program whichever of the three .exw files are invoked.
--  (Running >1 of them effectively runs the same program >1 times.)
--
-- Remote_Agent_Interface_IPC.e includes neither, and instead both of
-- Remote_Agent_Simulator.exw and
-- Remote_Agent_Agent_Logic.exw must be run separately.
--  (Running Remote_Agent_Interface.exw does nothing in that case.)
--
-- The idea is that further Remote_Agent_Interface_Xxx.e could be written
--  to implement pipes, sockets, files, etc. and nothing in either of the
--  other two .exw would need changing for them to work like IPC.e does.
--
-- There would of course be no problem with world/agent including those
--  files directly, as long as they are kept in sync, but of course that
--  way would lose the "no modification whatsoever to switch" aspect.
--
-- The Direct version checks whether both register_world and register_client
--  have been invoked before starting a single event loop, otherwise returns 
--  control immediately, effectively terminating one of world/client, whereas 
--  the IPC way sets up separate event loops in/for both register_world and 
--  register_client, and only returns control on game over (if even that).
--
-- Both world/client must operate reactively and maintain any internal state
--  that requires. For instance when the server receives a "forward" command 
--  it cannot send color; send ball; send stop; but must instead rely on 
--  being later asked for those events, and deliver them in order.
--
-- Both get_event() and get_command() must be a parameterless function that
--  returns a single byte, and both accept_command() and accept_event() must
--  be a procedure that accepts a single byte. As you should expect you use
--  () to invoke them, and omit the () when just passing their ids about.
--  The get_xxxx() routines, if provided, should return the current state
--  in a suitable format for use as per fn() in show() above.
--

Interface_Direct[edit]

This implementation of the above interface is a single standalone program

--
-- demo\rosetta\Remote_Agent_Interface_Direct.e
-- ============================================
--
include Remote_Agent_Simulator.exw
include Remote_Agent_Agent_Logic.exw

integer accept_command=0, get_event, get_world,
        get_command=0, accept_event, get_agent

procedure main_event_loop()
    integer handshake = get_event()
    if handshake!='A' then ?9/0 end if
    show_at(1,65,"server handshake recieved")
    handshake = get_command()
    if handshake!='A' then ?9/0 end if
    show_at(2,65,"agent handshake recieved")
    while true do
        integer command = get_command()
        show("agent",0,22,get_agent)
        accept_command(command) 
        show("world",command,1,get_world)
        while true do
            integer event = get_event()
            if event='+' then
                -- game over
                show("world",0,1,get_world)
                show("agent",'.',22,get_agent)
                show_at(5,66,"game over")
                {} = wait_key()
                return
            end if
            accept_event(event)
            show("world",0,1,get_world)
            show("agent",event,22,get_agent)
            if event='.' then exit end if   -- stop
        end while
    end while
end procedure

global procedure register_world(integer _accept_command, _get_event, _get_world=NULL)
    {accept_command, get_event, get_world} = {_accept_command, _get_event, _get_world}
    if get_command!=0 then main_event_loop() end if
end procedure

global procedure register_agent(integer _get_command, _accept_event, _get_agent=NULL)
    {get_command, accept_event, get_agent} = {_get_command, _accept_event, _get_agent}
    if accept_command!=0 then main_event_loop() end if
end procedure

Interface_IPC[edit]

This implementation of the above interface lets two separate programs communicate using ipc

--
-- demo\rosetta\Remote_Agent_Interface_IPC.e
-- =========================================
--
include builtins\ipc.e

-- The shared memory contains two bytes:
--  byte[1] (command) can only be set by the agent and cleared by the world,
--  byte[2] ( event ) can only be set by the world and cleared by the agent.

constant ra = "remote_agent"
atom pSharedMem = sm_open(ra)
if pSharedMem=SM_OPEN_FAIL then
    pSharedMem = sm_create(ra,2)
    if pSharedMem<=0 then
        crash("unable to create shared memory")
    end if
    poke2(pSharedMem,0) -- initialised to zero bytes
end if

procedure send_command_via_ipc(integer command)
    while peek(pSharedMem)!=0 do
        sleep(0.1)
    end while
    poke(pSharedMem,command)
end procedure

function get_command_from_ipc()
    integer command
    while true do
        command = peek(pSharedMem)
        if command!=0 then exit end if
        sleep(0.1)
    end while
    poke(pSharedMem,0)
    return command
end function

procedure send_event_via_ipc(integer event)
    while peek(pSharedMem+1)!=0 do
        sleep(0.1)
    end while
    poke(pSharedMem+1,event)
end procedure

function get_event_from_ipc()
    integer event
    while true do
        event = peek(pSharedMem+1)
        if event!=0 then exit end if
        sleep(0.1)
    end while
    poke(pSharedMem+1,0)
    return event
end function

procedure game_over()
    show_at(32,62,"game over")
    {} = wait_key()
end procedure

global procedure register_world(integer accept_command, get_event, get_world=0)
    integer event = get_event()
    if event!='A' then die("rw1") end if
    send_event_via_ipc(event)
    integer command = get_command_from_ipc()
    if command!='A' then die("rw2") end if
    show_at(1,60,"server handshake recieved")
    -- server loop:
    while true do
        command = get_command_from_ipc()
        if command='+' then
            game_over()
            return
        end if
        accept_command(command) 
        show("world",command,1,get_world)
        while true do
            event = get_event()
            send_event_via_ipc(event)
            if event='.' then exit end if   -- stop
        end while
    end while
end procedure

global procedure register_agent(integer get_command, accept_event, get_agent=0)
    integer command = get_command()
    if command!='A' then die("ra1") end if
    send_command_via_ipc(command) 
    integer event = get_event_from_ipc()
    if event!='A' then die("ra2") end if
    show_at(2,60,"agent handshake recieved")
    -- agent loop:
    while true do
        command = get_command()
        send_command_via_ipc(command) 
        while true do
            event = get_event_from_ipc()
            if event='+' then
                send_command_via_ipc(event) 
                show("agent",'.',22,get_agent)
                game_over()
                return
            end if
            accept_event(event)
            show("agent",event,22,get_agent)
            if event='.' then exit end if   -- stop
        end while
    end while
end procedure

PicoLisp[edit]

The interface logic for the PicoLisp solution is directly integrated into the client Remote agent/Agent logic#PicoLisp.

Tcl[edit]

Works with: Tcl version 8.6
package require Tcl 8.6
 
oo::class create AgentAPI {
variable sock events sectorColor ballColor
constructor {host port} {
set sock [socket $host $port]
fconfigure $sock -buffering none -translation binary -encoding ascii \
-blocking 0
# Hack to allow things to work in 8.6b1 and 8.6b2
if {![llength [info commands yieldto]]} {
interp alias {} yieldto {} ::tcl::unsupported::yieldTo
}
coroutine ReaderCoroutine my ReadLoop
}
destructor {
if {[llength [info command ReaderCoroutine]]} {
rename ReaderCoroutine {}
}
if {[llength [info command AgentCoroutine]]} {
rename AgentCoroutine {}
}
if {$sock ne ""} {
catch {close $sock}
}
}
method Log message {
}
 
# Commands
method ForwardStep {} {
my Log "action: forward"
puts -nonewline $sock "^"
my ProcessEvents [yield]
}
method TurnRight {} {
my Log "action: turn right"
puts -nonewline $sock ">"
my ProcessEvents [yield]
}
method TurnLeft {} {
my Log "action: turn left"
puts -nonewline $sock "<"
my ProcessEvents [yield]
}
method GetBall {} {
my Log "action: get ball"
puts -nonewline $sock "@"
my ProcessEvents [yield]
}
method DropBall {} {
my Log "action: drop ball"
puts -nonewline $sock "!"
my ProcessEvents [yield]
}
method ProcessEvents {events} {
set sectorColor {}
set ballColor {}
set err {}
set done 0
foreach e $events {
my Log "event: $e"
switch [lindex $e 0] {
sector {set sectorColor [lindex $e 1]}
ball {set ballColor [lindex $e 1]}
error {set err [lindex $e 1]}
gameOver {set done 1}
}
}
if {$err ne ""} {throw $err "can't do that: $err"}
return $done
}
 
# Event demux
method ReadLoop {} {
# Init handshake
fileevent $sock readable [info coroutine]
while 1 {
yield
if {[read $sock 1] eq "A"} break
}
puts -nonewline $sock "A"
# Main loop; agent logic is in coroutine
try {
coroutine AgentCoroutine my Behavior
while 1 {
yield
set ch [read $sock 1]
switch $ch {
"." {
# Stop - end of events from move
set e $events
set events {}
yieldto AgentCoroutine $e
if {"gameOver" in $e} break
}
"+" {lappend events gameOver}
"R" {lappend events {sector red}}
"G" {lappend events {sector green}}
"Y" {lappend events {sector yellow}}
"B" {lappend events {sector blue}}
"r" {lappend events {ball red}}
"g" {lappend events {ball green}}
"y" {lappend events {ball yellow}}
"b" {lappend events {ball blue}}
"|" {lappend events {error bumpedWall}}
"S" {lappend events {error sectorFull}}
"A" {lappend events {error agentFull}}
"s" {lappend events {error sectorEmpty}}
"a" {lappend events {error agentEmpty}}
}
}
} finally {
close $sock
set sock ""
}
}
 
method Behavior {} {
error "method not implemented"
}
}
 
# Export as package
package provide RC::RemoteAgent 1