Musings about Java and Swing

Farkle Simulation Using Java Swing

My last two articles have been short, simple Java projects. This is a much more complex example of a Farkle simulation using Java Swing.

Introduction

Farkle is a dice game that can be found on the Internet. The rules are fairly simple.

  1. A Farkle game consists of 10 rounds.
  2. For each round, you start by rolling 6 dice. You score some or all of the 6 dice.
  3. if you score the dice and have any dice left over, you can roll the left over dice again.
  4. If you score all 6 dice in one or more rolls, you can roll 6 more dice.
  5. At any time after you score or exceed the minimum score of 300, you may stop rolling the dice and take your score.
  6. If the dice you have rolled cannot score, you’ve rolled a Farkle, and your score is zero for the round..

The score for the dice is as follows.

  1. Straight – 1, 2, 3, 4, 5, 6 – 1,500 points.
  2. Three pair – 750 points.
  3. Three of a kind – The die number times 100 points. If the die number is one, then 1,000 points. Four of a kind doubles the points. Five of a kind triples the points. Six of a kind quadruples the points.
  4. Each individual one die – 100 points.
  5. Each individual five die – 50 points.

My Farkle simulator plays the Farkle game 100, 1,000, 10,000, 100,000, or 1,000,000 times, keeps the game with the highest score, the lowest score, and calculates the average score for all the games. The one million game simulation takes about 20 seconds on my laptop computer. The smaller simulations are faster.

Swing GUI

The control panel uses a JProgressBar to monitor the progress of the Farkle simulation. The Swing GUI uses a JTabbedPane to hold the control panel, the high Farkle game, and the low Farkle game.

Here are pictures of the Swing GUI after running a 10,000 game simulation. This particular simulation took 311 milliseconds. I created this GUI using Java 8 on a Windows 10 laptop.

Farkle Simulation Swing GUI.

Here we have the Farkle game with the highest score, 12,800 points. The font is 18 point Courier New, because my vision is poor.

Farkle Simulation Swing GUI.

Farkle Simulation Swing GUI.

Here we have the Farkle game with the lowest score, 750 points. You can see all of the Farkles.

Farkle Simulation Swing GUI.

Farkle Simulation Swing GUI.

Overview

I wrote 13 classes. Six classes create the Swing GUI and GUI model, and 7 classes create the Farkle simulator. I use the model / view / controller pattern (MVC).

When I say I use the model / view / controller pattern with Java Swing, I mean:

  1. The view may read values from the model.
  2. The view may not update the model.
  3. The controller will update the model.
  4. The controller will repaint / revalidate the view.

Basically, the model is ignorant of the view and controller. This allows you to change the view and controller from Swing to a web site, or an Android app.

The model / view / controller pattern allows you to focus on one part of the Swing GUI at a time. In general, you’ll create the model first, then the view, and finally the controllers. You will have to go back and add fields to the model. I guarantee that you’ll come up with something you didn’t think of when you created the first cut of the model classes.

Now for the code. The first class is the main Farkle class.

package com.ggl.farkle;

import javax.swing.SwingUtilities;

import com.ggl.farkle.model.FarkleGUIModel;
import com.ggl.farkle.view.FarkleFrame;

public class Farkle implements Runnable {

	public static void main(String[] args) {
		SwingUtilities.invokeLater(new Farkle());
	}

	@Override
	public void run() {
		new FarkleFrame(new FarkleGUIModel());
	}

}

This short class does 3 things.

  1. Puts the Swing components on the Event Dispatch thread (EDT) by executing the invokeLater method of SwingUtilities.
  2. Instantiates the FarkleGUIModel class.
  3. Instantiates the FarkleFrame class.

A version of this class is how I start all of my Swing applications.

Model

GUI Model

Let’s look at the model class, FarkleGUIModel.

package com.ggl.farkle.model;

import com.ggl.farkle.runnable.FarkleNonThreadedSimulation;

public class FarkleGUIModel {

	private int numberOfGames;

	private FarkleNonThreadedSimulation fnts;

	public FarkleGUIModel() {
		this.numberOfGames = 100;
		this.fnts = new FarkleNonThreadedSimulation(numberOfGames);
		fnts.run();
	}

	public void setFnts(FarkleNonThreadedSimulation fnts) {
		this.fnts = fnts;
	}

	public FarkleNonThreadedSimulation getFnts() {
		return fnts;
	}

	public int getNumberOfGames() {
		return numberOfGames;
	}

	public void setNumberOfGames(int numberOfGames) {
		this.numberOfGames = numberOfGames;
	}

}

The Farkle GUI model class is very simple. It holds the number of games for the simulator to run, and one instance of the FarkleNonThreadedSimulation class. This class runs the Farkle simulation in the GUI thread. Since the 100 game simulation only takes 2 or 3 milliseconds to run, I go ahead and run the simulation in the GUI thread so I have something to display when I create the GUI.

You can see how the FarkleNonThreadedSimulation is instantiated and run in the constructor of the Farkle GUI model class.

Later, in the Controller section of this article, I’ll show you the threaded Farkle simulation class. We run the simulation in a separate thread to keep the GUI responsive. Basically, to allow you to close the GUI while a simulation is running. Please don’t start a different simulation while a simulation is running.

Simulation Model

Next, we’ll take a look at the FarkleNonThreadedSimulation class.

package com.ggl.farkle.runnable;

import com.ggl.farkle.model.DiceRoll;
import com.ggl.farkle.model.DiceScore;

public class FarkleNonThreadedSimulation implements Runnable {

	protected int averageScore;
	protected int numberOfGames;
	protected int progressBarDivisor;

	protected FarkleGame highGame;
	protected FarkleGame lowGame;

	protected String elapsedTimeString;

	public FarkleNonThreadedSimulation(int numberOfGames) {
		this.numberOfGames = numberOfGames;
		this.progressBarDivisor = numberOfGames / 100;
	}

	@Override
	public void run() {
		long startTime = System.currentTimeMillis();
		long sumScore = 0L;

		DiceRoll diceRoll = new DiceRoll();
		DiceScore diceScore = new DiceScore();

		for (int index = 0; index < numberOfGames; index++) {
			FarkleGame farkleGame = new FarkleGame(diceRoll, diceScore);
			farkleGame.run();
			int farkleScore = farkleGame.getTotalScore();
			sumScore += (long) farkleScore;

			if (index % progressBarDivisor == 0) {
				monitorSimulation(index, numberOfGames);
			}

			if (index <= 0) {
				highGame = farkleGame;
				lowGame = farkleGame;
			} else {
				int highScore = highGame.getTotalScore();
				int lowScore = lowGame.getTotalScore();

				if (farkleScore > highScore) {
					highGame = farkleGame;
				}
				if (lowScore > farkleScore) {
					lowGame = farkleGame;
				}
			}
		}

		int half = numberOfGames / 2;
		averageScore = (int) ((sumScore + half) / numberOfGames);

		long elapsedTime = System.currentTimeMillis() - startTime;
		elapsedTimeString = generateElapsedTimeString(elapsedTime);
	}

	protected void monitorSimulation(int index, int numberOfGames) {

	}

	protected String generateElapsedTimeString(long elapsedTime) {
		if (elapsedTime < 1000L) {
			return "" + elapsedTime + " milliseconds";
		} else {
			int seconds = (int) ((elapsedTime + 500L) / 1000L);
			return "" + seconds + " seconds";
		}
	}

	public FarkleGame getHighGame() {
		return highGame;
	}

	public FarkleGame getLowGame() {
		return lowGame;
	}

	public int getAverageScore() {
		return averageScore;
	}

	public String getElapsedTimeString() {
		return elapsedTimeString;
	}

}

The FarkleNonThreadedSimulation class holds an instance of the FarkleGame class for the high Farkle game, an instance of the FarkleGame class for the low Farkle game, the average score of all the Farkle games, the number of Farkle games for the simulator to play, and some other internal states of the simulation.

The FarkleNonThreadedSimulation class is designed to be extended. That’s why all the fields and non-public methods are protected, rather than private. In the Controller section of this article, I’ll show you the threaded Farkle simulator class that extends the FarkleNonThreadedSimulation class.

The run method holds an instance of the DiceRoll class and an instance of the DiceScore class. These two classes are instantiated once for the entire simulation.

The monitorSimulation method is empty, because there’s no Swing component to monitor when running in a non-threaded mode. The threaded Farkle simulator class that extends this class will put JProgressBar code in this method.

Next, we’ll take a look at the FarkleGame class.

package com.ggl.farkle.runnable;

import com.ggl.farkle.model.DiceRoll;
import com.ggl.farkle.model.DiceScore;

public class FarkleGame implements Runnable {

	private static final int TOTAL_ROUNDS = 10;

	private int totalScore;

	private DiceRoll diceRoll;
	private DiceScore diceScore;

	private FarkleRound[] rounds;

	public FarkleGame(DiceRoll diceRoll, DiceScore diceScore) {
		this.diceRoll = diceRoll;
		this.diceScore = diceScore;
		this.totalScore = 0;
		this.rounds = new FarkleRound[TOTAL_ROUNDS];
	}

	@Override
	public void run() {
		for (int round = 1; round <= TOTAL_ROUNDS; round++) {
			FarkleRound farkleRound = new FarkleRound(diceRoll, diceScore, round);
			farkleRound.run();
			totalScore += farkleRound.getRoundScore();
			rounds[round - 1] = farkleRound;
		}
	}

	public int getTotalScore() {
		return totalScore;
	}

	@Override
	public String toString() {
		String lineSeparator = System.getProperty("line.separator");
		StringBuilder builder = new StringBuilder();
		builder.append("Total score: ");
		builder.append(String.format("%,6d", totalScore));
		builder.append(lineSeparator);
		builder.append(lineSeparator);

		for (FarkleRound farkleRound : rounds) {
			builder.append(farkleRound.toString());
			builder.append(lineSeparator);
		}

		return builder.toString();
	}
}

The FarkleGame class holds an array of 10 rounds, and the total score.

As you can see in the simulation model, we write a class to take care of each aspect of a Farkle game. This allows us to separate our model concerns, and allows us to focus on one part of the Farkle game at a time.

The toString method creates the output of a Farkle game that you saw in the Swing GUI. This toString method relies on the FarkleRound toString method to generate the output for each round of the Farkle game.

The FarkleGame class and the rest of the simulation model classes could be used to create a Farkle game GUI where you play against the computer.

Next, we’ll take a look at the FarkleRound class.

package com.ggl.farkle.runnable;

import java.util.ArrayList;
import java.util.List;

import com.ggl.farkle.model.DiceRoll;
import com.ggl.farkle.model.DiceScore;
import com.ggl.farkle.model.ScoreResponse;

public class FarkleRound implements Runnable {

	private int roundNumber;
	private int roundScore;

	private DiceRoll diceRoll;
	private DiceScore diceScore;

	private List<ScoreResponse> round;

	public FarkleRound(DiceRoll diceRoll, DiceScore diceScore, int roundNumber) {
		this.diceRoll = diceRoll;
		this.diceScore = diceScore;
		this.roundNumber = roundNumber;
		this.roundScore = 0;
		this.round = new ArrayList<>();
	}

	@Override
	public void run() {
		boolean rolling = true;
		int numberOfDice = DiceScore.getMaximumDiceCount();
		int minimumScore = DiceScore.getMinimumScore();

		do {
			int[] roll = diceRoll.rollDice(numberOfDice);
			ScoreResponse scoreResponse = diceScore.scoreDice(roll, roundScore);
			round.add(scoreResponse);

			int score = scoreResponse.getScore();
			int diceUsed = scoreResponse.getNumberOfDice();
			roundScore += score;

			if (score <= 0) {
				rolling = false;
				roundScore = 0;
			} else if (numberOfDice == diceUsed) {
				numberOfDice = DiceScore.getMaximumDiceCount();
			} else if (roundScore < minimumScore) {
				numberOfDice -= diceUsed;
			} else if (roundScore >= minimumScore) {
				numberOfDice -= diceUsed;
				if (numberOfDice < 3) {
					rolling = false;
				}
			} else {
				rolling = false;
			}
		} while (rolling);
	}

	public int getRoundScore() {
		return roundScore;
	}

	@Override
	public String toString() {
		String lineSeparator = System.getProperty("line.separator");
		String roundString = Integer.toString(roundNumber);

		StringBuilder builder = new StringBuilder();
		builder.append("Round ");
		builder.append(roundString);
		builder.append(rightPad("", ' ', 47 - roundString.length()));
		builder.append("Round score: ");
		builder.append(String.format("%,6d", roundScore));
		builder.append(lineSeparator);

		for (ScoreResponse scoreResponse : round) {
			builder.append("    ");
			builder.append(rightPad(scoreResponse.getDiceRoll(), ' ', 20));
			builder.append(rightPad(scoreResponse.getScoreString(), ' ', 35));
			builder.append("Score: ");
			builder.append(String.format("%,6d", scoreResponse.getScore()));
			builder.append(lineSeparator);
		}

		return builder.toString();
	}

	private StringBuilder rightPad(String s, char c, int length) {
		StringBuilder builder = new StringBuilder(length);
		builder.append(s);
		for (int i = s.length(); i < length; i++) {
			builder.append(c);
		}

		return builder;
	}
}

The FarkleRound class holds the round score and a List of ScoreResponse instances. We use a List because a round consists of 1 or more dice rolls. We don’t have a specific number of dice rolls in a round.

One advantage to breaking up the simulation into different aspects of the Farkle game is that we use simple data structures, like arrays and Lists. We don’t have to use Lists of Lists and get confused as to what the data model holds.

The run method contains a do-while loop. This ensures that we roll the dice at least one time. The if-else block contains the conditions that let the simulator decide whether or not to roll the dice again.

The toString method creates the output of a Farkle game round. I wrote my own rightPad method to space the text correctly when using a monospaced font like Courier New.

Next, we’ll take a look at the DiceScore class.

package com.ggl.farkle.model;

import java.util.Arrays;

public class DiceScore {

	private static final int MAXIMUM_DICE_COUNT = 6;
	private static final int MINIMUM_SCORE = 300;

	public static int getMaximumDiceCount() {
		return MAXIMUM_DICE_COUNT;
	}

	public static int getMinimumScore() {
		return MINIMUM_SCORE;
	}

	public ScoreResponse scoreDice(int[] dice, int roundScore) {
		String diceRoll = Arrays.toString(dice);

		if (dice.length >= MAXIMUM_DICE_COUNT) {
			if (isStraight(dice)) {
				return new ScoreResponse(diceRoll, "Straight", MAXIMUM_DICE_COUNT, 1500);
			}
			if (isThreePair(dice)) {
				return new ScoreResponse(diceRoll, "Three pair", MAXIMUM_DICE_COUNT, 750);
			}
		}

		int[] response = isSameValue(dice);
		int count = response[0];
		int dieValue = response[1];
		int score = 0;
		StringBuilder scoreString = new StringBuilder();

		if (count >= 3) {
			score = 100 * dieValue * (count - 2);
			if (dieValue == 1) {
				score *= 10;
			}
			scoreString.append(count);
			scoreString.append(" of a kind");
			dice = removeDice(dice, count, dieValue);
		}

		if (dieValue != 1) {
			int oneCount = isOnes(dice);
			if (oneCount > 0) {
				score += 100 * oneCount;
				count += oneCount;
				scoreString = addOnesString(oneCount, scoreString);
			}
		}

		if (dieValue != 5) {
			int fiveCount = isFives(dice);
			if (fiveCount > 0) {
				int testScore = roundScore + score + 50 * fiveCount;
				if (testScore >= MINIMUM_SCORE) {
					score += 50 * fiveCount;
					count += fiveCount;
					scoreString = addFivesString(fiveCount, scoreString);
				} else if (score <= 0) {
					score = 50;
					count = 1;
					scoreString = addFivesString(count, scoreString);
				}
			}
		}

		if (scoreString.length() <= 0) {
			scoreString.append("Farkle");
		}

		return new ScoreResponse(diceRoll, scoreString.toString(), count, score);
	}

	private boolean isStraight(int[] dice) {
		int[] count = getDieCounts(dice);

		for (int i = 0; i < count.length; i++) {
			if (count[i] != 1) {
				return false;
			}
		}

		return true;
	}

	private boolean isThreePair(int[] dice) {
		int[] count = getDieCounts(dice);

		int pairCount = 0;
		for (int i = 0; i < count.length; i++) {
			if (count[i] == 2) {
				pairCount++;
			}
		}

		if (pairCount == 3) {
			return true;
		} else {
			return false;
		}
	}

	private int[] isSameValue(int[] dice) {
		int[] count = getDieCounts(dice);

		int maxCount = 0;
		int maxPosition = 0;
		for (int i = 0; i < count.length; i++) {
			if (count[i] > maxCount) {
				maxCount = count[i];
				maxPosition = i;
			}
		}

		if (maxCount < 3) {
			maxCount = 0;
			maxPosition = -1;
		}

		int[] response = new int[2];
		response[0] = maxCount;
		response[1] = maxPosition + 1;
		return response;
	}

	private int[] removeDice(int[] dice, int count, int dieValue) {
		int[] result = new int[dice.length - count];
		int j = 0;
		for (int i = 0; i < dice.length; i++) {
			if (dice[i] != dieValue) {
				result[j++] = dice[i];
			}
		}

		return result;
	}

	private int isOnes(int[] dice) {
		int[] count = getDieCounts(dice);

		return count[0];
	}

	private int isFives(int[] dice) {
		int[] count = getDieCounts(dice);

		return count[4];
	}

	private int[] getDieCounts(int[] dice) {
		int[] count = new int[6];
		for (int i = 0; i < dice.length; i++) {
			count[dice[i] - 1]++;
		}

		return count;
	}

	private StringBuilder addOnesString(int count, StringBuilder scoreString) {
		if (scoreString.length() > 0) {
			scoreString.append(", ");
		}

		scoreString.append(count);
		if (count > 1) {
			scoreString.append(" ones");
		} else {
			scoreString.append(" one");
		}

		return scoreString;
	}

	private StringBuilder addFivesString(int count, StringBuilder scoreString) {
		if (scoreString.length() > 0) {
			scoreString.append(", ");
		}

		scoreString.append(count);
		if (count > 1) {
			scoreString.append(" fives");
		} else {
			scoreString.append(" five");
		}

		return scoreString;
	}

}

The DiceScore class creates a ScoreResponse instance for the current set of dice. The dice scoring is straightforward, except for scoring the individual 5 values.

We score the 5 values differently, depending on the current state of the round.

If we are below the minimum score of 300, we only score one 5, even if there are more than one five values. We do this because we hope to score more points on subsequent rolls with more dice.

If we are at or above the minimum score, we score all of the 5 values.

Next, we’ll take a look at the DiceRoll class.

package com.ggl.farkle.model;

import java.util.Random;

public class DiceRoll {

	private Random random;

	public DiceRoll() {
		this.random = new Random();
	}

	public int[] rollDice(int count) {
		int[] dice = new int[count];

		for (int i = 0; i < count; i++) {
			dice[i] = random.nextInt(6) + 1;
		}

		return dice;
	}

}

The DiceRoll class is so simple, the rollDice method could have been included in the DiceScore class. I left it in a separate class.

I actually created these classes in the reverse order that I’ve shown them to you. I created the DiceRoll class and tested the DiceRoll class with some temporary code in the Farkle class. Next, I created the DiceScore class and tested the DiceScore class. I did the same, one class at a time, until I’d created and tested the FarkleNonThreadedSimulation class. I didn’t start coding the GUI model and view until I had completed and tested the Farkle simulator.

Finally, we’ll take a look at the ScoreResponse class.

package com.ggl.farkle.model;

public class ScoreResponse {

	private final int numberOfDice;
	private final int score;

	private final String diceRoll;
	private final String scoreString;

	public ScoreResponse(String diceRoll, String scoreString, int numberOfDice, int score) {
		this.diceRoll = diceRoll;
		this.scoreString = scoreString;
		this.numberOfDice = numberOfDice;
		this.score = score;
	}

	public int getNumberOfDice() {
		return numberOfDice;
	}

	public int getScore() {
		return score;
	}

	public String getScoreString() {
		return scoreString;
	}

	public String getDiceRoll() {
		return diceRoll;
	}

}

The ScoreResponse class is a plain old Java object (POJO) that holds a String representation of the dice roll, the String that holds the score explanation, and the score for the roll.

We create an instance of ScoreResponse for each dice roll.

View

Now that we’ve defined the model classes, let’s look at the view classes. The first view class is the FarkleFrame class.

package com.ggl.farkle.view;

import javax.swing.JFrame;
import javax.swing.JTabbedPane;

import com.ggl.farkle.model.FarkleGUIModel;
import com.ggl.farkle.runnable.FarkleGame;

public class FarkleFrame {

	private int gameTextAreaCount;

	private ControlPanel controlPanel;

	private FarkleGUIModel model;

	private GamePanel highGamePanel;
	private GamePanel lowGamePanel;

	private JFrame frame;

	private JTabbedPane tabbedPane;

	public FarkleFrame(FarkleGUIModel model) {
		this.model = model;
		this.gameTextAreaCount = 0;
		createPartControl();
	}

	private void createPartControl() {
		frame = new JFrame("Farkle Simulator");
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		createTabbedPane();
		frame.add(tabbedPane);

		frame.pack();
		frame.setLocationByPlatform(true);
		frame.setVisible(true);
	}

	private void createTabbedPane() {
		FarkleGame highGame = model.getFnts().getHighGame();
		FarkleGame lowGame = model.getFnts().getLowGame();
		tabbedPane = new JTabbedPane();

		controlPanel = new ControlPanel(this, model);
		tabbedPane.addTab("Control", controlPanel.getPanel());

		String tabString = formatScore(highGame.getTotalScore());
		tabString = "High Game: " + tabString;
		highGamePanel = new GamePanel(highGame.toString());
		tabbedPane.addTab(tabString, highGamePanel.getPanel());

		tabString = formatScore(lowGame.getTotalScore());
		tabString = "Low Game: " + tabString;
		lowGamePanel = new GamePanel(lowGame.toString());
		tabbedPane.addTab(tabString, lowGamePanel.getPanel());
	}

	public void updatePartControl() {
		FarkleGame highGame = model.getFnts().getHighGame();
		FarkleGame lowGame = model.getFnts().getLowGame();

		String tabString = formatScore(highGame.getTotalScore());
		tabString = "High Game: " + tabString;
		tabbedPane.setTitleAt(1, tabString);
		highGamePanel.updatePartControl(highGame.toString());

		tabString = formatScore(lowGame.getTotalScore());
		tabString = "Low Game: " + tabString;
		tabbedPane.setTitleAt(2, tabString);
		lowGamePanel.updatePartControl(lowGame.toString());

		controlPanel.updatePartControl();
	}

	public void updateProgressBar(int value) {
		controlPanel.updateProgressBar(value);
	}

	private String formatScore(int score) {
		return String.format("%,6d", score);
	}
}

The createPartControl method is pretty much the same for all of my Java Swing projects. For this project, we create a JFrame, and add the JTabbedPane. For other projects, we’ll add one or more JPanels.

Notice that we don’t extend JFrame. We use a JFrame. The only reason you should extend a Swing component, or any Java class, is when you want to override one or more of the methods.

For most Swing applications, you want to create the Swing components first, then update them as needed. In the createTabbedPane method, we have the following hierarchy of Swing components.

JTabbedPane -> ControlPanel (JPanel)

JTabbedPane -> GamePanel (JPanel)

The control panel is sufficiently complicated that I created it in its own class. I created the game panel in its own class because we need two of them.

The updatePartControl method updates the Swing components created in the createPartControl method. Generally, when you create a Swing GUI, you create the Swing components and later update the Swing components. Beginner coders tend to get confused and try and delete and recreate Swing components. In the vast majority of GUI’s, this is not only unnecessary, but creates a bad user experience (UX).

The updateProgressBar method updates the JProgressBar.

Next, we’ll take a look at the ControlPanel class.

package com.ggl.farkle.view;

import java.awt.Component;
import java.awt.Container;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JTextField;

import com.ggl.farkle.controller.SimulationStartListener;
import com.ggl.farkle.model.FarkleGUIModel;
import com.ggl.farkle.runnable.FarkleGame;

public class ControlPanel {

	private static final Insets finalInsets = new Insets(6, 6, 6, 6);
	private static final Insets progressBarInsets = new Insets(6, 6, 50, 6);

	private FarkleFrame frame;

	private FarkleGUIModel model;

	private JPanel panel;

	private JProgressBar progressBar;

	private JTextField elapsedTimeTextField;
	private JTextField highScoreTextField;
	private JTextField averageScoreTextField;
	private JTextField lowScoreTextField;

	public ControlPanel(FarkleFrame frame, FarkleGUIModel model) {
		this.frame = frame;
		this.model = model;
		createPartControl();
	}

	private void createPartControl() {
		panel = new JPanel();
		panel.setLayout(new GridBagLayout());

		int gridy = 0;

		JLabel numberOfGamesLabel = new JLabel("Number of Games:");
		addComponent(panel, numberOfGamesLabel, 0, gridy, 1, 1, finalInsets, GridBagConstraints.LINE_START,
				GridBagConstraints.NONE);

		Integer[] values = { 100, 1000, 10_000, 100_000, 1_000_000 };

		JComboBox<Integer> numberOfGamesComboBox = new JComboBox<>(values);
		numberOfGamesComboBox.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent event) {
				int numberOfGames = (Integer) numberOfGamesComboBox.getSelectedItem();
				model.setNumberOfGames(numberOfGames);
			}
		});
		addComponent(panel, numberOfGamesComboBox, 1, gridy++, 1, 1, finalInsets, GridBagConstraints.LINE_START,
				GridBagConstraints.HORIZONTAL);

		SimulationStartListener listener = new SimulationStartListener(frame, model);

		JButton startSimulationButton = new JButton("Start Simulation");
		startSimulationButton.addActionListener(listener);
		addComponent(panel, startSimulationButton, 0, gridy++, 2, 1, finalInsets, GridBagConstraints.LINE_START,
				GridBagConstraints.HORIZONTAL);

		progressBar = new JProgressBar(0, 100);
		progressBar.setStringPainted(true);
		progressBar.setValue(100);
		addComponent(panel, progressBar, 0, gridy++, 2, 1, progressBarInsets, GridBagConstraints.LINE_START,
				GridBagConstraints.HORIZONTAL);

		JLabel elapsedTimeLabel = new JLabel("Elapsed Time:");
		addComponent(panel, elapsedTimeLabel, 0, gridy, 1, 1, finalInsets, GridBagConstraints.LINE_START,
				GridBagConstraints.NONE);

		String s = model.getFnts().getElapsedTimeString();
		elapsedTimeTextField = createTextField(s);
		addComponent(panel, elapsedTimeTextField, 1, gridy++, 1, 1, finalInsets, GridBagConstraints.LINE_START,
				GridBagConstraints.HORIZONTAL);

		JLabel highScoreLabel = new JLabel("High Score:");
		addComponent(panel, highScoreLabel, 0, gridy, 1, 1, finalInsets, GridBagConstraints.LINE_START,
				GridBagConstraints.NONE);

		s = formatScore(model.getFnts().getHighGame().getTotalScore());
		highScoreTextField = createTextField(s);
		addComponent(panel, highScoreTextField, 1, gridy++, 1, 1, finalInsets, GridBagConstraints.LINE_START,
				GridBagConstraints.HORIZONTAL);

		JLabel averageScoreLabel = new JLabel("Average Score:");
		addComponent(panel, averageScoreLabel, 0, gridy, 1, 1, finalInsets, GridBagConstraints.LINE_START,
				GridBagConstraints.NONE);

		s = formatScore(model.getFnts().getAverageScore());
		averageScoreTextField = createTextField(s);
		addComponent(panel, averageScoreTextField, 1, gridy++, 1, 1, finalInsets, GridBagConstraints.LINE_START,
				GridBagConstraints.HORIZONTAL);

		JLabel lowScoreLabel = new JLabel("Low Score:");
		addComponent(panel, lowScoreLabel, 0, gridy, 1, 1, finalInsets, GridBagConstraints.LINE_START,
				GridBagConstraints.NONE);

		s = formatScore(model.getFnts().getLowGame().getTotalScore());
		lowScoreTextField = createTextField(s);
		addComponent(panel, lowScoreTextField, 1, gridy++, 1, 1, finalInsets, GridBagConstraints.LINE_START,
				GridBagConstraints.HORIZONTAL);
	}

	public JPanel getPanel() {
		return panel;
	}

	public void updatePartControl() {
		FarkleGame highGame = model.getFnts().getHighGame();
		FarkleGame lowGame = model.getFnts().getLowGame();

		String s = model.getFnts().getElapsedTimeString();
		elapsedTimeTextField.setText(s);
		s = formatScore(highGame.getTotalScore());
		highScoreTextField.setText(s);
		s = formatScore(model.getFnts().getAverageScore());
		averageScoreTextField.setText(s);
		s = formatScore(lowGame.getTotalScore());
		lowScoreTextField.setText(s);
	}

	public void updateProgressBar(int value) {
		progressBar.setValue(value);
	}

	private JTextField createTextField(String text) {
		JTextField textField = new JTextField(10);
		textField.setEditable(false);
		textField.setHorizontalAlignment(JTextField.RIGHT);
		textField.setText(text);

		return textField;
	}

	private void addComponent(Container container, Component component, int gridx, int gridy, int gridwidth,
			int gridheight, Insets insets, int anchor, int fill) {
		GridBagConstraints gbc = new GridBagConstraints(gridx, gridy, gridwidth, gridheight, 0.0D, 0.0D, anchor, fill,
				insets, 0, 0);
		container.add(component, gbc);
	}

	private String formatScore(int score) {
		return String.format("%,6d", score);
	}

}

In the createPartControl method, I use a GridBagLayout. I create a separate GridBagConstraints for each Swing component. I do this because I want to specify all of the constraints for each Swing component. I don’t like to rely on defaults, and I don’t like to remember which constraints go with which component.

The GridBagLayout does not require a separate GridBagConstraints for each Swing component. I find it easier for people, including myself, to understand and debug a GridBagLayout with separate GridBagConstraints.

Since the JComboBox is not editable, we wrote an anonymous actionListener to update the model with the newly selected value. This makes the controller for the JButton simpler.

The updatePartControl method updates the Swing components created in the createPartControl method. Generally, when you create a Swing GUI, you create the Swing components and later update the Swing components. Beginner coders tend to get confused and try and delete and recreate Swing components. In the vast majority of GUI’s, this is not only unnecessary, but creates a bad user experience (UX).

The updateProgressBar method updates the JProgressBar.

Next, we’ll take a look at the GamePanel class.

package com.ggl.farkle.view;

import java.awt.Dimension;
import java.awt.Font;

import javax.swing.BorderFactory;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;

public class GamePanel {

	private JPanel panel;

	private JTextArea textArea;

	public GamePanel(String text) {
		createPartControl(text);
	}

	private void createPartControl(String text) {
		panel = new JPanel();

		textArea = new JTextArea(text);
		textArea.setCaretPosition(0);
		textArea.setEditable(false);
		textArea.setFont(new Font("Courier New", Font.PLAIN, 18));

		JScrollPane scrollPane = new JScrollPane(textArea);
		scrollPane.setPreferredSize(new Dimension(840, 560));
		scrollPane.setViewportBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
		panel.add(scrollPane);
	}

	public JPanel getPanel() {
		return panel;
	}

	public void updatePartControl(String text) {
		textArea.setText(text);
		textArea.setCaretPosition(0);
	}

}

The createPartControl method is short and straightforward. The JPanel uses the default FlowLayout, and the JScrollPane fills the JPanel.

The updatePartControl method updates the JTextArea created in the createPartControl method. Generally, when you create a Swing GUI, you create the Swing components and later update the Swing components. Beginner coders tend to get confused and try and delete and recreate Swing components. In the vast majority of GUI’s, this is not only unnecessary, but creates a bad user experience (UX).

Controller

Let’s look at the controller classes. The first controller class is the SimulationStartListener class.

package com.ggl.farkle.controller;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import com.ggl.farkle.model.FarkleGUIModel;
import com.ggl.farkle.runnable.FarkleThreadedSimulation;
import com.ggl.farkle.view.FarkleFrame;

public class SimulationStartListener implements ActionListener {

	private FarkleFrame frame;

	private FarkleGUIModel model;

	public SimulationStartListener(FarkleFrame frame, FarkleGUIModel model) {
		this.frame = frame;
		this.model = model;
	}

	@Override
	public void actionPerformed(ActionEvent event) {
		FarkleThreadedSimulation fts = new FarkleThreadedSimulation(frame, model);
		new Thread(fts).start();
	}

}

The actionPerformed method of the SimulationStartListener class starts a Farkle simulation thread. The simulation is run in a separate thread so the Swing GUI remains responsive.

Finally, let’s take a look at the FarkleThreadedSimulation class.

package com.ggl.farkle.runnable;

import javax.swing.SwingUtilities;

import com.ggl.farkle.model.FarkleGUIModel;
import com.ggl.farkle.view.FarkleFrame;

public class FarkleThreadedSimulation extends FarkleNonThreadedSimulation {

	private FarkleFrame frame;

	private FarkleGUIModel model;

	public FarkleThreadedSimulation(FarkleFrame frame, FarkleGUIModel model) {
		super(model.getNumberOfGames());
		this.frame = frame;
		this.model = model;
	}

	@Override
	public void run() {
		super.run();
		model.setFnts(this);
		update();
	}

	@Override
	protected void monitorSimulation(int index, int numberOfGames) {
		int value = (int) (100L * index / numberOfGames);
		SwingUtilities.invokeLater(new Runnable() {
			@Override
			public void run() {
				frame.updateProgressBar(value);
			}
		});
	}

	private void update() {
		SwingUtilities.invokeLater(new Runnable() {
			@Override
			public void run() {
				frame.updatePartControl();
			}
		});
	}
}

Even though the FarkleThreadedSimulation class extends the FarkleNonThreadedSimulation class we described earlier in the Model section of this article, the FarkleThreadedSimulation is considered part of the GUI controller, rather than the Farkle simulation model. This is because the FarkleThreadedSimulation class updates the Farkle GUI model and updates the GUI view.

The monitorSimulation method updates the JProgressBar while the Farkle simulation is running. This code shows you how to update a JProgressBar.

The monitorSimulation method and the update method put the Swing code inside of the SwingUtilities invokeLater method. This ensures that the Swing components are updated on the Event Dispatch thread.

This concludes this article. I hope we’ve shown you how to create a rather complicated application model, GUI model, GUI view, and one or more GUI controllers in Java Swing.

Post a Comment

Your email is kept private. Required fields are marked *