Mummichog
From Gardenwiki
Mummichog is a variation of Memory that does not necessarily have a perfect solution. This is a sample program for the YPP Grand Crafting Puzzle Project. Single player only, but watchers can see the board.
Contents |
Mummichog Design Pattern Tutorial
This tutorial is primarily intended for those who are planning to write a single player game for GCCP. We assume that you have already downloaded the GameGardens package and have learned how to compile and execute the sample programs using whatever IDE you prefer. You should also familiarize yourself with the Reversi Tutorial. Download the Mummichog source files to use as a reference and example before continuing.
Generate the Skeleton Game
Use ant newgame to generate the skeleton for your new game. See Reversi Skeleton for more information on this.
Add/Edit Useful Tools
Most games get complicated very quickly. Adding the following tools to your game's package will help you stay a bit more organized.
Log.java
There are many different ways to implement logging. One is to simply copy Mummichog's Log.java file, and change the package name on it to match your game's package. At any point in your game, if something interesting happens, add a line similar to this one:
Log.info("..... explain what just happened....");
If you detect an error of some kind, you can use Log.warning() in a similar fashion. Logging various events simplifies testing and debugging. When running on your desktop, the log messages will appear in either your server or client window. Client log messages of games run from GameGardens can be found on the client's hard drive after closing the game, in toybox.log.
Codes Interface
As you write your game, various constants will be needed in a multitude of locations. Even worse, many programmers who write quickly will simply use secret numbers directly in the code instead of creating constants. The best way to treat all these constants is to create an interface similar to MummichogCodes.java. Every time you need a constant, add a line to your Codes interface, and make sure the class you are editing at the moment implements your Codes interface.
You will probably want to include something similar to this in your Codes interface:
/** The message bundle identifier for translation messages. */ public static final String MUMMICHOG_MSG_BUNDLE = "mummichog";
Please note that if you edit the values of the constants in your Codes interface, you should clean your project before using ant dist again.
Properties
Your game will already have a file with a .properties extension, found in rsrc\i18n. This file gives text translations of messages.
The first edits you will want to make to this file will be to add lines for your various lobby parameters. Any time you need a new lobby parameter, you will add it in your game's .xml file and then add at least one line in the properties file. mummichog.properties gives examples of range and toggle parameters. Here is an example of a choice box's set of parameters:
m.choice_difficulty = Difficulty: m.choice_Easy = Easy m.choice_Medium = Medium m.choice_Hard = Hard m.choice_Expert = Expert m.choice_Rough = Rough m.choice_Headache = Headache m.choice_Insane = Insane m.choice_Suicidal = Suicidal
If you are writing your game with no thought to later translation to other languages, then you probably don't need to use the properties file for anything other than the lobby parameters. It is a better practice to have any text that appears in-game be defined using the properties file, even if it does take a bit of extra work to do so.
For more information, see the MessageBundle tutorial.
Add Lobby Parameters
You probably already have some idea of what parameters you want players to set in the lobby. You can always add more later of course. We assume that you have already added the lines needed to your properties file. Find the .xml file with the name of your game (not build.xml). For a single player game, the table match section will need to be:
<match type="table"> <min_seats>1</min_seats> <max_seats>1</max_seats> <start_seats>1</start_seats> </match>
Add a line for each of the parameters you need. mummichog.xml gives examples of range (integer) and toggle (boolean) parameters. If you need a choice box, here is an example:
<choice ident="difficulty" choices="Easy,Medium,Hard,Expert,Rough,Headache,Insane,Suicidal" start="Easy"/>
Please note that if you use spaces between strings and commas, then equals() will not match what you think it should match.
Read the Lobby Parameters
Normally the lobby parameters are read in your Manager class's gameWillStart() method. MummichogManager.java gives examples of range and toggle parameters. Choice boxes return Strings.
Edit your Manager's gameWillStart() method to read your lobby parameters. For each parameter, add a Log.info() line giving the value read.
First Test
This is a good point to compile and test. Check the server window for the log messages. Do you see any errors? Are your lobby parameters being read correctly?
If you prefer to see the log messages in-game, instead of using Log.info(), you can use lines similar to these in your Manager:
String msg = build a String of what you want to appear in chat window
msg = MessageBundle.tcompose("m.result", msg);
SpeakProvider.sendInfo(_gameobj, MummichogCodes.MUMMICHOG_MSG_BUNDLE, msg);
Data Structures
While it is possible to put all the shared game information into a single object class, you will likely find it easier to use two or three. Mummichog uses these three classes for data structures:
MummichogTile
A MummichogTile object is a logical representation of a single tile on the board. It has fields for each of the data that a tile needs to remember about itself, and a constructor for easy initialization of a new tile. If the tile were being used in a game where tiles move on the board, then the object would need fields for row and column and methods such as moveLeft(), moveRight(), etc.
MummichogObject
The MummichogObject class is our game object. It contains all information that must be shared by the server, the player(s) and any watchers. Add each variable that you know must be shared for your game. These new variables need to go between these two lines:
// AUTO-GENERATED: FIELDS END
// AUTO-GENERATED: METHODS START
Every time you finish adding variables to your game object class, you need to run ant gendobj. This creates the matching fields and methods for the new variables.
MummichogBoard
MummichogBoard is an abstract representation of the game board. For Mummichog, it is only used by MummichogManager. There are other games where each client creates its own instance of the Board class, but only the server can make actual changes to the game object.
This class is where we initialize the (logical) game board, and where we keep the multitude of methods needed for making changes to the (logical) game board.
Server Initializes the Game State
In your Manager's gameWillStart() method, initialize your _gameobj and _board variables.
Note: You can have a board variable as part of your game object. You can pass your _gameobj variable to _board when you construct it. Do not try to do both.
Second Test
It is a good idea to add Log.info() lines to your Manager's gameWillStart() method explaining the initial values that you are adding, and then test again using ant server, ant client. Look at your server's output window, and check that you are generating reasonable starting values.
Getting the Initial Board to Display
The next steps involve changes in several classes. For this step, we just want the board to display correctly when the game starts, without any actions.
BoardView Displays the Initial GameState
For the moment, you probably want your BoardView class to only implement a few things:
public class MummichogBoardView extends MediaPanel
implements PlaceView, MummichogCodes {
We can add the others (AttributeChangeListener, SetListener, AnimationObserver) later. It is important to add the following line as the first line in the constructor of your BoardView class:
super(ctx.getFrameManager());
If you are creating a GCPP puzzle, then you will want to add this method:
public Dimension getPreferredSize() {
// The size of the puzzle part of the user interface
return new Dimension(450, 600);
}
The BoardView class normally uses a lot of images. Your image files should be placed in rsrc\media. This is a good time to write a method that will initialize all your images, and store them into an array. Use the initImages() method as an example.
Some games call the initImages() method in the constructor, or in willEnterPlace(). If you are creating any LabelSprites, then they will not initialize correctly at that point. If your initImages() method needs to read values from _gameobj, then those values are not yet available in willEnterPlace(). For Mummichog, the decision was made to call initImages when the tiles were first displayed, which is triggered the first time a tile is added or modified. This means that watchers will not see any tiles until the player clicks on a tile, then the entire board is painted.
For this point in writing your game, omit any LabelSprites, and if you need information from _gameobj in order to initialize your images, just specify a default value. Invoke your initImages() method in the constructor.
Third Test
You can test either of two ways at this point. Testing using ant server and ant client, will show you both the board view and the panel, with full access to your game object.
ant viewtest will show you just the board view. This is useful while you are determining where on the screen to place various sprites. Unfortunately, it has no access to the game object, and complains if you try to place sprites in the board view's constructor. One way to handle this is to create a setupBoardView() method in your board view that adds sprites without referencing the game object. Then, in BoardViewTest's initInterface() method, add this line:
_view.setupBoardView();
If your board is extremely complicated, it might be worth the effort to write a setupBoardView() method and then test using ant viewtest, while you repeatedly tweak the placement of various sprites and test again. If the lack of access to your game object makes this too complicated, just test using ant server, ant client. Mummichog has a simple layout and did not need to use viewtest. Nibbler has a very complicated layout. Here is an example of the setupBoardView() method used by Nibbler, while Nibbler is still developing the various ClueSprites.
public void setupBoardView() {
// add background
BufferedImage source = _ctx.loadImage("media/" + BACKGROUND);
BufferedMirage mirage = new BufferedMirage(
source.getSubimage(0, 0, 225, 200));
for (int row = 0; row < 3; row++)
for (int col = 0; col < 2; col++) {
ImageSprite background = new ImageSprite(mirage);
background.setRenderOrder(-5);
background.setLocation(col * 225, row * 200);
addSprite(background);
}
// add parrot
_parrot = new ImageSprite(_images[PARROT_WOOD]);
_parrot.setRenderOrder(3);
_parrot.setLocation(380, 400);
addSprite(_parrot);
// add six crackers
numCrackers = 6;
int x = 45;
int y = 400;
int wobble = 40;
int deltaX, deltaY;
for (int ii = 0; ii < numCrackers; ii++) {
deltaX = (int)(wobble * Math.random());
deltaY = (int)(wobble * Math.random() / 2);
CrackerSprite cracker = new CrackerSprite(x + deltaX,
y + deltaY);
addSprite(cracker);
_crackers.put(ii, cracker);
y = y + deltaY;
}
// add starting tiles, face down
int count = SIZE * SIZE;
int xx, yy;
for (int ii = 0; ii < count; ii++) {
xx = ii % SIZE;
yy = ii / SIZE;
ImageSprite sprite = new ImageSprite(_images[ii]);
sprite.setLocation(xx * TILE_SIZE + LEFT_OFFSET,
yy * TILE_SIZE + TOP_OFFSET);
sprite.setRenderOrder(0);
addSprite(sprite);
// note: later the ii will be tileID instead
_tiles.put(ii, sprite);
}
// add three chosen fruits at bottom
int fruitID;
// note: later we will look up _gameobj.boards.get(myPlayerID).chosenItems[ii];
for (int ii = 0; ii < 3; ii++) {
fruitID = 2 + 3 * ii + BANANA;
xx = ii;
yy = 6;
ImageSprite sprite = new ImageSprite(_images[fruitID]);
sprite.setLocation((int)((xx + .67) * TILE_SIZE * 1.5 + LEFT_OFFSET),
yy * TILE_SIZE + 10 + TOP_OFFSET);
sprite.setRenderOrder(2);
addSprite(sprite);
_chosenItems.put(ii, sprite);
}
// add marker sprite
pickUpFruit(1);
// add pictorial clues to top and sides
String[] cluesArray = {"rowD: adjacent mango",
"rowB: bothToLeft mango pomegranate",
"rowA: nonAdjacent", "rowC: palindrome"};
for (int ii = 0; ii < cluesArray.length; ii++) {
String thisClue = cluesArray[ii];
ClueSprite sprite = new ClueSprite(thisClue, _images);
sprite.addToBoardView(this);
_clues.put(ii, sprite);
}
}
Once you are confident that your background and any other initial sprites are loading correctly, we proceed to adding the tiles.
SetListener for Tiles
First write methods for handling the tiles. Mummichog uses three: addTile, updateTile, and setupTiles. If your game will have tiles being removed from the board, you will also want a removeTile method.
Next, we modify BoardView so that it implements SetListener. This involves more than just adding the word "SetListener" to the top of the class next to MummichogCodes. In willEnterPlace, add this line:
_gameobj.addListener(this);
In didLeavePlace, add these:
if (_gameobj != null) {
_gameobj.removeListener(this);
_gameobj = null;
}
Now add three more methods, similar to Mummichog's entryAdded, entryUpdated, and entryRemoved methods. If your _gameobj has more than one DSet that will trigger changes to the BoardView, then your entryAdded, etc. methods will be more complicated than Mummichog's.
Fourth Test
Compile and test, using ant server and ant client. Your initial set of tiles should now appear correctly, as generated by your Manager's gameWillStart() method and stored in your game object.
Add User Interaction
Reversi User Interaction explains how to add mouse listening. It is also possible to implement a KeyListener for keyboard controls.
Most game have some mouse events that just trigger changes to the board view, without contacting the server. You can test mouse listening at this point even for the events that are supposed to contact the server by adding Log.info() lines to the appropriate sections of the code triggered by the mouse events.
The next several steps make it possible for the board view to send messages to the server.
Add a Controller as Parameter to BoardView
In the board view constructor, add a parameter for your game's controller. Inside the constructor, save the controller to a local variable. You will need to add the local variable to the bottom of the board view class.
Modify Panel to use the New BoardView Constructor
Find the line in Panel that invokes the board view constructor, and pass the controller.
Add Methods to Controller
For each different type of message we want to send to the server, we need another method in the Controller class. For testing using ant viewtest, these methods can simply print messages saying that they were invoked. For testing using ant server, ant client, you will need a line in each Controller method similar to this one:
_gameobj.manager.invoke("--method--", argument1, argument2);
The String is name of the method that appears in your Manager class. Use whatever arguments are appropriate. Zero arguments, one, several - all are good. Normally the data type of each argument is kept as simple as possible: int, double, or String rather than an array or object.
Edit BoardViewTest
If you wish to continue to test using ant viewtest, you will need to edit the line that invokes the board view constructor. For Mummichog this would be:
protected JComponent createInterface (ToyBoxContext ctx)
{
return _view = new MummichogBoardView(ctx, new MummichogController());
}
Add Methods to Manager
For each different method recently added to the Controller, we add a corresponding method to the Manager class. For example, if this line is in Controller:
_gameobj.manager.invoke("submitClick", row, col);
then we need to add this method to Manager:
public void submitClick(ClientObject caller, int row, int col) {
BodyObject user = (BodyObject)caller;
.....
}
Please note that the first parameter is always "ClientObject caller", even if your game is a one-player game that will never need to check which user made the request.
Invoke Controller Methods in BoardView
Finally, we return to the BoardView class and find the sections of code where we would like to request that the server do something (normally change the game state in some fashion). Add a line for each request to be made that invokes the appropriate Controller method. For example:
_ctrl.submitClick(row, col);
Fifth Test
Test using ant server, ant client. Is the board view updated to reflect changes made by the methods in the Manager class? If not, check that you are listening correctly with SetListener and/or AttributeChangeListener.
Finishing Touches
At this point, you have a working game, and can upload it to GameGardens if you wish. However, you will probably want to include a panel or tabbed pane with one or more items for the players to look at. If you only add one, then you don't need a tabbed pane; just add the one directly to the sidepanel.
Tabbed Pane
Add a tabbed pane to your Panel class. It will be added to the sidepanel, below the label. You can decide whether to have the chat panel below the tabbed pane, or in one of the tabs.
_tabbed_pane = new JTabbedPane();
// more lines adding individual tabs here
sidePanel.add(_tabbed_pane);
How to Play tab
Add a method to the Panel class that will generate the information to go on a How to Play tab. Mummichog does this by having a text file named help.text. The following method will read the text file, which can then be placed in a textArea.
protected String loadInfo() {
ResourceManager rmgr = _ctx.getToyBoxDirector().getResourceManager();
StringBuilder builder=new StringBuilder();
Reader in=null;
try {
in=new InputStreamReader(rmgr.getResource("data/help.txt"),
"UTF-8");
char[] buf=new char[1024];
int chars_read;
while ((chars_read=in.read(buf))!=-1) {
builder.append(buf,0,chars_read);
}
} catch (IOException e) {
builder.append("Error: ");
builder.append(e);
} finally {
if (in!=null) {
try {
in.close();
} catch (IOException e) {}
}
}
return builder.toString();
} // end loadInfo
You can be fancier, and have HTML text in the text file, or in your properties file. This is just one very quick way to get the information to your players.
Next you add the How to Play tab to the tabbed pane:
// add our instructions/stats textarea
_info = new JTextArea(loadInfo());
_info.setMargin(new Insets(4, 4, 4, 4));
_info.setLineWrap(true);
_info.setWrapStyleWord(true);
_info.setCaretPosition(0);
_tabbed_pane.addTab("How To Play", new SafeScrollPane(_info, 0 , 50));
Detailed Score table
The detailed score table is very useful for debugging, and for letting players see exactly how many points each move they make generates. Copy the MummichogScore and MummichogScorePanel classes, then edit any mention of Mummichog to fit the name of your own game.
Edit your game object class to include this line, replacing Mummichog of course, and running ant gendobj afterward:
public DSet<MummichogScore> scoresSet = new DSet<MummichogScore>();
In your Panel class, before the How to Play tab, add this line:
_tabbed_pane.addTab("Move Scores",
new MummichogScorePanel(ctx));
Now, in your Manager class, add a method similar to this one:
/** record info about one specific scoring move */
protected void addScore(int score, String text) {
int key=_gameobj.scoresSet.size();
MummichogScore scoreObj = new MummichogScore((Integer)key,
key + 1,
score,
text);
_gameobj.addToScoresSet(scoreObj);
} // end addScore
Find each method in Manager that processes a player request, and decide if you want the results of that request displayed to the player. If you do, invoke the addScore method, passing the points for that request and the text you would like the player to see, explaining the reason for those points.
Top Scores List
Mummichog doesn't need a Top Scores list, but this is the time to add one, if your game would benefit from one. See the Top scores list tutorial.
Score Animations
Animations make games interesting for the player, and when missing any watchers are usually clueless about what is happening on the board. You can find tutorials on many different types of animations in the complete list of tutorials.
Mummichog uses a single object variable in the game object to trigger score animations. Here are the steps to do this.
1. Make an object class. Mummichog calls it MatchPraise, but it could be named anything that makes sense. Mummichog places it directly inside MummichogObject, but you could make a separate class just as easily.
public static class MatchPraise extends SimpleStreamableObject {
private transient int id = 1;
public int praiseID;
public int score;
public int row;
public int col;
public String text;
public MatchPraise() {}
public MatchPraise(int score, int row, int col, String text) {
this.praiseID = id;
id++;
this.score = score;
this.row = row;
this.col = col;
this.text = text;
}
}
2. Add a corresponding line to the game object, and run ant gendobj:
public MatchPraise matchPraise;
3. In Manager, in each method that processes a player request, if the request does something that should trigger a nice floating text animation on all the clients, add lines similar to following. Row and col are integers that will specify the tile on the board where the animation should appear (approximately).
String text = ... whatever you want to appear ....;
int size = ... the relative size of the text ....;
_gameobj.setMatchPraise(new MummichogObject.MatchPraise(size,
row, col, text));
4. In BoardView, add these lines to the bottom:
/** keeps ScoreAnimations spaced */
private AnimationArranger _anim_arranger = new AnimationArranger();
private Rectangle _board_bounds =new Rectangle(0, 0,
450, 600);
If you want your score animations to appear in a certain area of the screen, edit the _board_bound numbers.
Add a method similar to the following to BoardView:
// creates a ScoreAnimation near the row and column
protected void showPraise(int row, int col, int score, String praise) {
int x = col * TILE_SIZE + LEFT_OFFSET;
int y = row * TILE_SIZE + TOP_OFFSET;
int fontsize = Math.min(12 + 2 * score, 56);
Label la = ScoreAnimation.createLabel(praise,
Color.WHITE,
new Font("Arial", Font.BOLD,
fontsize),
this);
ScoreAnimation sa = new ScoreAnimation(la, x,
y, 2000L);
addAnimation(sa);
_anim_arranger.positionAvoidAnimation(sa,_board_bounds);
}
If your BoardView does not yet implement AttributeChangeListener, you need to do so, adding an attributeChanged() method. If you already have one, just add another branch for MATCH_PRAISE, similar to the one here:
public void attributeChanged(AttributeChangedEvent event) {
if (MummichogObject.MATCH_PRAISE.equals(event.getName()))
{
MummichogObject.MatchPraise thisPraise =
(MummichogObject.MatchPraise)event.getValue();
showPraise(thisPraise.row, thisPraise.col,
thisPraise.score, thisPraise.text);
}
}
When you test the game again, both the player and any watchers should see the score animations praising (or scolding) for moves made.

