Square off

how-to
Mar 27, 200624 mins

Jump into game development with the Squares game

In the early 1990s, I created my first computer game, Squares, which ran in a Microsoft DOS environment. After leaving Squares “on the shelf” for many years, I recently decided to resurrect this game for this column.

This Java Fun and Games installment introduces you to Squares and presents this game as a Swing applet. Because this applet isn’t as entertaining as it could be, I introduce three more Swing applets that add sound effects, visual effects, and extra game-play levels to Squares.

Introducing Squares

Squares is played on a 3-by-3 game board divided into cells. Each cell displays either a colored square or nothing—the cell is black. When Squares begins, some of the cells are assigned colored squares; other cells are black. The choice of which cells initially contain colored squares is random. To win this game, you must clear all cells to black (no colored squares must be displayed) within 30 seconds.

To clear a colored cell to black, position the mouse pointer over that cell and either click a mouse button or press one of the keys on the numeric keypad. Likewise, if you click on a cell that is already black, that cell displays a colored square. This amounts to a toggling action: black square to colored square, and colored to black. Because this game would be too easy to win if mouse clicks or key-presses determined the outcomes of single cells only, I have designed Squares so that mouse clicks and key-presses determine the outcomes of target cells and their neighbors. Consult Figure 1.

Figure 1 reveals the game-board layout from the perspective of the numeric keypad (A). For example, the 7 key on the numeric keypad corresponds to the cell in the upper-left corner of the game board. Figure 1 also reveals those neighboring cells that are toggled when a specific cell is toggled (B, C, and D). If you toggle a corner cell, the cells on all sides of the toggled cell (this includes the center cell—think diagonally) are also toggled (B). Similarly, toggling the middle cell in the first row, last row, first column, or last column also causes the other two cells in the row or column to toggle (C). Finally, toggling the center cell also toggles the north, south, east, and west cells (D).

Java makeover

I originally developed Squares in the C language. Thanks to the similar syntax shared by C and Java, it wasn’t too hard to translate Squares into Java. Before I present the Java source code to my first Squares applet, you will want to know how to interact with that version’s GUI. Figure 2 shows you what this GUI looks like when you first run the applet.

The game-board component presents a tic-tac-toe-like grid with a white message area located immediately below. This component also presents a border; black when the component does not have the focus, blue when the component has the focus. The Change Square Color button is disabled until a game is in play—I don’t think it makes sense to change the square color when a game is not in play. To get a game into play, you must click or press the Start button. When you do this, you will see a GUI similar to what appears in Figure 3.

Figure 3 reveals a game in play. The message area shows you the number of seconds left in which to toggle all of the squares to black. When this countdown timer reaches zero, you lose. If you manage to toggle all squares to black before the countdown timer reaches zero, you win. While the game is in play, you can click the Change Square Color button to randomly choose a new square color. But when you lose or win, the Change Square Color button is disabled, and the Start button is enabled so that you can play another game.

And now for the source code:

Squares.java

 

// Squares.java

import java.awt.*; import java.awt.event.*; import java.util.Random; import javax.swing.*;

public class Squares extends JApplet { private void createGUI () { // Establish the GUI.

getContentPane ().setLayout (new FlowLayout ());

// Create the game-board component: each cell is 40 pixels on a side, // the square color is green, the component's border is blue when it // gets the focus, and the component's border is black when it loses the // focus. Add this component to the content pane.

final GameBoard gb; gb = new GameBoard (40, Color.green, Color.blue, Color.black); getContentPane ().add (gb);

// The rest of the GUI consists of two buttons. These buttons will be // placed in a panel so that they can be treated as a single unit. For // example, if the applet's width is made larger, both buttons (rather // than just one button) will appear to the right of the game board.

JPanel p = new JPanel ();

// Create the Change Square Color button and make sure it is disabled. // Square color should only be changed when a game is in play.

final JButton btnChangeSquareColor = new JButton ("Change Square Color"); btnChangeSquareColor.setEnabled (false);

// Establish the Change Square Color button's action listener. When this // button is clicked, it changes the square color to a randomly-chosen // color.

ActionListener al; al = new ActionListener () { public void actionPerformed (ActionEvent e) { Random rnd = new Random ();

while (true) { int r = rnd.nextInt (256); int g = rnd.nextInt (256); int b = rnd.nextInt (256);

// Disallow colors where all color components are less // than 192. Dark colors lead to poor contrast when // black is the background color.

if (r < 192 && g < 192 && b < 192) continue;

gb.changeSquareColor (new Color (r, g, b));

break; } } };

btnChangeSquareColor.addActionListener (al);

p.add (btnChangeSquareColor);

// Create the Start button.

final JButton btnStart = new JButton ("Start");

// Establish the Start button's action listener. When this button is // clicked, it disables itself (you cannot start a started game), // enables the Change Square Color button (you can change the square // color during game play), and starts the game. A done listener is // established to enable the Start button and disable the Change Square // Color button when the game ends.

al = new ActionListener () { public void actionPerformed (ActionEvent e) { btnStart.setEnabled (false); btnChangeSquareColor.setEnabled (true);

gb.start (new GameBoard.DoneListener () { public void done () { btnStart.setEnabled (true); btnChangeSquareColor.setEnabled (false); } }); } };

btnStart.addActionListener (al);

// Add both buttons, via the panel, to the content pane.

p.add (btnStart);

getContentPane ().add (p);

// Under Java 1.4.0, it is not possible to tab the focus from one // component to another component without establishing the JApplet as a // focus cycle root, and establishing a focus traversal policy. You can // learn more about this bug by pointing your Web browser to the link // http://bugs.sun.com/bugdatabase/view_bug.do;:YfiG?bug_id=4705205.

if (System.getProperty ("java.version").equals ("1.4.0")) { setFocusCycleRoot (true); setFocusTraversalPolicy (new LayoutFocusTraversalPolicy ()); } }

public void init () { // According to Sun's Java Tutorial, Swing components should be created, // queried, and manipulated on the event-dispatching thread. Because // most Web browsers don't invoke significant applet methods, such as // public void init(), from that thread, SwingUtilities.invokeAndWait() // is called to ensure the GUI is created on the event-dispatching // thread. The invokeAndWait() method is used instead of invokeLater() // because the latter method could allow init() to return before the GUI // was made; this would result in difficult-to-debug applet problems.

try { SwingUtilities.invokeAndWait (new Runnable () { public void run () { createGUI (); } }); } catch (Exception e) { System.err.println ("Unable to create GUI"); } } }

class GameBoard extends JPanel { // Game states.

private final static int INITIAL = 0; private final static int INPLAY = 1; private final static int LOSE = 2; private final static int WIN = 3;

// Size of border.

private final static int BORDER_SIZE = 5;

// Current game state.

private int state = INITIAL;

// Number of pixels between cell borders.

private int cellSize;

// Width of game board (plus borders).

private int width;

// Height of game board and message area (plus borders).

private int height;

// The color of each square.

private Color squareColor;

// The game board's focus border color.

private Color focusBorderColor;

// The game board's nonfocus border color.

private Color nonfocusBorderColor;

// The game board's current border color.

private Color borderColor;

// Cells: true means that the cell contains a colored square.

private boolean [] cells = new boolean [9];

// Reference to object that is listening for game to finish.

private GameBoard.DoneListener dl;

// Reference to a timer that counts down the timer counter, determines // whether the player has lost or won, and notifies the done listener when // either situation occurs.

private Timer timer;

// Timer counter.

private int counter;

// GameBoard constructor.

GameBoard (int cellSize, Color squareColor, Color focusBorderColor, Color nonfocusBorderColor) { this.cellSize = cellSize;

width = 3*cellSize+2+2*BORDER_SIZE; height = width + 50;

setPreferredSize (new Dimension (width, height));

this.squareColor = squareColor;

this.focusBorderColor = focusBorderColor;

this.nonfocusBorderColor = nonfocusBorderColor;

this.borderColor = nonfocusBorderColor;

addFocusListener (new FocusListener () { public void focusGained (FocusEvent e) { borderColor = GameBoard.this.focusBorderColor;

repaint (); }

public void focusLost (FocusEvent e) { borderColor = GameBoard.this.nonfocusBorderColor;

repaint (); } });

addKeyListener (new KeyAdapter () { public void keyTyped (KeyEvent e) { if (state != INPLAY) return;

char key = e.getKeyChar ();

// If the player has typed a digit key, map that // key to the cells array index, and toggle the // appropriate square and its neighbors.

if (Character.isDigit (key)) switch (key) { case '1': GameBoard.this.toggle (6); break;

case '2': GameBoard.this.toggle (7); break;

case '3': GameBoard.this.toggle (8); break;

case '4': GameBoard.this.toggle (3); break;

case '5': GameBoard.this.toggle (4); break;

case '6': GameBoard.this.toggle (5); break;

case '7': GameBoard.this.toggle (0); break;

case '8': GameBoard.this.toggle (1); break;

case '9': GameBoard.this.toggle (2); } } });

addMouseListener (new MouseAdapter () { public void mouseClicked (MouseEvent e) { if (state != INPLAY) return;

// When the mouse is clicked on the game board, // make sure the game board receives focus so // that the keyboard can be used as an // alternative for toggling squares.

GameBoard.this.requestFocusInWindow ();

// Which cell was clicked?

int cell = GameBoard.this. mouseToCell (e.getX (), e.getY ());

// If a cell was clicked (cell != -1), toggle // that cell's square and neighboring cell // squares.

if (cell != -1) GameBoard.this.toggle (cell); } });

setFocusable (true); }

// Change current square color to new square color. IMPORTANT: Must be // called from the event-dispatching thread.

void changeSquareColor (Color squareColor) { if (!SwingUtilities.isEventDispatchThread ()) return;

this.squareColor = squareColor; repaint (); }

// Paint component: borders first, message last.

public void paintComponent (Graphics g) { // It is always a good idea to invoke the superclass paintComponent() // method first.

super.paintComponent (g);

// Paint all four borders using the current border color.

g.setColor (borderColor); for (int i = 0; i < BORDER_SIZE; i++) g.drawRect (i, i, width-2*i-1, height-2*i-1);

// Paint the component's game board (less the borders and message area) // black.

g.setColor (Color.black); g.fillRect (BORDER_SIZE, BORDER_SIZE, width-2*BORDER_SIZE, width-2*BORDER_SIZE);

// Paint horizontal grid lines.

g.setColor (Color.white); g.drawLine (BORDER_SIZE, BORDER_SIZE+cellSize, BORDER_SIZE+width-2*BORDER_SIZE-1, BORDER_SIZE+cellSize);

g.drawLine (BORDER_SIZE, BORDER_SIZE+2*cellSize+1, BORDER_SIZE+width-2*BORDER_SIZE-1, BORDER_SIZE+2*cellSize+1);

// Paint vertical grid lines.

g.drawLine (BORDER_SIZE+cellSize, BORDER_SIZE, BORDER_SIZE+cellSize, BORDER_SIZE+width-2*BORDER_SIZE-1);

g.drawLine (BORDER_SIZE+2*cellSize+1, BORDER_SIZE, BORDER_SIZE+2*cellSize+1, BORDER_SIZE+width-2*BORDER_SIZE-1);

// Paint the squares.

g.setColor (squareColor); for (int i = 0; i < cells.length; i++) { if (cells [i]) { int x = BORDER_SIZE+(i%3)*(cellSize+1)+3; int y = BORDER_SIZE+(i/3)*(cellSize+1)+3;

int w = cellSize-6; int h = w;

g.fillRect (x, y, w, h); } }

// Paint the component's message area (below the game board, but still // within the borders) white.

g.setColor (Color.white); g.fillRect (BORDER_SIZE, width-BORDER_SIZE, width-2*BORDER_SIZE, height-width);

// Paint a message if the game-board state is not the initial state.

if (state != INITIAL) { g.setColor (Color.black);

String text;

switch (state) { case LOSE: text = "YOU LOSE!"; break;

case WIN: text = "YOU WIN!";

break;

default: text = "" + counter; }

g.drawString (text, (width-g.getFontMetrics ().stringWidth (text))/2, width-BORDER_SIZE+30); } }

// Start a new game, unless a game is in play. Register the done listener, // establish an initial pattern of colored squares, and begin a timer that // fires an action event every second. IMPORTANT: Must be called from the // event-dispatching thread.

void start (GameBoard.DoneListener dl) { if (!SwingUtilities.isEventDispatchThread ()) return;

if (state == INPLAY) return;

this.dl = dl;

Random rnd = new Random ();

while (true) { for (int i = 0; i < cells.length; i++) cells [i] = rnd.nextBoolean ();

int counter = 0; for (int i = 0; i < cells.length; i++) if (cells [i]) counter++;

if (counter != 0 && counter != cells.length) break; }

ActionListener al; al = new ActionListener () { public void actionPerformed (ActionEvent e) { // If the player has won, notify the done listener.

if (state == WIN) { timer.stop (); GameBoard.this.dl.done (); return; }

// When the counter reaches zero, the player has lost. // Notify the done listener.

if (--counter == 0) { state = LOSE; timer.stop (); GameBoard.this.dl.done (); }

repaint (); } };

timer = new Timer (1000, al);

state = INPLAY; counter = 30; timer.start (); }

// Map mouse coordinates to cell number [0,8], or -1 if mouse coordinates // do not identify a cell.

private int mouseToCell (int x, int y) { // Examine first column.

if (x >= BORDER_SIZE && x < BORDER_SIZE+cellSize) { if (y >= BORDER_SIZE && y < BORDER_SIZE+cellSize) return 0;

if (y >= BORDER_SIZE+cellSize+1 && y < BORDER_SIZE+2*cellSize+1) return 3;

if (y >= BORDER_SIZE+2*cellSize+2 && y < BORDER_SIZE+3*cellSize+2) return 6; }

// Examine second column.

if (x >= BORDER_SIZE+cellSize+1 && x < BORDER_SIZE+2*cellSize+1) { if (y >= BORDER_SIZE && y < BORDER_SIZE+cellSize) return 1;

if (y >= BORDER_SIZE+cellSize+1 && y < BORDER_SIZE+2*cellSize+1) return 4;

if (y >= BORDER_SIZE+2*cellSize+2 && y < BORDER_SIZE+3*cellSize+2) return 7; }

// Examine third column.

if (x >= BORDER_SIZE+2*cellSize+2 && x < BORDER_SIZE+3*cellSize+2) { if (y >= BORDER_SIZE && y < BORDER_SIZE+cellSize) return 2;

if (y >= BORDER_SIZE+cellSize+1 && y < BORDER_SIZE+2*cellSize+1) return 5;

if (y >= BORDER_SIZE+2*cellSize+2 && y < BORDER_SIZE+3*cellSize+2) return 8; }

return -1; }

// Toggle a cell and its neighbors. Figure 1A in the article presents the // following map of cells from the numeric keypad perspective: // // 7 8 9 // 4 5 6 // 1 2 3 // // Because the cells array is zero-based, it is more convenient to work // with the map below: // // 0 1 2 // 3 4 5 // 6 7 8 // // Therefore, when toggle() is called, the calling code must convert the // digit key that was pressed (1-9) to an index (0-8) that corresponds to // the map above.

private void toggle (int cell) { // Toggle the cells.

switch (cell) { case 0: cells [0] = !cells [0]; cells [1] = !cells [1]; cells [3] = !cells [3]; cells [4] = !cells [4]; break;

case 1: cells [0] = !cells [0]; cells [1] = !cells [1]; cells [2] = !cells [2]; break;

case 2: cells [1] = !cells [1]; cells [2] = !cells [2]; cells [4] = !cells [4]; cells [5] = !cells [5]; break;

case 3: cells [0] = !cells [0]; cells [3] = !cells [3]; cells [6] = !cells [6]; break;

case 4: cells [0] = !cells [0]; cells [2] = !cells [2]; cells [4] = !cells [4]; cells [6] = !cells [6]; cells [8] = !cells [8]; break;

case 5: cells [2] = !cells [2]; cells [5] = !cells [5]; cells [8] = !cells [8]; break;

case 6: cells [3] = !cells [3]; cells [4] = !cells [4]; cells [6] = !cells [6]; cells [7] = !cells [7]; break;

case 7: cells [6] = !cells [6]; cells [7] = !cells [7]; cells [8] = !cells [8]; break;

case 8: cells [4] = !cells [4]; cells [5] = !cells [5]; cells [7] = !cells [7]; cells [8] = !cells [8]; }

// Find out if the player has won the game. This code is not placed in // the start() method's action listener (where the code for decrementing // the counter and determining if the player has lost -- the counter // reaches 0 -- exists) because it would then be possible for the player // to occasionally toggle all squares black and quickly untoggle them -- // the player would win and possibly still lose. That behavior is not // desirable.

int i; for (i = 0; i < cells.length; i++) if (cells [i]) break;

if (i == cells.length) state = WIN;

// Draw game board with toggled cells.

repaint (); }

// Done listener interface. The start() method requires an object argument // whose class implements this interface.

interface DoneListener { void done (); } }

Because Squares.java is extensively commented, I haven’t much to say about the source code. There are only two items that I want to mention:

  • Instead of using the thread that runs javax.swing.JApplet‘s public void init() method to create the applet’s GUI, I use that thread to defer GUI creation to Swing’s event-dispatching thread, as recommended by Sun’s Java Tutorial. I avoid synchronization problems by restricting all applet activity to the event-dispatching thread.
  • Prior to J2SE 1.4 (the Java version on which I base this column), the focus system—the mechanism that governs what happens when you tab from one component to another component, for example—was buggy and inconsistent from platform to platform. J2SE 1.4 overhauled the focus system mainly by introducing the java.awt.KeyboardFocusManager class, focus cycle roots, and focus traversal policies. (You can learn more about these topics by studying your Java SDK documentation.) Because J2SE 1.4’s JApplet class depends upon the Abstract Window Toolkit’s focus traversal policy (appletviewer and the Java Plug-in both use the java.awt.Frame class as the top-level ancestor of JApplet, hence the dependence on the AWT focus traversal policy), it’s impossible to tab from one Swing component to another inside a JApplet without help. That help involves making the J2SE 1.4 JApplet the focus cycle root and establishing a focus traversal policy. Also, I invoke setFocusable (true); from within GameBoard‘s constructor to ensure that the game-board component can receive focus. (Even after doing all of this, neither the game-board component nor the two buttons have the focus when you start Squares. You must press Tab once, which gives the game-board component the focus.) This bug has been corrected (in various ways) in versions of Java subsequent to 1.4.

Sound effects

So far, the Squares game is not as entertaining as it could be. One way to make this game more entertaining is to introduce sound effects. At least three different sound effects could be introduced: a sound effect for when the player toggles a square (and its neighbors), a sound effect for when the player wins, and a sound effect for when the player loses.

I’ve prepared suitable sound effects for these three situations. They are located in the toggle.au, win.au, and lose.au files. (I decided to go with Sun audio files instead of Microsoft wave files for maximum portability.) The following code fragment, which I excerpted from a second applet version of Squares.java, loads these sound effects files into audio clips and passes the audio clips to the GameBoard class’s constructor during applet initialization:

 

// Load audio clips to play when square is toggled or player wins or loses.

AudioClip acToggle; acToggle = getAudioClip (getClass ().getResource ("toggle.au")); AudioClip acWin = getAudioClip (getClass ().getResource ("win.au")); AudioClip acLose = getAudioClip (getClass ().getResource ("lose.au"));

// Create the game-board component: each cell is 40 pixels on a side, the // square color is green, the component's border is blue when it gets // the focus, and the component's border is black when it loses the // focus. Add this component to the content pane.

final GameBoard gb; gb = new GameBoard (40, Color.green, Color.blue, Color.black, acToggle, acWin, acLose);

The code fragment employs getClass().getResource() so that these audio files can be conveniently embedded in a jar file with the applet’s classfiles.

Whenever the player wins or loses, the appropriate audio clip is played. This is demonstrated in the following excerpt from GameBoard‘s void start(GameBoard.DoneListener dl) method:

 

// If the player has won, notify the done listener.

if (state == WIN) { acWin.play (); timer.stop (); GameBoard.this.dl.done (); return; }

// When the counter reaches zero, the player has lost. // Notify the done listener.

if (--counter == 0) { state = LOSE; acLose.play (); timer.stop (); GameBoard.this.dl.done (); }

Finally, the toggle-sound audio clip is played when squares are toggled, as the following excerpt from GameBoard‘s private void toggle(int cell) method demonstrates:

 

// Draw game board with toggled cells.

repaint ();

// Play a toggling sound. You will probably not hear a sound (or maybe // just one sound) in early versions of Java 1.5.0 and later versions of // Java 1.4.x because a bug prevents short sound files from being heard. // You can learn more about this bug by pointing your Web browser to the // link http://bugs.sun.com/bugdatabase/view_bug.do;:YfiG?bug_id=6251460.

acToggle.play ();

You might not have come across this bug, but sounds with short durations typically do not play under J2SE 5.0 and additional versions of J2SE 5.x and J2SE 1.4 (you might hear a single instance of the sound, but that is all). For example, if you run the second (or third or fourth) Squares applet under J2SE 5.0, the short sound in toggle.au appears to play only once (I could not get it to play more than one time). Fortunately, this isn’t a problem with J2SE 1.4.

Visual effects

Another way to increase the entertainment value of Squares relies on visual effects. After experimenting with various effects, I chose something simple: scroll a message from right to left across the center of the applet whenever the player wins or loses. For example, when the player wins, the “Congratulations!” message shown in Figure 4 scrolls horizontally across the applet.

The following code fragment, which I excerpted from GameBoard‘s start() method in a third applet version of Squares.java, shows how simple it is to integrate this visual effect into the applet:

 

// If the player has won, notify the done listener and // animate a congratulations message.

if (state == WIN) { acWin.play (); timer.stop (); GameBoard.this.dl.done ();

animate ("Congratulations!", Color.red); return; }

// When the counter reaches zero, the player has lost. // Notify the done listener and animate a better-luck-next- // time message.

if (--counter == 0) { state = LOSE; acLose.play (); timer.stop (); GameBoard.this.dl.done ();

animate ("Better luck next time!", Color.red); }

When the player wins, animate ("Congratulations!", Color.red); causes a red “Congratulations!” message to scroll across the applet. Similarly, animate ("Better luck next time!", Color.red); causes a red “Better luck next time!” message to scroll when the player loses. In turn, these method calls result in GameBoard‘s private void animate(String message, Color msgColor) method setting up the appropriate animation:

 

// Animate a message on the glass pane by scrolling it from left to right.

private void animate (String message, Color msgColor) { ActionListener al; al = new ActionListener () { public void actionPerformed (ActionEvent e) { if (gp.isDone ()) { timerAnim.stop (); applet.getGlassPane ().setVisible (false); } } };

timerAnim = new Timer (100, al);

gp = new GlassPane (message, msgColor); applet.setGlassPane (gp); applet.getGlassPane ().setVisible (true);

// Prevent mouse events from reaching components under the glass pane.

applet.getGlassPane ().addMouseListener (new MouseAdapter () {}); applet.getGlassPane () .addMouseMotionListener (new MouseMotionAdapter () {});

timerAnim.start (); }

The animate() method achieves animation by creating a timer, creating a custom animation component, installing this component as JApplet‘s new glass pane (which covers the entire applet drawing surface), displaying the new glass pane, and starting the timer. The custom animation component is an instance of GameBoard‘s GlassPane inner class:

 

// GlassPane component class.

private class GlassPane extends JPanel { private String text;

private Color msgColor;

private boolean first = true;

private boolean done;

private int width, height;

private int scrollTextHeight, scrollTextWidth;

private int xOffset, yOffset;

private Font font;

GlassPane (String text, Color msgColor) { this.text = text;

this.msgColor = msgColor;

setOpaque (false);

font = new Font ("Serif", Font.BOLD, 24); }

boolean isDone () { repaint ();

return done; }

public void paintComponent (Graphics g) { super.paintComponent (g);

g.setFont (font);

// It is easiest to get the width and height of the glass pane during // the first paintComponent() call.

if (first) { width = getWidth (); height = getHeight ();

FontMetrics fm = g.getFontMetrics ();

scrollTextWidth = fm.stringWidth (text); scrollTextHeight = fm.getHeight ();

xOffset = width; yOffset = (height-scrollTextHeight)/2;

first = false; }

// Display text shadow.

g.setColor (Color.gray); g.drawString (text, xOffset+1, yOffset+1);

// Display text.

g.setColor (msgColor); g.drawString (text, xOffset, yOffset);

// Compute next leftmost position. Once all text has scrolled left of // the display, set the done flag.

xOffset -= 10; if (xOffset < -scrollTextWidth) done = true; } }

When the entire message has scrolled to the right of the applet window, the Boolean done variable is set to true. This change is detected in the animate() method’s action listener, which stops the timer and hides the glass pane.

Although I’m generally pleased with the behavior of the animation, two items could be improved. Rather than make these improvements for you, I’m leaving them as exercises for you:

  • In spite of the fact that clicks and other mouse events cannot reach the components underneath the visible glass pane, the same cannot be said for tabs and other keyboard operations. Modify the third and fourth Squares applets so that you cannot start and interact with a game while a message is scrolling.
  • It’s a bit confusing to see the Start button in its enabled state while a message is scrolling. This is due to a GameBoard.this.dl.done (); method call occurring before the animation completes. Modify the third and fourth Squares applets so that GameBoard.this.dl.done (); isn’t called until the animation is finished.

Extra levels

Although sound effects and visual effects can enhance your experience with Squares, they do nothing to prevent boredom. Once you discover appropriate patterns in the squares so that you can always win, you’ll probably get bored with this game. To overcome boredom, it’s necessary to introduce extra game-play levels into Squares.

I’ve created a fourth Squares applet that introduces four levels of game play. The first level displays squares (as in the previous three applets), the second level displays cubes (something different to look at), and the third and fourth levels revert to displaying squares and cubes (respectively), but with a difference. On these levels, one of the squares is intentionally not displayed. Because it takes some time to figure out which square is not displayed, and because you only have 30 seconds to toggle all squares to black, you will probably find it somewhat harder to win. Each time you win, however, you progress to the next level. But if you lose, you’re thrown back to the first level. Should you win on the fourth level, Squares cycles back to the first level. Figure 5 reveals the second level of game play.

Figure 5. The current level appears in the message area

In addition to introducing levels of game play, I’ve made cosmetic changes to the game-board component. A real Swing border replaces the previous black-and-blue border, and the message area presents the current game-play level. And, of course, cubes are displayed.

Coding support for four levels of game play turned out to be easy. The following code fragment, excerpted from GameBoard‘s start() method, shows how the next level is selected when the player clicks the Start button:

 

timer = new Timer (1000, al);

// Based on previous outcome state, adjust the level. If this is the // first time, the default level = 1 will be used.

if (state == WIN) { if (++level > MAXLEVELS) level = 1; } else if (state == LOSE) level = 1;

state = INPLAY; counter = 30; timer.start ();

The timer being created is, of course, the countdown seconds timer. Most of the remaining level-oriented code can be found in GameBoard‘s public void paintComponent(Graphics g) method.

I have four exercises for you to accomplish on your own:

  • Introduce new levels that display different shapes.
  • Think about introducing new levels that display 16 (4-by-4), 25 (5-by-5), or more squares/cubes. What rules would you use to toggle squares/cubes? Could you still use the keyboard? If so, how?
  • It seems a shame to cycle back to the first level without doing something really special when you win at the fourth level. What could you do to enhance Squares to make this point in the game very interesting?
  • Lots of games display a numeric score and present a gallery of high scores. Do you think a numeric score is appropriate for Squares? How would you handle scoring?

Conclusion

Because Squares is the first computer game I created, I decided to present it before any of my other computer games. Future Java Fun and Games installments will share some of these other (even more interesting) games with you.

Jeff Friesen is a freelance software developer and educator specializing in C, C++, and Java technology.