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):023:0> t = Thread.new {move(q, 1000, 0)} => #<Thread:0xb7755974 run> irb(main):024:0> t.status => "sleep" irb(main):025:0> t.status => "sleep" irb(main):026:0> t.status # terminated normally => false irb(main):027:0> t.value # move returns nil => nil
Issues
- Loading a quagent into the virtual environment sometimes takes several tries.