Puzzle yourself with the HiddenWords puzzle-game applet Because many computer games (such as Tetris) are based on puzzles, this installment of Java Fun and Games introduces you to a single puzzle-based computer game implemented as a Swing-based applet.Note: Unlike most of the previous Java Fun and Games installments, which I wrote from the perspective of J2SE 1.4, this installment requires Java SE 5.Hidden wordsMy HiddenWords puzzle-game applet is based on the well-known Jumble word puzzle. Created in 1954 by Martin Nadle, and seen in many newspapers, Jumble presents a set of scrambled letters that you must unscramble to unveil the hidden word or words. As with Jumble, HiddenWords presents a set of scrambled letters for you to unscramble — examine the figure below. The figure’s simple GUI consists of a custom puzzle-board component and a label. The puzzle board’s tiles display a set of scrambled letters, and the label specifies a clue for discovering the underlying word(s). Use the mouse to select and swap tiles (and their letters) — the first selected tile is highlighted (black on yellow). After solving the puzzle, an animated message appears; a new set of letters and a clue then display.Code tourOne of the files stored in this article’s code archive (available for download from Resources), HiddenWords.java, contains the source code to the HiddenWords applet. This source code is divided into three classes:HiddenWords: The main applet classPuzzleBoard: The main puzzle-game component classWordClue: The unscrambled word-and-clue datastructure class: class WordClue { char [] word; String clue; } The HiddenWords class’s public void init() method initializes the applet by reading resources and creating the GUI. Resources consist of a wordsclues.txt file that stores unscrambled words and clues, a click.wav file that stores an audible click, and a flashing.gif file that stores an animation for use in the animated message. The init() method is shown below: public void init () { // Read the words-and-clues text file's contents into the wordsClues // ArrayList. The file must exist. InputStream is = getClass ().getResourceAsStream (WORDS_CLUES_FILE); if (is == null) { System.err.println (WORDS_CLUES_FILE+" does not exist"); return; } InputStreamReader isr = new InputStreamReader (is); BufferedReader br = new BufferedReader (isr); try { String line; while ((line = br.readLine ()) != null) { // Each line contains two parts separated by a colon character. String [] parts = line.toUpperCase ().split (":"); // parts.length returns 1 for blank lines. This if statement // prevents an exception that occurs if you accidentally insert a // blank line into the file (typically at the end of the file). if (parts.length != 2) break; WordClue wc = new WordClue (); wc.word = parts [0].trim ().toCharArray (); wc.clue = parts [1].trim (); wordsClues.add (wc); } } catch (IOException ioe) { System.err.println (ioe); return; } finally { try { br.close (); } catch (IOException ioe) { } } // Obtain the audio file used to give audible feedback for mouse clicks. // This file must exist. audioFile = getClass ().getResource (AUDIO_FILE); if (audioFile == null) { System.err.println (AUDIO_FILE+" is missing"); return; } // Obtain the image for use with the label. This file must exist. imageFile = getClass ().getResource (IMAGE_FILE); if (imageFile == null) { System.err.println (IMAGE_FILE+" is missing"); return; } // Create the GUI on the event-dispatching thread. try { SwingUtilities.invokeAndWait (new Runnable () { public void run () { createGUI (); } }); } catch (Exception exc) { System.err.println (exc); } } The init() method invokes Class‘s public InputStream getResourceAsStream(String name) and public URL getResource(String name) methods to read the resources. I chose to read the resources in this manner so that I could conveniently distribute the entire applet as a single jar file.After reading the resources, init() invokes createGUI() to create the GUI. Instead of directly invoking this method, however, init() indirectly invokes createGUI() via SwingUtilities.invokeAndWait(). This executes the GUI-creation code (which I show below) on the event-dispatching thread (which Sun’s Java Tutorial recommends for Swing applets): private void createGUI () { // This applet uses a glass pane to prevent puzzle tiles from being // selected between correctly unscrambling a word and the applet // presenting the next scrambled word. JPanel pnlGlass = new JPanel (); pnlGlass.setOpaque (false); pnlGlass.addMouseListener (new MouseAdapter () {}); pnlGlass.addMouseMotionListener (new MouseMotionAdapter () {}); setGlassPane (pnlGlass); // Create the GUI components. A clue that associates with the current // scrambled word is presented via a label. The scrambled word is // presented via a puzzle board as a horizontal sequence of tiles (one // tile per word character). JLabel lblClue = new JLabel (""); lblClue.setForeground (Color.white); PuzzleBoard pb = new PuzzleBoard (wordsClues, audioFile, imageFile, pnlGlass, lblClue); // Lay out the GUI such that the label is centered below the centered // puzzle board. Box vBox = Box.createVerticalBox (); vBox.add (Box.createVerticalStrut (10)); Box hBox = Box.createHorizontalBox (); hBox.add (Box.createHorizontalGlue ()); hBox.add (pb); hBox.add (Box.createHorizontalGlue ()); vBox.add (hBox); vBox.add (Box.createVerticalStrut (10)); hBox = Box.createHorizontalBox (); hBox.add (Box.createHorizontalGlue ()); hBox.add (lblClue); hBox.add (Box.createHorizontalGlue ()); vBox.add (hBox); // Install the GUI. setContentPane (vBox); // Give the GUI a dark green background. setBackground (new Color (0, 128, 0)); } Before creating the puzzle-board component, createGUI() creates a glass-pane component. The glass pane prevents the user from selecting tiles, while the applet briefly pauses to display an animated message — all mouse activity is trapped when the glass pane is installed and visible. The createGUI() method passes the glass-pane component as one of the arguments to PuzzleBoard‘s constructor: PuzzleBoard (ArrayList<WordClue> wordsClues, URL audioFile, URL imageFile, Component pnlGlass, JLabel lblClue) { this.wordsClues = wordsClues; this.audioFile = audioFile; imageIcon = new ImageIcon (imageFile); this.pnlGlass = pnlGlass; this.lblClue = lblClue; WordClue wc = wordsClues.get (prevIndex = rnd (wordsClues.size ())); word = wc.word; scramWord = scramble (word); lblClue.setText ("Clue: "+wc.clue); font = new Font ("Arial", Font.BOLD, 20); addMouseListener (new MouseAdapter () { public void mouseClicked (MouseEvent e) { doSelection (e.getX (), e.getY ()); } }); // The Box container's BoxLayout manager requires a component's maximum // size to be set equal to its preferred size (for proper layout). setPreferredSize (new Dimension (word.length*TILE_SIZE, TILE_SIZE)); setMaximumSize (new Dimension (word.length*TILE_SIZE, TILE_SIZE)); } Among its various tasks, the constructor must ensure that the puzzle-board component’s maximum size equals its preferred size. Setting the maximum size to the preferred size causes the puzzle board to be laid out with just enough space for the tiles. The constructor also randomly selects a word from wordsClues and scrambles this word, as follows: private char [] scramble (char [] word) { char [] scramWord = new char [word.length]; System.arraycopy (word, 0, scramWord, 0, word.length); mainloop: do { for (int i = 0; i < 100; i++) // 100 is arbitrary { int x = rnd (word.length); int y = rnd (word.length); char ch = scramWord [x]; scramWord [x] = scramWord [y]; scramWord [y] = ch; } // Make sure word is scrambled in the forward and reverse directions. // For example, if word contains "JavaWorld", scramWord must not // contain "JavaWorld" or "dlroWavaJ" (must make it harder to guess). for (int i = 0; i < word.length; i++) if (word [i] != scramWord [i] && word [i] != scramWord [word.length-1-i]) break mainloop; } while (true); return scramWord; } The scrambling logic swaps letters an arbitrary number of times (I could have specified a constant instead of hard-coding 100, but will probably never change 100 to some other value because I don’t think it would make a difference as to how well the letters are scrambled). It then ensures that the word is scrambled in the forward and reverse directions.Another constructor task: install a mouse listener. Whenever the user clicks the mouse while the mouse is over the puzzle-board component, this listener’s public void mouseClicked(MouseEvent e) method is invoked. In turn, this method invokes the following method (with the current mouse coordinates) to select the appropriate tile: private void doSelection (int mouseX, int mouseY) { playWAV (audioFile); // Identify the tile that was clicked. int tileID = mouseX/TILE_SIZE; // If no tile has been selected, highlight this tile. Its associated // character will be swapped with the character belonging to the next // tile (which is not highlighted) to be selected. if (!tileSelected) { tileSelected = true; selectedTileID = tileID; repaint (); return; } // Swap the characters in the scrambled word character array that are // associated with the highlighted tile (identified by selectedTileID) // and the second selected tile (identified by tileID). char ch = scramWord [selectedTileID]; scramWord [selectedTileID] = scramWord [tileID]; scramWord [tileID] = ch; tileSelected = false; // Display the swapped characters as soon as doSelect() and its // mouseClicked() method caller exit. repaint (); // Return if the word has not been unscrambled. for (int i = 0; i < word.length; i++) if (word [i] != scramWord [i]) return; // Inform the user that the word has been unscrambled as soon as // doSelect() and its mouseClicked() method caller exit. lblClue.setIcon (imageIcon); lblClue.setHorizontalTextPosition (JLabel.CENTER); lblClue.setText ("You got it!"); // Activate the glass pane. pnlGlass.setVisible (true); // Lazily create the action listener that takes us to the next scrambled // word. The action listener is created only once to help minimize // garbage collections. if (al == null) al = new ActionListener () { public void actionPerformed (ActionEvent e) { // Prevent the same word from being chosen as the next // word. int index; while ((index = rnd (wordsClues.size ())) == prevIndex); prevIndex = index; WordClue wc = wordsClues.get (index); word = wc.word; scramWord = scramble (word); lblClue.setText ("Clue: "+wc.clue); Dimension dim; dim = new Dimension (word.length*TILE_SIZE, TILE_SIZE); setPreferredSize (dim); setMaximumSize (dim); revalidate (); lblClue.setIcon (null); pnlGlass.setVisible (false); } }; // Register an action listener with a non-repeating (one-shot) timer. // This listener is contacted after a suitable delay. javax.swing.Timer t = new javax.swing.Timer (DELAY, al); t.setRepeats (false); // Start the timer. t.start (); } To give audible tile-selection feedback, doSelection()‘s first task is to play a click sound. Because I chose to play a wave-based audio file (via the Java Sound API) instead of an audio file based on Sun’s AU format, I created my own wave-playing method. This method requires a single URL argument that identifies the audio file: private void playWAV (URL url) { try { // Get an AudioInputStream from the specified file (which must be a // valid audio file, otherwise throw UnsupportedAudioFileException. AudioInputStream ais = AudioSystem.getAudioInputStream (url); // Get the AudioFormat for the sound data in the AudioInputStream. AudioFormat af = ais.getFormat (); // Create a DataLine.Info object that describes the format for data // to be output over a Line. DataLine.Info dli = new DataLine.Info (SourceDataLine.class, af); // Do any installed Mixers support the Line? If not, we cannot play // a sound file. if (AudioSystem.isLineSupported (dli)) { // Obtain matching Line as a SourceDataLine (a Line to which // sound data may be written). SourceDataLine sdl = (SourceDataLine) AudioSystem.getLine (dli); // Acquire system resources and make the SourceDataLine // operational. sdl.open (af); // Initiate output over the SourceDataLine. sdl.start (); // Size and create buffer for holding bytes read and written. int frameSize = af.getFrameSize (); int bufferLenInFrames = sdl.getBufferSize ()/8; int bufferLenInBytes = bufferLenInFrames*frameSize; byte [] buffer = new byte [bufferLenInBytes]; // Read data from the AudioInputStream into the buffer and then // copy that buffer's contents to the SourceDataLine. int numBytesRead; while ((numBytesRead = ais.read (buffer)) != -1) sdl.write (buffer, 0, numBytesRead); } } catch (LineUnavailableException e) { } catch (UnsupportedAudioFileException e) { } catch (IOException e) { } } Once doSelection() determines that the scrambled letters have been completely unscrambled, it displays an animated message. Because this animation won’t start until doSelection() finishes, this method must exit. Before exiting, doSelection() installs a timer to limit the animation duration and present the next puzzle.Following a suitable delay, the timer invokes its action listener. This listener chooses a new puzzle based on a different word (or words). After adjusting the component’s preferred and maximum sizes to reflect the number of tiles in the new puzzle, the listener executes revalidate(); to ensure the puzzle board correctly displays. This causes the component to be repainted, as follows: protected void paintComponent (Graphics g) { int width = getWidth (); int height = getHeight (); // This component is always opaque, so color entire background. g.setColor (Color.blue); g.fillRect (0, 0, width, height); // Highlight the first selected tile. if (tileSelected) { g.setColor (Color.yellow); g.fillRect (selectedTileID*TILE_SIZE, 0, TILE_SIZE, height); } g.setFont (font); FontMetrics fm = g.getFontMetrics (); // Paint the tiles and their associated characters. for (int i = 0; i < word.length; i++) { g.setColor (Color.white); g.drawLine (i*TILE_SIZE, 0, i*TILE_SIZE+TILE_SIZE-1, 0); g.drawLine (i*TILE_SIZE, 0, i*TILE_SIZE, height-1); g.setColor (Color.black); g.drawLine (i*TILE_SIZE, height-1, i*TILE_SIZE+TILE_SIZE-1, height-1); g.drawLine (i*TILE_SIZE+TILE_SIZE-1, 0, i*TILE_SIZE+TILE_SIZE-1, height-1); Graphics2D g2d = (Graphics2D) g; g2d.setRenderingHint (RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); if (tileSelected && i == selectedTileID) g.setColor (Color.black); else { g.setColor (Color.black); // Render the shadow. g.drawString (""+scramWord [i], i*TILE_SIZE+ (TILE_SIZE-fm.stringWidth (""+scramWord [i]))/2+3, 3*height/4+3); g.setColor (Color.yellow); } g.drawString (""+scramWord [i], i*TILE_SIZE+ (TILE_SIZE-fm.stringWidth (""+scramWord [i]))/2, 3*height/4); } } Because a game’s look is important to its players, the protected void paintComponent(Graphics g) method handles certain aesthetics. These aesthetics include a border for each tile, antialiased text to avoid jagged letters, a shadow that underlies each letter (only on non-highlighted tiles), and centered letters on their respective tiles.Deploy the appletThe HiddenWords applet is easy to compile (javac HiddenWords.java), but is cumbersome to deploy because of the various classfiles and resource files. The best way to deploy the applet is to package these files into a jar file, as shown below: jar cf HiddenWords.jar *.class *.gif *.txt *.wav After creating HiddenWords.jar, you need to identify this jar file in the applet’s HTML. The following applet HTML identifies the JAR via an archive attribute, and also specifies a suitable size for the applet: <applet archive="HiddenWords.jar" code="HiddenWords.class" width=400 height=110></applet> ConclusionAlthough it is fun to play, you could improve the HiddenWords applet to increase its entertainment value. One enhancement to consider: score keeping and a countdown timer. Each time the user unscrambles a puzzle, the user’s score is increased. If the user fails to unscramble the puzzle before a countdown timer reaches 0, a buzzer sounds and the score decreases.After playing with HiddenWords, you might want to develop your own puzzle-game applets. For some inspiration, read the Java Fun and Games articles “The Knight’s Tour” and “Square Off.”Jeff Friesen is a freelance software developer and educator who specializes in Java technology. Check out his Website to discover all of his published Java articles and more. JavaSoftware Development