Multistage Developer's Guide | ![]() |
Introduction
The main feature of multistage is that the creation of new extensions should be quick and painless. In this README, all the resources needed to make extensions will be outlined. The main concepts of the multistage framework as well as the common implementations are covered in the Server-Side and Client-Side sections. Other topics such as asynchronous games, and other advanced topics have their own sections.
Prerequisites
In order to build Multistage extensions, a mixture of tools are needed.
- The Multistage source tree
- The API Documentation for Multistage
- Java tools (compiler, IDE, etc.)
Server Side
The class files for the server implementation are located in multistage.server.control.gamecontrollers.* There are numerous examples already in this directory to use as a guide. The source code of these extensions is not perfect and might even be horridly wrong, but it can still be useful for comprehension.
Server Basics
The server-side class has several properties:
- It extends ServerControl (located in edu.caltechUcla.sselCassel.projects.multistage.server.control
- It must follow the naming convention %GameName%Control. This is hard- coded into the Multistage framework and is absolutely necessary.
Server Methods
This section covers the methods to override in the extension of ServerControl
initialize: This method is the place to assign variables. Generally, what you would normally put in the constructor, you should put here instead. There shouldn't be anything in the constructor. Initialize normally uses the accessing methods (described later) to retrieve information from a configuration file for later use during the round.
runSequence: This is where the bulk of the extension will be. This is where information objects are created and sent to the client. It is also where information is processed after the client has moved, and payoffs are assigned. The tasks done in this methods are outlined in later sections.
respondToChat: This helps handle chatting (discussed later) on the server side.
processRound: This is only used in asynchronous games. In an asynchronous game, this is where the bulk of the actions will be performed including sending/receiving information objects as well as determining when to advance a round. See the section on asynchronous games for more information.
Configuration Files
In addition to the classes necessary for the extension, the extension must have a configuration file. There is not strict convention for the file name or location, but the location must be on the classpath. The name/location for the configuration file is read from the parameters_home key from the mult-server.properties file (usually located in dist/conf). The format of the configuration should be similar to the following (in ASCII plaintext):
# indicates a line to ignore, usually for comments
# global parameters do not have any preceding text
#match specific parameters start with match.%roundNumber%
globalparameter = value
match.roundNumber.matchspecificparam = othervalue
match.roundNumber.etc = something
roundNumber ranges from 0 to maxRounds. If roundNumber of 0 is specified, it is the initial condition.
Now that there is a configuration file with keys associated to values, there
are many accessing methods to extract these values from the configuration
file. Each method has two standard forms:
get%Type%Property(String prop): where Type is the expected return type
--and--
get%Type%Property(String prop, defaultValue) This is the same as
above, with defaultValue as the value if it is not found in the configuration
file.
A full list of accessor methods:
- getBooleanProperty
- getFloatProperty
- getIntProperty
- getStringProperty
Client/Server Communication
Introduction to Information Objects
Information Objects compose the bulk of the communication between the server and the clients. The Information object is wrapped around a hashtable; it is filled by using the add%Type%Info method. All of these methods are outlined in the Information object API documentation, but follow the form:
add%Type%Info(String key, Object ) where %Type% is the store type
get%Type%Info(String key) where %Type% is the return type
An important thing to note is that although a Type is passed in, when the information is stored in the hashtable, it does not retain its type information. Thus, it is crucial to specify compatible types when storing and retrieving information.
Get Methods:
- getInfo
- getBooleanInfo
- getFloatInfo
- getIntInfo
- getStringInfo
Corresponding Set Methods:
- setInfo
- setBooleanInfo
- setFloatInfo
- setIntInfo
- setStringInfo
Passing Information Objects
The Information object is passed to the client side through a number of methods. Specifically, the methods are:
For Synchronous:
askAllPlayers(Information[]) - sends Information[i] to player number i.
askPlayer(Information, int partnerNum) - sends a single Information
object to player number parterNum.
For Asynchronous:
Asynchronous has its own section, but the method is called:
askAsynchronous(Information []) - sends Information[i] to player number
i as indexed by ServerControl.
Asynchronous games can have a time limit, which is discussed in the
Asynchronous section.
Client History Panels
These objects are inserted into the GUI of the client (discussed later).
Once they are added to the client side, they are accessed through methods
on the server side of the form:
addColumnData( int partnerNum, String column, String data [, Color color])
Note: the color parameter is optional.
This will set the header "column" with the value of "data." If a column does not already exist by that name, a new column will be created. This means that in order for data to appear in continuous columns/rows, the headers need to be identical (case sensitive). A common request is to start out with a blank row before a round has occurred. To do this, simply write to the specified columns with an empty String "". The first round will overwrite this and continue correctly.
Messaging:
It is often desirable to send messages to the players to make announcements or give reminders. To do this, use one of the following methods:
- displayMessag - sends a message immediately to a single player
- displayUniversalMessage - sends a message immediately to all players
- displayMessagePostRound - stores the message to be sent after the current round is over for all other ServerControl objects.
Each of these has several method signatures, which correspond to various situations. The decision to use a particular signature of these methods is beyond the scope of this document. Please consult the API Documentation for ServerControl.
Chatting
Many games require players to be able to communicate amongst themselves to cooperate or compete. Multistage has a built in customizable chat system. The methods used to control the chatting system are:
- activateGroupChatRouting: allows everyone to talk to everyone else
- clearChatRoutingTable: removes all chat routes
- insertChatRoute(int sender, int receiver): allows sender to talk to receiver but does not necessarily allow receiver to talk to sender.
- insertChatRoutingTable( Vector[] ): given an array of Vectors, this will allow player #i to talk to the players corresponding to integers in Vector[i].
Surveys
Many games need to question subjects to obtain enough information for the experiment to be complete. There are 28 method signatures to choose from for this purpose. For intimate details on each of these wonderful methods, consult the friendly API Documentation. Note that all of these questions automatically record data to the output file (unless testing for correctness). Here is short rundown of the signatures to choose from:
- questionSubject: sends a Vector of questions to a single subject
- questionSubjects: sends a Vector of questions to all subjects
- questionSubjectsPostMatch: stores a Vector of questions to be answered after the current match
- questionSubjectsPostRound: stores a Vector of questions to be answered after the current round
Each method has its own specification, but some features to notice are:
- Timelimit: a timelimit can be set
- Blocking: This specifies if the round/match can continue while the question is being asked. If blocking is set to true, then a game will pause while the question is being asked. If blocking is false, the game will continue.
- Customized Button/Label texts: The labels and buttons are fully customizable with the proper method calls.
The question Vectors are constructed from Question objects. Some question types to investigate are (found in multistage.shared.data):
- SliderQuestion
- TextQuestion
- EssayQuestion
- DegreeQuestion
- ChoiceQuestion
Administrative Actions
This section covers administrative actions such as assigning payoffs, setting the number of rounds, shutting down the experiment, and out- putting data to the output file.
Assigning Payoffs
Two methods are used to assign payoffs to players.
- addPayoff( int partnerNum, float payoff) - gives a single payoff to a player
- addPayoffs( float[] payoffs) - gives each member of a float array to each player according to partnerNum.
Setting the Number of Rounds (and other configuration file tricks):
This achieved in the configuration file described earlier. The parameters
that are important for administration of the experiment are:
NumOfMatches = number of matches that the experiment will run
## Match specific parameters start with Match.%roundNum% ##
Match.0.groupSize = how many participants are in a group
Match.0.maxRounds = number of rounds per match
Match.0.exchangeRate = exchange rate for each round.
Note that these properties can be changed from round to round by specifying the round number like so:
Match.1.groupSize = 2
Match.2.groupSize = 4
## Et cetera.
Aborting the round
The extension writer has little to do with this in staged games, however, there are some issues in the code that will be necessary to make certain that everything aborts correctly. Never catch AbortRoundExceptions. If you wish to catch other Exceptions, use a double catch clause like thus:
try{
//code
}catch(AbortRoundException abort){
throw abort;}
catch(Exception e){ do something }
Asynchronous is treated in its own section.
Outputting Data to the output file
To output data to the output file, use the methods:
- addOutput group specific, round specific output
- addMatchOutput group specific, whole match output
See the API Documentation for specifics on the parameters and the XML output format used by the outputService. Just remember that questions can automatically write to the output file, one only needs to record player interactions.
Client-Side
The Client-Side is controls all interaction with the user and is responsible for collecting the data which is then sent to the server. The class files for the client implementation are located in multistage.client.interfaces.gameinterfaces.* Once again, there are numerous examples already in this directory to use as a guide, but don't take them to be exactly correct.
Client Basics
The Client-Side is largely GUI building and manipulating Information objects, but there are several properties that the client class must have.
- It must have a class name of the form %GameName%GUI. This is the convention used up until this point.
- It extends ClientGUI (located at edu.caltechUcla.sselCassel.projects.multistage.client.interfaces.ClientGUI)
Client Methods
constructLayout: create the layout of the GUI, usually using input from the Information object passed by the server.
initialize: pulls in variables from the information object and initialize any variables needed. This method is for the same purpose as its counterpart in ServerControl.
constructWaitingLayout: construct the layout for the waiting screen
processMessage: used to change the default message processing. For example to send a postRound message, one could use the sendUpdatePostRound method. It is important to note that no matter what is put in this function, it should always super(arguments) as it's default.
testingProcedures: This method is for use in integrated testing. It is called to determine the response of the client for testing. Generally, random data generation occurs here to make sure that the data is safely processed.
respond: for asynchronous (will be discussed in the Asynchronous section)
close: perform any actions to shut down the client. Generally, this is used to remove any actionListeners or GUI components that need to be garbage collected.
reauthenticate: used to restart a client when a failure occurs. This is almost never used for staged games, as the default procedure restarts most staged games correctly. This is important to asynchronous games (and will be discussed in the asynchronous section).
Thread Safety Precautions
If Swing is used for the GUI, there is a specific procedure to use to make
sure that Swing does not crash due to simultaneous transactions. There is about
a 5% chance that these errors will occur if this procedure is not followed, and
generally the errors are incredibly hard to track down. However, if the following
procedure is used, no error can occur.
Use this format:
Runnable doUpdate = new Runnable() {
public void run() {
// code to set up GUI
}
}
SwingUtilities.invokeAndWait(doUpdate); // or invokeLater(doUpdate)
Sending and Receiving Information Objects
Information objects were covered on the server side. There are only a few minor comments on how to handle them on the client side. Generally, GUI events are what one wants to record. A large portion of games add information during the actionListener section of the code. Once all this information is collected, it can be sent back to the server at the end of the round by calling makeMove(); makeMove returns true if the Information was successfully sent and false if there was a problem. In a unistage game, there is a single call to makeMove at the end of the round. In a multistage game, there can be multiple calls back and forth between the client and server. However, for each call to askAllPlayers (or equivalent methods) there is exactly one makeMove. This is what is meant by staged games being one-to-one transactions. The functions inside ClientGUI responsible for adding data to the information object passed back to the serverside are:
- addInfo
- addBooleanInfo
- addFloatInfo
- addIntInfo
- addStringInfo
These are local functions built into the ClientData object, and do not require the extension to make an Information object to return.
GUI Addons
History Panels: To include the history panel in the client GUI, add a ClientHistoryPanel to the GUI where it is needed. By default the ClientGUI object contains an instance of this object called clientHistory. It can be used by calling:
getContentPane().add(clientHistory);
In order to update a ClientHistoryPanel manually (i.e. before a round is over),
use the method:
updateHistory (see the API Documentation for details).
Chat Panel: To allow clients to chat with each other, a ClientMessagePanel should be added to the GUI layout. The ClientGUI has a built in instance of this object called clientMessages. All the extension writer needs to do is call
getContentPane().add(clientMessages);
Asynchronous
Asynchronous Introduction
Asynchronous is an entirely different model from staged games and generally requires much more sophisticated programming and implementation. It is also so flexible that there can be no real 'guide' to asynchronous games as each game can be radically different from all others.
Asynchronous uses the call method askAsynchronous. This method is different from the askPlayer (and equivalent) methods as before it returns, it calls processRound to handle information. processRound is to be overridden in the extension to communicate between the client and server. The necessary functions are:
Server Side
send - sends a message to the client
receive - wait for messages from the client
Client Side
send - sends a message to the server
respond - this is the message handler for the client side.
It is called every time a message is received on the client side.
As usual, for the actual method signatures, please consult the API
Documentation.
Administration in Asynchronous
Reauthenticating in Asynchronous
While most staged games can be restarted and reauthentication is not needed, asynchronous does not have that feature. The default procedure in the case of a failure is to restart the whole round. This can be very bad for games with long rounds or games that don't want to restart. The way to deal with this is to override the reauthenticate method on the client side. The procedure for dealing with a round failure will be different for each game, but generally Information objects are sent back and forth. In order for this custom method to be called, the asyncReauthMode parameter must be set to 'manual'
Ending the Round in Asynchronous
To end the game before it has reached the number of rounds specified, one can either throw an AbortRoundException, or call abortRound on the server side.
Exceptions in Asynchronous
AbortRoundExceptions should not be processed by the extension. Instead they should always be sent to the super class. See the Section "Aborting the Round" under the server section of this document for more information on how to handle this.
Timelimits
Asynchronous games support time limits. The only change that needs to be made is to look for a TIME_ELAPSED_MESSAGE when processing the messages.
Advanced Topics
These are topics generally unneeded in most games that require some extra effort and precaution to implement.
Sharing Info across groups/matches:
There are two Hashtables that resides in ServerVariables that are specifically for this use:
- sharedData: this stores data that can be accessed by all groups. It is flushed between matches, so it is not suitable for cross-match data storage.
- experimentData: this stores data that can be accessed by all groups and is maintained between matches.
There are a few precautions when using these hashtables, as when they are used improperly, it will result in thread errors. The proper, thread-safe way to be sure that sharing violations do not occur is to follow a simple procedure:
- always put in data before ask* methods
- always retrieve data after ask* methods
Parsing
Sometimes messages and other elements need customization, such as inserting player names or extra descriptors. This can be achieved by using the built in parsing capability of Multistage.
General Parsing
Parsing replaces a particular key expression with a corresponding value.
The implementation chosen for Multistage is to have the keys in the form
of a key-pod. A key-pod is an expression of a key surrounded by '%' For
example, %this% is a key-pod.
Further implementation details (these apply to both server and client side):
- A key-pod will be ignored if it is preceded by a \. For example, this \%key-pod% is ignored. This is intended to be used to write %'s without being interpreted.
- If a key-pod is not found in any of the resources available, the key-pod will not be replaced.
Server-Side Parsing
Parsing can be done on server side using the parseString method. The server-side has two information sources to search: the configuration file and a Hashtable passed in. The default order for searching is to search the Hashtable first, and then resort to the configuration file only if it does not find it in the Hashtable. If no Hashtable is passed into the parseString method, then it defaults to searching the configuration file.
Client-Side Parsing
Parsing can also be done on client side using the parseString method. Unlike server-side parsing, the client-side has only one source of information. All keys must be passed in as keys in the Information object sent in the ask* methods. Also, unlike the server-side, there are several key-pods that are reserved for system use (or rather, they take precedence over any user defined keys of the same name). These keys are currently:
- id: the ClientData ID
- groupSize: the group size
- partnerNum: the partner number index
Parsing Danger
There is no implementation in the parsing software to stop a spinlock. This means that if a key-pod points to itself it will not break from it. For example, don't set the following:
#conf file or hashtable
first = %first%
Since the process is iterative, it will not stop parsing. This will not be a problem unless you make it a problem. There is no legitimate use.
Appendix
This section is to explain some concepts of game play and how it relates to writing the extension.
ROUND / MATCH:
A match consists of one or more rounds. A round is conducted by a
ServerControl object (the extension extends off ServerControl) within
a group.
GROUP:
The extension (a ServerControl object) administers a group. Multistage
will run as many Extension objects as necessary to accomodate the number of
groups. Groups are constant throughout all of the rounds of a match, but can
change between matches.
THREAD SAFETY:
Multistage uses a large number of threads. This makes it easier to manage,
but requires some precautions to make sure that two threads aren't writing
to the same space at the same time. Specific procedures are outlined in the
README.
FLOW:
Flow is the termed used to describe the organization and order of
advancement. Currently, two types of flow are possible in multistage. The
players can advance synchronously (staged) or asynchronously.
Synchronous (Unistage):
All the groups begin each round at the same time. This means that if a
group finishes a round before the other groups, that group will wait until
every other group is finished. In general, a single Information object is
sent to each client and a single Information object is returned (containing
the events of the round). Importantly, there is a one-to-one relationship
between the number of Information objects sent and received, and in the case
of Unistage, there is only one Information object in each direction.
Synchronous (Multistage):
This is identical to synchronous unistage except that more than one
information object can be sent back and forth. It still will maintain
a one-to-one relationship between objects sent and received.
Asynchronous:
This might be considered laissez faire. With synchronous, there is a one-to-
one relationship between Information objects sent and received. This is not
so with asynchronous. Asynchronous allows any number of Information objects
sent in either direction. Objects will be sent back and forth until the
certain conditions are met, so the extension writer has complete control
over the behavior of clients.


