Skip to main content

Section 9.12 A GUI-Based Game (Optional Graphics)

Most modern computer games do not use a command-line interface. This section addresses this shortcoming by expanding our ComputerGame hierarchy so that it works with Graphical User Interfaces (GUIs) as well as Command-Line User Interfaces (CLUIs).

The Sliding Tile Puzzle is a puzzle game. It is played by one player, a human. The puzzle consists of six tiles arranged on a board containing seven spaces. Three of the tiles are labeled L and three are labeled R. Initially the tiles are arranged as RRR_LLL. In other words, the R tiles are arranged to the left of the L tiles, with the blank space in the middle. The object of the puzzle is to rearrange the tiles into LLL_RRR. The rules are that tiles labeled R can only move right. Tiles labeled L can only move left. Tiles may move directly into the blank space or they can jump over one tile into the blank space.

Our purpose in this section is to develop a GUI that plays this game. An appropriate GUI is shown FigureĀ 9.12.1. Here the tiles and the blank space are represented by an array of buttons. To make a move the user clicks on the 'tile' he or she wishes to move. The GUI will assume that the user wants to move that tile into the blank space. If the proposed move is legal, the GUI will carry out the move. Otherwise, it will just ignore it. For example, if the user were to click on the third R button from the left, a legal move, the GUI would rearrange the labels on the buttons so that their new configuration would be RR_RLLL. On the other hand, if the user were to click on the rightmost L button, the GUI would ignore that move, because it is illegal.

Figure 9.12.1. The Sliding Tile Puzzle.

Subsection 9.12.1 The GUIPlayableGameInterface

How should we extend our game-playing hierarchy to accommodate GUI-based games? As we learned in ChapterĀ 4, one difference between GUI-based applications and CLUI-based applications, is the locus of control. In a CLUI-based application, control resides in the computational object which, for games, is the game object. That's why the play() method in our CLUI-based games contains the game's control loop. By contrast, control resides in the GUI's event loop in GUI-based applications. That's why, we learned how to manage Java's event hierarchy in ChapterĀ 4. Thus, in the GUI shown in FigureĀ 9.12.1, the GUI will listen and take action when the user clicks one of its buttons.

However, given that control will reside in the GUI, there is still a need for communication between the GUI and the game object. In the CLUI-based games, we have used the CLUIPlayableGame interface to manage the communication between the game and the user interface. We will follow the same design strategy in this case. Thus, we will design a GUIPlayableGame interface that can be implemented by any game that wishes to use a GUI (FigureĀ 9.12.2).

Figure 9.12.2. The GUIPlayableGame interface.

What method(s) should this interface contain? One way to answer this question is to think about the type of interaction that must take place when the user clicks one of the tiles. If the user clicks the third R button, the GUI should pass this information to the game. The game should then decide whether or not that is a legal move and communicate this back to the GUI. Assuming it is a legal move, the game should also update its representation of the game's state to reflect that the tile array has changed. And it should somehow communicate the game's state to the GUI.

Because it is impossible to know in advance just what form of data a game's moves might take, we will use Java String s to communicate between the user interface and the game object. Thus, a method with the following signature will enable us to submit a String representing the user's move to the game and receive in return a String representing the game object's response to the move:

submitUserMove(String move): String;

In addition to this method, a GUI interface could use the reportGameState() and getGamePrompt() methods that are part of the IGame interface. The design shown in FigureĀ 9.12.2 leads to the following definition for the GUIPlayableGame interface:

public interface GUIPlayableGame extends IGame {
    public String submitUserMove(String theMove);
}

Because it extends IGame, this interface inherits the getGamePrompt() and reportGameState() from the IGame interface. The GUI should be able to communicate with any game that implements this interface.

Subsection 9.12.2 The SlidingTilePuzzle

Let's now discuss the design and details of the SlidingTilePuzzle itself. Its design is summarized in FigureĀ 9.12.3. Most of the methods should be familiar to you, because the design closely follows the design we employed in the WordGuess example. It has implementations of inherited methods from the ComputerGame class and the GUIPlayableGame interface.

Figure 9.12.3. The SlidingTilePuzzle design.

We will represent the sliding tile puzzle in a one-dimensional array of char. We will store the puzzle's solution in a Java String and we will use an int variable to keep track of where the blank space is in the array. This leads to the following class-level declarations:

private char puzzle[] = {'R','R','R',' ','L','L','L'};
private String solution = "LLL RRR";
private int blankAt = 3;

Note how we initialize the puzzle array with the initial configuration of seven characters. Taken together, these statements initialize the puzzle's state.

Because a puzzle is a one-person game and our sliding tile puzzle will be played by a human, this leads to a very simple constructor definition:

public SlidingTilePuzzle() {
     super(1);
}

We call the super() constructor (ComputerGame()) to create a one-person game.

The puzzle's state needs to be communicated to the GUI as a String. This is the purpose of the reportGameState() method:

public String reportGameState() {
     StringBuffer sb = new StringBuffer();
     sb.append(puzzle);
     return sb.toString();
 }

We use a StringBuffer() to convert the puzzle, a char array, into a String.

The most important method for communicating with the GUI is the submitUserMove() method:

public String submitUserMove(String usermove) {
  int tile = Integer.parseInt(usermove);
  char ch = puzzle[tile];
  if (ch == 'L' && (blankAt == tile-1 || blankAt == tile-2))
    swapTiles(tile,blankAt);
  else if (ch == 'R' && (blankAt == tile+1 || blankAt == tile+2))
    swapTiles(tile,blankAt);
  else
    return "That's an illegal move.\n";
  return "That move is legal.\n";
}

This is the method that processes the user's move, which is communicated through the GUI. As we saw, the puzzle's 'tiles' are represented by an array of buttons in the GUI. The buttons are indexed 0 through 6 in the array. When the user clicks a button, the GUI should pass its index, represented as a String to the submitUserMove() method. Given the index number of the tile that was selected by the user, this method determines if the move is legal.

The Integer.parseInt() method is used to extract the tile's index from the method's parameter. This index is used to get a 'tile' from the puzzle array.

The logic in this method reflects the rules of the game. If the tile is an L, then it can only move into a blank space that is either 1 or 2 spaces to its left. Similarly, an R tile can only move into a blank space that is 1 or 2 spaces to its right. All other moves are illegal.

For legal moves, we simply swap the tile and the blank space in the array, a task handled by the swap() method. In either case, the method returns a string reporting whether the move was legal or illegal.

FigureĀ 9.12.4 shows the full implementation for the SlidingTilePuzzle, the remaining details of which are straight forward.

public class SlidingTilePuzzle extends ComputerGame
                            implements GUIPlayableGame {
  private char puzzle[] = {'R','R','R',' ','L','L','L'};
  private String solution = "LLL RRR";
  private int blankAt = 3;

  public SlidingTilePuzzle() { super(1); }

  public boolean gameOver() { // True if puzzle solved
    StringBuffer sb = new StringBuffer();
    sb.append(puzzle);
    return sb.toString().equals(solution);
  }
  public String getWinner() {
    if (gameOver())
      return "\nYou did it! Very Nice!\n";
    else return "\nGood try. Try again!\n";
  }
  public String reportGameState() {
    StringBuffer sb = new StringBuffer();
    sb.append(puzzle);
    return sb.toString();
  }
  public String getGamePrompt() {
    return "To move a tile, click on it.";
  } //prompt()
  public String submitUserMove(String usermove) {
    int tile = Integer.parseInt(usermove);
    char ch = puzzle[tile];
    if (ch=='L' && (blankAt==tile-1 || blankAt==tile-2))
      swapTiles(tile,blankAt);
    else if (ch=='R' && (blankAt==tile+1 || blankAt==tile+2))
      swapTiles(tile,blankAt);
    else
      return "That's an illegal move.\n";
    return "That move is legal.\n";
  }
  private void swapTiles(int ti, int bl) {
    char ch = puzzle[ti];
    puzzle[ti] = puzzle[bl];
    puzzle[bl] = ch;
    blankAt = ti;   // Reset the blank
  }
} //SlidingTilePuzzle
Listing 9.12.4. Implementation of the SlidingTilePuzzle class.

Subsection 9.12.3 The SlidingGUIClass

Let's now implement a GUI that can be used to play the sliding tile puzzle. We will model the GUI itself after those we designed in ChapterĀ 4.

Figure 9.12.5. The SlidingGUI design.

FigureĀ 9.12.5 provides a summary of the design. As an implementor of the ActionListener interface, SlidingGUI implements the actionPerformed() method, which is where the code that controls the puzzle is located. The main data structure is an array of seven JButton s, representing the seven tiles in the puzzles. The buttons' labels will reflect the state of the puzzle. They will be rearranged after every legal move by the user. The reset button is used to reinitialize the game. This allows users to play again or to start over if they get stuck.

The puzzleState is a String variable that stores the puzzle's current state, which is updated repeatedly from the SlidingTilePuzzle by calling its reportGameState() method. The private labelButtons() method will read the puzzleState and use its letters to set the labels of the GUI's buttons.

The implementation of SlidingGUI is shown in ListingĀ 9.12.6. Its constructor and buildGUI() methods are responsible for setting up the GUI. We use of a for loop in buildGUI() to create the JButton s, associate an ActionListener with them, and add them to the GUI. Except for the fact that we have an array of buttons, this is very similar to the GUI created in ChapterĀ 4. Recall that associating an ActionListener with the buttons allows the program to respond to button clicks in its actionPerformed() method.

Note how an instance of the SlidingTilePuzzle is created in the constructor, and how its state is retrieved and stored in the puzzleState variable:

puzzleState = sliding.reportGameState();

The labelButtons() method transfers the letters in puzzleState onto the buttons.

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
public class SlidingGUI extends JFrame implements ActionListener {
  private JButton tile[] = new JButton[7];
  private JButton reset = new JButton("Reset");
  private SlidingTilePuzzle sliding;
  private String puzzleState;
  private Label label;
  private String prompt = "Goal: [LLL RRR]. " +
    " Click on the tile you want to move." +
    " Illegal moves are ignored.";

  public SlidingGUI(String title) {
    sliding = new SlidingTilePuzzle();
    buildGUI();
    setTitle(title);
    pack();
    setVisible(true);
  } // SlidingGUI()

  private void buildGUI() {
    Container contentPane = getContentPane();
    contentPane.setLayout(new BorderLayout());
    JPanel buttons = new JPanel();
    puzzleState = sliding.reportGameState();
    for (int k = 0; k < tile.length; k++) {
      tile[k] = new JButton(""+puzzleState.charAt(k));
      tile[k].addActionListener(this);
      buttons.add(tile[k]);
    }
    reset.addActionListener(this);
    label = new Label(prompt);
    buttons.add(reset);
    contentPane.add("Center", buttons);
    contentPane.add("South", label);
  } // buildGUI()

  private void labelButtons(String s) {
    for (int k = 0; k < tile.length; k++)
      tile[k].setText(""+ s.charAt(k));
  } // labelButtons()
  public void actionPerformed(ActionEvent e) {
    String result = "";
    if (e.getSource() == reset) { // Reset clicked?
      sliding = new SlidingTilePuzzle();
      label.setText(prompt);
    }
    for (int k = 0; k < tile.length; k++) // Tile clicked?
      if (e.getSource() == tile[k])
        result = ((GUIPlayableGame)sliding).submitUserMove(""+ k);
    if (result.indexOf("illegal") != -1)
      java.awt.Toolkit.getDefaultToolkit().beep();
    puzzleState = sliding.reportGameState();
    labelButtons(puzzleState);
    if (sliding.gameOver())
      label.setText("You did it! Very nice!");
  } // actionPerformed()

  public static void main(String args[]) {
    new SlidingGUI("Sliding Tile Puzzle");
  } // main()
} // SlidingGUI
Listing 9.12.6. Implementation of the SlidingGUI class.

The most important method in the GUI is the actionPerformed() method. This method controls the GUI's actions and is called automatically whenever one of the GUI's buttons is clicked. First, we check whether the reset button has been clicked. If so, we reset the puzzle by creating a new instance of SlidingTilePuzzle and re-initializing the prompt label.

Next we use a for loop to check whether one of the tile buttons has been clicked. If so, we use the loop index, k, as the tile's identification and submit this to the puzzle as the user's move:

if (e.getSource() == tile[k])
  result = ((GUIPlayableGame)sliding).submitUserMove(""+ k);

The cast operation is necessary here because we declared sliding as a SlidingTilePuzzle rather than as a GUIPlayableGame. Note also that we have to convert k to a String when passing it to submitUserMove().

As a result of this method call, the puzzle returns a result, which is checked to see if the user's move was illegal. If result contains the word ā€œillegalā€, the computer beeps to signal an error:

if (result.indexOf("illegal") != -1)
    java.awt.Toolkit.getDefaultToolkit().beep();

The java.awt.Toolkit is a class that contains lots of useful methods, including the beep() method. Note that no matter what action is performed, a reset or a tile click, we update puzzleState by calling reportGameState() and use it to relabel the tile buttons. The last task in the actionPerformed() method is to invoke the puzzle's gameOver() method to check if the user has successfully completed the puzzle. If so, we display a congratulatory message in the GUI's window.

Finally, the main() for a GUI is very simple, consisting of a single line of code:

new SlidingGUI("Sliding Tile Puzzle");

Once a SlidingGUI is created, with the title of ā€œSliding Tile Puzzle,ā€ it will open a window and manage the control of the puzzle.

Activity 9.12.1.

Try the Sliding Tile Puzzle below

You have attempted of activities on this page.