[ Exercises | Chapter Index | Main Index ]

Solution for Programming Exercise 6.3


This page contains a sample solution to one of the exercises from Introduction to Programming Using Java.


Exercise 6.3:

Write a program that shows a pair of dice. When the user clicks on the panel in the program, the dice should be rolled (that is, the dice should be assigned newly computed random values). Each die should be drawn as a square showing from 1 to 6 dots. Since you have to draw two dice, its a good idea to write a subroutine, "void drawDie(Graphics g, int val, int x, int y)", to draw a die at the specified (x,y) coordinates. The second parameter, val, specifies the value that is showing on the die. Assume that the size of the panel is 100 by 100 pixels. Here is a picture of the panel that displays the dice:

a pair of dice


Discussion

This is largely an exercise in precision drawing.

We need a subclass of JPanel to make the panel. We will also need an object to respond to mouse events. As always, there are several ways to approach this: Let the panel implement MouseListener and listen for events on itself, use a separate class to define the listener object, or use an anonymous inner class (see Subsection 6.3.5). In this case, I decided to use an anonymous class, since the mouse-handling code is very simple. The listener object is created and registered to listen for mouse events in the constructor of the main class:

addMouseListener( new MouseAdapter() {
    public void mousePressed(MouseEvent evt) {
        roll();
    }
});

By using MouseAdapter instead of MouseListener as the superclass for the listener, I avoid the necessity of providing empty definitions for mouseReleased, mouseClicked, mouseEntered, and mouseExited. MouseAdapter is discussed in Subsection 6.3.5. In the mousePressed() method, roll() is a method that is defined in the main class; it just rolls the dice. It would be reasonable to put the code for rolling the dice in mousePressed(), but writing a subroutine to do it makes the program a little easier to modify for the next exercise. Anyway, when you can identify a self-contained, meaningful task to be performed, it's never a bad a idea to write a subroutine to do it. It will make the program more readable, if nothing else. My roll subroutine assigns random values to the dice and calls repaint() so that the new values will be shown.

The hardest part of this exercise is drawing the dice. I made each die 35 pixels wide, leaving a 10 pixel border on each side and 10 pixels between the dice, for a total of 100 pixels. The top left corner of the left die is at (10,10), the top left corner of the right die is at (55,55). The 55 includes the 10 pixel border on the left, the 35 pixel width of the other die, and the 10 pixels between the dice. The paintComponent() method calls a drawDie() routine to draw each die, using the commands:

drawDie(g, die1, 10, 10);
drawDie(g, die2, 55, 55);

where die1 is the numerical value shown on the first die and die2 is the numerical value of the second die.

As for the drawDie routine, there are two quite different algorithms that could have been used for drawing the dots. Either:

if the value shown is 1
    draw 1 dot (in the center)
else if the value shown is 2
    draw 2 dots (in the top-left and bottom-right corners)
  .
  .
  .
else if the value shown is 6
    draw 6 dots (along the left and right edges)

Or:

if the value has a dot in the top-left corner
     draw the top-left dot
else if the value has a dot in the top-right corner
     draw the top-right dot
   .
   .
   .
else if the value has a dot in the bottom-right corner
     draw the bottom-right dot

Although the first algorithm is more obvious, the second requires much less typing. (The first algorithm ends up using 21 drawOval() commands, while the second uses only 7.) Furthermore, after drawing the dice on paper, I found that the conditions for testing when a given dot needs to be drawn are simpler than I expected. For example, the values that need a dot in the top-left position are all the values greater than 1. The algorithm leads to my drawDie() routine:

/**
 * Draw a die with upper left corner at (x,y).  The die is
 * 35 by 35 pixels in size.  The val parameter gives the
 * value showing on the die (that is, the number of dots).
 */
private void drawDie(Graphics g, int val, int x, int y) {
     g.setColor(Color.white);
     g.fillRect(x, y, 35, 35);
     g.setColor(Color.black);
     g.drawRect(x, y, 34, 34);
     if (val > 1)  // upper left dot
        g.fillOval(x+3, y+3, 9, 9);
     if (val > 3)  // upper right dot
        g.fillOval(x+23, y+3, 9, 9);
     if (val == 6) // middle left dot
        g.fillOval(x+3, y+13, 9, 9);
     if (val % 2 == 1) // middle dot (for odd-numbered val's)
        g.fillOval(x+13, y+13, 9, 9);
     if (val == 6) // middle right dot
        g.fillOval(x+23, y+13, 9, 9);
     if (val > 3)  // bottom left dot
        g.fillOval(x+3, y+23, 9, 9);
     if (val > 1)  // bottom right dot
        g.fillOval(x+23, y+23, 9,9);
}

It took some care to figure out the numbers to use in the fillOval commands. The individual dots have a diameter of 9 pixels. There are three rows of dots, which have a combined height of 27 pixels. That leaves 35 minus 27, or 8 pixels for spacing. I use 3 pixels between the dots and the edge of the die, and 1 pixel between rows. This puts the tops of the rows at 3, 3+9+1, and 3+9+1+9+1, that is, at 3, 13, and 23. The columns use the same numbers. (If you believe that I got all this right the first time, I won't disillusion you!)

The paintComponent method just draws the two dice and the border around the panel

public void paintComponent(Graphics g) {
   super.paintComponent(g);  // fill with background color.
   Graphics2D g2 = (Graphics2D)g;
   g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                               RenderingHints.VALUE_ANTIALIAS_ON);
   g.setColor( Color.BLUE );
   g.drawRect(0,0,99,99);
   g.drawRect(1,1,97,97);
   drawDie(g, die1, 10, 10);
   drawDie(g, die2, 55, 55);
}

The paintComponent() method turns on antialiasing, since without it the dots on the dice have a jagged appearance (see Subsection 6.2.5).

One more small remark on the solution: The constructor of the DicePanel class sets the preferred size of the panel to be 100-by-100 pixels, using the command:

setPreferredSize( new Dimension(100,100) );

Since the main() routine for the program will use the pack() method to set the size of the frame, it is essential that the panel have a preferred size. If not, the size of the frame will not be set correctly.


The Solution

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

/**
 * Shows a pair of dice that are rolled when the user clicks on the
 * program.  It is assumed that the panel is 100-by-100 pixels.
 */
public class DicePanel extends JPanel {
 
    /**
     * A main routine allows this class to be run as an application.
     */
    public static void main(String[] args) {
        JFrame window = new JFrame("Dice");
        DicePanel content = new DicePanel();
        window.setContentPane(content);
        window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        window.setLocation(120,70);
        window.pack();
        window.setVisible(true);
    }

    //---------------------------------------------------------------------

    private int die1 = 4;  // The values shown on the dice.
    private int die2 = 3;
 
    /**
     *  The constructor adds a mouse listener to the panel.  The listener
     *  will roll the dice when the user clicks the panel.  Also, the
     *  background color and the preferred size of the panel are set.
     */
    public DicePanel() {
       setPreferredSize( new Dimension(100,100) );
       setBackground( new Color(200,200,255) );  // light blue
       addMouseListener( new MouseAdapter() {
           public void mousePressed(MouseEvent evt) {
               roll();
           }
       });
    }
    
    /**
     * Draw a die with upper left corner at (x,y).  The die is
     * 35 by 35 pixels in size.  The val parameter gives the
     * value showing on the die (that is, the number of dots).
     */
    private void drawDie(Graphics g, int val, int x, int y) {
       g.setColor(Color.white);
       g.fillRect(x, y, 35, 35);
       g.setColor(Color.black);
       g.drawRect(x, y, 34, 34);
       if (val > 1)  // upper left dot
          g.fillOval(x+3, y+3, 9, 9);
       if (val > 3)  // upper right dot
          g.fillOval(x+23, y+3, 9, 9);
       if (val == 6) // middle left dot
          g.fillOval(x+3, y+13, 9, 9);
       if (val % 2 == 1) // middle dot (for odd-numbered val's)
          g.fillOval(x+13, y+13, 9, 9);
       if (val == 6) // middle right dot
          g.fillOval(x+23, y+13, 9, 9);
       if (val > 3)  // bottom left dot
          g.fillOval(x+3, y+23, 9, 9);
       if (val > 1)  // bottom right dot
          g.fillOval(x+23, y+23, 9,9);
    }
 
 
    /**
     * Roll the dice by randomizing their values.  Tell the
     * system to repaint the panel, to show the new values.
     */
    void roll() {
       die1 = (int)(Math.random()*6) + 1;
       die2 = (int)(Math.random()*6) + 1;
       repaint();
    }
    
    
    /**
     * The paintComponent method just draws the two dice and draws
     * a one-pixel wide blue border around the panel.  Antialiasing
     * is turned on to make the ovals look nicer.
     */
    public void paintComponent(Graphics g) {
       super.paintComponent(g);  // fill with background color.
       Graphics2D g2 = (Graphics2D)g;
       g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                                   RenderingHints.VALUE_ANTIALIAS_ON);
       g.setColor( Color.BLUE );
       g.drawRect(0,0,99,99);
       g.drawRect(1,1,97,97);
       drawDie(g, die1, 10, 10);
       drawDie(g, die2, 55, 55);
    }
 
} // end class DicePanel

[ Exercises | Chapter Index | Main Index ]