Reversi Game State
From Gardenwiki
This page is part of the Reversi Tutorial.
Contents |
Step 2. Define the Shared Game State
Since we already know how our game works (see these rules if you don't know how to play Reversi), the next thing we need to do is to model the state of the game in a way that a computer can understand it.
Reversi consists of a bunch of pieces that are either black or white placed on a board which is an eight by eight grid (though one can easily imagine playing it on a board of a different size). In addition to creating a model that we can manipulate in software, we need to think about what information needs to be shared between the players in our game so that we can make sure that information is represented in the distributed state of our game.
In the case of Reversi, there are two different pieces of information that must be shared:
- the dimensions of the board, which is specified before the game starts and never changes;
- the individual pieces, which are added to the game as it is played.
We can imagine displaying a score during the game indicating how many pieces of each color are on the board. However, that is not distributed state. That score information can be computed from the distributed state by simply counting up the number of pieces of each type.
Distributed Objects
When we generated our skeleton game, a class was created called ReversiObject. This class will contain our distributed state and is known as a distributed object. A distributed object is like a normal Java object (a POJO if you like acronyms), but we'll do some special things with it.
When a client first enters a game, it downloads a copy of the game distributed object (in this case the ReversiObject) and so any values that were set by the server before the game started will be accessible to the client. Once the client has a copy of the object, it will then receive events notifying it when any other fields of the object change. Thus these events make sure that every client has an up-to-date view of the distributed object and they provide a way to trigger actions on all of the clients as the game state changes.
For example, our ReversiObject extends GameObject which defines the following field:
/** The game state, one of PRE_GAME, IN_PLAY, GAME_OVER, or CANCELLED. */ public int state = PRE_GAME;
When the game is created, state is set to PRE_GAME, but once all of the clients have entered the game room and reported to the server that they are ready, the game is started and the server changes the value of state to IN_PLAY. The server does this by calling the:
public void setState (int value)
method in the GameObject, which changes the value of the field and generates the appropriate event, which is sent to all of the clients.
Distributed Object Events
Let's look at how the GameController reacts to the game state changing by listening for the AttributeChangedEvent created by the setState() call.
GameController implements the AttributeChangeListener interface:
public abstract class GameController extends PlaceController implements AttributeChangeListener
Then, when it receives a reference to the GameObject in a method called willEnterPlace (which we'll describe later), it adds itself as a listener like so:
public void willEnterPlace (PlaceObject plobj)
{
super.willEnterPlace(plobj);
// obtain a casted reference
_gobj = (GameObject)plobj;
// and add ourselves as a listener
_gobj.addListener(this);
Now that it has been added as a listener, any time an attribute of the GameObject changes, it will receive a call to the attributeChanged() method:
public void attributeChanged (AttributeChangedEvent event)
{
// deal with game state changes
if (event.getName().equals(GameObject.STATE)) {
int newState = event.getIntValue();
if (!stateDidChange(newState)) {
Log.warning("Game transitioned to unknown state " +
"[gobj=" + _gobj + ", state=" + newState + "].");
}
}
}
You can see that the GameController first checks to see which attribute changed by calling event.getName(). Every attribute change event provides the name of the attribute (which is what a Java object field is called by the distributed object system) that has changed, along with the new and old value of that attribute. Look at the documentation for AttributeChangedEvent for more details.
Note: this is an important thing to understand because it is the underlying mechanism by which all games work. A shared state is defined and configured on the server, and all clients download that shared state and are subsequently sent events when the state changes. By reacting to those change events and updating their local display, they show the evolving state of the game. By making requests to the server (which we'll describe later) they trigger new state changes that represent their actions in the game and the game proceeds. All of this will hopefully make a bit more sense as we make our way through the tutorial.
Distributed Object Attributes
Now we have learned that AttributeChangedEvents are sent whenever an attribute of a distributed object is changed using the appropriate setter method. Where do these setter methods come from? Fortunately, you don't have to write them, you simply declare the attributes in your distributed object and then run another special ant task which reads your object and inserts the appropriate setter methods for your fields. Let's try that now.
Open up ReversiObject.java in your favorite editor. It should look something like this:
//
// $Id$
package com.samskivert.reversi;
import com.threerings.parlor.game.data.GameObject;
/**
* Maintains the shared state of the game.
*/
public class ReversiObject extends GameObject
{
}
Now insert a new attribute like so:
public class ReversiObject extends GameObject
{
/** This is a test. */
public int testAttribute;
}
And be sure to save the file. Now go back to your terminal and run a special ant task that will automatically update your distributed objects with setter methods and constants that you can use when handling attribute change events:
% ant gendobj
Now reload ReversiObject in your editor and you should see the following:
public class ReversiObject extends GameObject
{
// AUTO-GENERATED: FIELDS START
/** The field name of the testAttribute field. */
public static final String TEST_ATTRIBUTE = "testAttribute";
// AUTO-GENERATED: FIELDS END
/** This is a test. */
public int testAttribute;
// AUTO-GENERATED: METHODS START
/**
* Requests that the testAttribute field be set to the
* specified value. The local value will be updated immediately and an
* event will be propagated through the system to notify all listeners
* that the attribute did change. Proxied copies of this object (on
* clients) will apply the value change when they received the
* attribute changed notification.
*/
public void setTestAttribute (int value)
{
int ovalue = this.testAttribute;
requestAttributeChange(
TEST_ATTRIBUTE, Integer.valueOf(value), Integer.valueOf(ovalue));
this.testAttribute = value;
}
// AUTO-GENERATED: METHODS END
}
As you might expect, the blocks of code in between the AUTO-GENERATED comments will be manipulated by this ant task, and the code outside those comments will be left alone. Remember that when editing your distributed objects.
Distributed object attributes can be any primitive Java object type, or the boxed version of that type, or String:
- boolean, byte, short, int, long, float, double
- Boolean, Byte, Short, Integer, Long, Float, Double
- String
Arrays
Distributed objects can also contain array attributes which are treated just like primitive attributes, but with one special addition. When you add an array attribute, a special setAttributeAt() method is also generated that allows you to update just a single element of the array with an event. Let's see that in action. Go back to ReversiObject and add another attribute (new code is shown in bold, the previously generated code is not shown):
public class ReversiObject extends GameObject
{
/** This is a test. */
public int testAttribute;
/** This is another test. */
public int[] testArray;
}
Save that, then go back to the command line and run the gendobj ant task again:
% ant gendobj
Now let's have a look at the new methods that were generated:
/**
* Requests that the testArray field be set to the
* specified value. The local value will be updated immediately and an
* event will be propagated through the system to notify all listeners
* that the attribute did change. Proxied copies of this object (on
* clients) will apply the value change when they received the
* attribute changed notification.
*/
public void setTestArray (int[] value)
{
int[] ovalue = this.testArray;
requestAttributeChange(
TEST_ARRAY, value, ovalue);
this.testArray = (value == null) ? null : (int[])value.clone();
}
/**
* Requests that the indexth element of
* testArray field be set to the specified value.
* The local value will be updated immediately and an event will be
* propagated through the system to notify all listeners that the
* attribute did change. Proxied copies of this object (on clients)
* will apply the value change when they received the attribute
* changed notification.
*/
public void setTestArrayAt (int value, int index)
{
int ovalue = this.testArray[index];
requestElementUpdate(
TEST_ARRAY, index, Integer.valueOf(value), Integer.valueOf(ovalue));
this.testArray[index] = value;
}
If you look closely, you'll see that setTestArray() calls requestAttributeChange() whereas setTestArrayAt() calls requestElementUpdate(). That's an important distinction because when you update a single element of an array, a different event is generated, the ElementUpdatedEvent and there is a different listener used to listen for such events, the ElementUpdateListener.
It is still possible to set the whole array all at once, which will generate an AttributeChangedEvent. One can also set just a single element at a time. Let's look at what some code might look like that does this:
ReversiObject revobj = // ...
revobj.setTestArray(new int[] { 1, 2, 3, 4 }); // triggers an AttributeChangedEvent
revobj.setTestArrayAt(99, 3); // changes 4 to 99 and triggers an ElementUpdatedEvent
Now let's look at the final type of distributed object attribute, which is also one of the most useful.
Distributed Sets
Frequently, when modeling a game, one will use an array to represent the game state. You can imagine our Reversi board being represented by an 8x8 array of ints which represent the color of the piece in that spot or some special constant to indicate that the spot was empty.
However, with such a modeling, you have to generate two events to represent the movement of a piece from one location on the board to the next. You have to first set the piece's previous location to the unoccupied constant and then set the piece's new location to the constant representing that color. This works, but it seems unelegant.
To the rescue come distributed sets. Instead of modeling the board, what we should really model are the pieces. We can create a distributed set which has one entry for each piece on the board and when we want to move a piece, we update the object that represents the piece, changing the location and then generate a single event that broadcasts the change to that piece.
Let's see how that looks in actual code. First we have to define the class that will represent the piece and then we define the distributed set that will hold the pieces. I'll show you all the code up front and then explain things bit by bit (note that we've deleted our earlier test attributes from ReversiObject):
import com.threerings.presents.dobj.DSet;
import com.threerings.parlor.game.data.GameObject;
public class ReversiObject extends GameObject
{
public static class Piece implements DSet.Entry
{
public int pieceId;
public int owner;
public int x, y;
public Comparable getKey () {
return pieceId;
}
}
public DSet<Piece> pieces = new DSet<Piece>();
}
First, notice that Piece implements DSet.Entry. Anything that is going into a DSet has to implement DSet.Entry. That interface defines one method:
public Comparable getKey ();
The key is an object that identifies the Entry. It is generally part of the entry but not the whole thing. The astute reader will realize that a distributed set is really more like a map, but the key is part of the value. Other elements of a set entry can change throughout its lifespan, but its key should never change. When a set entry is updated, the entry with the matching key is replaced by the new entry.
In this case there is no "natural" key for our pieces, so we'll just number them arbitrarily when we create the pieces at the start of our game.
Other important things to note about a distributed set entry are that it must have a zero-argument constructor so that the behind the scenes code can create instances of it when sending it across the network.
As you might expect, there are new setter methods and a new listener interface to go along with the distributed set. Let's see what those look like by saving our new ReversiObject.java and running good old ant gendobj:
public class ReversiObject extends GameObject
{
// AUTO-GENERATED: FIELDS START
/** The field name of the pieces field. */
public static final String PIECES = "pieces";
// AUTO-GENERATED: FIELDS END
public static class Piece implements DSet.Entry
{
public int pieceId;
public int owner;
public int x, y;
public Comparable getKey () {
return pieceId;
}
}
public DSet<Piece> pieces = new DSet<Piece>();
// AUTO-GENERATED: METHODS START
/**
* Requests that the specified entry be added to the
* pieces set. The set will not change until the event is
* actually propagated through the system.
*/
public void addToPieces (ReversiObject.Piece elem)
{
requestEntryAdd(PIECES, pieces, elem);
}
/**
* Requests that the entry matching the supplied key be removed from
* the pieces set. The set will not change until the
* event is actually propagated through the system.
*/
public void removeFromPieces (Comparable key)
{
requestEntryRemove(PIECES, pieces, key);
}
/**
* Requests that the specified entry be updated in the
* pieces set. The set will not change until the event is
* actually propagated through the system.
*/
public void updatePieces (ReversiObject.Piece elem)
{
requestEntryUpdate(PIECES, pieces, elem);
}
/**
* Requests that the pieces field be set to the
* specified value. Generally one only adds, updates and removes
* entries of a distributed set, but certain situations call for a
* complete replacement of the set value. The local value will be
* updated immediately and an event will be propagated through the
* system to notify all listeners that the attribute did
* change. Proxied copies of this object (on clients) will apply the
* value change when they received the attribute changed notification.
*/
public void setPieces (DSet<com.samskivert.reversi.ReversiObject.Piece> value)
{
requestAttributeChange(PIECES, value, this.pieces);
@SuppressWarnings("unchecked") DSet<com.samskivert.reversi.ReversiObject.Piece> clone =
(value == null) ? null : value.typedClone();
this.pieces = clone;
}
// AUTO-GENERATED: METHODS END
}
Three new methods have been added in addition to the standard setPieces(). These are: addToPieces(), updatePieces() and removeFromPieces(). addToPieces() does just what you might expect, it adds a new entry to the pieces distributed set. If an entry with the same key is already in the pieces distributed set, a warning will be logged and the new entry will not be added. If you want to update an entry that is already in the set, you should instead call updatePieces() which will replace the entry with the key that matches the key of the entry you pass to the method. Lastly, if you want to remove an entry you would call removeFromPieces(). Note that you pass just the key to removeFromPieces() not a whole entry.
To go along with these three distributed state manipulations there is a new listener and new kinds of events. The new listener is the Set Listener and the new events are the EntryAddedEvent, EntryRemovedEvent and EntryUpdatedEvent. We'll be making use of these a little later.
Before we get back to our game, let's take a quick look at what code might look like that manipulated a distributed set:
ReversiObject revobj = // ... Piece piece = new Piece(); piece.pieceId = 1; piece.owner = 0; // 0 is the black player, 1 is the white player piece.x = 4; piece.y = 0; revobj.addToPieces(piece); // generates an EntryAddedEvent // player moves that piece forward one square (you can't really do this in reversi but bear with me) Piece piece = revobj.pieces.get(1); // lookup by pieceId piece.y = 1; revobj.updatePieces(piece); // generates an EntryUpdatedEvent
Now let's see how we can put all of this good stuff to use in our own game.
ReversiObject
As we mentioned way back up at the start of this section, the only changing game state we need to model are the pieces and we've now learned that distributed sets are a great way to do this. We'll just clean up our previous example a bit and we're done:
public class ReversiObject extends GameObject
{
/** The index into the {@link #players} array of the black player. */
public static final int BLACK = 0;
/** The index into the {@link #players} array of the white player. */
public static final int WHITE = 1;
/** Represents a single piece on the game board. */
public static class Piece implements DSet.Entry
{
public int pieceId;
public int owner;
public int x, y;
public Comparable getKey () {
return pieceId;
}
}
/** Contains the pieces on the game board. */
public DSet<Piece> pieces = new DSet<Piece>();
}
Be sure to run ant gendobj one last time to make sure that all of the proper setter methods are generated.
Now that we've got the shared game state defined, we can move on to the more exciting process of displaying it with some fancy graphics.
Next step: Create the Game Display
Up to the Reversi Tutorial.

