Volity

Creating a Volity SVG UI

When you create a Volity game, you must create two things: the game logic (which runs inside the referee), and the game's user interface (which is displayed by the client). The user interface is a Scalable Vector Graphics (SVG) document with ECMAScript (Javascript) code embedded in it.

This is not as scary as it sounds.


1. What You Need To Have Already

This guide assumes that you have a game in mind, and you've decided on its ruleset. (You do not need to have written your referee logic.) We will use Tic Tac Toe as a working example.

We also assumes that you know ECMAScript. This is the officially-standardized version of Javascript. If you know Javascript, you're ready. If not, it looks a lot like Java; nearly as much like C; and rather a lot like every other scripting language.

Finally, we assume that you have some idea of how SVG works. Since SVG is likely to be the Volity language you are least familiar with, this guide will explain the rudiments of SVG, and describe its SVG examples as it goes. However, this is not an SVG tutorial, nor an SVG reference manual.


2. What Is SVG?

SVG is a document format, based on XML, for describing vector graphics. An SVG image is fundamentally made of lines, arcs, and areas — that is, vector shapes — and not of pixels. Therefore, an SVG image can be displayed at any size without becoming "pixelated" and ugly.

Note

An SVG image can display inline images, just as HTML can. These images can be pixel formats like JPEG, GIF, and PNG. So it is possible to make an SVG document that scales badly. You just have to work at it.

Just like an HTML file, an SVG file in your client is structured as a DOM tree — a hierarchy of XML tags. (Yes, HTML isn't really XML. Pretend we said XHTML, if the difference is important to you.) Like HTML, an SVG file can contain ECMAScript, which manipulates the DOM tree in response to various events. This is how a Volity UI works. The ECMAScript code in a UI must:

  • Accept move requests from the player, and transmit them off to the referee.

  • Receive game updates from the referee, and display them to the player (by updating the DOM tree). "Game updates" can include

    • moves by other players;

    • notifications that the game is starting or ending;

    • complete descriptions of a game in progress.

(That last occurs when a player joins a game table in the middle of the game. His UI then has to accept the entire game so far as a single update.)


3. Getting Started

The ECMAScript of a UI will eventually have to do a lot, but it's easiest to start without it. Most games will have a fixed frame and board image, which will not change over the course of the game; the game will just add things on top of the board. So we will start by drawing this board as a simple SVG file.

Create a file named my-ttt.svg, containing the following:


<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.2"
  xmlns:xlink="http://www.w3.org/1999/xlink"
  viewBox="0 0 300 300" preserveAspectRatio="xMidYMid" >

  <line x1="5" y1="100" x2="295" y2="100"
    stroke="black" stroke-width="5" stroke-linecap="round" />
  <line x1="5" y1="200" x2="295" y2="200"
    stroke="black" stroke-width="5" stroke-linecap="round" />
  <line x1="100" y1="5" x2="100" y2="295"
    stroke="black" stroke-width="5" stroke-linecap="round" />
  <line x1="200" y1="5" x2="200" y2="295"
    stroke="black" stroke-width="5" stroke-linecap="round" />

</svg>

To explain this, line by line:

The first line is an XML header.

The next three lines are the top-level <svg> tag, which contains several attributes. The xmlns and version attributes are required for any SVG document. The xmlns:xlink is required for any SVG document which has <use> elements; in practical terms, that means all of them. (We will describe the <use> element later.)

The preserveAspectRatio attribute controls how the SVG image fits into the client window when it is displayed. Declaring preserveAspectRatio="xMidYMid" means that the image will be centered, and scaled to the window size, but not distorted. (If the window is wider than it is tall, the image will be displayed with blank space to either side, not stretched.) This is normally what you want for a game UI.

The viewBox attribute defines the coordinate system for your image. That sounds mathematically intense, but it merely means you get to decide what values to use to lay out the image. Setting viewBox="0 0 300 300" means that the top left corner is (0,0), and the bottom right corner is (300,300). This is a particularly handy way to lay out a Tic Tac Toe board, since it's a three-by-three grid, so each square winds up being 100 by 100 units.

Note that these measurements are in arbitrary units, not in pixels or centimeters or any other fixed unit. That makes sense, because the whole point is that this image is going to scale to fit in the player's client window. It has no "natural" size.

Note

The four values of viewBox are actually "LEFT TOP WIDTH HEIGHT". If you wanted your image's coordinates to stretch from (-200,-200) to (200,200) — with the origin in the center — you would declare viewBox="-200 -200 400 400". But it is generally easier to leave the top left corner at (0,0).

Contained within the <svg> element are four <line> elements. Each one works the way you might expect: it describes a line stroke drawn from (x1,y1) to (x2,y2). The four lines have similar attributes (black, 5 units wide, rounded ends) and together they form a Tic Tac Toe grid.

Note that the lines are drawn slightly short of the edges of the 300-by-300 canvas. For example, the first one is drawn from (5,200) to (295,200). This is because the rounded line-caps are drawn around the mathematical endpoints of each stroke. If the stroke went all the way to the edges — (0,200) to (300,200) — then the line-caps would extend outside the image, and possibly outside the client window. We don't want them to be cut off.

Finally, the last line of the document closes the <svg> tag.


4. Displaying Your UI

If you've been following along, you've typed in (or copied and pasted) the short SVG file from the previous section. Now you want to display it.

There are several SVG interpreters available, both as standalone applications and as web browser plugins. The SVG file we've got should display correctly in all of them. However, we will soon want to go farther; we'll want to test the Volity script code, and simulate moves of a game. Testbench is a Volity tool which helps with this.

Run Testbench as you would any other Java application. A dialog box will appear which prompts you to choose an SVG file. On a command-line OS, you could also supply the SVG file as a command-line argument:


  java -jar Testbench.jar my-ttt.svg

Either way, you will see a window appear which is rather similar to a Gamut client window. The Tic Tac Toe board will be displayed in the main part of the window.

If there is anything wrong with the SVG file, you will see error messages of greater or lesser helpfulness. You do not need to quit Testbench to deal with these. Fix them, and then use the "Reload" menu item (Linux/Windows: Ctrl-R; Mac: Cmd-R) to reload the SVG from the file. This is how you will typically develop a UI file; make changes with a text editor, then reload it in Testbench to see if it worked.

Note

Yes, it would be nice to have a true Volity IDE — a tool which let you draw SVG elements directly with your mouse. We don't have that yet. There are drawing tools like Illustrator and Inkscape which let you drawn SVG, but they don't have Testbench's facilities for testing script code. A possible happy medium is to draw complex (but static) image elements with a drawing tool, include them in the UI file via <use> tags, and test the whole in Testbench.


5. Adding Metadata

A polite UI file declares its name, its author, its version number, and its ruleset URI. This allows the Volity bookkeeper to index it tidily, and pass all that information on to clients which are trying to play your game.

SVG allows this sort of information to be placed in a <metadata> element, which must be immediately contained within the top-level <svg>. For a Volity UI, it might look like this:


<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.2"
  xmlns:xlink="http://www.w3.org/1999/xlink"
  viewBox="0 0 300 300" preserveAspectRatio="xMidYMid" >

  <metadata
      xmlns:dc="http://purl.org/dc/elements/1.1/"
      xmlns:volity="http://volity.org/protocol/metadata" >

    <volity:ruleset>http://volity.org/games/tictactoe</volity:ruleset>
    <dc:title>Tic Tac Toe example UI</dc:title>
    <dc:creator>Andrew Plotkin</dc:creator>
    <volity:version>1.0.example</volity:version>
    <dc:modified>2006-04-14</dc:modified>

  </metadata>

  <line x1="5" y1="100" x2="295" y2="100"
    stroke="black" stroke-width="5" stroke-linecap="round" />
  <line x1="5" y1="200" x2="295" y2="200"
    stroke="black" stroke-width="5" stroke-linecap="round" />
  <line x1="100" y1="5" x2="100" y2="295"
    stroke="black" stroke-width="5" stroke-linecap="round" />
  <line x1="200" y1="5" x2="200" y2="295"
    stroke="black" stroke-width="5" stroke-linecap="round" />

</svg>

There are two namespaces declared in the <metadata> element. The dc namespace represents Dublin Core, a standard vocabulary for describing documents. The dc:title, dc:creator, and dc:modified lines give the title, creator's name, and the date the UI was created. (You can use any dc property, in fact. However, Volity software only pays attention to these and a few others.)

The volity namespace represents properties which have special meaning to Volity. The volity:ruleset should be the URI of the ruleset which the UI supports — in this case, http://volity.org/games/tictactoe. And volity:version is a version number for the UI. You can put any string here; it is wise to update it every time you release a new version of the UI file. This will reduce confusion when you have players playing your game and encountering UI bugs.

Add the <metadata> section to your SVG file, and reload it in Testbench. Then try the "UI Metadata..." menu item (Linux/Windows: Ctrl-I; Mac: Cmd-I). A dialog box will appear that displays the metadata, as Volity understands it.


6. Adding Xs and Os

We now want to start doing what a Tic Tac Toe game is all about: drawing X and O on the board.

Doing this in SVG is easy. We can be a little bit clever by defining an X and an O symbol inside a <defs> element:


  <defs>

    <g id="x-mark">
      <line x1="25" y1="25" x2="75" y2="75"
        stroke="blue" stroke-width="10" stroke-linecap="round" />
      <line x1="25" y1="75" x2="75" y2="25"
        stroke="blue" stroke-width="10" stroke-linecap="round" />
    </g>

    <g id="o-mark">
      <circle cx="50" cy="50" r="25"
        stroke="blue" fill="none" stroke-width="10" />
    </g>

  </defs>

The <defs> element should be contained within the top-level <svg>, right after <metadata>. (We will not give the complete SVG file at every stage, but copies of the sample SVG file at each stage of this tutorial are available from the Volity web site.)

This introduces two more SVG elements: <circle> and <g>. The <circle> is just what it says; it has cx and cy attributes to define the center of the circle, and r is the radius. Note that we also have to specify fill="none". By default, an SVG shape has no outline and is filled with black. (The lines we've seen so far have no interior, so the fill doesn't matter.)

The group (<g>) element simply contains a bunch of other SVG elements. This lets us draw an X — two lines — with a single <use> statement later on. Each group has an id attribute which identifies it for later <use>.

Note that the two symbol groups, x-mark and o-mark, are defined in a 100-by-100 unit space. The center of each symbol is (50,50). This accords with our original decision to make the board be 300-by-300.

Now, if you reload, you'll see that adding the <defs> hasn't changed the image at all. Elements defined inside <defs> are not drawn directly in the image. Instead, they are kept as templates, which may be "stamped" one or many times in the image. We do this with a <use> element. Put these lines at the end of the file, after the four <line> elements that make up the grid:


  <use xlink:href="#x-mark" x="100" y="100" />
  <use xlink:href="#o-mark" x="0" y="200" />
  <use xlink:href="#x-mark" x="100" y="200" />
  <use xlink:href="#o-mark" x="100" y="0" />

This puts us four moves into a game. (And X has a decided advantage — but there's still room for an O comeback!) Each <use> statement has an xlink:href property, which refers to the id of an element in the <defs> section. And it has x and y properties, which adjust the overall position of the replicated element. The three columns of our board begin at x="0", x="100", and x="200". The rows, similarly, have y="0", y="100", and y="200". Since the x-mark and o-mark elements are centered at (50,50) — relative to the positions they are drawn at — this puts everything where we want it.


7. Adding and Erasing Xs and Os

Putting Xs and Os directly into the image, as we did in the last section, is not in fact what we need. After all, the first four moves probably won't be the ones we entered. We want an image which doesn't have any marks, but which can acquire them as a game proceeds.

This means writing some ECMAScript. At this point, we're not trying to hook the scripts up to game RPCs or to player input. We just want a basic set of routines that can draw and erase game marks.

First, we need a <script> element to put the code into.


  <script type="text/ecmascript">
    <![CDATA[
// ECMAScript goes here...
    
    // ]]>
  </script>

This should be contained in the top-level <svg>, between the <metadata> and the <defs>. The <![CDATA[ ]]> lines are an XML construct that let you write literal code, without having to escape < and > signs. Between them goes the ECMAScript code. So far this is only one comment; let us add to that.

The code to create a new <use> element, like the one we had before, is straightforward. Delete the four <use> lines from the end, and add this to the ECMAScript section:


  mark = document.createElementNS("http://www.w3.org/2000/svg", "use");
  mark.setAttributeNS("http://www.w3.org/1999/xlink", "href", "#x-mark");
  mark.setAttribute("id", "mark-4");
  mark.setAttribute("x", 100);
  mark.setAttribute("y", 100);
  document.getRootElement().appendChild(mark);

Reload the SVG, and you'll see an X in the center square, which is... not any better than simply having the <use> statement. In fact it's exactly the same. (Except that we're setting an id attribute, which we will make use of presently.) The code runs when the document is loaded, and its effect is to create a <use> element with the given attributes, and attach it to the end of the SVG DOM tree.

Let's change that to a function, and also move the ugly URI strings to global constants:


  var svg_ns = "http://www.w3.org/2000/svg";
  var xlink_ns = "http://www.w3.org/1999/xlink";
  var position_x_values = [ 0, 100, 200, 0, 100, 200, 0, 100, 200 ];
  var position_y_values = [ 0, 0, 0, 100, 100, 100, 200, 200, 200 ];

  // Add an X or O to the board.
  // Which is a string, "x" or "o" (lower-case). Pos is a number from 0 to 8.
  function draw_mark(which, pos) {
    var mark, grp;

    mark = document.createElementNS(svg_ns, "use");
    mark.setAttributeNS(xlink_ns, "href", "#"+which+"-mark");
    mark.setAttribute("id", "mark-"+pos);
    mark.setAttribute("x", position_x_values[pos]);
    mark.setAttribute("y", position_y_values[pos]);

    grp = document.getElementById("marks-group");
    grp.appendChild(mark);
  }

The arguments to the draw_mark() function follow the conventions laid out in the Tic Tac Toe ruleset. To indicate which player's mark to draw, pass one of the strings "x" or "o"; these are the seat names defined in the ruleset. To indicate a position, we use an integer:


  0 1 2
  3 4 5
  6 7 8

The lookup tables position_x_values[] and position_y_values[] let us convert a position number to (x,y) coordinates.

This code doesn't attach the newly-created <use> to the end of the document. It attaches it to something called marks-group. What is this? Something we need to declare:


  <g id="marks-group" />

This is an empty group. Put it at the end of the SVG file, after the four lines. Why an empty group? In this case, we're using the group as a "layer" — a set of elements which are rendered together. SVG is drawn "back to front"; later elements appear on top of earlier ones. Since the group comes after the board-grid, everything in the group will appear on top of the grid.

(It is also particularly easy to delete everything inside a group. This will be handy for erasing the Xs and Os without destroying the board.)

Can we test this draw_mark() function? Certainly. Click in the bottommost, empty panel of the Testbench window. In Gamut, this is the chat input pane; in Testbench, it lets you type in ECMAScript. Anything you type is immediately executed. This makes testing very simple. Type this:


  draw_mark("x", 4)

An X should appear in the center square. (If you get an error like "'draw_mark' is not defined," you need to reload the SVG that you just edited.)

Note

This function doesn't do any error-checking. You can type draw_mark("q", 17) to see some fascinating error messages. This is, in general, a good tradeoff; it's much easier to catch errors on the referee side, and if the referee is wrong, the players are hosed anyway. You should approach UI creation with the idea that the referee is always right. Of course, players aren't so tidy; you'll have to check player input carefully for validity.

We also want to be able to erase the board. You can do this by hitting Reload, but you don't want the player to have to reload his UI for every new game. To do this in code:


  // Erase all the Xs and Os from the board.
  function erase_marks() {
    var grp;
    grp = document.getElementById("marks-group");
    remove_children(grp);
  }

  // Delete all the child nodes of the given object (usually a group).
  function remove_children(parent) {
    var obj, ls;
    ls = parent.childNodes;
    while (ls.length > 0) {
      obj = ls.item(0);
      parent.removeChild(obj);
    }
  }

The remove_children() function is a generic tool which you will find useful in most of your UIs. The erase_marks() function uses remove_children() to remove all the children of the marks-group. To test this, reload your SVG, and then enter these commands in your script input pane (one at a time):


  draw_mark("x", 4)
  draw_mark("o", 3)
  draw_mark("x", 0)
  erase_marks()


8. Receiving Updates From the Referee

When the client receives RPCs from the referee, it invokes functions in your UI code. For example, when the Tic Tac Toe referee sends game.mark(seat_id, location), it calls a UI function named game.mark() and passes in those two arguments. There are also functions associated with general game activity. For example, when the game starts, a UI function named volity.start_game() is called.

(No, an ECMAScript function can't have periods in its name. What actually happens is that the client provides ECMAScript objects named game and volity. You must create anonymous functions, and assign them to be named properties of these two objects.)

The ruleset describes just four game RPCs that the UI must handle, and we can handle them easily enough:


  var seat_whose_turn_it_is = null;

  // Referee adds a move to the board.
  game.mark = function(seat_id, location) {
    draw_mark(seat_id, location);
  }

  // Referee says that it's the given seat's turn to play.
  game.must_mark = function(seat_id) {
    seat_whose_turn_it_is = seat_id;
  }

  // Referee declares a win.
  game.win = function(seat_id, loc_1, loc_2, loc_3) {
    seat_whose_turn_it_is = null;
  }

  // Referee declares a tie.
  game.tie = function() {
    seat_whose_turn_it_is = null;
  }

The global variable seat_whose_turn_it_is keeps track of whose turn it is. This will be important when we start thinking about letting the player make moves. (Since we only want to allow that when it is, in fact, the player's turn.)

Aside from setting seat_whose_turn_it_is, the only RPC that does anything interesting is game.mark(). We handle that by calling our draw_mark() function.

We do need to do some work when the game starts; we need to clear the board from any previous game.


  // Game has begun.
  volity.start_game = function() {
    erase_marks();
    seat_whose_turn_it_is = null;
  }

(We're a little overzealous in keeping seat_whose_turn_it_is correct, but that's for the best.)


9. Seat Marks

Seat marks are the little arrows and crown symbols that appear in Gamut's seating panel. The UI is responsible for setting these, and it does this by calling the seatmark() function.

The simplest case is setting the blue arrow that indicates whose turn it is. You do this by calling seatmark(seat_id); the argument should be the identifier of a seat. Conveniently, this is just what we have in the game.must_mark() function, so we can write this:


  game.must_mark = function(seat_id) {
    seat_whose_turn_it_is = seat_id;
    seatmark(seat_id);
  }

(If you test this in Testbench, you won't see a blue arrow, but the seat mark change will be noted in the chat pane.)

Setting the crown mark, to indicate the winner of a game, is a bit harder. We must create an ECMAScript array which maps seat identifiers to mark constants.


  game.win = function(seat_id, loc_1, loc_2, loc_3) {
    seat_whose_turn_it_is = null;

    var seatmarks = [];
    seatmarks[seat_id] = "win";
    seatmark(seatmarks);
  }

  game.tie = function() {
    seat_whose_turn_it_is = null;

    var seatmarks = [];
    seatmarks["x"] = "win";
    seatmarks["o"] = "win";
    seatmark(seatmarks);
  }

In game.win(), we create an array with one entry. In game.tie(), we create two — both seats have won. Both of these use the constant "win", which represents the standard Gamut crown mark. (The value constants are "turn", "win", "first", and "other". Calling seatmark() with a seat ID is equivalent to passing an array which maps that ID to "turn".)


10. Accepting Player Input

The player will certainly appreciate all this display technology, but at some point he will want to make a move.

Nearly all player input will come in the form of mouse events. These work the same as they do in HTML; you can set an onclick attribute on any SVG element, or any of a large handful of other event properties.

Note

Each SVG element also provides an addEventListener() ECMAScript property. You can call this instead of using the onclick family of attributes. However, onclick is simpler, so we will use it in this example.

We will therefore need SVG elements to click on. Nine of them, to make life easier — one for each grid square the player might click on. (We could instead use a single SVG element, and figure out which grid square the player selected by examining the coordinates of the mouse event. However, that is more work.)

It would be easy to create nine <rect> objects in our SVG by hand. This would be tedious in a more complicated game, however. It is worth demonstrating how to do this in ECMAScript code. In fact, it is not very different from the draw_mark() function we have already written:


  function initialize() {
    var pos;
    var grp = document.getElementById("squares-group");

    for (pos=0; pos<9; pos++) {
      var square = document.createElementNS(svg_ns, "rect");
      square.setAttribute("id", "square-"+pos);
      square.setAttribute("x", position_x_values[pos]);
      square.setAttribute("y", position_y_values[pos]);
      square.setAttribute("width", 100);
      square.setAttribute("height", 100);
      square.setAttribute("fill", "white");
      square.setAttribute("onclick", "square_clicked("+pos+")");

      grp.appendChild(square);
    }
  }

  initialize();

We must call this initialize() function from the top level of the ECMAScript section, so that it is executed when the document is loaded.

Like the X and O marks, we are putting all of these square objects into an SVG group. In this case, the sole reason is to confine them to a layer. We want them drawn first — that is, rearmost in the display order — so that they don't cover up either the grid lines or the Xs and Os. Place this declaration before the four grid <line> objects:


  <g id="squares-group" />

The initialize() function uses ECMAScript string concatenation to create a different onclick attribute for each square: "square_clicked(0)", "square_clicked(1)", and so on. If you try them out, you'll see a bunch of "'square_clicked' is not defined" errors, so we'd better define one:


  // onclick handler for squares.
  function square_clicked(pos) {
    rpc("mark", pos);
  }

The rpc() function is the gateway to the referee. When you call it, the UI sends an RPC to the referee — in this case, game.mark(), with the integer pos as the sole argument. This is the correct way, according to the Tic Tac Toe ruleset, for the player to request a move.


11. Not Accepting Player Input

At this point, we have a usable Tic Tac Toe UI. It can display moves and accept them. However, it does not make any attempt to validate player input. It will send off an rpc() call whenever the player clicks, even if it isn't the player's turn. In fact, it will do this even if the game hasn't started yet, or if the player is an observer and not involved in the game at all.

These erroneous RPCs will not destroy the game; the referee will notice them and return error messages to the client. However, it is better for the UI to pay attention and only send valid RPCs.

To do this, we will use the seat_whose_turn_it_is global variable, which we defined earlier. We will also need to know where the player is sitting. You can find this out by evaluating info.seat. This contains the name of your seat, if you are seated, or null if you are observing.

Note

The info object is another ECMAScript object provided by the client. It has several fields that you can read for useful information.

With these tools in hand, we can write a function which determines if you are allowed to make a move right at the moment:


  // Return true if you are a seated player and it is your turn.
  function it_is_my_turn() {
    if (!info.seat)
      return false;
    if (!seat_whose_turn_it_is)
      return false;
    if (info.seat == seat_whose_turn_it_is)
      return true;
    else
      return false;
  }

We also need a function to determine whether a square is empty or not. The easiest way to do this is to inspect the DOM tree. Recall that our draw_mark() function adds an id to each <use> element it creates: mark-0 for the mark in square 0, mark-1 for the mark in square 1, and so on. We can therefore write this function by calling document.getElementById(), and seeing if the result is null:


  // Return true if the given square has an X or O.
  function is_square_marked(pos) {
    var obj;
    obj = document.getElementById("mark-"+pos);
    if (obj)
      return true;
    else
      return false;
  }

Note

If we needed to know whether the square contained an X or an O, we could call obj.getAttributeNS(xlink_ns, "href") and see whether the resulting value was "#x-mark" or "#o-mark".

With these tools, we can make square_clicked() work more cleanly:


  // onclick handler for squares.
  function square_clicked(pos) {
    if (!it_is_my_turn())
      return;
    if (is_square_marked(pos))
      return;
    rpc("mark", pos);
  }

After these changes, we find that the UI is not sending RPCs when you click on squares. This is perfectly correct: after a Reload, the UI believes it is displaying a fresh table. No game has started, and therefore no moves should be made.

You might then wonder how the game code is going to get tested. Testbench, not surprisingly, has some features to support this. At the top of the window, you'll see buttons labelled "Start Game" and "End Game", and a text field.

If you hit "Start Game", Testbench will behave as if the game had begun, with you as an observer. Your volity.start_game() function will be called, with info.seat set to null.

This is not exciting, since the UI's behavior as an observer is the same as its behavior when the game is not in progress — it ignores mouse clicks. To set a game in motion, fill in the text field at the top of the window with the name of a seat: "x" or "o". (Lower-case, and without the quote marks.) This changes info.seat to the string you typed, simulating the playing sitting down. Then hit "Start Game".

At this point, the UI knows you are seated, but it believes it is not your turn. To notify it that it should begin accepting clicks, you'll have to simulate the RPC call (from the referee) that notifies the players whose turn it is. Type in the text entry area:


  game.must_mark("x")

After this — assuming you are "x"; you entered "x" in the top field, right? — the UI will understand that it is your turn. Clicking on the board squares will trigger (simulated) RPCs to the referee.

To simulate O's turn, you would type:


  game.must_mark("o")

(Testbench lets you hit up-arrow to display the last command you typed. This is handy for repeating commands, which is what most of game testing consists of.)

The "End Game" button ends the game. This causes a call to volity.end_game(). You don't have a function of that name, so this is not a big deal. But in more complex games, it will be important.

Note

Testbench doesn't care if you push "Start Game" twice in a row; it will happily call your volity.start_game() function twice, or a dozen times for that matter. In actual Volity play, you would not see volity.start_game() twice without a volity.end_game() in between.


12. A Little More Feedback

We can do a bit more for player comfort. It would be nice if the board squares darkened when you clicked on them — not permanently darkened, just a momentary "down" state, in the usual manner of buttons.

The color of each square is controlled by its fill attribute. We create the squares filled with white, but of course any color is possible. To make this simpler, we'll first create a helper function to set a square's color:


 // Set a single square's color.
  function set_square_color(pos, color) {
    var obj;
    obj = document.getElementById("square-"+pos);
    obj.setAttribute("fill", color);
  }

You can test this, rather startlingly, by typing:


  set_square_color(4, "red")
  set_square_color(5, "blue")

(Mondrian simulator comes free!)

Note

SVG colors can be common strings ("white", "black", "red", etc), or three- or six-digit hex strings ("#000", "#FFF", "#7F7F7F").

To highlight the squares when the mouse button is pressed, and unhighlight them when it is unpressed, we'll need to add more event-handling attributes to the square objects.


  function initialize() {
    var pos;
    var grp = document.getElementById("squares-group");

    for (pos=0; pos<9; pos++) {
      var square = document.createElementNS(svg_ns, "rect");
      square.setAttribute("id", "square-"+pos);
      square.setAttribute("x", position_x_values[pos]);
      square.setAttribute("y", position_y_values[pos]);
      square.setAttribute("width", 100);
      square.setAttribute("height", 100);
      square.setAttribute("fill", "white");
      square.setAttribute("onclick", "square_clicked("+pos+")");
      square.setAttribute("onmousedown", "square_mousedown("+pos+")");
      square.setAttribute("onmouseup", "square_mouseup("+pos+")");
      square.setAttribute("onmouseout", "square_mouseup("+pos+")");

      grp.appendChild(square);
    }
  }

And the handlers:


  // mousedown handler for squares &emdash; turn the square gray.
  function square_mousedown(pos) {
    if (!it_is_my_turn())
      return;
    if (is_square_marked(pos))
      return;
    set_square_color(pos, "gray");
  }

  // mouseup/mouseout handler for squares &emdash; turn the square white.
  function square_mouseup(pos) {
    if (!it_is_my_turn())
      return;
    if (is_square_marked(pos))
      return;
    set_square_color(pos, "white");
  }

We call the highlight function on the mousedown event, and the unhighlight function on mouseup. We also call the unhighlight function on mouseout events. This covers the case where the player moves the mouse outside the square before releasing the button. (The onclick event only fires when the mouse button is pressed and released in the same SVG object; if the mouse moves outside, it's not a click event. Our highlight code will follow the same rule.)

Note that we only do highlighting work when it's the player's turn — just as we only send RPCs to the referee when it's the player's turn. The highlighting effect is supposed to indicate to the player that he is taking a real action. If the highlight occurs when the action is blocked — or vice versa — then it won't be meaningful feedback.

Note

Careful coders will notice a small hole in this setup. If the player presses the mouse button, but it stops being his turn before he releases it, then the square will get stuck in the gray state! Similarly, if the square is marked before he releases it, the same thing will happen. These are unlikely cases, but there are a couple of ways they can happen. A more careful implementation would keep track of the highlighted square, and clear it whenever the seat_whose_turn_it_is variable changes, or when the square is marked, or when the game ends or suspends.

While we're in the business of coloring squares, let's set up the UI to highlight the winning condition when someone wins the game.


  // Referee declares a win.
  game.win = function(seat_id, loc_1, loc_2, loc_3) {
    seat_whose_turn_it_is = null;

    set_square_color(loc_1, "yellow");
    set_square_color(loc_2, "yellow");
    set_square_color(loc_3, "yellow");

    var seatmarks = [];
    seatmarks[seat_id] = "win";
    seatmark(seatmarks);
  }

We don't want those yellow highlights to stick around into the next game, however. We must clear all the squares to white at the beginning of each new game.


  // Game has begun.
  volity.start_game = function() {
    erase_marks();
    var ix;
    for (ix=0; ix<9; ix++)
      set_square_color(ix, "white");
    seat_whose_turn_it_is = null;
    seatmark();
  }

To test these, type:


  game.win("x", 2, 4, 6)

...and then hit "Start Game" to see the yellow highlights vanish.

Note

What if the player clicks on a yellow square? The highlighting code clears a square to white on mouse-up, so you might worry that this would erase the highlight. And it would — except that the yellow marks only appear when the game ends, and after the game ends, it can't be your turn!


13. Testing For Real

Testbench is a fine thing, but we do want to test the UI against an actual Tic Tac Toe parlor. We have now reached the point where this will work.

Start up Gamut, and create a new table at a Tic Tac Toe parlor. (At the time of writing, Volity maintains a Tic Tac Toe parlor at tictactoe@volity.net.)

If you have never played the game before, and the Volity bookkeeper does not have a default UI registered, you will immediately see a dialog box asking you to choose a UI. Hit "Select File" and choose your SVG file. It will appear in a Gamut window, and you will be ready to play the game.

If there is a default UI available — either because you've played before, or because the bookkeeper provides one — then it will appear when you create the table. To switch to your own UI, use the "Select New Interface" menu item (under the Game menu). The selection dialog box will again appear, and you can hit "Select File" as described above.


14. Translation Tokens

In the Gamut window's seating panel, you may note that the seats are labelled with the bare seat IDs "x" and "o". This is accurate, but terse.

The ruleset document (remember the ruleset document?) also defines three "translation tokens". These are errors that can result from the UI's RPC calls. The UI we have created is very careful to avoid making erroneous RPC calls, but it still might happen.

What connects these two apparently unrelated comments? They are both handled by Volity's token translation system. This is a set of resources, provided by the UI, which translates short ruleset-defined strings (such as seat IDs or errors) into human-readable text. Since humans can read in lots of different languages, we allow a UI to provide several different translation resources.

The translation resources are not packaged into the SVG file. Instead, you must assemble a directory with files in particular locations.

Set up a directory structure like this:

  • ttt-ui [top-level folder]

    • main.svg [your SVG file]

    • locale [subfolder]

      • en [subfolder]

        • seattokens.xml [seat ID translations]

        • gametokens.xml [error token translations]

The SVG file must be named main.svg (or MAIN.SVG, but no other variations) in the top directory.

The locale subdirectory contains all the translation resources. It has one sub-subdirectory for each language which the UI supports. In this example, we are only supporting English, in the en directory. (Each language is represented by its standard two-letter Internet code.)

The en directory contains two XML files: one translating seat IDs, and one translating error tokens. seattokens.xml should look like this:


<?xml version="1.0"?>
<volitytokens>
  <token>
    <key>x</key>
    <value>Player X</value>
  </token>
  <token>
    <key>o</key>
    <value>Player O</value>
  </token>
</volitytokens>

And gametokens.xml should look like this:


<?xml version="1.0"?>
<volitytokens>
  <token>
    <key>invalid_location</key>
    <value>The location \1 was not a valid location.</value>
  </token>
  <token>
    <key>already_marked</key>
    <value>That location is already marked.</value>
  </token>
</volitytokens>

Testbench can read this directory structure in place. Start Testbench and select the main SVG file, or pass it in on the command line:


  java -jar Testbench.jar ttt-ui/main.svg

(You could also simply use the ttt-ui directory as the command-line argument.)

To test the tokens, type:


  message("game.already_marked")
  message("game.invalid_location", 5)
  message("seat.x")
  message("seat.o")

The message() translates its argument and prints it in the chat output pane. (As you see, some error tokens require further information. That's why the "invalid_location" token entry in gametokens.xml contains "\1".)

Note

When you provide seat IDs in this way, Testbench loads them into the seat field at the top of the window. You can then select "x" or "o" via the pull-down menu, instead of having to type it in.

Gamut, on the other hand, does not expect to be handed a tree of directories. If you run Gamut, do "Select New Interface", and choose main.svg, Gamut will not recognize the translation resources.

Instead, you must wrap the directory tree up as a ZIP file. The mechanism to do this varies on different platforms. In MacOS, you can ctrl-click on the folder in Finder and select "Create Archive..." In Windows, you must use WinZip or a similar tool. On a Unix system, you would type:


  zip -r ttt-ui ttt-ui

Choose the resulting ttt-ui.zip file from Gamut, and you will see that the seat panels are properly labelled "Player X" and "Player O".

Note

The multi-language features of Volity are not yet supported by the client software. Both Gamut and Testbench are locked to English (en) at the moment. This will change at some point.


15. The Four Sadnesses

This is the point when you believe your UI is done. Unfortunately, it isn't quite.

There are several standard Volity features which can cause trouble for your UI code. You really do have to handle them, because players will inevitably try them. Fortunately, they don't usually cause a lot of trouble. These features are:

  • A player who watches a game, but does not participate.

  • A player who joins the table in mid-game.

  • Players who share a seat.

  • Suspending and unsuspending games.


16. Sadness the First: Observation

Observation is the easiest problem to consider. Run two instances of the Gamut application. From one Gamut, start a Tic Tac Toe table. Join the table from the other Gamut. (Be sure to select different table nicknames in the two clients; you can't connect twice with the same nickname.)

Note

If your game is not listed in the Game Finder — most games in development are not — then you'll have to join the table using the "Join Table At..." menu option. (Unfortunately, you cannot send a Volity game invitation to yourself.) After creating the table window, use the "Game Info" menu option (in the open table window) and copy out the Table ID. Select "Join Table At..." in the second Gamut, and paste in the table ID. A second window will appear, displaying the same table.

In one table window, sit down and play a game against a bot. (If you are testing a game that has no bots, you'll probably have to run a third Gamut and play against yourself.)

Since we have been careful, the UI code in our example does all of this correctly.


17. Sadness the Second: Joining a Table

Joining a table in mid-game takes a bit more work. When you join a table, the referee sends you the entire table state at once. (This is called a "state recovery burst".) These will probably be the same RPCs you'd have gotten if you'd been at the table all along. In the case of Tic Tac Toe, this means game.mark() calls. (And game.must_mark(), but only for the most recent turn — the player who currently must make a move.)

There is one odd exception, however. The beginning of the game is not marked by a volity.start_game() RPC; that is only sent out when the referee actually begins the game. In a state recovery burst, if the game is already in progress, you receive volity.game_has_started() instead. This is a minor distinction, and most UIs will want to undertake the same work in each function. The easiest way to handle this:


  // Joining a game which has begin.
  volity.game_has_started = function() {
    volity.start_game();
  }

(This is not crucial in our Tic Tac Toe UI, because the only work done in volity.start_game() is resetting the UI to its initial state. If you're joining a table, your UI is in its initial state already. But it's good practice to handle this RPC in this way anyhow. A more complex game might need to draw an initial board layout — something not visible during game setup.)

To test table-joining, you can of course get a game going and then have a new Gamut client join it. An easier way to test the same thing is the "Restart Interface" menu option. This restarts your UI from scratch — all ECMAScript variables return to their initial values, and the initial SVG state is restored. The UI then requests and receives a state recovery burst. If your UI can survive that, it can handle table-joining as well.


18. Sadness the Third: Sharing Seats

Volity permits any number of players (or bots) to occupy the same game seat. Any player in the seat is allowed to make a move for that seat. (If a bot is present, the bot will usually make all that seat's moves, simply because bots think quickly.)

This means that your UI must be prepared for the possibility that its turn will begin and then end with no interaction from the player. This is why the Tic Tac Toe code does not draw an X or O when the player clicks; it merely sends off a move request. You will almost always want to use this model for your UI code. Mouse clicks can set temporary highlighting, but that highlighting is optional; real game state changes come only from the referee.


19. Sadness the Fourth: Suspend and Resume

Any seated player can suspend a Volity game at any time. The UI has to pay attention to this for a couple of reasons. First, when the game is suspended, nobody should be allowed to make any moves. Second, players can stand up and sit down — or even swap seats — when the game is suspended. Third, the state recovery bursts sent out during suspension may not be the same as normal state recovery bursts.

Your UI can provide volity.suspend_game() and volity.resume_game() RPC handlers to keep track of when the game is suspended. However, an easier way to do this is to check the info.state variable. This will be "setup", "active", "suspended", "disrupted", or "abandoned". (You don't have to worry about the last two — they're variants of "active".) We can guard against game suspension by enhancing the it_is_my_turn() function:


  // Return true if you are a seated player and it is your turn.
  function it_is_my_turn() {
    if (!info.seat)
      return false;
    if (!seat_whose_turn_it_is)
      return false;
    if (info.state == "setup" || info.state == "suspended")
      return false;
    if (info.seat == seat_whose_turn_it_is)
      return true;
    else
      return false;
  }

(We really already had the "setup" state covered, because we were careful to ensure that seat_whose_turn_it_is is always null when the game is not in progress. But it doesn't hurt to check twice.)

Changing seats and state recovery is not an issue in Tic Tac Toe; the ruleset does not declare anything special about game suspension and unsuspension. However, some game rulesets do. If a game has hidden information — say, a private hand of cards for each player — then state recovery bursts during suspension will not include that private information. (That is to say: when the game is suspended, nobody has the privilege of seeing any seat's private information. It's too easy to switch seats during suspension.)

The flip side is this: when such a game resumes, the referee will send out updates to each player, giving him the private information for the seat he has chosen. A player who stood up during suspension will not receive any private information. The UI must correctly redraw each player's display to reflect his new position.

Fortunately for us, Tic Tac Toe has no hidden information.