Mummichog

From Gardenwiki

Jump to: navigation, search

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.

Links

Personal tools