wiki:Ruby Client Tutorial

Version 5 (modified by kedwar10, 13 years ago) (diff)

--

Getting Started

This client implements a subset of the features found on the Clojure client. All commands in this client block until completion.

To run the client, open up a terminal and cd to the top level directory and type

bash-4.2$ sh run
irb(main):001:0> 

This script will load core.rb and start the ruby interpreter. To open up a map use the 'ioquake' function with a mapname.

irb(main):001:0> ioquake 'sat'

ioquake should now be running in a separate process. Set the fullscreen option to false in setup if ioquake is maximized. Now hit ` to free the cursor from ioquake. A new quagent can be added by calling Quagent.new (this may take several tries):

irb(main):007:0> q1 = Quagent.new
=> #<Quagent:0xb7696538 @conn=#<TCPSocket:0xb76964d4>>
irb(main):008:0> Quagent connected at socketfd 30.
loaded skill 1 from bots/default_c.c
loaded skill 1 from bots/sarge_c.c
loaded bots/sarge_i.c
loaded bots/sarge_w.c
loaded sarge from bots/sarge_t.c
ev jo 1 quagent

Organization of Code

  • util.rb : general functions.
  • protocol_one.rb : contains the quagent class which interfaces with protocol zero.
  • commands.rb : contains user-friendly wrappers for common protocol zero commands.
  • core.rb : general workbench; load this file when using irb.

Defining Commands

When a Quagent is instantiated via the 'new' method, it connects to the ioquake server. The 'write' method can be used to issue commands through the socket. (Notice that protocol zero op codes have been mapped to more user-friendly strings.)

def whereis (quagent)
  quagent.write("now",                          # scheduling
                "location",                     # op
                "",                             # args
                nil,                            # initial value
                lambda { |prev, data|           # reduction function
                   return data_to_doubles(data)
                   })
end

Sample output:

whereis(q1)
=> [-104.0, 96.0, -31.875]

The write method takes the arguments "now", "location", and "" and sends the string "n lc #" (where # is an integer) to the server. It then creates a separate thread that reads replies from the socket. When the reply is of type "rs", 'write' calls its last argument on the accumulator (initially the penultimate argument) and the data from the last response (an array of strings). When the reply is of type "cp", 'write' terminates the thread and returns the accumulator. In the previous example, there was no need to specify an initial value since the server only sends data once. The radar op, however, sends data for every object it finds. The 'scan' function uses a hash map as the initial value and then builds a list of (relative) locations for each item type.

def scan (quagent, radius)
  quagent.write("now",
                "radar",
                "#{radius}",
                {},
                lambda { |prev, data|
                  _, type, *location = data
                  if prev[type] == nil
                    return prev.merge({type => [data_to_doubles location]})
                  end
                  return prev.merge({type => prev[type].concat([data_to_doubles location])})
                })
end

Sample output:

irb(main):008:0> scan(q1, 8000)
=> {"info_player_deathmatch"=>[[32.000244, -90.0, 0.223811],
                               [0.125, 0.0, 90.0]],
    "player"=>[[313.243591, -13.452801, 0.0]],
    "quagent_item_gold"=>[[375.085327, 56.309933, 1.374918],
                          [1019.278626, 42.455196, 0.505915],
                          [905.141357, 8.130102, 0.569713]],
    "quagent_item_treasure"=>[[572.16864, 20.462269, 0.901278],
                              [697.711304, 63.434952, 0.739097]]}

Operating Multiple Quagents

To issue a command to a quagent asynchronously, use the following.

irb(main):009:0> t = Thread.new {move(q, 1000, 00)}

Issues

  • Loading a quagent into the virtual environment sometimes takes several tries.