10
\$\begingroup\$

I’ve been getting used to Java and I’ve written a simple hangman game using event-driven programming in Java 8. Please let me know of any improvements regarding the readability and design of the program, along with any pointers on how to neaten up the code.

Also, any feedback regarding the use of JavaDoc style comments is welcome.

Main - the class used to start the game

package Hangman;

public class Main {
    public static void main(String[] args){
        new Logic().go();
    }
}

Logic - contains all the game logic

package Hangman;

import java.awt.Toolkit;
import java.util.ArrayList;
import java.util.List;

/** Logic - Deals with the logic aspect of Hangman */
public class Logic {
    private GUI gui;
    private Information information;
    private WordReader wordReader;


    /** Logic constructor */
    public Logic(){
        wordReader = new WordReader();
        information = new Information(wordReader);
        gui = new GUI(this, information);
    }


    /** checkUserGuess - runs the code for when a user guesses a letter that's in the secret word */
    void checkUserGuess(String guess){
        if(isGuessValid(guess)) {
            updateSecretWord(guess);
            gui.drawSecretWord();

            if(information.getLettersGuessed().size() != 0) {      // No letters have been guessed by the user at the beginning
                gui.drawLettersGuessed();
            }
            // Checks if the user has won or lost
            if (isGameWon()) {
                winSequence();
            }else if (information.getGuessesRemaining() == 0) {
                loseSequence();
            }
        }
    }

    void go(){
        information.createAlphabetSet();
        wordReader.readFile("words.txt");
        gui.build();
        setUpGame();
    }

    /** isGameWon - sees if the user has won the game */
    private boolean isGameWon(){
        for(boolean isLetterShown : information.getLettersRevealed()){
            if(!isLetterShown) {
                return false;
            }
        }
        return true;
    }

    /** isGuessValid - get input from the user & checks to see if it's a valid guess */
    private boolean isGuessValid(String guessedLetter){
        if(guessedLetter.length() == 1 && !information.getLettersGuessed().contains(guessedLetter.charAt(0))
                && information.getAlphabet().contains(guessedLetter.charAt(0))) {
            gui.setText(null);
            information.addGuessedLetter(guessedLetter.charAt(0));
            return true;
        }else{
            Toolkit.getDefaultToolkit().beep();
            gui.getTextField().requestFocusInWindow();
        }
        return false;
    }

    /** loseSequence - when the the user runs out of guesses */
    private void loseSequence(){
        for(int i = 0; i < information.getLettersRevealed().length; i++){
            information.revealLetter(i);
        }
        gui.drawSecretWord();
        playAgain("Tough luck. The secret word was " + information.getSecretWord() +
                ".\nWould you like to play another game of hangman?");
    }

    /** playAgain - Allows the user to play another game of hangman */
    private void playAgain(String message){
        if(gui.playAgainDialog(message)){
            setUpGame();
            gui.updateDrawing();
        }else{
            System.exit(0);
        }
    }

    /** setUpGame - sets up the variables appropriately */
    void setUpGame(){
        information.resetGameData();
        // Redraws the JLabels when playing another game of hangman
        gui.drawSecretWord();
        gui.drawLettersGuessed();
        gui.drawGuessesRemaining();
    }

    /** updateSecretWord - updates which letters of the secret word have been revealed */
    private void updateSecretWord(String guessedLetter){
        List<Integer> changeBool = new ArrayList<>();
        if(information.getSecretWord().contains(guessedLetter)){
            // Searches through secretWord & notes down all letters that equal the user's guess
            for(int i=0; i<information.getSecretWord().length(); i++){
                if(information.getSecretWord().charAt(i) == guessedLetter.charAt(0)) {
                    changeBool.add(i);
                }
            }
            // Changes the boolean value for those letters @ their corresponding indexes
            for(Integer i : changeBool) {
                information.revealLetter(i);
            }
        }else{
            information.setGuessesRemaining(information.getGuessesRemaining() - 1);
            gui.drawGuessesRemaining();
            gui.updateDrawing();
        }
    }

    /** winSequence - when the user has correctly guessed the secret word */
    private void winSequence(){
        playAgain("Well done! You guessed " + information.getSecretWord() + " with " + information.getGuessesRemaining() +
                        " guesses left!\nWould you like to play another game of hangman?"
        );
    }
}

GUI - contains everything related to the GUI

package Hangman;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;
import static javax.swing.SwingUtilities.invokeLater;
import javax.swing.WindowConstants;

/** GUI - Deals with the GUI aspect of Hangman */
public class GUI {
    private JFrame frame;
    private JPanel contentPane;
    private JPanel containerPanel;
    private JTextField textField;
    private JLabel guessesRemainingLabel;
    private JLabel lettersGuessedLabel;
    private JLabel secretWordLabel;

    private static final Dimension FRAME_SIZE = new Dimension(250, 350);

    private Information information;
    private Logic logic;


    /** GUI constructor */
    public GUI(Logic logic, Information information){
        this.logic = logic;
        this.information = information;
    }


    void build(){
        invokeLater(
            () -> {
                buildFrame();
                buildContentPane();
                buildContainerPanel();
                buildLabel();
                buildInputPanel();
                displayFrame();
            }
        );
    }

    private void buildContentPane(){
        contentPane = new DrawingPanel(information);
        contentPane.setLayout(new BorderLayout());
        contentPane.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 0));
        frame.setContentPane(contentPane);
    }

    private void buildContainerPanel(){
        containerPanel = new JPanel(new BorderLayout());
        contentPane.add(BorderLayout.SOUTH, containerPanel);
    }

    private void buildFrame(){
        frame = new JFrame("Hangman");
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        frame.setMinimumSize(FRAME_SIZE);
    }

    private void buildInputPanel(){
        JButton checkButton = new JButton("Guess");
        GuessListener guessListener = new GuessListener();
        checkButton.addActionListener(guessListener);
        textField = new JTextField();
        textField.addActionListener(guessListener);

        JPanel inputPanel = new JPanel(new BorderLayout());
        inputPanel.add(BorderLayout.CENTER, textField);
        inputPanel.add(BorderLayout.EAST, checkButton);
        inputPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
        containerPanel.add(BorderLayout.SOUTH, inputPanel);
    }

    private void buildLabel(){
        JPanel labelPanel = new JPanel();
        labelPanel.setLayout(new BoxLayout(labelPanel, BoxLayout.PAGE_AXIS));
        guessesRemainingLabel = new JLabel("Guesses remaining: " + String.valueOf(Information.NUM_OF_GUESSES));
        lettersGuessedLabel = new JLabel("Already guessed: ");
        secretWordLabel = new JLabel();
        guessesRemainingLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
        lettersGuessedLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
        secretWordLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
        labelPanel.add(guessesRemainingLabel);
        labelPanel.add(lettersGuessedLabel);
        labelPanel.add(secretWordLabel);
        containerPanel.add(BorderLayout.NORTH, labelPanel);
    }

    private void displayFrame(){
        frame.setSize(FRAME_SIZE);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    /** drawGuessesRemaining - Shows the guesses remaining that the user has left */
    void drawGuessesRemaining(){
        invokeLater(() -> guessesRemainingLabel.setText("Guesses remaining: " +
                String.valueOf(information.getGuessesRemaining())));
    }

    /** drawLettersGuessed - Shows the letters guessed by the user */
    void drawLettersGuessed(){
        StringBuilder lettersBuilder = new StringBuilder();
        for (Character guessedLetter : information.getLettersGuessed()) {
            String s = guessedLetter + " ";
            lettersBuilder.append(s);
        }
        setLettersGuessedLabel(lettersBuilder.toString());
    }

    /** drawSecretWord - draws secretWord with dashes & letters that the user has correctly guessed so far */
    void drawSecretWord(){
        StringBuilder word = new StringBuilder();
        for(int i = 0; i < information.getLettersRevealed().length; i++){
            if (information.getLettersRevealed()[i]) {
                String letter = information.getSecretWord().charAt(i) + " ";
                word.append(letter);
            } else {
                word.append("_ ");
            }
        }
        setSecretWordLabel(word.toString());
    }

    /** playAgainDialog - asks the user if they want to play another game of hangman */
    boolean playAgainDialog(String message){
        int optionChosen = JOptionPane.showConfirmDialog(frame, message, "Play again?", JOptionPane.YES_NO_OPTION,
                JOptionPane.PLAIN_MESSAGE);
        return optionChosen == JOptionPane.YES_OPTION;
    }

    void updateDrawing(){
        contentPane.repaint();
    }


    // GETTERS
    JTextField getTextField(){
        return textField;
    }


    // SETTERS
    void setLettersGuessedLabel(String text){
        invokeLater(() -> lettersGuessedLabel.setText("Already guessed: " + text));
    }

    void setText(String text){
        invokeLater(() -> textField.setText(text));
    }

    void setSecretWordLabel(String text){
        invokeLater(() -> secretWordLabel.setText(text));
    }


    // LISTENERS
    /** GuessListener - deals with the user's guess */
    private class GuessListener implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent ev){
            logic.checkUserGuess(getTextField().getText());
        }
    }
}

DrawingPanel - used to draw the hangman figure

package Hangman;

import java.awt.BasicStroke;
import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.swing.JPanel;

/** DrawingPanel - Panel used to draw the hangman figure onto the frame */
public class DrawingPanel extends JPanel {
    // Constants to do with the hanging frame
    private static final int STARTING_X_POSITION = 55;
    private static final int STARTING_Y_POSITION = 30;
    private static final int BASE_Y_POSITION = STARTING_Y_POSITION + 200;
    private static final int WIRE_LENGTH = 40;

    // Constants to do with the hangman himself
    private static final int HEAD_RADIUS = 17;
    private static final int BODY_LENGTH = 50;
    private static final int BODY_X_POSITION = STARTING_X_POSITION + 120;
    private static final int ARM_LENGTH = 25;
    private static final int ARM_Y_POSITION = STARTING_Y_POSITION + WIRE_LENGTH + (2 * HEAD_RADIUS) + 20;
    private static final int LEG_LENGTH = 30;
    private static final int LEG_Y_POSITION = STARTING_Y_POSITION + WIRE_LENGTH + (2 * HEAD_RADIUS) + 50;

    private Information information;    // Solely used to get the number of guesses remaining


    /** DrawingPanel constructor */
    public DrawingPanel(Information information){
        this.information = information;
    }


    /** drawHead - draws the head onto the panel */
    private void drawHead(int xPosition, int yPosition, Graphics g){
        g.drawOval(xPosition, yPosition, 2 * HEAD_RADIUS, 2 * HEAD_RADIUS);
    }

    /** drawBody - draws the body onto the panel */
    private void drawBody(int xPosition, int yPosition, Graphics g){
        g.drawLine(xPosition, yPosition, xPosition, yPosition + BODY_LENGTH);
    }

    /** drawLeftArm - draws the left arm onto the panel */
    private void drawLeftArm(int xPosition, int yPosition, Graphics g){
        g.drawLine(xPosition, yPosition, xPosition - ARM_LENGTH, yPosition);
    }

    /** drawLeftLeg - draws the left leg onto the panel */
    private void drawLeftLeg(int xPosition, int yPosition, Graphics g){
        g.drawLine(xPosition, yPosition, xPosition - LEG_LENGTH, yPosition + LEG_LENGTH);
    }

    /** drawRightArm - draws the right arm onto the panel */
    private void drawRightArm(int xPosition, int yPosition, Graphics g){
        g.drawLine(xPosition, yPosition, xPosition + ARM_LENGTH, yPosition);
    }

    /** drawRightLeg - draws the right leg onto the panel */
    private void drawRightLeg(int xPosition, int yPosition, Graphics g){
        g.drawLine(xPosition, yPosition, xPosition + LEG_LENGTH, yPosition + LEG_LENGTH);
    }

    /** setUpDrawing - draws the wire frame of the hangman drawing when the user hasn't lost a guess yet */
    private void setUpDrawing(int xPosition, int yPosition, Graphics g){
        // Base of the frame
        Graphics2D g2D = (Graphics2D) g;
        g2D.setStroke(new BasicStroke(2));
        g2D.drawLine(xPosition - 15, BASE_Y_POSITION, xPosition + 50, BASE_Y_POSITION);

        // Frame support
        g2D.setStroke(new BasicStroke(1));
        g2D.drawLine(xPosition, yPosition, xPosition, BASE_Y_POSITION);
        g2D.drawLine(xPosition, yPosition + 20, xPosition + 20, yPosition);
        g2D.drawLine(xPosition, yPosition, BODY_X_POSITION, yPosition);

        // Wire from which the person hangs from
        g2D.drawLine(BODY_X_POSITION, yPosition, BODY_X_POSITION, yPosition + WIRE_LENGTH);
    }

    /** paintComponent - used to draw the hangman drawing onto the panel */
    @Override
    protected void paintComponent(Graphics g){
        super.paintComponent(g);
        switch (information.getGuessesRemaining()) {
            case 0: drawLeftArm(BODY_X_POSITION, ARM_Y_POSITION, g);

            case 1: drawRightArm(BODY_X_POSITION, ARM_Y_POSITION, g);

            case 2: drawLeftLeg(BODY_X_POSITION, LEG_Y_POSITION, g);

            case 3: drawRightLeg(BODY_X_POSITION, LEG_Y_POSITION, g);

            case 4: drawBody(BODY_X_POSITION, STARTING_Y_POSITION + WIRE_LENGTH + (2 * HEAD_RADIUS), g);

            case 5: drawHead(BODY_X_POSITION - HEAD_RADIUS, STARTING_Y_POSITION + WIRE_LENGTH, g);

            case 6: setUpDrawing(STARTING_X_POSITION, STARTING_Y_POSITION, g);
        }
    }
}

Information - class that deals with all the information obtained from the user

package Hangman;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

/** Information - Deals with all the information that the user has inputted into one game of Hangman */
public class Information {
    private static final int ALPHABET_SIZE = 26;
    static final int NUM_OF_GUESSES = 6;

    private String secretWord;
    private Set<Character> alphabet;
    private Set<Character> lettersGuessed;    // letters the user has guessed
    private boolean[] lettersRevealed;       // determines if the letter should be revealed or not
    private int guessesRemaining;

    private WordReader wordReader;


    /** Information constructor */
    public Information(WordReader wordReader){
        this.wordReader = wordReader;
    }


    /** addGuessedLetter - adds the guessed letter to lettersGuessed */
    void addGuessedLetter(char guessedLetter){
        lettersGuessed.add(guessedLetter);
    }

    /** createAlphabetSet - Creates the alphabet set that's used to ensure that the user's
     * guess is not a number nor a special character */
    void createAlphabetSet(){
        alphabet = new HashSet<>(ALPHABET_SIZE);
        for(Character c = 'a'; c<='z'; c++){
            alphabet.add(c);
        }
    }

    /** resetGameData - clears the information from the previous game of Hangman */
    void resetGameData(){
        guessesRemaining = NUM_OF_GUESSES;
        setSecretWord(wordReader.chooseSecretWord());
        lettersRevealed = new boolean[getSecretWord().length()];
        Arrays.fill(lettersRevealed, false);
        lettersGuessed = new HashSet<>(ALPHABET_SIZE);
    }

    /** revealLetter - causes a letter to be shown on the GUI (occurs only once the user has correctly guessed a letter
     * in secretWord) */
    void revealLetter(int index){
        lettersRevealed[index] = true;
    }


    // GETTERS
    public Set<Character> getAlphabet() {
        return alphabet;
    }

    public int getGuessesRemaining() {
        return guessesRemaining;
    }

    public Set<Character> getLettersGuessed() {
        return lettersGuessed;
    }

    public boolean[] getLettersRevealed() {
        return lettersRevealed;
    }

    String getSecretWord(){
        return secretWord;
    }

    // SETTERS
    public void setGuessesRemaining(int guessesRemaining) {
        this.guessesRemaining = guessesRemaining;
    }

    public void setSecretWord(String secretWord) {
        this.secretWord = secretWord;
    }
}

WordReader - class for loading and selecting a word for the user to guess

package Hangman;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

/** WordReader - Deals with loading and selecting a word from a file containing a collection of words */
public class WordReader {
    String[] wordList;

    /** chooseSecretWord - randomly selects a word from wordLst for the user to guess */
    String chooseSecretWord(){
        return wordList[(int)(Math.random() * wordList.length)];
    }

    /** readFile - read in the list of words from a specific location and saves the words in wordList */
    void readFile(String location){
        try(BufferedReader input = new BufferedReader(
                new InputStreamReader(this.getClass().getClassLoader().getResourceAsStream(location)))){
            wordList = input.readLine().split(" ");
        }catch(IOException ioException) {
            ioException.printStackTrace();
        }
    }
}
\$\endgroup\$
3
\$\begingroup\$

Comments

any feedback regarding the use of JavaDoc style comments is welcome.

The structure of your method comments is:

/** methodName - what method does */

But JavaDocs are actually structured like this:

/**
 * short description of what method does.
 * <p>
 * Longer description, special cases, etc.
 * 
 * @param paramName what param does
 * @param anotherParam what it does
 * @return what is returned
 */

The important parts are that the method name should not be part of the comment (it will be added automatically when generating a JavaDoc), and that there are annotations for method arguments and return values. You can also add additional annotations.

Many of your in-code comments are a bit redundant, for example lettersGuessed: letters the user has guessed.

Structure

  • Information is a very generic name. What information? It seems to just contain everything where you were unsure of where to put it: It contains the alphabet, the words, the user input, etc. I would split this up into different classes. I do see why you did this though: So that the GUI can get one object, from which it then can obtain state to be presented. Instead of creating one god object which contains all the information, you could instead add parameters to the GUI methods, and pass the information to them that way (eg drawLettersGuessed() -> drawLettersGuessed(Set<Character> guessedLetters)).
  • Your Logic class also does a bit too much: It handles the game state (updateSecretWord, isGuessValid, etc), but also the game loop (eg playAgain).
  • your GUI on the other hand is structured quite well, with the separation of DrawingPanel and the general GUI.

Misc

  • use the explicit private/public keywords instead of the default.
  • the WordReader always has to read a file, right? Otherwise it's useless. So I would add a constructor and read the file when constructing it.
  • size() != 0 can be written as !isEmpty().
  • instead of just crashing when the word file doesn't exist or is empty, an informative error message would be nice (Could not find file /path/to/file/filename and File /path/to/file/filename is empty or malformed).
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Great, thanks for this! I’d just like to ask a couple of things. I decided to use the default access modifier as it restricts the access to the methods, seeing as they're only really used within the package itself. Wouldn't it be unnecessary to make them public? Also, wouldn't using JavaDoc style comments in its full form make the code harder to read and navigate through? Thanks for your help! \$\endgroup\$ – Calculus5000 Mar 24 '15 at 0:08
  • 1
    \$\begingroup\$ @Calculus5000 If you use that reasoning, you shouldn't have public at all, but you do (whatever you do end up choosing, try to be internally consistent). Also, in that case I would use protected, as it's a bit stricter than package private. And no, I don't think that JavaDoc makes code harder to read. Most Java programmers are used to them, so they are actually easier to read. Also, the formatting is not only relevant for the source code, but also when generating the actual JavaDoc document, which can be shown in stuff like auto-complete messages. \$\endgroup\$ – tim Mar 24 '15 at 9:27

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.