Chapter 13

Inside WordSmith

At this point in the book, you've explored various concepts for building channels and examined the source code to various small examples that demonstrate those concepts. But sometimes it's difficult to figure out how to combine those smaller concepts into a larger one. For just that reason, in this chapter you'll finish up the meat of this book by exploring a complete, running example of a complex channel. The channel in question is a game called WordSmith. WordSmith combines Bongo presentations and very sophisticated custom widgets with Java channel code to handle automatic updates, save local data, and uses a transmitter plug-in as well. We won't cover all aspects of this channel here—just those that relate specifically to channel development—but you can explore the source code on your own to get a feel for how the channel was put together.

WordSmith was written by Carl W. Haynes III, a Marimba employee who has written a number of excellent channels (including the extremely popular SameGame channel). He has graciously released the source code for other channel programmers to see. You can get this source code on the CD-ROM for this book, or from the Marimba Web site at http://www.marimba.com/developer/samplecode.html. The version I discuss here—the one that comes with this book—is the version current as of this writing; by the time you read this, WordSmith may have new features or updates added to it, and to see and explore those, you'd need to get the source code from the Web site.



Note

There are two versions of the Wordsmith software: one with a dictionary file, and one without. You'll need the dictionary to play the game; but if all you're interested in is the source code, you can download the smaller of the two versions. The version that comes on the CD-ROM with this book has the dictionary.


The Game

The first step towards understanding how any channel or program works is to play with the final version (or, if the program isn't written yet, to have a general plan for how the program will operate).

When you subscribe to and start the WordSmith channel, the main WordSmith window appears and a new game is set up. Figure 13.1 shows that initial window.

Figure 13.1. The WordSmith channel.

Game play works like this: You can drag and drop tiles from the tops of the columns in the main part of the screen to the top of the screen to form words (you can only use the letters at the top of each column; the letters underneath are unavailable until uncovered). Once you've spelled a word of at least three letters, click the Check button. WordSmith compares your word against a dictionary, and if you've created a real word, those tiles are cleared and you're given a score for that word. (Scoring works exponentially based on the number of letters in the word; you don't get extra points for the Zs and Qs like you do in scrabble.) You can also choose the Clear button to put the letter back in the columns where they were before.

Continue building words until you've used all the letters on the screen (you can also create nonsense words to get unusable letters out of the way, but you won't get any points for those letters).

The part of WordSmith that makes it more than just a simple game is the management of high scores. WordSmith keeps track of two lists of high scores: a local list for the version of the channel running on your system and a global list for everyone who is running that same channel. The WordSmith channel keeps track of the global high scores through the use of channel feedback and updates so that the global high scores will always reflect the top WordSmith players in the world (if you feel particularly competitive about it).



Note

The WordSmith channel has a lot in common—both in game play and in underlying infrastructure—with the SameGame channel, which I mentioned earlier on in this book as an example of a channel to play with. (It's available from trans.marimba.com.) If you've played SameGame, you'll most likely recognize many of the same features.


Channel Organization

Let's start exploring the WordSmith channel by looking at the channel directory and its organization. Figure 13.2 shows the contents of the WordSmith channel directory.

Figure 13.2. The complete WordSmith directory.

The channel directory contains the following files and directories:

An important aspect of this channel organization: the Java class files for this channel are not arbitrarily placed in the wordsmith directory. Instead, that directory is serving as a package for all the WordSmith code; each of the Java files inside the wordsmith directory are defined to be part of the wordsmith package. For the most part, this doesn't affect how you develop your channel; it's simply a good habit to get into as your create groups of interrelated classes.

The Presentations

WordSmith uses a number of Bongo presentations for the various windows and dialogs in the game. Each of these presentations is stored in the top-level channel directory as a .gui file. Those presentations are

I'm not going to go over the contents of each of these presentations here; you can use Bongo to view them and their scripts. The main presentation—wordsmith.gui—however, is worth at least a glance, as it uses a custom widget called WordSmithWidget.

In fact, if you open the wordsmith.gui presentation and browse it, you'll notice that most of the basic functionality of the game is contained in this presentation through the WordSmithWidget class. The tiles are created and laid out in columns, and you can move them to the rack at the top of the screen or choose the Check or Clear buttons to register the word or to reset the tiles back to normal.

The wordsmith.gui presentation is, for the most part, just a display surface for WordSmithWidget. WordSmithWidget is the heart of the WordSmith application and controls the vast majority of the game play, including choosing and laying out the tiles, animating the tiles when you move them, handling checking your word against a dictionary file, and keeping track of the score as you play.

The code for this widget is contained in the Java file WordSmithWidget.java, stored in the wordsmith directory. This large file is worth examining, particularly from the standpoint of creating sophisticated custom widgets (a topic that is outside the scope of this book; see the Official Marimba Guide to Bongo for more information). I won't go into detail on this file, but there are a few parts I do want to mention.

The WordSmithWidget code uses two utility classes, Rack and Dict, the source of which is also contained in the wordsmith directory. Rack keeps track of which tiles can be moved and how many tiles are left in the game; Dict manages the dictionary and does the testing to see if the word you've spelled is actually in the dictionary. It also has a handle to an instance of the class HighScoreManager for dealing with high scores—we'll go into this class later on in this chapter.

One of the more important parts of the WordSmith widget, which links it to the rest of the application is the checkForWin() method, which is called periodically during the game to check to see if the game is over yet. Listing 13.1 shows the code for this method

Listing 13.1 The checkforWin() method in WordSmith widget.

 1:public void checkForWin() {

 2:   if (tilesLeft > 2)

 3:      return;

 4:   try {

 5:      int place = -1;

 6:      if ((place = highScores.place(score, HighScoreManager.LOCAL)) != -1) {

 7:          new HighScoreFrame(getFrame(), new URL(getPresentation().getBase(),"highscore.gui"),

 8:              highScores, score, getPresentation()).show();

 9:       }   

10:      else {

11:         new PlayerDialog(getFrame(), new URL(getPresentation().getBase(),

12:             "youwon.gui")).show();

13:      }   

14:   } 

15:   catch (MalformedURLException e) {

16:      e.printStackTrace();

17:   }

18:}

Here's a general overview of what's going on here:

You see how the checkForWin() method connects the WordSmith custom widget to the rest of the channel as this chapter progresses.

Managing High Scores

A significant part of the WordSmith application is dedicated to the management of high scores: reading them, storing them, displaying them in their own window, testing to see if a current score qualifies as a high score, and logging them back to the transmitter for the plug-in to process.

The WordSmith application keeps track of two sets of high scores: a local set, for the local version of the channel; and a global set, which contains scores for everyone who is subscribed to the WordSmith channel. When you win at WordSmith, the application tests to see if your score counts as a high score in either the local or global lists and stores your score, accordingly. If you've been good enough to merit a spot in the global list, the application logs your high score information to be sent to the transmitter at the next update. WordSmith also provides a way for you to display the current lists of global and local high scores.

There are four class files for managing and displaying high scores:

Let's start with the HighScoreManager class because that's the central class to manage high scores.

The HighScoreManager Class

The core of high-score management is the HighScoreManager class, which is responsible for

Let's work through the basic life cycle of the HighScoreManager. When the class is first instantiated as the application starts up, the HighScoreManager creates Vector objects to store the local and global high scores and initializes them with empty HighScore objects. Then, two methods are called: readLocalHighScores() and readGlobalHighScores().

The local high scores file is a file called highscores.txt and is contained in the local data directory for the channel, so the readLocalHighScores() method uses get getDataDirectory() method to locate it. The remainder of the readLocalHighScores() method simply opens that file (if it exists), and parses the scores into a HighScore object and then inserts that object into the vector in the right spot. (All high scores are sorted by score, highest to lowest.)

The readGlobalHighScores() method performs essentially the same operations as readLocalHighScores, except that the global high scores file is part of the channel's data; you'll use context.getBase() to locate that file and read its values into the vector for the global scores.

With the initialization of the existing scores files done, the HighScoreManager class doesn't do anything else until the game has been played and a new score is available. WordSmithWidget, given a new score, uses the HighScoreManager's place() method to determine whether a score counts as a high score or not. All place() does is determine whether a score can be placed in the high score list (if the score is higher than the lowest score in the list). If it can, the widget uses HighScoreFrame to get the user's name and address, and HighScoreFrame in turn calls the insert() method to actually put the new score into the high score list.

The insert() method in the HighScoreManager is shown in Listing 13.2:

Listing 13.2. The insert() method.

 1:public void insert(HighScore hs) {

 2:   int i = place(hs.getScore(), LOCAL);

 3:   if (i != NOT_IN_TOPTEN)

 4:       insertAt(hs, i, LOCAL);

 5:   

 6:   i = place(hs.getScore(), GLOBAL);

 7:   if (i != NOT_IN_TOPTEN) {

 8:       insertAt(hs, i, GLOBAL);

 9:       logGlobal(hs);

10:       context.update();

11:   }

12:}

13:

There are two steps in the insert() method: first, to insert the score in the local high score list, insert() calls place() to find out where to insert the score (line 2), and then calls the insertAt() utility method to actually put it in that spot (line 4). The second step is to do basically the same thing for the global high score list: find out where to place it, and if it actually merits entry in the global high scores (which are generally higher than the local high scores), then it does three things:

  1. Uses insertAt() to insert the score in the global high score list (line 8).
  2. Calls a method called logGlobal() (line 9), to log the score as feedback data to the transmitter (the logGlobal() method simply calls appendLog() with a string representation of the score).
  3. Calls the context.update() method (line 10), which forces an update of the channel to send the new global high score to the transmitter for processing. (More about the update later on.)

The only other main operation the HighScoreManager class does is to save the local high scores as a local data file when the application is ready to quit. A method called writelocalHighScores() will do this, opening the local data file and writing each of the local scores to that file (the application doesn't need to write global high scores; those come from the transmitter as part of the channel update).

HighScoreFrame

The second of the major classes for managing high scores is HighScoreFrame. HighScoreFrame is an instance of the PlayerFrame class for displaying the highscore.gui presentation and interpreting the data you get from that window. The highscore.gui dialog is shown in Figure 13.3.

Figure 13.3. The High Score Dialog.

The WordSmithWidget class is responsible for creating and showing this dialog, once the game has been won and the widget has determined that the new score merits being added to the high score list. After entering their name and e-mail address into this window, the user clicks OK, an event which is trapped by the handleEvent() method in the HighScoreFrame class, which calls the handleOKButton() method. The contents of handleOKButton() is shown in Listing 13.3

Listing 13.3. The handleOKButton() method from HighScoreFrame.

 1:public void handleOKButton() {

 2:   TextBoxWidget name_tbw = 

 3:     (TextBoxWidget)player.getPresentation().getWidget("name");

 4:   String name = name_tbw.getText().trim();

 5:   hsm.lastNameUsed = name;

 6:   if (name.equals(""))

 7:       name = "<empty>";

 8:   TextBoxWidget email_tbw = 

 9:      (TextBoxWidget)player.getPresentation().getWidget("email");

10:   String email = email_tbw.getText().trim();

11:   hsm.laste-mailUsed = email;

12:   if (email.equals(""))

13:       email = "<empty>";

14:   HighScore hs = new HighScore(score, name, email, new Date());

15:   hsm.insert(hs);

16:

17:   dispose();

18:}

The handleOKButton is responsible for two main things: first, it builds a HighScore object with the information from the dialog's widgets and the score from the WordSmithWidget. Then it calls the insert() method in HighScoreManager (line 15) to insert the new HighScore object in the right spot in the list. That done, it calls dispose() to get rid of itself.

HighScoreListFrame

The only high score class left to discuss is the HighScoreListFrame class, which uses the highscorelist.gui presentation to display the current list of local and global high scores. This class is creates in response to the Show High Scores menu item. All it does is display the highscores.gui presentation and fills in the list widget in that presentation with the scores information stored in the HighScoreManager class. See the code for HighScoreListFrame.java for details.

The Main Application

If the WordSmithWidget class is responsible for all the actual game play, and the HighScoreManager processes all the high score data, what's left for the main application to do? The main application ties together all the basic behavior of the widget and the high-score manager, builds and processes a menubar for various options in the application, and manages live updates from the transmitter.

The file WordSmithApplication.java in the wordsmith directory contains the source for the WordSmith channel. WordSmithApplication is a subclass of ApplicationPlayerFrame, and implements all the channel methods you've come to know and love: setContext(), start(), stop(), notifyAvailable(), notifyInstall(), and handleEvent().

Initializing the Channel

Let's start with the setContext() method, as that's the first method that's called when the channel starts up. setContext() looks like this:

public void setContext(ApplicationContext context) {

   super.setContext(context);

   

   highScores = new HighScoreManager();

   WordSmithWidget wsw = (WordSmithWidget)util.getWidget("wordsmith");

   wsw.setContext(context);

   wsw.setHighScoreManager(highScores);

}

The setContext() method does three main things to initialize the channel:

The next step is the start() method:

public void start() {

   super.start();



   highScores.setContext(context);

   highScores.readLocalHighScores();

   highScores.readGlobalHighScores();

}

Here's where the high-score manager gets initialized, and where the local and global high-score lists are read into memory. After start() finishes executing, the game is ready to be played.



Note

The start() method actually contains two more lines I've edited out here: a call to a method called readState() and a call to show(). The former is a local method that is not implemented in this version of the channe (or rather, it's implemented empty) and the latter is actually already called in super.start() so it's redundant to call it here.


Handling Updates

Live updates to the WordSmith channel happen in the form of updated global high-score lists. An update can occur both because the update interval for the channel has passed, and also in response to a high score being logged by the HighScoreManager.(Remember that HighScoreManager calls the context.update() method explicitly.) To handle the new high score data, the WordSmithApplication channel overrides both notifyAvailable() and notifyInstall().

Both these methods should look extremely familiar to you; I used similar versions in the Weather channel class from Chapter 11, "Managing Updates and User Information." Listing 13.4 shows both notifyAvailable() and notifyInstall().

Listing 13.4. Update methods in WordSmithApplication.

 1: public void notifyAvailable(String dir) {

 2:    if ("restart".equals(context.getParameter("update.action"))) {

 3:        context.restart();

 4:    }

 5:   

 6:    context.installData(dir);

 7: }

 8:     

 9: public void notifyInstall(String dir) {

10:    highScores.readGlobalHighScores();

11: }

The notifyAvailable() method, as you know, is called after an update has occurred, and there's new data to be installed. In this method, if the channel has been published with the Data Available property as restart(), the channel will be restarted. In any case, however, we'll want the new data to be installed, so context.installData is called at the end of that method.

The notifyInstall() method processes the new data. In this case, the high-score manager is responsible for managing the global high scores that have arrived as part of the update, so you call the readGlobalHighScores() method in that class to re-read the list of high scores.

The combination of these two methods, in addition to the methods in the HighScoreManager class, ensure that the running channel has the most up-to-date version of the high score list at all times.

The Menubar

The WordSmithApplication also has one other feature we haven't covered in this book: it has a menubar with several menus and menu items.

Adding menus and menubars for a channel that uses a presentation is extremely similar to adding a menubar to a normal Java application, with one significant difference: instead of using the java.awt classes for handling menus, you use special classes in the marimba.desktop package (part of the bongo.zip library). These special classes include AppMenuBar, AppMenu, AppMenuItem, and AppCheckboxMenuItem and are very similar to their counterparts in the java.awt package. You can find out more about these classes (and about adding menus to presentations) in the Official Marimba Guide to Bongo.

The WordSmithApplication adds the menubar to the channel as part of the constructor for the class; because it's initialization code for the window itself and doesn't fit into setContext() very well, this is a good place for it. You can see the actual code in the Java file for the WordSmith application.

The handleEvent() method for the WordSmithApplication class also manages menu events, calling different methods in different objects for each different menu item. Listing 13.5 shows the part of the handleEvent() method that deals with menus. Note in particular the "Playing WordSmith" and "About WordSmith" menu items in lines 11 through 29, which display the help.gui and about.gui presentations in their own frames, respectively.

Listing 13.5. A partial handleEvent(). to handle menu events.

1:if ((""+evt.arg).equalsIgnoreCase("quit")) {

 2:   context.stop();

 3:}

 4:else if ((""+evt.arg).equalsIgnoreCase("high scores...")) {

 5:   showHighScoreList();

 6:}

 7:else if ((""+evt.arg).equalsIgnoreCase("New Game")) {

 8:   WordSmithWidget wsw = (WordSmithWidget)util.getWidget("wordsmith");

 9:   wsw.newGame();

10:}

11:else if ((""+evt.arg).equalsIgnoreCase("Playing WordSmith...")) {

12:  try {

13:    PlayerFrame pf = new PlayerFrame();

14:    pf.util.setPresentation(new URL(context.getBase(), "help.gui"));

15:    pf.show();

16:  }

17:  catch(MalformedURLException e) {

18:     e.printStackTrace();

19:  }   

20:}

21:else if ((""+evt.arg).equalsIgnoreCase("About WordSmith...")) {

22:  try {

23:     PlayerFrame pf = new PlayerFrame();

24:     pf.util.setPresentation(new URL(context.getBase(), "about.gui"));

25:     pf.show();

26:  }

27:  catch(MalformedURLException e) {

28:     e.printStackTrace();

29:  }       

30:}

Stopping the Channel

Once users gets bored playing the game, they select Quit from menu channel's menu, the channel stops, and the stop() method is called. Here's that stop() method, which called the writeLocalHighScores() method in HighScoreManager to save the state of the local high scores. The hide() and dispose() methods are part of the super.stop() implementation of this method; you could just have easily called super.stop() here instead:

public void stop() {

   highScores.writeLocalHighScores();

   hide();

   dispose();

}



Note

As with the start() method, I've deleted references called writeState() from this method. In the version of WordSmith I used for this chapter, writeState() doesn't do anything.


The Plug-In

When a user scores well enough in the WordSmith game to merit being listed in the high scores file, the high-score manager logs the score they got and calls update(). That log data is sent to the transmitter as feedback data; a plug-in on the transmitter site will then process that global high score data.

The plug-in for the WordSmith channel is contained in the plugin directory of the channel, as it should be. The code itself for the channel, the file WordSmithPlugin.java is in the wordsmith directory inside plugin; the code is defined as inside the wordsmith package, so it needs to be in that directory. The properties.txt file for the plug-in takes that package into account:

main=wordsmith.WordSmithPlugin

The WordSmithPlugin class follows all the basic rules for a plug-in that you learned in the last chapter, including implementing init() and processRequest() events. In addition, because the procedures the plug-in uses to insert scores into the transmitter's version of the high-scores list are very similar to those the-high score manager uses to insert scores into its version of the list, a lot of the code is very similar to that of the HighScoreManager class. I'll point out the similarities as they appear.

Let's start with init() shown in Listing 13.6.

Listing 13.6. The init() method from WordSmithPlugin.java.

 1:public synchronized void init() {

 2:   highScoreFile = getDataFile(HIGHSCOREFILE);

 3:   // initialize array

 4:   for (int i = 0 ; i < HighScoreManager.MAXHIGHSCORES ; i++) {

 5:       HighScore hs = new HighScore();

 6:       highScores.addElement(hs);

 7:   }

 8:   readHighScores();

 9:

10:   highScoreChecksum = calculateChecksum(scoresToBytes());

11:    }

12:

The init() method here does essentially the same thing the HighScoreManager class does to initialize itself: it sets up a vector object to store the scores, initializes it, and then reads the highscore.txt file into that vector (using the readHighScores() method). Notice that the plug-in only has one set of high scores to deal with, and it stores that file in the local data directory for the plug-in (line 2).

To finish up, the init() method calculates a checksum for the existing high-score data (the plug-in will keep a running checksum throughout the channel). The scorestoBytes() method is a utility method that converts the scores vector into a byte array.

One important point to note here is that the init() method uses the HighScore class for each entry in the high scores list. Although this may not seem surprising, it does demonstrate a feature of plug-ins: the CLASSPATH for a plug-in includes both the plug-in directory and the channel directory itself—so you can share files such as HighScore between the channel and the plug-in.

With the plug-in initialized with the current high scores, the plug-in waits for an update request. processRequest(), shown in Listing 13.7, handles each request:

Listing 13.7. processRequest() from WordSmithPlugin.java.

1:public synchronized void processRequest(RequestContext context) 

 2:   throws IOException {

 3:   Vector logs = getLogEntries(context);

 4:   for (Enumeration e = logs.elements() ; e.hasMoreElements() ; ) {

 5:     LogEntry le = (LogEntry)e.nextElement();

 6:     String s = new String(le.data, 0);

 7:     add(s);

 8:

 9:     highScoreChecksum = calculateChecksum(scoresToBytes());

10:   }

11:

12:   byte b[] = scoresToBytes();

13:   context.addFile(HIGHSCOREFILE, b, highScoreChecksum, PERSISTENT);

14:   

15:   try {

16:      if (highScoreFile.exists() == false) {

17:         File dir = new File(highScoreFile.getParent());

18:         if (dir.exists() == false) {

19:            if (dir.mkdirs()){

20:            }

21:         }

22:      }

23:      FastOutputStream fos = new FastOutputStream(highScoreFile);

24:      for (Enumeration e = highScores.elements() ; e.hasMoreElements() ; ) {

25:         HighScore hs = (HighScore)e.nextElement();

26:         fos.println(hs.toString());

27:      }

28:      fos.close();

29:   }

30:   catch(IOException e) {

31:       e.printStackTrace();

32:   }

33:   super.processRequest(context);

34:}

This method should look familiar; it's almost identical to the processRequest() method I used in the last chapter for the survey channel. Here are the most important parts of this method:

After the plug-in is finished processing the request, the data for the new high-scores file is sent back to the channel, which uses notifyInstall() to install the new data. In this way, a central high-scores list can be updated from each running channel and the changes can be pushed back out to each one.

Functional Flow

Got all that? The WordSmith channel is fairly complicated, involving several different classes and widgets, all interacting, to accomplish its purpose. Now that you have a basic idea of the various parts, here's an overview of how the typical flow of control works in the WordSmith channel as it's played:

Channel Properties

The last part of the WordSmith channel to go over are the actual properties for this channel. Figure 13.4 shows the general properties.

Figure 13.4. General Properties for WordSmith.

The only new thing here is the name of the main class for the application—because the channel is contained in the wordsmith package and inside the wordsmith directory, you'll have to include the name of that package here so that the right class can be launched.

The icon properties for the channel, shown in Figure 13.5 are also worth examining.

Figure 13.5. Icon properties.

There are four icons for this channel: the GIF and BMP icons are each a 64-pixel square icon that is used as the icon for the channel (there's two different formats, one for Windows and one for Unix), and two thumbnail icons for the channel in the tuner. All of these icons are stored in the image directory.

Summary

WordSmith is a reasonably complex channel that makes use of most of the features of the Castanet and Bongo technologies: Bongo presentations, custom widgets, channel classes, local state, automatic updates, and channel feedback (whew!). Throughout this chapter you've explored how this channel uses all these features, and how the various different parts of the channel interact. At this point, you should be able to create your own Java channels that use any of these features. Good luck!