3
\$\begingroup\$

This is a homework project (Soft. Eng.) which I have completed. My teacher gave me a 91, but I am unhappy with this grade because my code is quite messy. (Please excuse incomplete Javadoc, the code is fully functioning.)

  • In particular, I am worried about checking the win condition in a view: TilePanel.java's paintComponent method.

  • I am also worried about the long method calls that my methods require, for example in TilePanelMouseAdapter.java.

  • Lastly, I am not too sure of general best practices and how "OOP"-friendly my code is.

Here is the project repo for convenience: https://github.com/eacdev/meansum

GameFrame.java

package logic;

import javax.swing.Timer;

import ui.TimerActionListener;

import javax.swing.JFrame;
import javax.swing.SwingUtilities;

/**
 * NOTE: YOU DO NOT HAVE TO MAKE ANY CHANGES TO THIS CLASS
 * 
 * The game frame is the main window of the game. It instantiates the model and
 * the view-controller. The frame is filled by a single panel containing all the
 * elements, which is the {@link GameViewController} object.
 *
 */
public class GameFrame extends JFrame {

  private static final long serialVersionUID = 1L;
  /**
   * Handle both the graphical interface (tiles, labels, buttons, etc.) and the
   * user input (mouse events, button clicks, etc.)
   */
  private GameViewController gameViewController;

  /**
   * Initialize the main properties of the game window
   */
  public void initUI() {
    setTitle("Mean Sum");
    setSize(800, 300);
    setLocationRelativeTo(null);
    setDefaultCloseOperation(EXIT_ON_CLOSE);
  }

  /**
   * The constructor instantiates the model and view-controller and builds the
   * game window.
   */
  public GameFrame() {
    // Initialize the interface
    initUI();

    // Initialize the view and set it as the main component our window
    gameViewController = new GameViewController();

    setContentPane(gameViewController);
  }

  public static void main(String[] args) {
    SwingUtilities.invokeLater(new Runnable() {
      @Override
      public void run() {
        GameFrame game = new GameFrame();
        game.setVisible(true);
      }
    });
  }

}

GameModel.java

package logic;

import java.util.ArrayList;
import java.util.Random;

/**
 * The game model handles the logic of the game (generating the numbers, etc.).
 * The instance of the model is used by the view-controller module to trigger
 * actions (for example, generate a new game) and retrieve information about the
 * current status of the game (the digits, the goal, etc.).
 *
 */
public class GameModel {

  /**
   * List of numbers that are available.
   */
  private ArrayList<Integer> numbers;

  /**
   * List of selections. Every time a selection is made, it is added to this list.
   */
  private ArrayList<Integer[]> selections;

  private int resets;

  /**
   * Minimum number of number groups to generate.
   */
  private static final int MIN_NUMBER_OF_GROUPS = 3;

  /**
   * Maximal number of number groups to generate.
   */
  private static final int MAX_NUMBER_OF_GROUPS = 6;

  /**
   * Generates numbers with a certain probability and a certain number of groups.
   * These numbers are used throughout the game and are drawn on the tiles.
   */
  private ArrayList<Integer> generateNumbers(ArrayList<Integer> numbers, int numberOfGroups) {
    numbers.clear();

    for (int i = 0; i <= numberOfGroups; i++) {
      int generatedNumber = RandomUtils.getRandomGameNumber();
      numbers.add(generatedNumber);
    }

    System.out.println(numbers);

    return numbers;
  }

  /**f the concatenated numbers.
   */
  public String numbersToString(ArrayList<Integer> numbers) {
    StringBuilder digits = new StringBuilder();

    for (int i = 0; i < numbers.size(); i++) {
      digits.append(numbers.get(i).toString());
    }

    return digits.toString();
  }

  /**
   * Get the current sum of the selections.
   * 
   * The numbers array is first converted to a char array. Each selection is a
   * integer array, which contains the indexes in the numbers char array that
   * correspond to a single selection.
   * 
   * This is a selection: {1, 2} which means that numbersArray[1] and
   * numbersArray[2] are in the same selection.
   * 
   * Therefore, if a selection has more than 1 integer in it, we know to combine
   * the numbers to form a 2 digit number.
   * 
   * @return sum of the selections.
   */
  public int getSelectionSum(ArrayList<Integer> numbers, ArrayList<Integer[]> selections) {
    int currentSum = 0;
    char[] numbersArray = numbersToString(numbers).toCharArray();

    for (Integer[] selection : selections) {
      if (selection.length > 1) {
        int combinedNumber = Integer
            .valueOf(String.valueOf(numbersArray[selection[0]]) + String.valueOf(numbersArray[selection[1]]));

        currentSum += combinedNumber;

        continue;
      }

      currentSum += Integer.valueOf(String.valueOf(numbersArray[selection[0]]));
    }

    return currentSum;
  }

  /**
   * Gets the goal of the game which is a sum of all the generated number.
   * 
   * @return the goal of the game
   */
  public int getGoal(ArrayList<Integer> numbers) {
    int goal = 0;

    for (int i = 0; i < numbers.size(); i++) {
      goal += numbers.get(i);
    }

    return goal;
  }

  /**
   * Checks if a selection is valid or not.
   * 
   * @param indexA
   * @param indexB
   * @return selection is valid or not.
   */
  private boolean isValidSelection(ArrayList<Integer> numbers, ArrayList<Integer[]> selections, int indexA,
      int indexB) {
    // If one of the tiles is already selected.
    for (Integer[] selection : selections) {
      for (int i = 0; i < selection.length; i++) {
        if (indexA == selection[i] || indexB == selection[i]) {
          return false;
        }
      }
    }

    // If they are not valid.
    if (indexA != indexB && indexA + 1 != indexB && indexB != -1) {
      return false;
    }

    boolean indexAOutOfBounds = indexA < 0 || indexA > numbersToString(numbers).toCharArray().length;
    boolean indexBOutOfBounds = indexB != -1 && indexB < 0 || indexB > numbersToString(numbers).toCharArray().length;

    if (indexAOutOfBounds || indexBOutOfBounds) {
      return false;
    }

    return true;
  }

  /**
   * Selects a single tile.
   * 
   * @param indexA Index of the tile we want to select.
   * @return If it's been selected or not.
   */
  public boolean select(ArrayList<Integer> numbers, ArrayList<Integer[]> selections, int indexA) {
    if (!isValidSelection(numbers, selections, indexA, -1))
      return false;

    selections.add(new Integer[] { indexA });

    return true;
  }

  /**
   * Create a new group selection.
   * 
   * @param indexA
   * @param indexB
   * @return if the selection is successful.
   */
  public boolean select(ArrayList<Integer> numbers, ArrayList<Integer[]> selections, int indexA, int indexB) {
    if (!isValidSelection(numbers, selections, indexA, indexB))
      return false;

    selections.add(new Integer[] { indexA, indexB });

    return true;
  }

  /**
   * Checks if all tiles are selected. Necessary for win or lose conditions.
   * 
   * @return if all tiles are selected.
   */
  private boolean allTilesAreSelected(ArrayList<Integer> numbers, ArrayList<Integer[]> selections) {
    int numberOfSelections = 0;

    for (Integer[] selection : selections) {
      for (int i = 0; i < selection.length; i++) {
        numberOfSelections++;
      }
    }

    return numberOfSelections == numbersToString(numbers).length();
  }

  /**
   * A win is defined by all tiles being selected and the sum of the selections is
   * equal to the game's goal.
   * 
   * @return if the user won.
   */
  public boolean checkWin(ArrayList<Integer> numbers, ArrayList<Integer[]> selections) {
    return allTilesAreSelected(numbers, selections) && getSelectionSum(numbers, selections) == getGoal(numbers);
  }

  /**
   * A loss is defined by all tiles being selected and the sum of selections is
   * not equal to the game's goal.
   * 
   * @return if the user lost.
   */
  public boolean checkLose(ArrayList<Integer> numbers, ArrayList<Integer[]> selections) {
    return allTilesAreSelected(numbers, selections) && getSelectionSum(numbers, selections) != getGoal(numbers);
  }

  public void newGame(ArrayList<Integer> numbers, ArrayList<Integer[]> selections) {
    numbers = generateNumbers(numbers, RandomUtils.getRandom(MIN_NUMBER_OF_GROUPS, MAX_NUMBER_OF_GROUPS));

    selections.clear();

    setSelections(new ArrayList<Integer[]>());
  }

  public void setSelections(ArrayList<Integer[]> selections) {
    this.selections = selections;
  }

  public ArrayList<Integer[]> getSelections() {
    return this.selections;
  }

  public ArrayList<Integer> getNumbers() {
    return this.numbers;
  }

  public void setNumbers(ArrayList<Integer> numbers) {
    this.numbers = numbers;
  }

  public int getResets() {
    return this.resets;
  }

  public void setResets(int resets) {
    this.resets = resets;
  }

  public GameModel() {
    this.numbers = generateNumbers(new ArrayList<Integer>(),
        RandomUtils.getRandom(MIN_NUMBER_OF_GROUPS, MAX_NUMBER_OF_GROUPS));

    this.selections = new ArrayList<Integer[]>();
  }

}

GameViewController.java

package logic;

import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.Timer;

import ui.NextButtonActionListener;
import ui.TilePanel;
import ui.TilePanelMouseAdapter;
import ui.TimerActionListener;
import ui.ResetButtonActionListener;

/**
 * The view-controller class handles the display (tiles, buttons, etc.) and the
 * user input (actions from selections, clicks, etc.).
 *
 */
public class GameViewController extends JPanel {

  private static final long serialVersionUID = 1L;

  /**
   * Instance of the game (logic, state, etc.)
   */
  private GameModel gameModel;

  /**
   * A single tile panel displays all the tiles of the game
   */
  private TilePanel tilePanel;

  private Timer timer;

  /**
   * Button used to generate a new game.
   */
  private JButton nextButton;

  /**
   * Button used to reset the current game and try again.
   */
  private JButton resetButton;

  /**
   * Label used to display the current sum of the selections.
   */
  private JLabel sumLabel;

  /**
   * Label used to display the goal sum of the current game.
   */
  private JLabel goalLabel;

  private JLabel timeLabel;

  private JLabel numberOfResetsLabel;

  private final String GOAL_LABEL = "[Goal] ";

  private final String SUM_LABEL = "[Sum] ";

  private final String TIME_LABEL = "[Time] ";

  private final String DEFAULT_TIME_LABEL = "00:00";

  private final String RESETS_LABEL = "[Resets] ";

  private final String NEXT_BUTTON_LABEL = "Next";

  private final String RESET_BUTTON_LABEL = "Reset";

  /**
   * Setup the listeners for our buttons and components.
   */
  private void setupListeners() {
    nextButton.addActionListener(new NextButtonActionListener(this));
    resetButton.addActionListener(new ResetButtonActionListener(this));

    tilePanel.addMouseListener(new TilePanelMouseAdapter(this));
  }

  /**
   * Create new instances and set text for all UI elements.
   */
  private void initializeUIElements() {
    tilePanel = new TilePanel(gameModel);
    this.add(tilePanel);

    this.goalLabel = new JLabel(GOAL_LABEL + gameModel.getGoal(gameModel.getNumbers()));
    this.add(goalLabel);

    this.sumLabel = new JLabel(
        SUM_LABEL + gameModel.getSelectionSum(gameModel.getNumbers(), gameModel.getSelections()));
    this.add(sumLabel);

    this.timeLabel = new JLabel(TIME_LABEL + DEFAULT_TIME_LABEL);
    this.add(timeLabel);

    this.numberOfResetsLabel = new JLabel(RESETS_LABEL + String.valueOf(gameModel.getResets()));
    this.add(numberOfResetsLabel);

    this.nextButton = new JButton(NEXT_BUTTON_LABEL);
    this.add(nextButton);

    this.resetButton = new JButton(RESET_BUTTON_LABEL);
    this.add(resetButton);
  }

  /**
   * Reset the labels to a new game's values.
   */
  public void resetLabels() {
    this.goalLabel.setText(GOAL_LABEL + gameModel.getGoal(gameModel.getNumbers()));
    this.sumLabel.setText(SUM_LABEL + gameModel.getSelectionSum(gameModel.getNumbers(), gameModel.getSelections()));

    this.updateResetsLabel(gameModel.getResets());
  }

  public void updateTimeLabel(int numberOfMinutes, int numberOfSeconds) {
    this.timeLabel.setText(TIME_LABEL + String.format("%02d:%02d", numberOfMinutes, numberOfSeconds));
  }

  public void updateResetsLabel(int numberOfResets) {
    this.numberOfResetsLabel.setText(RESETS_LABEL + String.valueOf(numberOfResets));
  }

  public JLabel getSumLabel() {
    return this.sumLabel;
  }

  public JLabel getGoalLabel() {
    return this.goalLabel;
  }

  public JLabel getTimeLabel() {
    return this.timeLabel;
  }

  public Timer getTimer() {
    return this.timer;
  }

  public GameModel getGameModel() {
    return this.gameModel;
  }

  public TilePanel getTilePanel() {
    return this.tilePanel;
  }

  public GameViewController() {
    gameModel = new GameModel();

    // The layout defines how components are displayed
    // (here, stacked along the Y axis)
    setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));

    initializeUIElements();

    setupListeners();

    timer = new Timer(1000, new TimerActionListener(this));
    timer.start();
  }
}

TilePanel.java

package ui;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;

import javax.swing.JPanel;
import javax.swing.Timer;

import logic.GameModel;
import logic.RandomUtils;

import ui.TimerActionListener;

/**
 * The tile panel displays all the tiles (one per digit) of the game.
 *
 */
public class TilePanel extends JPanel {

  private static final long serialVersionUID = 1L;

  /**
   * The tile panel object holds a reference to the game model to request
   * information to display (view) and to modify its state (controller)
   */
  private GameModel gameModelHandle;

  /**
   * A table of colours that can be used to draw the tiles
   */
  private Color[] colours;

  /**
   * Each index corresponds to each tile's colour.
   */
  private Color[] currentTileColours;

  Color selectionColour = null;

  /**
   * The number of tiles in the interface.
   */
  private int numberOfTiles;

  @Override
  protected void paintComponent(Graphics g) {
    super.paintComponent(g);

    // White tiles by default.
    colourAllTiles(Color.WHITE, g);

    // If there are selections, colour them.
    if (gameModelHandle.getSelections().size() != 0) {
      colourSelectionTiles(g);
    }

    // Green tiles when user wins.
    if (gameModelHandle.checkWin(gameModelHandle.getNumbers(), gameModelHandle.getSelections())) {
      colourAllTiles(Color.GREEN, g);
    }

    // Red tiles when user loses.
    if (gameModelHandle.checkLose(gameModelHandle.getNumbers(), gameModelHandle.getSelections())) {
      colourAllTiles(Color.RED, g);
    }

    // At the end, draw the tile numbers over the tiles.
    drawTileNumbers(g);
  }

  /**
   * Initialize an array of pre-set colours. The colours are picked to ensure
   * readability and avoid confusion.
   */
  private void initializeColours() {
    // Some tile colours in the '0xRRGGBB' format
    String[] tileColourCodes = new String[] { "0x89CFF0", "0xF4C2C2", "0xFFBF00", "0xFBCEB1", "0x6495ED", "0x9BDDFF",
        "0xFBEC5D", "0xFF7F50", "0x00FFFF", "0x98777B", "0x99BADD", "0x654321" };

    // Allocate and fill our colour array with the colour codes
    colours = new Color[tileColourCodes.length];
    for (int i = 0; i < colours.length; ++i)
      colours[i] = Color.decode(tileColourCodes[i]);

    currentTileColours = new Color[numberOfTiles];
    for (int i = 0; i < currentTileColours.length; i++) {
      currentTileColours[i] = Color.WHITE;
    }
  }

  /**
   * Gets the index of the tile corresponding to the coordinates.
   * 
   * @param x X position of the coordinates.
   * @param y Y position of the coordinates.
   * @return the tile index (starting from 0) or -1 if invalid tile.
   */
  public int getTileIndex(int windowWidth, int numberOfTiles, int x, int y) {
    // 128 is the height of the tiles. If we're below them, it's not
    // a valid tile.
    if (y > 128) {
      return -1;
    }

    // Algorithm which determines the tile index from the X coordinate.
    return (int) Math.ceil(x / (windowWidth / numberOfTiles));
  }

  /**
   * Checks if a colour is already present in one of the selections.
   * 
   * @param colour A colour we want to use.
   * @return If the colour is already in use or not.
   */
  private boolean selectionColourAlreadyInUse(Color colour) {
    for (int i = 0; i < currentTileColours.length; i++) {
      if (currentTileColours[i] == colour)
        return true;
    }

    return false;
  }

  /**
   * Draw tile numbers in a black font in the middle of each tile.
   * 
   * @param g Graphics object
   */
  private void drawTileNumbers(Graphics g) {
    // For each tile.
    for (int i = 0; i < numberOfTiles; i++) {
      g.setFont(new Font("Monospace", Font.PLAIN, 20));
      g.setColor(Color.BLACK);

      // Draw a number in the middle of each tile.
      g.drawString(String.valueOf(gameModelHandle.numbersToString(gameModelHandle.getNumbers()).toCharArray()[i]),
          i * (this.getWidth() / numberOfTiles) + (this.getWidth() / numberOfTiles) / 2, 128 / 2);
    }
  }

  /**
   * Colours a single tile
   * 
   * @param colour    the colour to use.
   * @param tileIndex the tile to colour
   * @param g         the graphics object
   */
  private void colourTile(Color colour, int tileIndex, Graphics g) {
    g.setColor(colour);

    g.fillRect(tileIndex * (this.getWidth() / numberOfTiles), 0, this.getWidth() / numberOfTiles, 128);
  }

  /**
   * Colours all tiles in a given colour.
   * 
   * @param colour the colour to use.
   * @param g      the graphics object.
   */
  private void colourAllTiles(Color colour, Graphics g) {
    for (int i = 0; i < numberOfTiles; i++) {
      colourTile(colour, i, g);
    }
  }

  /**
   * Returns a colour that has not been used in a selection before.
   * 
   * @return a color
   */
  private Color getRandomUniqueColour() {
    Color selectionColour;

    do {
      selectionColour = colours[RandomUtils.getRandom(0, colours.length - 1)];
    } while (selectionColourAlreadyInUse(selectionColour));

    return selectionColour;
  }

  /**
   * Colours a selection. A selection of 2 tiles has 1 colour.
   * 
   * @param g Graphics object
   */
  private void colourSelectionTiles(Graphics g) {
    // Make sure we don't use the same colour for 2 consecutive selections.
    selectionColour = getRandomUniqueColour();

    for (Integer[] selection : gameModelHandle.getSelections()) {
      // Draw the indexes in the selection in that color.
      for (int i = 0; i < selection.length; i++) {
        // When a tile already has a color, we draw it again with the same color.
        if (currentTileColours[selection[i]] != Color.WHITE) {
          colourTile(currentTileColours[selection[i]], selection[i], g);
          continue;
        }

        // The tile is white, so we assign the selection's random colour to it.
        colourTile(selectionColour, selection[i], g);

        // Keep track of the tile's colour.
        currentTileColours[selection[i]] = selectionColour;
      }
    }
  }

  public void newGame() {
    this.numberOfTiles = gameModelHandle.numbersToString(gameModelHandle.getNumbers()).length();

    this.selectionColour = null;

    initializeColours();

    this.repaint();
  }

  public int getNumberOfTiles() {
    return this.numberOfTiles;
  }

  public TilePanel(GameModel gameModel) {
    if (gameModel == null)
      throw new IllegalArgumentException("Should provide a valid instance of GameModel!");
    gameModelHandle = gameModel;

    this.numberOfTiles = gameModelHandle.numbersToString(gameModelHandle.getNumbers()).length();

    // Initialize our array of tile colours
    initializeColours();
  }

}

TilePanelMouseAdapter.java

package ui;

import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import logic.GameViewController;

/**
 * The tile panel mouse adapter controls the actions performed when a mouse
 * click is registered on the TilePanel component.
 *
 */
public class TilePanelMouseAdapter extends MouseAdapter {

  /**
   * A reference to the main GameViewController instance.
   */
  private GameViewController gameViewController;

  /**
   * The tile the user started his click in.
   */
  private int clickStartTile;

  /**
   * The tile the user ended his click in.
   */
  private int clickEndTile;

  public TilePanelMouseAdapter(GameViewController gameViewController) {
    super();
    this.gameViewController = gameViewController;
  }

  @Override
  public void mousePressed(MouseEvent e) {
    // Set the start tile index using mouse coordinates.
    clickStartTile = gameViewController.getTilePanel().getTileIndex(gameViewController.getTilePanel().getWidth(),
        gameViewController.getTilePanel().getNumberOfTiles(), e.getX(), e.getY());
  }

  @Override
  public void mouseReleased(MouseEvent e) {
    // Set the end tile index using mouse coordinates.
    clickEndTile = gameViewController.getTilePanel().getTileIndex(gameViewController.getTilePanel().getWidth(),
        gameViewController.getTilePanel().getNumberOfTiles(), e.getX(), e.getY());

    // getTileIndex returns -1 if it's not a valid tile, so if any of the tiles
    // are -1, it's an invalid tile.
    boolean isValidTile = clickStartTile > -1 && clickEndTile > -1;

    // Click is started in a tile but not ended in one.
    if (!isValidTile) {
      return;
    }

    // If we don't end our click in the same tile,
    // it's a group of 2.
    if (clickStartTile != clickEndTile) {
      gameViewController.getGameModel().select(gameViewController.getGameModel().getNumbers(),
          gameViewController.getGameModel().getSelections(), clickStartTile, clickEndTile);
    } else {
      gameViewController.getGameModel().select(gameViewController.getGameModel().getNumbers(),
          gameViewController.getGameModel().getSelections(), clickStartTile);
    }

    // Set the sum label.
    gameViewController.resetLabels();

    gameViewController.getTilePanel().repaint();
  }

}

NextButtonActionListener.java

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

import logic.GameViewController;

/**
 * The next button action listener controls the actions performed
 * when the next button is pressed.
 *
 */
public class NextButtonActionListener implements ActionListener {

  private GameViewController gameViewController;

  public NextButtonActionListener(GameViewController gameViewController) {
    this.gameViewController = gameViewController;
  }

  @Override
  public void actionPerformed(ActionEvent e) {
    // Generate new numbers, clear selections, etc.
    gameViewController.getGameModel().newGame(gameViewController.getGameModel().getNumbers(), 
        gameViewController.getGameModel().getSelections());

    // Reset tiles to white and paint new numbers.
    gameViewController.getTilePanel().newGame();

    gameViewController.getGameModel().setResets(0);

    TimerActionListener timer = (TimerActionListener) gameViewController.getTimer().getActionListeners()[0];
    timer.resetTime();

    gameViewController.resetLabels();
  }

}

ResetButtonActionListener.java

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

import logic.GameViewController;

/**
 * The reset button action listener controls the action performed
 * when the reset button is pressed.
 *
 */
public class ResetButtonActionListener implements ActionListener {

  GameViewController gameViewController;

  public ResetButtonActionListener(GameViewController gameViewController) {
    this.gameViewController = gameViewController;
  }

  @Override
  public void actionPerformed(ActionEvent e) {
    // Reset tiles to white and paint new numbers.
    gameViewController.getTilePanel().newGame();

    gameViewController.getGameModel().getSelections().clear();

    gameViewController.getGameModel().setResets(gameViewController.getGameModel().getResets() + 1);;
    gameViewController.updateResetsLabel(gameViewController.getGameModel().getResets());

    gameViewController.resetLabels();
  }

}
\$\endgroup\$
  • 1
    \$\begingroup\$ Note that all code must be posted here directly as text and formatted. Paste it here, then highlight it and press the {} button in the editor. \$\endgroup\$ – Carcigenicate Apr 15 at 21:11
  • \$\begingroup\$ Ok, I am working on it now. \$\endgroup\$ – bestinthewest Apr 15 at 21:12
  • \$\begingroup\$ the code is not complete - TilePanel.getTileIndex(..) is missing a return statement ^_^ \$\endgroup\$ – Martin Frank Apr 16 at 10:39
  • \$\begingroup\$ I added the return statement and missing method. \$\endgroup\$ – bestinthewest Apr 16 at 11:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.