GUI
No notes
Syntax:
Java
package aufgabe_4; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.GridLayout; import java.awt.Insets; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.Arrays; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.border.Border; import javax.swing.border.CompoundBorder; import javax.swing.border.EmptyBorder; import javax.swing.border.LineBorder; /** * This class models the GUI for the Mastermind game. */ public class MastermindView extends JFrame { /** * This stores whether the game has ended. * The game can end by running out of turns, the user cheating or * the combination being guessed correctly. */ private boolean gameOver = false; /** * This is the Mastermind game that the GUI corresponds to. */ private MastermindModel theGame = new MastermindModel(); /** * This is the label on top of the game board that shows status updates. */ private JPanel descriptionPanel = new JPanel(); /** * This is the game board with the panels for moves and ratings. */ private JPanel gameBoard = new JPanel(); /** * This is the label separting the game board from the secret panel. */ private JPanel secretLabel = new JPanel(); /** * This is the secret panel where the secret can be stored while the * machine "guesses" or where the secret is shown at the end of a game * where the user guessed. */ private JPanel secretPanel = new JPanel(); /** * This holds all the circles representing colors in ColorCodes, including * the panel for the secret code. Thus, moveCircles.length is one more than * MastermindGame.MAX_MOVES. * * The circles in the secretPanel are in * moveCircles[MastermindGame.MAX_MOVES]. */ private JPanel[][] moveCircles = new JPanel[MastermindGame.MAX_MOVES + 1][MastermindGame.NUMBER_SLOTS]; /** * This holds the circles used for rating. */ private JPanel[][] ratingCircles = new JPanel[MastermindGame.MAX_MOVES][MastermindGame.NUMBER_SLOTS]; /** * This defines the width of a JPanel that represents a code circle. */ private static final int MOVE_CIRCLE_WIDTH = 240 / MastermindGame.NUMBER_SLOTS; /** * This defines the width of a JPanel that represents a rating circle. */ private static final int RATING_CIRCLE_WIDTH = (MOVE_CIRCLE_WIDTH / 2) - 4; /** * This is the Color for the background of both the descriptionPanel and * the secretLabel. */ private static final Color LABEL_COLOR = new Color(205, 104, 57); /** * This Color is the background for the game board and the secret panel. */ private static final Color GAMEBOARD_COLOR = new Color(200, 215, 250); /** * This constant represents a circle that isn't set. */ public static final Color SLOT_NOT_SET = Rating.RATING_COLORS[2]; /** * This black one-pixel LineBorder is used for look and feel. */ private static final Border LINE = new LineBorder(Color.BLACK); /** * This one-pixel EmptyBorder is used for look and feel. */ private static final Border EMPTY = new EmptyBorder(1, 1, 1, 1); /** * This contains enum objects representing the buttons used to play the * game. Its purpose is managing control flow in the method * createButton(ButtonOperation). The order of the elements determines * the order of the buttons in the GUI (but not their respective function). */ enum ButtonOperation { /** * The button to make the next move/evaluate the machine's guess. */ MOVE, /** * The button to switch the user */ SWITCH, /** * Tht button to start a new game with the same guesser. */ NEW, /** * The button to quit the program. */ QUIT } /** * This represents the possible different game situations. * It is used to control the updates on the descriptionPanel. */ enum GameSit { /** * The user has to set the secret panel before the machine starts * "guessing". */ USER_MOVE_SET_SECRET_PANEL, /** * The user has to enter a rating for the machine's move. */ USER_MOVE_MACHINE_GUESSES, /** * The user has to enter a guess. */ USER_MOVE_USER_GUESSES, /** * The ColorCode entered by the user in the current move is invalid. */ INVALID_COLORCODE, /** * The user cheated. */ USER_CHEATED, /** * The user won. */ USER_WON, /** * The user lost. */ USER_LOST } /** * Create a new MastermindView. * The constructor builds all the elements of the game and structures them. */ public MastermindView() { setTitle("Mastermind - have fun!"); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLayout(new GridBagLayout()); GridBagConstraints constraints = new GridBagConstraints(); //create and add the label on top of the window showing status texts changeTextLabel(descriptionPanel, getDescr(GameSit.USER_MOVE_USER_GUESSES), LABEL_COLOR); constraints.fill = GridBagConstraints.HORIZONTAL; constraints.gridx = 0; constraints.weightx = 1.0; constraints.weighty = 0.0; add(descriptionPanel, constraints); //create and add the game board resetGameBoard(); add(gameBoard, constraints); //label the secret board changeTextLabel(secretLabel, "The secret", LABEL_COLOR); constraints.weighty = 1.0; add(secretLabel, constraints); //create the panel where the secret is shown after a user-is-guessing //game or the user will set the secret in a machine-is-guessing //game secretPanel = resetMovePanel(secretPanel, MastermindGame.MAX_MOVES); constraints.fill = GridBagConstraints.NONE; constraints.weighty = 0.0; constraints.insets = new Insets(0, 0, 2, 0); add(secretPanel, constraints); //create and add the buttons on the bottom used to make moves etc. constraints.fill = GridBagConstraints.HORIZONTAL; constraints.anchor = GridBagConstraints.PAGE_END; constraints.insets = new Insets(0, 1, 1, 1); add(createButtonPanel(), constraints); //adjust the frame's size and location, lock the size and show the frame pack(); setLocationRelativeTo(null); setResizable(false); setVisible(true); } /** * Changes the text shown in a JPanel. * This method is used to update the descriptionPanel. * * @param textPanel The JPanel that is being changed. * @param text The new text. * @param background The background for the JPanel. */ private void changeTextLabel(JPanel textPanel, String text, Color background) { //remove everything from the JPanel, then build it up //the Layout is set so the JPanel will stretch when this JFrame calls //pack() textPanel.removeAll(); JLabel addMe = new JLabel(text); addMe.setHorizontalAlignment(JLabel.CENTER); CompoundBorder theBorder = new CompoundBorder(EMPTY, LINE); addMe.setBorder(theBorder); addMe.setFont(new Font("Arial", Font.BOLD, 16)); textPanel.add(addMe); textPanel.setLayout(new GridLayout()); textPanel.setBackground(background); } /** * Resets the gameBoard to the state it is in when the game starts. */ private void resetGameBoard() { //remove everything, then build the board fresh gameBoard.removeAll(); JPanel move = null; JPanel codePanel = null; JPanel ratingPanel = null; gameBoard.setLayout(new GridLayout(0, 1)); int height = 0; //add a panel for the code and the rating for each move for (int i = 0; i < MastermindGame.MAX_MOVES; i++) { move = new JPanel(); codePanel = new JPanel(); move.setLayout(new BoxLayout(move, BoxLayout.X_AXIS)); codePanel = resetMovePanel(codePanel, i); ratingPanel = resetRatingPanel(i); height += codePanel.getPreferredSize().getHeight(); move.add(codePanel); move.add(ratingPanel); gameBoard.add(move); } //adjust the preferred size gameBoard.setPreferredSize(new Dimension( MastermindGame.NUMBER_SLOTS * MOVE_CIRCLE_WIDTH, height)); } /** * Resets a panel with circles for color codes. * This is used while adding code circles to the game board and building * and resetting the secret panel. * * @param move The panel that will be reset. * @param row The row the panel is in. * @return Returns the freshly reset panel. */ private JPanel resetMovePanel(JPanel move, int row) { //empty the panel and build it fresh move.removeAll(); move.setLayout(new GridLayout(1, MastermindGame.NUMBER_SLOTS + 1, 3, 3)); move.setBackground(GAMEBOARD_COLOR); JPanel circle = null; //add it to moveCircles in the place it belongs for (int i = 0; i < MastermindGame.NUMBER_SLOTS; i++) { circle = createMoveCircle(); moveCircles[row][i] = circle; move.add(circle); } //adjust for size int width = (MOVE_CIRCLE_WIDTH) * MastermindGame.NUMBER_SLOTS; move.setPreferredSize(new Dimension(width + 6, MOVE_CIRCLE_WIDTH + 6)); //giving the panel a border CompoundBorder panelBorder = new CompoundBorder(EMPTY, LINE); panelBorder = new CompoundBorder(panelBorder, EMPTY); move.setBorder(panelBorder); return move; } /** * Resets a panel with circles for ratings. * This is used while adding rating circles to the game board. * * @param move The panel that will be reset. * @param row The row the panel is in. * @return Returns the freshly reset panel. */ private JPanel resetRatingPanel(int row) { //empty the panel and build it anew JPanel ratingSlots = new JPanel(); ratingSlots.setBackground(GAMEBOARD_COLOR); int rows = (MastermindGame.NUMBER_SLOTS / 2) + (MastermindGame.NUMBER_SLOTS % 2); ratingSlots.setLayout(new GridLayout(rows, 2)); ratingSlots.setPreferredSize(new Dimension(MOVE_CIRCLE_WIDTH, MOVE_CIRCLE_WIDTH)); JPanel circle = null; //add the rating circles to the panel for (int i = 0; i < MastermindGame.NUMBER_SLOTS; i++) { circle = createRatingCircle(); ratingCircles[row][i] = circle; ratingSlots.add(circle); } CompoundBorder panelBorder = new CompoundBorder(EMPTY, LINE); panelBorder = new CompoundBorder(panelBorder, EMPTY); ratingSlots.setBorder(panelBorder); return ratingSlots; } /** * Creates a new circle representing a color on the game board. * This also adds the required MouseAdapter to the circle. * * @return The created circle as a JPanel. */ private JPanel createMoveCircle() { //put a circle the size of a color code circle on a new panel final JPanel circle = new JPanel() { @Override public void paintComponent(Graphics g) { g = (Graphics2D) g; g.fillOval(0, 2, MOVE_CIRCLE_WIDTH - 4, MOVE_CIRCLE_WIDTH - 4); } }; //get a popupmenu with the valid Colors for the circle final CodePopupMenu codePopup = new CodePopupMenu(circle); //add a MouseAdapter responding to right clicks circle.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent event) { //only react to right clicks if ((event.isMetaDown()) && (!gameOver)) { /* * in case the machine is guessing, only show the panel for * the circles in the secret panel * if the user is guessing, show it for the circles for the * current move */ if (theGame.isMachineGuessing()) { //only show the popup for the secret panel before //the machine made its first move if (circleBelongsToPanel(circle, moveCircles, MastermindGame.MAX_MOVES) && (moveCircles[0][0].getForeground() == SLOT_NOT_SET)) { codePopup.show(event.getComponent(), event.getX(), event.getY()); } } else { //only show the popup for the current move if (circleBelongsToPanel(circle, moveCircles, theGame.getMoveCount())) { codePopup.show(event.getComponent(), event.getX(), event.getY()); } } } } }); circle.setForeground(SLOT_NOT_SET); circle.setSize(new Dimension(MOVE_CIRCLE_WIDTH, MOVE_CIRCLE_WIDTH)); return circle; } /** * This creates a rating circle analogously to createMoveCircle(). * * @param row The row which the circle belongs to * @return */ private JPanel createRatingCircle() { //put a circle the size of a rating circle on a new panel final JPanel circle = new JPanel() { @Override public void paintComponent(Graphics g) { g.fillOval(2, 2, RATING_CIRCLE_WIDTH - 4, RATING_CIRCLE_WIDTH - 4); } }; final RatingPopupMenu codePopup = new RatingPopupMenu(circle); //add the MouseAdapter; make it only respond to right clicks while //the machine is guessing with the circle lieing in the right row circle.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent event) { if ((event.isMetaDown()) && (!gameOver) && (theGame.isMachineGuessing()) && (circleBelongsToPanel(circle, ratingCircles, theGame.getMoveCount()))) { codePopup.show(event.getComponent(), event.getX(), event.getY()); } } }); circle.setForeground(SLOT_NOT_SET); circle.setSize(new Dimension(RATING_CIRCLE_WIDTH, RATING_CIRCLE_WIDTH)); return circle; } /** * Creates the button panel. * * @return The newly created button panel. */ private JPanel createButtonPanel() { JButton addMe = null; JPanel buttonPanel = new JPanel(); buttonPanel.setLayout(new GridLayout(1, 0, 3, 0)); //for each operation, create a button and add it to the panel for (ButtonOperation op : ButtonOperation.values()) { addMe = createButton(op); buttonPanel.add(addMe); } return buttonPanel; } /** * Creates a button with the tooltip and ActionListener required by its * operation. * * @param op The operation which the button has to perform when clicked. * @return The created button. */ private JButton createButton(ButtonOperation op) { JButton button = null; switch(op) { case MOVE: button = new JButton("move"); button.setToolTipText("Makes a move; rates the guess if the " + "user is guessing or evaluates the machine's guess " + "according to the user's rating in machine-mode."); button.addActionListener(createMoveListener()); break; case NEW: button = new JButton("new"); button.setToolTipText("Starts a new game without changing who " + "is guessing."); button.addActionListener(createNewListener()); break; case SWITCH: button = new JButton("switch"); button.setToolTipText("Switches between user and machine as " + "guesser and starts a new game."); button.addActionListener(createSwitchListener()); break; case QUIT: button = new JButton("quit"); button.setToolTipText("Ends the program."); button.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { System.exit(0); } }); break; default: break; } button.setFont(new Font("Dialog", Font.PLAIN, 14)); return button; } /** * Creates an ActionListener for the "move"-button. * * @return Returns the adjusted ActionListener. */ private ActionListener createMoveListener() { return new ActionListener() { @Override public void actionPerformed(ActionEvent e) { int moveCount = theGame.getMoveCount(); //only react if the game is still going on if (!(moveCount < MastermindGame.MAX_MOVES) || gameOver) { return; } boolean machineIsGuessing = theGame.isMachineGuessing(); //check if the machine is guessing; if so, make a machine //move, else let the user play if (machineIsGuessing) { makeMachineMove(); } else { //check whether the user has entered a valid color combo //already; if so rate it, else update the description panel if (!validateCode(moveCount)) { updateDescr(getDescr(GameSit.INVALID_COLORCODE)); return; } //rate the user's guess ColorCode humanGuess = readColorCode(moveCount); theGame.humanMove(humanGuess); moveCount = theGame.getMoveCount(); Rating moveRating = theGame.getRating(moveCount - 1); updateRatingPanel(moveCount - 1, moveRating); //update the description panel according to the user's //performance if (MastermindModel.SUCCESS.isEqualTo(moveRating)) { updateDescr(getDescr(GameSit.USER_WON)); updateMovePanel(MastermindGame.MAX_MOVES, theGame.getSecret()); } else if (moveCount == MastermindGame.MAX_MOVES) { updateDescr(getDescr(GameSit.USER_LOST)); updateMovePanel(MastermindGame.MAX_MOVES, theGame.getSecret()); } } repaint(); } }; } /** * Makes a machine move. */ private void makeMachineMove() { int moveCount = theGame.getMoveCount(); //if the secret code has been set, check whether the first move has //been chosen; if not, do so, else process ratings if (readColorCode(MastermindGame.MAX_MOVES).isValidCode()) { updateDescr(getDescr(GameSit.USER_MOVE_MACHINE_GUESSES)); ColorCode firstMove = theGame.getGameState(0); if (firstMove == null) { firstMove = theGame.machineMove(); updateMovePanel(moveCount, firstMove); } else { ColorCode curGuess = theGame.getGameState(moveCount); Rating curRating = readRating(moveCount); theGame.processEval(curGuess, curRating.getBlacks(), curRating.getWhites()); //if the machine's guess was successful, say "yay" //else, remove possibilities that are no longer possible if (MastermindModel.SUCCESS.isEqualTo(curRating)) { updateDescr(getDescr(GameSit.USER_LOST)); } else { moveCount = theGame.getMoveCount(); //if it was the last turn, say "aww machine fail" //else, get a new machine guess if (moveCount == MastermindGame.MAX_MOVES) { updateDescr(getDescr(GameSit.USER_WON)); } else { curGuess = theGame.machineMove(); //if there are possibilities left, print the first //one else, the user cheated - call him a cheater if (curGuess == null) { updateDescr(getDescr(GameSit.USER_CHEATED)); } else { updateMovePanel(moveCount, curGuess); } } } } } } /** * Creates an ActionListener for the "switch"-button. * * @return The adjusted ActionListener. */ private ActionListener createSwitchListener() { return new ActionListener() { @Override public void actionPerformed(ActionEvent e) { theGame.switchGuesser(); startNewGame(); } }; } /** * Creates an ActionListener for the "new"-button. * * @return Return the adjusted ActionListener. */ private ActionListener createNewListener() { return new ActionListener() { @Override public void actionPerformed(ActionEvent e) { startNewGame(); } }; } /** * Starts a new game by resetting everyting. This method is used by the * ActionListeners of the "new"-button and the "switch"-button. */ private void startNewGame() { boolean machineIsGuessing = theGame.isMachineGuessing(); gameOver = false; theGame = new MastermindModel(); //if the machine is supposed to guess, switch the user //since a new MastermindModel starts out with the user //guessing; update the textPanel accordingly if (theGame.isMachineGuessing() != machineIsGuessing) { theGame.switchGuesser(); } //update the description panel if (theGame.isMachineGuessing()) { updateDescr(getDescr(GameSit.USER_MOVE_SET_SECRET_PANEL)); } else { updateDescr(getDescr(GameSit.USER_MOVE_USER_GUESSES)); } gameOver = false; resetGameBoard(); gameBoard.revalidate(); secretPanel = resetMovePanel(secretPanel, MastermindGame.MAX_MOVES); secretPanel.revalidate(); repaint(); } /** * Updates the color panel for a given move with a given ColorCode. * * @param moveCount The number of the move. * @param code The ColorCode that will be displayed. */ private void updateMovePanel(int moveCount, ColorCode code) { Color[] theCodeColors = code.getColors(); for (int i = 0; i < MastermindGame.NUMBER_SLOTS; i++) { moveCircles[moveCount][i].setForeground(theCodeColors[i]); } repaint(); } /** * Updates the rating panel for a given move with a given Rating. * * @param moveCount The number of the move. * @param rating The Rating that will be displayed. */ private void updateRatingPanel(int moveCount, Rating rating) { int blacks = rating.getBlacks(); int whites = rating.getWhites(); for (int i = 0; i < blacks; i++) { ratingCircles[moveCount][i].setForeground(Color.BLACK); } for (int i = 0; i < whites; i++) { ratingCircles[moveCount][i + blacks].setForeground(Color.WHITE); } repaint(); } /** * Updates the description panel with a given text. * * @param newText The new text for the panel. */ private void updateDescr(String newText) { changeTextLabel(descriptionPanel, newText, descriptionPanel.getBackground()); descriptionPanel.revalidate(); repaint(); } /** * Gives back a String describing a given game situation. * Also, in the case of USER_WON and USER_LOST, gameOver is set to true. * * @param situation The game situation. * @return Returns a String describing the game situation. */ private String getDescr(GameSit situation) { String textDescribingSituation = null; switch(situation) { case INVALID_COLORCODE: textDescribingSituation = "Pick a color for every slot!"; break; case USER_CHEATED: textDescribingSituation = "You cheated! - Click 'new' or 'switch'."; gameOver = true; break; case USER_WON: textDescribingSituation = "You win! - Click 'new' or 'switch'."; gameOver = true; break; case USER_LOST: textDescribingSituation = "Computer wins! - Click 'new' or 'switch'."; gameOver = true; break; case USER_MOVE_MACHINE_GUESSES: textDescribingSituation = "Rate the guess and click 'move'."; break; case USER_MOVE_USER_GUESSES: textDescribingSituation = "Pick your guess and click 'move'!"; break; case USER_MOVE_SET_SECRET_PANEL: textDescribingSituation = "Set the secret code and click 'move'!"; break; default: break; } return textDescribingSituation; } /** * Reads a ColorCode entered by the user into the color code circles. * * @param moveCount The current turn. * @return Returns a ColorCode representing the current turn. */ private ColorCode readColorCode(int moveCount) { byte[] theCodeAsNumbers = new byte[MastermindGame.NUMBER_SLOTS]; Arrays.fill(theCodeAsNumbers, ColorCode.COLOR_INVALID); //transform each slot into the number corresponding to its Color for (int i = 0; i < MastermindGame.NUMBER_SLOTS; i++) { Color foreground = moveCircles[moveCount][i].getForeground(); theCodeAsNumbers[i] = (byte) ColorCode.colorToNumber(foreground); } return new ColorCode(theCodeAsNumbers); } /** * Reads a Rating entered by the user into the rating circles. * * @param moveCount The current turn. * @return Returns a Rating representing the current turn. */ private Rating readRating(int moveCount) { byte blacks = 0; byte whites = 0; for (JPanel ratingCircle : ratingCircles[moveCount]) { if (ratingCircle.getForeground() == Rating.RATING_COLORS[0]) { blacks++; } if (ratingCircle.getForeground() == Rating.RATING_COLORS[1]) { whites++; } } return (new Rating(blacks, whites)); } /** * Checks whether a circle belongs to the panel of the current turn. * This method is used to control the matrix-like structure of the game * board. * * @param circle The circle that is supposed to belong to the panel. * @param panel The panel. * @param moveCount The current turn. * @return Returns true if the circle belongs to the panel for the current * turn, else false. */ private boolean circleBelongsToPanel(JPanel circle, JPanel[][] panel, int moveCount) { for (int i = 0; i < MastermindGame.NUMBER_SLOTS; i++) { if (panel[moveCount][i] == circle) { return true; } } return false; } /** * Checks if the ColorCode that was set in a given row is valid. * * @param moveCount The number of the row. * @return Returns true if the color combination set in the row is valid * (which means it has no unset slots), else false. */ private boolean validateCode(int moveCount) { boolean codeIsValid = false; //get the color code from the row moveCount into a byte[] byte[] code = new byte[MastermindGame.NUMBER_SLOTS]; Arrays.fill(code, ColorCode.COLOR_INVALID); for (int i = 0; i < MastermindGame.NUMBER_SLOTS; i++) { Color foreground = moveCircles[moveCount][i].getForeground(); code[i] = (byte) ColorCode.colorToNumber(foreground); } //make that byte[] into a ColorCode and check if it's valid ColorCode move = new ColorCode(code); codeIsValid = move.isValidCode(); return codeIsValid; } }