Getting Started with UR-Quagent

Running UR-Quake

UR-Quake is a modified version of Id Software's Quake II. To run UR-Quake, simply invoke

/u/cs242/quake/bin/run-quagent.sh

If you want to what's under the hood, here is a little background. Dynamically, Quake II can be divided into a server part and a client part. The client part is in charge of graphics and the user controls (keyboard, mouse, game pad, etc.). The server updates the world states and feeds this information back to the client for it to redraw the graphics accordingly. Statically, Quake II consists of an executable and two DLLs (dynamical link library). The executable (quake2) has both the server and client code and is generally referred as the game engine. A video refresh library (ref_softx.so) provides the game engine basic graphics abilities. The game itself, containing various monsters including our quagents, is in gamei386.so. This arrangement enables users to load different games using a single executable. To load gamei386.so in current directory, e.g., we issue

quake2

To run the game, we also need to set the environment variables QUAKE2_BASE and QUAKE2_CONF to tell where the map is and a place to store our configurations. The script /u/cs242/quake/bin/run-quake does just that.

If you want to run UR-Quagent at home. You can copy the executable, the refresh DLL and the game module to your own machine. You also need the game maps, which you can find in /u/cs242/quake/lib/baseq2. So if you don't have the original QUAKE game CD, you need to copy baseq2 (the whole directory), too. It's big and the copying will take some time, especially if you are outside the CS department. Here is what you do.

  1. Copy everything to your own machine, assuming that you are outside of the department.
    %cd
    %mkdir myquake
    %cd myquake
    %scp your_id@anymachine.csug.rochester.edu:/u/cs242/quake/src/quake2UR/linux/releasei386-glibc/quake2 .
    %scp your_id@anymachine.csug.rochester.edu:/u/cs242/quake/src/quake2UR/linux/releasei386-glibc/ref_softx.so .
    %scp your_id@anymachine.csug.rochester.edu:/u/cs242/quake/src/quagent/gamei386.so .
    %scp -r your_id@anymachine.csug.rochester.edu:/u/cs242/quake/lib/baseq2 .
  2. You should see quake2, ref_softx.so, gamei386.so and a diretory baseq2 in this directory.
  3. Set up the environment variables.
    %setenv QUAKE2_BASE ~/myquake/baseq2
    %setenv QUAKE2_CONF .
  4. Run the game by ./quake2.

Compiling from source

At some point, you may need to modify the game directly. There is a document in process to navigate you through the code. Here we describe how you can compile the executable and the game DLL from source. For now, the source is only installed on the research networks.
  1. Check out the source code from the CVS repository.
    %mkdir quaking
    %cd quaking
    %cvs -d /p/quake/cvs co quake2UR
    %cvs -d /p/quake/cvs co quagent
    This assumes the CVS tree is installed under /p/quake/cvs. If you are on the research network side, it's true. If you are on the undergrad network, point -d to where the CVS repository is.
  2. Compile the executable (server+client, the game engine).

    Now you have two subdirectories quake2UR and quagent. As we've said there are 3 parts in Quake. The executable quake2 contains both the game client and the game server. The executable needs a video refresh DLL to render the graphics. The actual game play is in the game DLL. To compile the former two parts:
    %cd quake2UR
    %cd linux
    %./build
    You'll see a new directory releasei386-glibc, in which you'll find the executable quake2 and the video refresher ref_softx.so.

  3. Compile the game DLL, or the quagent module, as we call it.
    %cd ../../quagent
    %make
    You'll see gamei386.so created. Now we need to tell the executable to load this game.
  4. Load the quagent module.
    %cd ../quake2UR/linux/releasei386-glibc
    %ln -s ../../../quagent/gamei386.so .
    %setenv QUAKE2_BASE /p/quake/baseq2
    %setenv QUAKE2_CONF .
  5. That's it. You can run the game by issuing quake2.

Interacting with A Quagent

We interact with quagents through the quagent protocol. The protocol is in plain text, so we can use telnet to try it out. When the game is launched, it appears that it grabs the mouse. If you want to move the mouse to another window to do telnet-ing, hold down the SHIFT key and then move the mouse. You can also change to another window by using the key-combination to switch windows defined by your window manager. For KDE, this is usually ALT-TAB. You'll find this handy when you try your own agent program. Now let's see a real session with the quagent module.

%telnet localhost 33333 <--- Connect to a quagent 
Connected to quakeII bot.<--- Greeting message: connected 
do walkby 100<--- Issue a WALKBY command 
OK  (do walkby 100)<--- Quagent acknowledges by echoing the command 
do turnby 90<--- Let the quagent turn by 90 degrees 
OK (do turnby 90)
TELL STOPPED 60.0045<--- We let the quagent work 100.  It has walked 60.0045 and 
                          stopped, probably hit a wall, triggering a STOPPTED event
                          Note that this event is asynchronous, i.e. you don't know
                          when it'll come. 
TELL KRYPTONITEFAR<--- We get yet another asynchrounous event that tells
                          us there's a kryptonite within 200 units.  Apparently
                          the consequence of the walkby
ask radius 100<--- Issue a query, what's out there? 
OK (ask radius 100) 1 player 156.5 86.9 23.9<--- Only sees the player (that's you)

...

Programming Quagent

To help you understand the protocol better, let's build a random-walking quagent using Java. You can use any programming language. We use Java because (1) we'll take advantage of its OO constructs and (2) it's easier to integrate with Jess. We call our random-walking quagent DrunkenMaster. It only does three things until it dies of aging. It reports what's around it, walks for a random distance and then turns by a random angle. Before we go directly design the DrunkenMaster, we can build a basic quagent, which does basic things such as sending requests to the quagent module and receiving responses from it. This layer of abstraction insulates us from the boring details of composing requests and parsing responses. The outline of our DrunkenMaster looks like this:


public class DrunkenMaster extends BasicQuagent
{
	public void Run() {

            while (alive) {
 	        RadiusQuery qry = new RadiusQuery(100.0f);
	        send(qry);
	        Response rsp_rad = getResponse(qry);
	        if(rsp_rad.isOK())
		    ((RadiusResponse)rsp_rad).showItems();

                double dist = Math.random() * 100.;
                WalkCommand walk_cmd = new WalkCommand(dist);
	        send(walk_cmd);
	        Response rsp_walk = getResponse(walk_cmd);

                double angle = Math.random() * 180.;
                TurnCommand turn_cmd = new TurnCommand(angle);
                send(turn_cmd);
	        Response rsp_turn = getResponse(turn_cmd);
            }
	}
}
Our DrunkenMaster is a subclass of BasicQuagent, which provides send() and getResponse() to send and receive messages to and from the quagent module. If your quagent has more sophisticated behavior, you can call an inference engine like Jess in the polymorphic function Run(). BasicQuagent cares about sending and receiving messages and doesn't need to know about the underlying communication channels. We make another level of abstraction for network communications. The whole picture looks like this:

The bottom layer, let's call it QuagentSocket, handles the hairy details of interpreting network packages.

The quagent protocol doesn't follow the canonical synchronous server/client convention. As you can see from the above telnet session, asynchronous events occur in an unpredictable way. You'll probably find it handy to use non-blocking communication. The Java Socket provides blocking IO. That is, if you read from the socket and nothing's to be read yet, you will be put waiting until something comes. Say you want to implement checkEvent, which just takes a peek at the socket to see if there is an event. If there is, you process the event. If there isn't, you want to continue to issue commands or queries. With blocking communication, you'll be put on hold at checkEvent and can't continue with the commands and queries. Non-blocking IO comes in handy here. Non-blocking IO was introduced in JDK1.4 in the java.nio.* package. Client-side non-blocking IO is quite straightforward. We implemented some simple functions, such as read and write strings, in QuagentSocket.java using non-blocking socket. Note that, you don't have to use non-blocking IO. The good old Java Socket can achieve the same thing, probably with some form of multi-threading.

You might want to insulate yourself from the details of dealing with messages by providing objects and methods that encapsulate the parsing details. We can define a Request class and a Response class to represent messages to and from the quagent module. Commands (DO THINGS) and queries (ASK THINGS) are concrete subclasses of Request. Different responses are derived from Response, too. BasicQuagent only takes care of sending requests and receiving responses. The polymorphic functions in the specific requests and responses do the real job. E.g.,


public class BasicQuagent {
	...
        QuagentSocket socket;

	public Request send(Request request) {

	    socket.send(request.toString());
	    return request;

	}
}
Each command or query composes protocol-abiding messages in their own toString() functions. Send() returns the request, so you can write more succinct expression like the following

Response radius_response = getResponse(qc.send(radius_query));

Downloading the Sample Code

The sample code can be downloaded as a zip file. To compile and run the sample, unzip it and do
%javac QuagentDemo.java
%java QuagentDemo
It tries to connect to a quagent module running at local machine. If you want to connect to a remote machine, change "localhost" to the host name of that machine in DrunkenMaster.java. We didn't implement all the requests and responses. So it is not the whole thing yet. The file/class names should make clear what they are.


Last modified: