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. The dice are drawn on a Canvas. You can assume that the size of the canvas is 100 by 100 pixels. When the user clicks on the canvas, 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, such as "void drawDie(GraphicsContext g, int val, int x, int y)", to draw a die at the specified (x,y) coordinates. The second parameter, val, specifies the number of dots that are showing on the die. Here is a picture of a canvas displaying two the dice:
This is largely an exercise in precision drawing.
We will need a Canvas for drawing the dice. In my program, it is a global variable. We need to add a MousePressed event handler to the canvas. My handler calls a method, roll(), that will assign new values to the dice and redraw the canvas. The code is in the start() method:
canvas = new Canvas(100,100); // Canvas is 100-by-100 pixels. draw(); // Draw the original dice. canvas.setOnMousePressed( e -> roll() ); // Add the event handler.
The draw() method completely redraws the canvas. The hardest part of this exercise was 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 fillOval() 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(GraphicsContext g, int val, int x, int y) { g.setFill(Color.WHITE); g.fillRect(x, y, 35, 35); g.setStroke(Color.BLACK); g.strokeRect(x+0.5, y+0.5, 34, 34); g.setFill(Color.BLACK); 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 draw() method fills the canvas with light blue, draws a 2-pixel blue border around the canvas, and draws the two dice:
public void draw()) { GraphicsContext g = canvas.getGraphicsContext2D(); g.setFill(Color.rgb(200,200,255)); g.fillRect(0,0,100,100); // fill canvas with light blue g.setLineWidth(2); g.strokeRect(1,1,98,98); // draw a blue border drawDie(g, die1, 10, 10); // draw the dice drawDie(g, die2, 55, 55); }
Note the different parameters to g.fillRect() and g.strokeRect(). To cover the entire canvas, we need to fill a 100-by-100 rectangle, with its top-left corner at (0,0), so the command is g.fillRect(0,0,100,100). But when a rectangle is stroked, the center of the pen is dragged along the boundary of the rectangle. For a 2-pixel border, we need to stroke a rectangle that is inset from the boundary of the canvas by half the size of the pen, that is, by one pixel. Since the rectangle is inset by 1 pixel on each side, it is 2 pixels smaller than the full 100-pixel canvas, hence its width and height are 98. If I had said g.strokeRect(0,0,100,100), half of the pen would lie outside the canvas, and we would only see a 1-pixel border.
import javafx.application.Application; import javafx.stage.Stage; import javafx.scene.Scene; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.layout.Pane; import javafx.scene.paint.Color; /** * Shows a pair of dice that are rolled when the user clicks on the * canvas where the dice are drawn. */ public class RollDice extends Application { public static void main(String[] args) { launch(); } //--------------------------------------------------------------------- private int die1 = 4; // The values shown on the dice. private int die2 = 3; private Canvas canvas; // The canvas on which the dice are drawn. /** * The start() method sets up the GUI and installs a mouse listener * on the canvas where the dice are to be drawn. */ public void start(Stage stage) { canvas = new Canvas(100,100); draw(); // Draw the original dice. canvas.setOnMousePressed( e -> roll() ); Pane root = new Pane(canvas); Scene scene = new Scene(root); stage.setScene(scene); stage.setTitle("Dice!"); stage.setResizable(false); stage.show(); } /** * 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(GraphicsContext g, int val, int x, int y) { g.setFill(Color.WHITE); g.fillRect(x, y, 35, 35); g.setStroke(Color.BLACK); g.strokeRect(x+0.5, y+0.5, 34, 34); g.setFill(Color.BLACK); 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 canvas, to show the new values. */ private void roll() { die1 = (int)(Math.random()*6) + 1; die2 = (int)(Math.random()*6) + 1; draw(); } /** * The draw() method just draws the two dice and draws * a two-pixel wide blue border around the canvas. */ private void draw() { GraphicsContext g = canvas.getGraphicsContext2D(); g.setFill(Color.rgb(200,200,255)); g.fillRect(0,0,100,100); g.setStroke( Color.BLUE ); g.setLineWidth(2); g.strokeRect(1,1,98,98); drawDie(g, die1, 10, 10); drawDie(g, die2, 55, 55); } } // end class RollDice