Recently, I have been working on a simple little game called Factman. It started off as a text based game in a console window, but I wanted to expand it to make a GUI using swing. I got it to work but I know I am breaking some of the rules of OOP. I tried starting over and doing it the right way (as far as I am aware, from my limited knowledge from online tutorials) using the MVC outline.
So my question to the SO community is, how can I divide this program into separate classes for the GUI, game logic, and a controller to interface the two (ie pass user input to the logic and pass altered game parameters to the gui).
Here is my "bad" code:
package games;
import java.awt.Color;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import javax.swing.Box;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
class Factman_GUI_old extends JFrame {
static JPanel panel;
static JMenuBar menubar;
static JCheckBoxMenuItem hideBoard;
static JLabel p1ScoreLabel, p2ScoreLabel, turnIndicator, gameAreaLabel;
static String userInput;
static int userSelection = -1;
static boolean newGameFlag = false;
public Factman_GUI_old() {
initGUI();
}
private void initGUI() {
panel = new JPanel(new GridBagLayout());
add(panel);
// Generate the menu at the top of the window
createMenu();
//////////////////////////////////////////////////
// First row, score labels
// Score values will split all extra space evenly
//
GridBagConstraints constraints = new GridBagConstraints();
JLabel p1Label = new JLabel("Player 1 Score: ");
constraints.anchor = GridBagConstraints.LINE_START;
constraints.gridx = 0;
constraints.gridy = 0;
panel.add(p1Label, constraints);
constraints = new GridBagConstraints();
p1ScoreLabel = new JLabel("0");
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.anchor = GridBagConstraints.LINE_START;
constraints.gridx = 1;
constraints.gridy = 0;
constraints.weightx = 0.5;
panel.add(p1ScoreLabel, constraints);
constraints = new GridBagConstraints();
JLabel p2Label = new JLabel("Player 2 Score: ");
constraints.anchor = GridBagConstraints.LINE_START;
constraints.gridx = 2;
constraints.gridy = 0;
panel.add(p2Label, constraints);
constraints = new GridBagConstraints();
p2ScoreLabel = new JLabel("0");
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.anchor = GridBagConstraints.LINE_START;
constraints.gridx = 3;
constraints.gridy = 0;
constraints.weightx = 0.5;
panel.add(p2ScoreLabel, constraints);
//////////////////////////////////////////////////
// Second row, main content area.
// This spans all 4 columns and 2 rows
//
constraints = new GridBagConstraints();
JPanel gameArea = new JPanel();
gameArea.setLayout(new GridBagLayout());
constraints.fill = GridBagConstraints.BOTH;
constraints.gridx = 0;
constraints.gridy = 1;
constraints.gridwidth = 4;
constraints.gridheight = 2;
constraints.weighty = 1;
panel.add(gameArea, constraints);
constraints = new GridBagConstraints();
gameAreaLabel = new JLabel("[]");
constraints.anchor = GridBagConstraints.CENTER;
constraints.fill = GridBagConstraints.BOTH;
gameArea.add(gameAreaLabel, constraints);
//////////////////////////////////////////////////
// Third row, input area
// This row contains another panel with its own layout
// The first row indicates whose turn it is,
// the second row takes user input. The text
// field will take up all extra space
//
JPanel inputPanel = new JPanel();
inputPanel.setLayout(new GridBagLayout());
constraints = new GridBagConstraints();
constraints.fill = GridBagConstraints.BOTH;
constraints.gridx = 0;
constraints.gridy = 3;
constraints.gridwidth = 4;
constraints.weightx = 1;
panel.add(inputPanel, constraints);
constraints = new GridBagConstraints();
turnIndicator = new JLabel("It is Player 1's Turn");
constraints.gridx = 0;
constraints.gridy = 0;
constraints.gridwidth = 4;
inputPanel.add(turnIndicator, constraints);
constraints = new GridBagConstraints();
JLabel inputLabel = new JLabel("Enter your selection: ");
constraints.fill = GridBagConstraints.BOTH;
constraints.gridx = 0;
constraints.gridy = 1;
inputPanel.add(inputLabel, constraints);
constraints = new GridBagConstraints();
final JTextField inputField = new JTextField();
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.gridx = 1;
constraints.gridy = 1;
constraints.gridwidth = 3;
constraints.weightx = 1;
inputField.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
userInput = inputField.getText();
try {
userSelection = Integer.parseInt(userInput);
} catch (NumberFormatException e) {
System.out.println("No number entered...");
}
System.out.println(userInput);
inputField.setText("");
}
});
inputPanel.add(inputField, constraints);
// Set basic window properties
setTitle("Factman Game");
setSize(600,200);
setLocationRelativeTo(null);
setDefaultCloseOperation(EXIT_ON_CLOSE);
}
private void createMenu() {
menubar = new JMenuBar();
// Create the file menu
JMenu filemenu = new JMenu("File");
filemenu.setMnemonic(KeyEvent.VK_F);
JMenuItem newGame = new JMenuItem("New Game");
newGame.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N,
ActionEvent.CTRL_MASK));
newGame.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent event) {
int response = JOptionPane.showConfirmDialog(
panel,
"You are about to start a new game.\n"+
"Your current game will be lost.",
"Confirm New Game",
JOptionPane.OK_CANCEL_OPTION,
JOptionPane.WARNING_MESSAGE);
if (response == 0) newGameFlag = true;
}
});
JMenuItem quit = new JMenuItem("Exit");
quit.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q,
ActionEvent.CTRL_MASK));
quit.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent event) {
if(JOptionPane.showConfirmDialog(
null,
"Are you sure you want to quit Factman?",
"Quit",
JOptionPane.YES_NO_OPTION,
JOptionPane.WARNING_MESSAGE) == 0) {
System.exit(0);
}
}
});
filemenu.add(newGame);
filemenu.addSeparator();
filemenu.add(quit);
// create the view menu
JMenu viewmenu = new JMenu("View");
viewmenu.setMnemonic(KeyEvent.VK_V);
hideBoard = new JCheckBoxMenuItem("Hide Game Board");
hideBoard.setState(false);
viewmenu.add(hideBoard);
// Create the help menu
JMenu helpmenu = new JMenu("Help");
helpmenu.setMnemonic(KeyEvent.VK_H);
JMenuItem gameInstructions = new JMenuItem("How to Play");
gameInstructions.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
JOptionPane.showMessageDialog(
panel,
"<html>" +
"<p>Factman is a pretty simple game once you know the rules.<br>" +
"To play, each player will take turns selecting a number<br>" +
"from the list. The player will earn the number of points<br>" +
"equal to the number they selected. But be careful, if you<br>" +
"choose a number not in the list, you loose a turn!</p>" +
"<p></p>" +
"<p>When a player chooses a number, the other player will gain<br>" +
"the number of points for each of the factors in the list.<br>" +
"Any number that is used (selected or a factor) is removed<br>" +
"from the list.</p>" +
"<p></p>" +
"<p>The player with the highest score when the list is empty wins.</p>" +
"<p></p>" +
"<p>Good Luck!</p>" +
"</html>",
"How to Play",
JOptionPane.INFORMATION_MESSAGE);
}
});
helpmenu.add(gameInstructions);
// Populate the menu bar
menubar.add(filemenu);
menubar.add(viewmenu);
menubar.add(helpmenu);
// Set the menu bar in the panel
setJMenuBar(menubar);
}
}
public class Factman_Swing extends Factman_GUI_old {
static ArrayList<Integer> gameBoard;
static int upperBound, factorIndex, p1Score = 0, p2Score = 0;
static boolean player1 = true;
public static void main(String args[]) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
Factman_GUI_old factman = new Factman_GUI_old();
factman.setVisible(true);
}
});
playGame();
}
public static void playGame() {
// set the flag false to prevent a new game when someone wins
newGameFlag = false;
// make sure the label text is black
//gameAreaLabel.setForeground(Color.black);
// create a popup window to get the upper bound
upperBound = Integer.parseInt(JOptionPane.showInputDialog(
panel, "Enter the upper bound for this game", null));
System.out.println("Upper bound = " + upperBound);
// generate the arraylist with the given upper limit
gameBoard = createList(upperBound);
System.out.println(gameBoard);
// as long as there are numbers left in the list, keep looping the game
while(!gameBoard.isEmpty()) {
// if the new game option was selected, go back to main
if (newGameFlag) return;
// show the list in the GUI
gameAreaLabel.setVisible(!hideBoard.getState());
gameAreaLabel.setText(gameBoard.toString());
// indicate whose turn it is in the GUI
if(player1) turnIndicator.setText("It's Player 1's Turn");
else turnIndicator.setText("It's Player 2's Turn");
// userSelection becomes non-zero when a
// number is entered in the text field
if (userSelection >= 0) {
// save the input and set it back to zero
// so the loop doesnt fire again
int selection = userSelection;
userSelection = -1;
System.out.println("User selected " + selection);
// wrap the selection in an Integer object for comparison with the list
Integer number = new Integer(selection);
// the player will loose his/her turn if an invalid number is entered
if (!gameBoard.contains(number)) {
JOptionPane.showMessageDialog(
panel,
"The number you selected is not in the list.\nYou loose a turn",
"OOPS",
JOptionPane.ERROR_MESSAGE);
player1 = !player1;
continue;
}
// add the selection to the current player's score
if (player1) p1Score += selection;
else p2Score += selection;
// search for and remove the selection from the list
removeInt(gameBoard, selection);
// as long as there are factors, add them to the other
// players score and remove them from the list
do {
factorIndex = findFactor(gameBoard, selection);
if (factorIndex >= 0) {
int value = gameBoard.get(factorIndex).intValue();
if (player1) p2Score += value;
else p1Score += value;
// remove the factor
removeInt(gameBoard, value);
}
} while (factorIndex >= 0); // loop until no factor is found
// show the scores in the GUI
p1ScoreLabel.setText(String.valueOf(p1Score));
p2ScoreLabel.setText(String.valueOf(p2Score));
// switch players
player1 = !player1;
}
}
// Show who won
gameAreaLabel.setForeground(Color.blue);
if (p1Score > p2Score) gameAreaLabel.setText("PLAYER 1 WINS!!!!");
else if (p1Score < p2Score) gameAreaLabel.setText("PLAYER 2 WINS!!!!");
else gameAreaLabel.setText("Somehow, you managed to tie. Nice going.");
}
/**
* Create a list of Integer objects from 1 to limit, inclusive.
* @param limit the upper bound of the list
* @return an ArrayList of Integer type
*/
public static ArrayList<Integer> createList(int limit) {
ArrayList<Integer> temp = new ArrayList<Integer>();
for (int i = 1; i <= limit; i ++) {
temp.add(new Integer(i));
}
return temp;
}
/**
* Search for the specified value in the list and remove the object
* from the list. The remove method of the ArrayList class removes
* the object and shifts all of the objects following it to the
* left one index.
* @param list an ArrayList of Integers to search
* @param value the value to remove from the list
* @see java.util.ArrayList#remove
*/
private static void removeInt(ArrayList<Integer> list, int value) {
// loop through the list until the value of the object matches
// the specified value, then remove it
for (Integer element : list) {
if(element == value) {
list.remove(element);
break;
}
}
}
/**
* Returns the index of the first factor of the specified number in
* the specified ArrayList. If no factor is found, -1 is returned.
* @param list an ArrayList of Integers to search
* @param number the value to find factors of
* @return the index of the first factor, or -1 if no factors exist
*/
private static int findFactor(ArrayList<Integer> list, int number) {
// loop through the list until the end or the specified number
// this prevents index exceptions
for (int i = 0; i < list.size() && i < number; i ++) {
// check if the value divides evenly into the number
if (number % list.get(i).intValue() == 0) {
return i;
}
}
// we only get here if no index was found
return -1;
}
}
Please note that some functionality is missing. I was having trouble with the new game control loop, so I took it out entirely. I was handling new games with an infinite loop in main
of Factman_Swing
around the playGame();
call.
Therefore, pressing New Game or entering CTRL+N will not do anything. The intent is for another window to pop up as at the beginning of the game and ask for an upper limit.
And I might as well show you what I re-wrote too.
Game Logic:
package games.factman;
import java.util.ArrayList;
public class Factman {
private ArrayList<Integer> list;
private int playerTurn = 1;
private int p1Score = 0, p2Score = 0;
public Factman() {
list = new ArrayList<Integer>();
}
public void startGame(int gameSize) {
// fill the list with numbers
for (int i = 1; i <= gameSize; i ++) {
list.add(i);
}
}
public boolean makeMove(int playerNumber, int selection) {
int selectionIndex = list.indexOf(selection);
if (selectionIndex < 0) {
return false;
}
// Remove the selection now, so its not counted as a factor
list.remove(selectionIndex);
int factorIndex, factorSum = 0;
do {
factorIndex = findFirstFactor(selection);
if (factorIndex >= 0) {
int factorValue = list.get(factorIndex);
factorSum += factorValue;
list.remove(factorIndex);
}
} while (factorIndex > -1);
if (playerNumber == 1) {
p1Score += selection;
p2Score += factorSum;
}
else if (playerNumber == 2) {
p2Score += selection;
p1Score += factorSum;
}
// return true to indicate a successful move
return true;
}
public int getPlayerScore(int playerNumber) {
if (playerNumber == 1) {
return p1Score;
}
else if (playerNumber == 2) {
return p2Score;
}
else {
return 0;
}
}
public String getList() {
return list.toString();
}
private int findFirstFactor(int number) {
for (int i = 0; (i < list.size()) && (i < number); i ++) {
if (number % list.get(i) == 0) {
return i;
}
}
return -1;
}
public static void main(String... args) {
Factman game1 = new Factman();
game1.startGame(15);
System.out.println(game1.getList());
game1.makeMove(1, 10);
System.out.println(game1.getList());
game1.makeMove(2, 8);
System.out.println(game1.getList());
System.out.println("P1: " + game1.getPlayerScore(1));
System.out.println("P2: " + game1.getPlayerScore(2));
}
}
The GUI is basically the same, just pasted into a separate class.