Debugging
From Gardenwiki
Testing and debugging of GameGardens games can be tricky, due to the high degree of interaction between the various classes before you can even get the initial board to show. Here is a collection of techniques that can be used while tracking down the elusive bugs.
Contents |
Logging
The com.samskivert.util.Log class provides utility for logging game internal information on the default output or into a file.
The source code to Tasty is in Subversion and may be downloaded from svn://samskivert.com/games/trunk/tasty .
The com.samskivert.tasty.Log class provides the log methods for the "tasty" module in a static way. TastyManager invokes methods from this Log class in several places. Here is an example:
try
{.... processing of an array element here .....
} catch (ArrayIndexOutOfBoundsException aioobe) {
Log.warning("Array out of bounds " +
"[event=" + evt +
", turnHolder=" + _tobj.turnHolder + "].");
}
TastyPanel also invokes methods from the Log class, such as:
public void willEnterPlace (PlaceObject plobj)
{
Log.info("Panel entered place.");
}
Each Log.info() or Log.warning() call appends a line to the output window of the appropriate thread (server or client) which you can see when running the project locally. If a bug is preventing your panel from loading correctly, then the easiest way to trace the cause is to scatter Log.info() calls liberally in GameManager, GamePanel, and GameBoardView, then examine both the server and client output windows.
The default log level shows only Log.info() messages and above. Please complete if you know how to enable Log.debug() messages
Note that when the game is played with the GameGardens server, the client-side log file can be found under $HOME/.toybox/toybox.log . (Under windows, the $HOME directory is usually C:\WINDOWS.)
For more information on logging, see samskivert Log API
If you have relevant experience using logs, please add to this section!
Panel JLabel, JTextArea
It is often easier for testing to directly show the feedback messages on the game panel than to change windows to check a Log file.
Mummichog has two variables in its MummichogPanel class that are helpful in debugging:
protected JTextArea _info; protected JLabel _status;
In the MummichogController class, the handleSubmitClick() method gives ways to pass messages from the MummichogManager class to the MummichogPanel:
public void handleSubmitClick (Object source, final String click)
{
MummichogService.ResultListener rl = new MummichogService.ResultListener() {
public void requestProcessed (Object result) {
String msg = "Submitted " + click + ". Result: ";
msg = msg + result.toString() + "\n";
_panel._info.append(msg);
}
public void requestFailed (String cause) {
_panel.displayStatus(cause);
}
};
_myobj.service.submitClick(_ctx.getClient(), click, rl);
}
Notice that when requestProcessed() is called, the result string is appended to the _info JTextArea. When requestFailed() is called, the MummichogPanel's displayStatus() method is invoked. That method simply changes the text that shows on the _status JLabel.
Keeping this in mind, we now look at MummichogManager's submitClick() method. This method builds a reply string, and passes it to the controller:
rl.requestProcessed(reply);
This is the message that will be appended to the _info JTextArea. If there is a problem found while processing the click in the submitClick() method, we use lines similar to these:
if (!_myobj.isInPlay()) {
throw new InvocationException("m.game_already_ended");
}
This sends "m.game_already_ended" as the cause to the controller's requestFailed() method. This is translated by the panel's displayStatus() method, and the result becomes the new text on the _status JLabel.
Using the combination of _status and _info, we can pass an enormous amount of relevant information discovered in the MummichogManager's submitClick() method to the panel for display while the game is being played.
Please note that only the client that submitted the click will be able to see these changes to the panel's _info and _status.
Chat Window Messages
Another technique that a GameManager can use to relay information to players while a game is in progress is to send a chat window message. Here is an example from MummichogManager's checkForEndOfGame() method:
String msg = "" + _myobj.score;
msg = MessageBundle.tcompose("m.game_over_score", msg);
systemMessage(MummichogCodes.MUMMICHOG_MSG_BUNDLE, msg);
This sends a message that every player and watcher can see. To send any string to chat, you can use these lines:
String msg = .... build a complicated String here ..... ;
msg = MessageBundle.tcompose("m.result", msg);
systemMessage(MummichogCodes.MUMMICHOG_MSG_BUNDLE, msg);
This last set of lines can easily be added to any of the GameManager's methods that are invoked after the chat panel is visible, such as gameWillStart(), submitClick(), etc.
One of the newer additions to GameManager has a less awkward way to broadcast messages in the chat window. The easiest way to use it is to add a method similar to this one from BigTwo and Nibbler:
private void message(String key, Object... args)
{
String msg = (args.length == 0) ? key :
MessageBundle.tcompose(key, args);
systemMessage(NIBBLER_MSG_BUNDLE, msg);
}
Then anywhere in your Manager that you wish to send a message to the chat window, add a line similar to this:
message("m.replacing_player", oplayer, nplayer);
Step by Step Debugging
If you use Eclipse or NetBeans as a development envrionment, you can use the "debugging" functionalities to set breakpoints and step through your code, either on client or on server side.
Using a Test Class
If constructing a valid initial board for your game is extremely tricky, it is useful to create a separate object class with just the code for getting a valid random board. The Parfait game is a good example of this: first a random valid solution was generated, then a set of valid clues which had to be just enough clues that the solution was uniquely determined, and finally those clues had to be translated into the specified language. Three extra classes were involved in this, two object classes and one static class.
A test class was written that acted as a driver for these extra classes. This test was changed several times, as various parts were being tested. The general idea was to loop hundreds or thousands of times, creating objects and checking for validity, then exit the loop and output copious information if an invalid board was generated.
This type of test class is also useful in establishing benchmarks for server load. If constructing the initial board takes more than 200 milliseconds on average, it is too much to expect the server to handle and the code needs to either be streamlined or some tasks moved to the client. The final version of Parfait took less than 20 milliseconds to construct the initial board, and averaged about ten.