1

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.

Eric Roch
  • 125
  • 2
  • 10
  • Just to note, MVC isn't a game design pattern, so don't try to base your game off of it. – Jon Egeland Feb 22 '15 at 00:47
  • @Jon I was under the impression that MVC was a general use design that allowed for the separation of the logic from what the user sees. I am brand new to the world of swing and fairly new to Java in general. Is there a better design for what I want. Could you recommend any tutorials on the subject? – Eric Roch Feb 22 '15 at 00:49
  • Ok, the way to start this is to separate *all* the GUI from anything else. Don't be concerned with controller vs. model issues yet. Just start with a dumb GUI that has all of its functionality accessible via methods. Place absolutely *no* intelligence within that GUI. What you'll have done in that regard is the first OO step in splitting away how things are viewed from what controls that view. –  Feb 22 '15 at 00:56
  • @tgm1024 ok, that's all fine and dandy, but the problem I was running into was that I could no longer use the event listeners on the menus/text field. With a separated GUI class, how does the logic know what was pressed and when? – Eric Roch Feb 22 '15 at 02:00
  • You'd feed whatever needs a listener a listener. Listeners are classes, and you can make them anonymous inner classes (as I assume you do now) or you could make the Listener its own class file and reference is as easy as importing and initializing. – MeetTitan Feb 22 '15 at 02:24
  • For [example](http://stackoverflow.com/a/24726834/230513) and [example](http://stackoverflow.com/a/2687871/230513). – trashgod Feb 22 '15 at 11:59
  • @Eric, as I understand it, you understood already what MeetTitan and trashgod are saying, but want a more "pure" approach, correct? The issue is more that a listener need not live within the GUI itself: it's not necessarily confined to the GUI....it can usually be thought of as a fairly clean connection to the controller/model. –  Feb 22 '15 at 17:15
  • BTW: This raises an issue: I believe that most of the time an overly-strict adherence to such boundaries between model and controller smacks of over engineering; they inherently have an overlap in charter. However, the GUI usually is best cordoned off as best you can. –  Feb 22 '15 at 17:19
  • @MeetTitan, Java listeners aren't "classes", they're interfaces. If you have a fairly complex class, it'll often be cleanest to have it implement the proper listener without a single additional class being made. You may be thinking of *adapters* (large scale "blank" implementations of interfaces), but these are convenience implementations only. –  Feb 22 '15 at 17:22

0 Answers0