Shamrock Treasure Hunt

Leapin’ Leprechauns!  It’s St. Patrick’s Day and the leprechauns still can’t find a safe place to hide their gold.  They think they’ve gotten crafty and found a better place, but this game shows they might need to try again.  Similar to minesweeper, the Shamrock Treasure Hunt displays the distance to the leprechauns’ gold when a tile is clicked.

This program introduces several concepts including 2D arrays and GUI’s.

Use of Arrays

To keep track of the program logic, we will need to have a 2D array storing the location of the leprechaun’s gold and the distance of each square to it.  A 2D array really just looks like a large grid like this, with values in this case representing the distance to the gold:

5 3 2 2 2 2
4 3 2 1 1 1
4 3 2 1 -1 1
4 3 2 1 1 1
5 3 2 2 2 2

Wait a minute, what is -1 doing as the distance?  We are using -1 as a placeholder or sentinel value to mark the spot of the treasure.  We can’t use 0 because Java automatically initializes the values of all array elements to zero unless each element’s value is specified.  If we used 0, then the logic of some of our methods wouldn’t work.

Dive deep!  Click here to learn more about sentinel values.

Important!  2D arrays access elements as [row][column] where rows go across and columns go down.  This means that if you are storing x and y values for an image, you will access the array as array[y][x].  Confusing these can result in lots of errors.

LOGIC

Setup – Getting the board ready

Setup is pretty simple:  we call a method to hide the treasure and then call another helper method to fill the rest of the table.

public void setup(Group g) {
  hideTreasure();
  fillTable(g);
}

Hide Treasure

Quick!  Did you see the leprechaun disappear?  He’s just magically hidden his treasure.  Well, not quite. We use random numbers to select where the treasure goes so it will be in a new spot each game.

public void hideTreasure() {
  treasX = (int) (Math.random() * cols);
  treasY = (int) (Math.random() * rows);
  System.out.println("x, y: " + treasX + ", " + treasY);
  System.out.println("rows, cols: " + rows + ", " + cols);
  board = new int[rows][cols];
  board[treasY][treasX] = -1;
}

Math.random() returns a pseudo-random number between 0 and 1, based on the clock’s internal computer.  We then multiply the result by the number of columns (or rows) to get a value between 0 and the number of columns.  Then we cast this back into an integer so we can use it as an index for the row and column.

Dive deep!  Learn more about random numbers.

The two print statements aren’t needed to make the program run, but I left them in to show a part of the debugging process used to make sure the program logic was running correctly.  If there is any doubt what value a variable has, a print statement with the variable name and value can really help!

Dive deep!  Learn more about debugging.

Finally,  we initialize a new array and store it at the pointer provided by board.

Fill table

How many steps do I need to get to the treasure?  Let’s see… 1, 2, 3, … Fortunately, there’s a faster way to do the calculations.

public void fillTable(Group g) {
  for (int row = 0; row < rows; row++) {
    for (int col = 0; col < cols; col++) {
      if (board[row][col] != -1) {
        board[row][col] = Math.max(Math.abs(row - treasY), Math.abs(col - treasX));
      }
      drawTile(col * width/cols, row * height/rows, width/cols, -1, g);
    }
  }	
}

The basic idea here is to iterate through all the elements in the array, calculating their distance from the treasure. By using a nested for-loop, we can automate the process of changing the row and column numbers, travelling through all the elements in the first row before visiting the second.  Inside the inner for-loop, we check to make sure the current position does not contain the treasure before calculating the distance. Because this game allows diagonal steps, we take the maximum of the absolute value of the distances from the current row to the treasure row and the current column to the treasure column.  Then we draw the tile with a placeholder value of -1 before continuing the for-loop.

GRAPHICS

Group

A group is a general all-purpose container to hold the shapes drawn.  Groups like g can be thought of a bit like a sheet of paper. You can put one group inside another like taping a paper to another paper.  You can also pass a group to multiple methods in a row like passing a paper down a lineup with each person drawing something on it before handing it to the next person.  

Shapes

Most of the ways of drawing things in JavaFX rely on shapes.  A few shapes with their parameters and an example call are presented below:

Shape Parameters Example
Ellipse centerX, centerY, radiusX, radiusY new Ellipse(0, 0, 10, 10)
Rectangle left-side, top, width, height new Rectangle(0, 0, 30, 40)
Arc centerX, centerY, radiusX, radiusY, startingAngle, degreesToRotate new Arc(100, 100, 70, 50, 0, 180)

You can learn more about the shapes provided in JavaFX at the javadocs website:

https://docs.oracle.com/javase/8/javafx/api/javafx/scene/shape/Shape.html

Draw Tile

Here’s the first of several methods devoted to graphics.  It’s main purpose is drawing a tile for the game board.

public void drawTile(double x, double y, double s, int dist, Group g) {
  Rectangle r = new Rectangle(x, y, s, s);
  r.setFill(Color.rgb(205, 235, 205));
  r.setStroke(Color.BLACK);
  g.getChildren().add(r);
  drawShamrock(x + s * 0.1, y + s * 0.05, s * 0.75, s * 0.75, Color.rgb(0, 200, 0), g);
  if (dist > 0) {
    Text text = new Text(x, y + 3 * s/5 - 5, "" + dist);
    text.setFill(Color.rgb(255,  195,  0));
    text.setFont(new Font(s/3));
    if ( dist < 10) {
      text.setX(x + s/2 - s/9);
    } else {
      text.setX(x + s/2 - s/6);
    }
    g.getChildren().add(text);
  }
}

The parameters x and y are the coordinates for the top left corner of the tile, and s is the length of the sides of the tile (our tiles are squares).  We make a point of using variables of type double when dealing with graphics because we will often want to represent parts of a pixel to keep the screen display from having random white or black pixels.  The integer dist is the value of the tile’s distance (in tiles) from the treasure. Finally g is a general all-purpose container to hold the shapes drawn. Groups like g can be thought of a bit like a sheet of paper.  You can put one group inside another like taping a paper to another paper. You can also pass a group to multiple methods in a row like passing a paper down a lineup with each person drawing something on it before handing it to the next person.  

The first thing we do is draw a rectangle and set its fill color and its line color. Then we add it to the group and call a helper method to draw a shamrock. Then, if the distance is greater than 0, we draw text displaying the number of steps to the treasure.  If this distance is a single digit, we center it on the shamrock by finding the center(x + s / 2) and subtracting 1/9 of the tile’s width. If this distance is two digits, we find the center and subtract ⅙ of the width of the tile.  Then we add the text to the group as well.

Draw Shamrock

public void drawShamrock(double x, double y, double w, double h, Color c, Group g) {
  Ellipse e1 = new Ellipse(x + w / 2, y + h / 4, w / 4, h / 4);
  e1.setStroke(c);
  e1.setFill(c);
  Ellipse e2 = new Ellipse(x + w/4, y + (h * 3/5), w/4, h/4);
  e2.setStroke(c);
  e2.setFill(c);
  Ellipse e3 = new Ellipse(x + 3 * w / 4, y + (h * 3/5), w/4, h/4);
  e3.setStroke(c);
  e3.setFill(c);
  Rectangle rect = new Rectangle(x + (w * 7/16), y + h/3, w/8, h * 3/5);
  rect.setStroke(c);
  rect.setFill(c);
  g.getChildren().addAll(e1, e2, e3, rect);
}

The logic here is once again primarily geometry.  We take in parameters representing the x and y coordinates of the top left corner of the shamrock’s bounding box as well as its width, height, and color.  We also have a parameter for the group of shapes to which the shamrock will be added. Then we draw the shamrock of three circles that each have a radius of ¼ the height of the shamrock, and a rectangle centered in the middle as a stem.  For each of these shapes, we set the line color and fill color to be the same to create a uniform appearance. Finally, we add them to their group using the g.getChildren().addAll() command. This command lets us add any number of shapes to the list of shapes g already has:  the getChildren() is just another way of requesting the group’s list of shapes.

End screen – Display the reward

public void displayEndScreen(Group g) {
  drawRainbow2(width/2, height/4, width * 0.35, height * 0.35, g);
  drawPotOGold(g);
}

This short little method makes another screen showing a pot ‘o gold at the end of the rainbow when the player successfully completes the game.   It calls two helper methods to accomplish its work.

Draw the Rainbow

Scientists know rainbows are created by the diffraction of sunlight through raindrops, but we’re making one from concentric arcs.

public void drawRainbow(double x, double y, double w, double h, Group g) {
  double wid = w / 12;
  Arc red = new Arc(x + width/2, y, w, h, 0, 180);
  red.setFill(Color.rgb(255,  0,  0));
  Arc orange = new Arc(x + width/2, y, w - wid, h - wid, 0, 180);
  orange.setFill(Color.rgb(255,  195,  0));
  Arc yellow = new Arc(x + width/2, y, w - (2 * wid), h - (2 * wid), 0, 180);
  yellow.setFill(Color.rgb(255,  255,  0));
  Arc green = new Arc(x + width/2, y, w -  (3 * wid), h - (3 * wid), 0, 180);
  green.setFill(Color.rgb(190,  255,  190));
  Arc blue = new Arc(x + width/2, y, w - (4 * wid), h - (4 * wid), 0, 180);
  blue.setFill(Color.rgb(0,  190,  255));
  Arc purple = new Arc(x + width/2, y, w - (5 * wid), h - (5 * wid), 0, 180);
  purple.setFill(Color.rgb(85,  85,  200));
  Arc none = new Arc(x + width/2, y, w - (6 * wid), h - (6 * wid), 0, 180);
  none.setFill(Color.rgb(240,  240,  240));
  g.getChildren().addAll(red, orange, yellow, green, blue, purple, none);
}

We take in the x and y coordinates for the center of the arc as well as the width and height we want our rainbow to be.  As usual, we also have a parameter for the group of shapes to which to add our new shape. The first thing we do is divide the width by 12 so we have a variable by which to decrement the width of the arc.  Then we draw each arc individually. The first two values taken as parameters by the arc function are the same for each arc because they represent the central point around which it rotates. Then we choose the width and height for the arcs by giving the radii for its associated ellipse, subtracting a linearly increasing amount each time we draw the arc so that the previous arcs will be visible.  The final two parameters are the starting angle and the number of degrees we will rotate through. Then we set the color of each arc. At the end, we all all the arcs to the group at once, drawing a rainbow that looks like this:

You may be asking, why did we draw 7 arcs, but only see 6?  If you look closely, you will see a thin line along the bottom of the rainbow’s center.  This is the bottom edge of the seventh arc which is set to be approximately the same color as the background.  You may have also noticed that there’s a lot of repetition in the code above. This same image can be more efficiently created using a for-loop, but it is slightly harder to follow the order of the colors.

public void drawRainbow2(double x, double y, double w, double h, Group g) {
  double wid = w / 12;
  Color [] rainbow = {
      Color.rgb(255,  0,  0), Color.rgb(255,  195,  0), Color.rgb(255,  255,  0),
      Color.rgb(190,  255,  190), Color.rgb(0,  190,  255), Color.rgb(85,  85,  200),
      Color.rgb(240,  240,  240)
  };
  for (int i = 0; i < 7; i++) {
    Arc arc = new Arc(x + width/2, y, w - (i * wid), h - (i * wid), 0, 180);
    arc.setFill(rainbow[i]);
    g.getChildren().add(arc);
  }
}

Here we again calculate the width of each band of the rainbow.  Then we store all our rainbow colors in a color array. Then the loop draws the arcs from the outermost red band to the innermost gray one.  This overlays the inner arcs over the outer ones, creating the rainbow. When we draw each arc, we make it have a size of the full width minus the product of the width of each arc multiplied by the number of arcs we’ve drawn.  After drawing the arc, we set its color using the index variable to get the right color out of the array. Then we add it to the group. Now we have a rainbow.

Draw Pot ‘o Gold

This is a simple method with straight-forward logic.  We draw a rectangle for the body of the pot, an arc for the bottom, and an ellipse for the open top.

public void drawPotOGold(Group g) {
  Rectangle rect = new Rectangle(1000, 300, 200, 200);
  rect.setFill(Color.YELLOW);
  rect.setStroke(Color.rgb(255, 205, 0));
  Arc arc = new Arc(1100, 500, 100, 25, 180, 180);
  arc.setFill(Color.YELLOW);
  arc.setStroke(Color.rgb(255, 205, 0));
  Ellipse ellipse = new Ellipse(1100, 300, 100, 50);
  ellipse.setFill(Color.YELLOW);
  ellipse.setStroke(Color.rgb(255, 205, 0));
  Rectangle r = new Rectangle(1000, 700, 0, 0);
  r.setFill(Color.rgb(245,  245,  245));
  g.getChildren().addAll(rect, arc, ellipse, r);
}

We make sure to set the fill color to be yellow for all three shapes.  Then we set the stroke or line color to be not quite yellow.  Why?  Try setting the stroke to yellow to see.  (Hint:  with the stroke set to yellow, does the pot look 3D or flat?)

GAME CONTROLS

Menus

Menus:  things placed on restaurant tables to tell patrons what they can order. This is a slightly different kind of menu, but it serves a similar purpose:  giving users options in playing the game. If we’re going to have a game that can be repeated or change level, we will want access to a menu to let users do these things.

public MenuBar makeMenu(Stage stage) {
  MenuBar bar = new MenuBar();
  Menu game = new Menu("Game");
    
  MenuItem newGame = newGameItem(stage);
  MenuItem level = newLevelItem(stage);
  MenuItem exit = newExitItem();

  game.getItems().addAll(newGame, level, exit);
  bar.getMenus().add(game);
  return bar;
}

A menu has three main components:  a menu bar, which is that narrow strip across the top of many computer applications; one or more menus (subdirectories of options), and one or more menu items (each belonging to their own subdirectory).  This method creates only one menu, “Game” which then contains three items: new game, level, and exit. Because menu items are complex critters that need to be able to respond to user input, it is convenient to make helper methods that construct and return them.  Once all the menu items are made, they are added to their menu, which is then added to the menu bar. Finally, we return the menu bar. The single parameter variable is the window in which everything is displayed. Notice we pass it to two of the menu items.

Exit Item – Let’s Get Outta Here!

Your game is going badly and you feel like the leprechauns must have cast a spell on you to dull your wits.  Maybe if you end the game you can break the spell. You have two options. You can, of course, close the big red X button to close this like any other window, but you want something with more pizazz.  That’s where the exit item comes in.

private MenuItem newExitItem() {
  MenuItem exit = new MenuItem("Exit");
  exit.setOnAction(new EventHandler<ActionEvent>() {

    @Override
    public void handle(ActionEvent event) {
      System.exit(0);
    }
  });

  return exit;
}

We create a new menu item, aptly named exit.  Then we need to attach an event handler to it so it responds to being clicked.  To do this, we use the setOnAction method which embeds the creation of a new EventHandler as its parameter.  This event handler is of type ActionEvent, basically meaning a mouse click with either button. We are then required to implement a method to tell the computer how to handle this action.  Here, we use System.exit(0) to say “End the whole program and let the computer know we exited in a normal state.” Specifically, System.exit will shut down all currently running Java processes – fine for an application like this, but a really bad idea for homework assignments.  The 0 is used to indicate normal exiting, while other integers are used to designate error messages.  Finally we return the new menu item.

New Game – Start it up

One game is okay, but what if you want multiple games?  A new game option lets you do just that.

private MenuItem newGameItem(Stage stage) {
  MenuItem newGame = new MenuItem("New Game");
  newGame.setOnAction(new EventHandler<ActionEvent>() {

    @Override
    public void handle(ActionEvent event) {
      // create a new game
      stage.setScene(newGame(stage));
    }

  });
  return newGame;
}

This looks a lot the same as the exit item, except here we take in a parameter for the current window and handle the click differently.  We handle this event by resetting the scene for the whole window with a brand new one. We do this by calling the newGame method which initializes each game.

New Game – the logic

We have to do a lot of prep to make a new game:  set all the variables, make a data representation of the board, and draw it.

public Scene newGame(Stage stage) {
  if (cols * height/rows < 3 * Screen.getPrimary().getBounds().getWidth()/4) {
    width = cols * height/rows;
  } else {
    height = rows * width/cols;
  }
  BorderPane big = new BorderPane();
  Group group = new Group();
  MenuBar bar = makeMenu(stage);
  setup(group);
  mouseInput(group, big);
  big.setBottom(group);
  big.setTop(bar);
  return new Scene(big, width, height + 45);
}

The first thing we do is set the size of the window based on the number of columns and rows.  We create the constraint that we want the window to be no wider than ¾ of the screen. To get the screen width of the user’s monitor, we use Screen.getPrinary().getBounds().getWidth().  Then we set the window width to be the number of columns times the height / the number of rows. If the width is greater than ¾ of the screen, then we shrink the height so we can keep the tiles square at the same width.  

Then we make a big pane to contain all the visible graphics in our window, including the game board and the menu bar.  We also make a group to pass to the helper methods for them to draw shapes into. We make our menu bar, setup our board, add mouse input, and place things where we want them.  Because big is a BorderPane, it has five sections:  top, bottom, left, right, and center. Any part that doesn’t contain another node has no size, so setting the menu bar to top, and group (our board) to bottom makes the board almost the whole size of the screen.  Then we create a scene holding our BorderPane and return it to the caller.

New Level Item – Time to take it to another level!

We already have a game we can repeat, but a one-level game starts to get boring.  So let’s give the user the option to set their level to any positive number of rows and columns.

private MenuItem newLevelItem(Stage stage) {
  MenuItem level = new MenuItem("Change Level");
  level.setOnAction(new EventHandler<ActionEvent>() {
    @Override
    public void handle(ActionEvent event) {
      // Call the methods to change the level of the game
      Stage chooseLevel = levelChooser(stage);
      chooseLevel.show();
    }
  });
  return level;
}

We start by making a menu item and setting the action when it is clicked.  In our case, we want a new window to open with text boxes to input the rows and columns.  To do this, we create a new stage with the helper method levelChooser, and tell the stage to show itself.

Level Chooser – Pick Your Level

Choices, choices.  We’re bored with finding the treasure too quickly so we want a harder level.  No problem. Level chooser lets you specify any number of rows and columns.

private Stage levelChooser(Stage stage) {
  Stage chooseLevel = new Stage();
  TextField r = new TextField();
  TextField c = new TextField();
  Label ro = new Label("Rows: ");
  Label co = new Label("Columns: ");
  BorderPane bor = new BorderPane();
  GridPane grid = new GridPane();
  grid.add(ro, 0, 0);
  grid.add(co, 0, 1);
  grid.add(r, 1,  0);
  grid.add(c, 1,  1);
  Button ok = new Button("OK");
  ok.setOnAction(new EventHandler<ActionEvent> () {

    @Override
    public void handle(ActionEvent arg0) {
      // Choose the level
      chooseLevel(r, c);
      chooseLevel.hide();
        
      // Update the window
      Scene scene = newGame(stage); 	// Make a new game
      stage.setScene(scene);			// Set the scene
      stage.sizeToScene();			// Resize the window
    }

  });
  bor.setTop(grid);
  bor.setBottom(ok);
  Scene s = new Scene(bor, 400, 400);
  chooseLevel.setScene(s);
  return chooseLevel;
}

This is a big chunk of code, so let’s break it down into smaller bits.  The first 12 lines make a new window, create and label text fields, make a button, and organize it all in a grid pane.  A gridpane is a layout that puts things in tables. When adding a node to a gridpane, it is best to specify in which row and column you want it placed.  These are the parameters after the node, in the order row then column.

Once we’ve make the button, we want to decide what happens when it is clicked.  Unlike with the main window, we don’t want the program to close, so we decide to simply hide it.  First we call the helper method chooseLevel to take care of the logic of changing the number of rows and columns.  When we hide the level chooser, we call newGame(), assign the new game to be the scene, and resize it to fit the current board.

Finally, we put the button in the smaller window, set the top and bottom displays, set the scene to show our organized scene, and display our window.

Choosing a level – hidden helper

This short little method could really be included in the big one above, but parsing out the contents of the text fields is important enough they seem to deserve their own method.

private void chooseLevel(TextField r, TextField c) {
  rows = Integer.parseInt(r.getText());
  cols = Integer.parseInt(c.getText());
}

We take in two text fields representing rows and columns, and use the Integer method parseInt to get the int value from the string contained in each text field.  “But I entered numbers in the text fields!” you say. Well, yes. However, the text field converts them into a string returned by its method getText(). While it’s not implemented in this case, chooseLevel would be the perfect place to do input validation to ensure a user entered an integer greater than or equal to 1 in the text fields.  For now, we’re just trusting the users to know what they’re doing (and having our game freeze if they don’t), but a real computer game should check to make it foolproof. If you’re curious how to get error messages, try typing in words instead of numbers. You could also make it check for certain keywords like “two” if you want. 🙂

Mouse Input

“Squeak! Squeak!  I’d like more cheese, please.”  Not that sort of mouse – but a similar idea.  We want our mouse to tell the computer when we’ve clicked a certain square because we want to know how close we are to the treasure.

public void mouseInput(Group table, BorderPane window) {
  table.setOnMouseClicked(new EventHandler<MouseEvent>() {

    @Override
    /**
     * Handle mouse-clicks on game tiles.
     */
    public void handle(MouseEvent event) {
      double mouseX = event.getSceneX();
      double mouseY = event.getSceneY() - 45;
      System.out.println(mouseY);
      // Determine which tile the mouse clicked
      int r = (int)(mouseY/(height/rows)); // Calculate which row
      int c = (int)(mouseX/(width/cols));  // Calculate which column
      if (board[r][c] == -1) {
        // The pot 'o gold is discovered
        //group1 = new Group();  // uncomment to remove double rainbow effect
        displayEndScreen(group1);
        window.setBottom(group1);
      } else {
        // A normal tile is clicked
        drawTile(c * width/cols, r * height/rows, width/cols, board[r][c], table);
      }
    }
  });
}

We really just have a fancy “setOnAction” here like we encountered with buttons and menu items.  But it does some special jobs to handle the main mouse clicks of the game which occur on the board.  The first thing we do is determine where the mouse clicked in 2D coordinate space. Then we calculate which tile that correlates to.  Then, if the square in our array representing the board has a value of -1 (contains the treasure), we display the end screen at the bottom of the window.  Otherwise, we draw a tile with the number on it.

Note:  we currently have the line creating and assigning a new group to group1 commented, resulting in the double rainbow effect below whenever the size of the board has gotten larger for the second game:

What causes this?  Well, if we don’t give the board a new group, it remembers the graphics of the old one, and simply adds more on, creating a graphic artifact.  Since a double rainbow looks twice as lucky, I decided to leave that line commented, but if you want to get rid of the glitch, just uncomment it.

Pulling it all together

Main

Unlike normal non-graphic Java, the main method for a JavaFX app has only one command.

public static void main(String[] args) {
  launch(args);// run the app
}

launch(args) tells the program to get the start method running passing in any command-line arguments.

Start

Start does things like set up the screen, give it a scene, and make it visible.

public void start(Stage primaryStage) throws Exception {
  Scene scene = newGame(primaryStage);
  primaryStage.setScene(scene);
  primaryStage.sizeToScene();
  primaryStage.show();
}

First, we create a new scene.  then  we set the stage scene to our new scene.  Next we resize the stage or window to fit our scene.  Finally, we display it.

 

That’s about it for making a simple game with JavaFX.  The complete code is below:

ShamrockTreasureHunt.txt