6
\$\begingroup\$

This is actually my version of a project I'm giving to my class of High School juniors. I'm not the greatest programmer and also a new teacher so I was hoping someone could check out this code and give me any suggestions or advice they see on it.

Please just keep in mind that it's written to be at a High School student level, not a college or professional level.

You have been commissioned to create a Mad Libs game. Mad Libs is a phrasal template word game which consists of prompting a player for a list of words to substitute for blanks in a story before reading aloud. The game will present the player with a welcome screen which allows them to select a story from a list of available stories; they will also be allowed the option for the computer to randomly choose a story for them if they would like.

After selecting a story, the player will be asked to enter a series of words to substitute for “blanks” (placeholders) in the story (i.e. “give me a noun”, “give me a verb”, “give me a color”, etc). The player’s responses will be saved and recalled later once all the words have been entered for that story. Finally, the story will be displayed, substituting the “blanks” with the responses the player entered in earlier.

In order to support the complexity of having multiple Mad Libs stories along with keeping track of their placeholders, we will be creating this using object oriented programming (OOP) in Java. You will implement the following Java classes:

  • MadLibsStory - This will hold an entire Mad Libs story, along with a list of placeholders.
  • MadLibsPlaceholder - This will hold both the placeholder’s type (i.e. noun, adjective, verb, etc) and the word (the response from the player)
  • MadLibsGame - This will be the game’s “driver” class that will set up the list of stories, display them to the player, handle the user’s selection, and ultimately display the story.

A utility class called MadLibsStoryBuilder, along with a Main class, have been provided to you. There are a number of Mad Lib stories that have been saved in text files as well. The Main class will use the MadLibsStoryBuilder class to read the contents of the text files, create an array of MadLibsStory objects, and then create a new instance of MadLibsGame using this array. (Note, this has been provided to you and you will not have to create this part).

In order to implement this according to the description provided in the previous section, your game will need to support the following:

  1. Display a welcome message.

  2. Display a list of all the stories available to the player. a. Every story listed will need to display the title and the choice number (The number the player will need to enter to select that story) starting with number 1.

  3. Ask the player to choose a story using the stories’ choice number.

  4. Allow the player to select a random story by entering the number 0.

  5. Once the player selects a story, it should begin by asking them for all the placeholder words.

  6. Once the player finishes entering all the placeholder words, it needs to display the story using the responses the player entered in.

  7. After displaying the story, it should go back to step 2.

Main

import java.io.File;

// Class supplied to students. Shouldn't be modified.
public class Main {

    public static void main(String[] args) {
        File directory = new File("stories/");
        String[] directoryList = directory.list();
        MadLibsStory[] stories = new MadLibsStory[directoryList.length];
        for (int i = 0; i < stories.length; i++) {
            stories[i] = MadLibsStoryBuilder.makeStoryFromFile("stories/" + directoryList[i]);
        }
        MadLibsGame ml = new MadLibsGame(stories);
        ml.play();
    }
}

MadLibsStoryBuilder

import java.io.BufferedReader;
import java.io.FileReader;
import java.util.ArrayList;

// Class supplied to students. Shouldn't be modified.
public class MadLibsStoryBuilder {

    /**
     * Precondition: The format of the file
     * @param fileName
     * @return
     */
    public static MadLibsStory makeStoryFromFile(String fileName) {
        StringBuilder sb = new StringBuilder();
        String[] storyList;
        ArrayList<MadLibsPlaceholder> placeholderList =  new ArrayList<>();

        try {
            BufferedReader br = new BufferedReader(new FileReader(fileName));
            String line = br.readLine();
            while(line != null) {
                sb.append(line).append('\n');
                line = br.readLine();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        String storyStr = new String(sb);
        storyList = storyStr.split("\\[.*?\\]");

        while(storyStr.contains("[")) {

            int i = storyStr.indexOf("[");
            int j = storyStr.indexOf("]");
            String placeholderType = storyStr.substring(i + 1, j);
            MadLibsPlaceholder placeholder = new MadLibsPlaceholder(placeholderType);
            placeholderList.add(placeholder);
            storyStr = storyStr.substring(j + 1);
        }

        String title = fileName.substring(fileName.indexOf("/") + 1, fileName.indexOf(".txt"));
        MadLibsPlaceholder[] placeholderArray = new MadLibsPlaceholder[placeholderList.size()];
        placeholderArray = placeholderList.toArray(placeholderArray);

        return new MadLibsStory(title, storyList, placeholderArray);
    }
}

MadLibsPlaceholder

public class MadLibsPlaceholder {

    private String type;
    private String word;

    public MadLibsPlaceholder() {
        type = "NOUN";
        word = Constants.PLACE_HOLDER_PHRASE;
    }

    public MadLibsPlaceholder(String type) {
        this.type = type;
        word = Constants.PLACE_HOLDER_PHRASE;
    }

    public String getType() {
        return type;
    }

    public String getWord() {
        return word;
    }

    public void setWord(String word) {
        this.word = word;
    }

    public String toString() {

        return String.format("%s (%s)", word, type);

        //Same just without the formating
        //return word + " (" + type + ")";
    }
}

MadeLibsStory

// Change this file


public class MadLibsStory {

    private final String title;
    private final String[] story;
    private final MadLibsPlaceholder[] placeholders;
    private int placeholderIndex;

    public MadLibsStory() {
        title = "";
        story = new String[0];
        placeholders = new MadLibsPlaceholder[0];
    }
    public MadLibsStory(String title, String[] story, MadLibsPlaceholder[] placeholders) {
        this.title = title;
        this.story = story;
        this.placeholders = placeholders;
    }

    public String getTitle() {
        return title;
    }

    public MadLibsPlaceholder getNextPlaceholder()
    {
        // The current index is held in a variable, and is incremented each time.s
        if (placeholderIndex >= placeholders.length) {
            placeholderIndex = 0;
            return null;
        }
        return placeholders[placeholderIndex++];
    }

    public String toString()
    {

        StringBuilder finishedStory = new StringBuilder(title + "\n\n");

        // Since the story does not end with a placeholder, do not do last item in the loop
        for (int i = 0; i < story.length - 1; i++) {

            System.out.println(story[i] + "/");
            finishedStory.append(story[i]);
            finishedStory.append(placeholders[i].getWord());
        }

        // Append the last part of the story
        finishedStory.append(story[story.length - 1]);

        return finishedStory.toString();
    }
}

MadLibsGame

import java.util.Scanner;
public class MadLibsGame {

    private final MadLibsStory[] stories;
    public MadLibsGame() {
        this.stories = new MadLibsStory[0];
    }

    public MadLibsGame(MadLibsStory[] stories) {
        this.stories = stories;
    }

    public MadLibsStory getRandomStory() {

        // Generate a random index between 0 and the story's length. stories.length is exclusive. Then return it
        int index = (int) (Math.random() * stories.length);
        return stories[index];
    }

    // Generate the listing string, with index being 1 over i (so it starts at 1 not 0.)
    public void listStories() {

        for (int i = 0; i < stories.length; i++)
            System.out.printf(("%d. %s%n"), (i+1), stories[i].getTitle());
    }

    public void play() {

        String selection;

        System.out.println("Welcome to Mad Libs, the most popular phrasal word template game.\n");
        Scanner scan = new Scanner(System.in);

        do {
            int index = -1;

            // Print menu and take user input
            printMenu();
            selection = scan.nextLine();

            // Allow user to exit
            if (selection.equalsIgnoreCase("e")) {
                break;
            }

            // Handle input
            try {
                index = Integer.parseInt(selection);

            } catch (NumberFormatException fe) {

                System.out.println("Not a valid input, a number was expected. Please try again");
            } finally {

                if (boundsCheck(index)) {

                    // Print random story or user defined story
                    MadLibsStory story = (index == 0) ? getRandomStory() : getStoryAtIndex(index-1) ;
                    assert story != null;
                    fillStory(story, scan);
                    System.out.println(story);

                } else {

                    System.out.println("Please choose a number 1-" + stories.length);
                }
            }

        } while (true);
    }

    // Helper functionss
    private void printMenu(){

        System.out.println("******************************\n");
        System.out.println("SELECT A STORY (to pick a random story enter '0', to exit enter 'E')\n");

        listStories();

        System.out.print("\nSelection: ");
    }
    private boolean boundsCheck(int i){
        return i >= 0 && i <= stories.length;
    }
    private MadLibsStory getStoryAtIndex(int index) {

        // If index is less than 0 or greater/equal to stories.length, return null
        if (index < 0 || index >= stories.length) return null;

        return stories[index];
    }
    private void fillStory(MadLibsStory story, Scanner scan){

        MadLibsPlaceholder placeholder = story.getNextPlaceholder();

        // While there is a next placeholder, keep replacing them.
        while (placeholder != null) {

            System.out.println("Give me a " + placeholder.getType());

            String newWord = scan.nextLine();
            placeholder.setWord(newWord);
            placeholder = story.getNextPlaceholder();
        }
    }
}

Constants - I actually thought I was going to use this class more than I did. Probably don't need it tbh.

public class Constants {

    public static final String PLACE_HOLDER_PHRASE = "[BLANK]";

}
\$\endgroup\$
4
  • \$\begingroup\$ What are the students supposed to do in MadLibsStory? \$\endgroup\$
    – Eric Stein
    Jun 14 at 3:03
  • \$\begingroup\$ I believe what @EricStein is asking is what functionality are the students supposed to add to the MadLibsStory class. That should probably be in the comments at the top of the file. \$\endgroup\$
    – pacmaninbw
    Jun 14 at 11:59
  • \$\begingroup\$ @pacmaninbw I added most of the instructions that I gave to the students as well. Hopefully it better explains what the task was. \$\endgroup\$
    – Sean Riley
    Jun 14 at 22:50
  • \$\begingroup\$ Could you please show an input template file? \$\endgroup\$
    – Reinderien
    Jun 15 at 11:54

1 Answer 1

1
\$\begingroup\$

Please just keep in mind that it's written to be at a High School student level, not a college or professional level.

Fine, but: I'll also claim that

  • whereas high school students are inexperienced, they are not stupid
  • professional code (or at least my attempt at it) is much shorter and easier to understand, even if it does invoke some libraries with hidden complexity
  • if you really want "simple at the cost of everything else", Java is probably not the language of choice.

So my proposal is going to be, vaguely, best-practices modern Java that does not shy away from streams.

Specifics:

File.list() has a newer, Path-oriented replacement walk that avoids the need for your "stories/" + concatenation.

Consider moving the directory-walking code to a wrapper method in your builder class.

Your makeStoryFromFile has a placeholder JavaDoc comment that should either be filled out or deleted. In that function, your BufferedReader should go away, and your manual parsing is probably better-represented by one regex with named capturing groups.

Your placeholder class should be immutable, and should not store word. The whole story can be entirely modelled by a list of placeholder instances if those instances contain a prefix, type and suffix, with an on-the-fly substitution method to fill the type when needed. You ask:

could you explain what you're returning in the fillStory method?

This method

  1. Starts a stream() operation on the list of story placeholders;
  2. for every placeholder, calls fillPlaceholder which in turn prompts the user, gets input, substitutes the phrase and returns the substituted string;
  3. joins all of the filled strings from each placeholder together into one string; and
  4. returns that string.

I don't see much need for your constants class so I have not included it in my suggested code.

Your story class holds state (your placeholderIndex) that creates more problems than it solves. The class should be immutable: don't store information on where the iteration is currently. And the iteration over placeholders should be stateless from a class member perspective.

Rather than Math.random prefer Random which has direct integer support.

Prefer a for-each loop in listStories. You ask:

In the 'listStories' method, you used a foreach in place of a for. Is that a preference or are foreach loops more efficient than for loops?

Efficiency is debatable and doesn't really factor in here one way or the other; at this scale performance doesn't matter. Instead, legibility and maintainability take priority, and the for-each style is slightly more legible as it implies the length check instead of requiring it explicitly.

One way of breaking up your main input loop into subroutines is to throw and catch application-specific exceptions upon input validation failure. You ask:

you created a new class to handle exceptions, is that something that is typically done for all programs?

It's a pretty common pattern, and defining, throwing and catching application-specific exception classes is preferred over (a) reusing in-built exception types or (b) following non-exception error-handling patterns.

Suggested

public class InputException extends Exception {
    public InputException(String message) {
        super(message);
    }

    public InputException(String message, Throwable cause) {
        super(message, cause);
    }
}
import java.util.List;
import java.util.Random;
import java.util.Scanner;
import java.util.stream.Collectors;

import static java.lang.System.out;

public class MadLibsGame {
    private final Random rand = new Random();
    private final Scanner scan = new Scanner(System.in);
    private final List<MadLibsStory> stories;

    public MadLibsGame(List<MadLibsStory> stories) {
        this.stories = stories;
    }

    public void play() {
        out.println("Welcome to Mad Libs, the most popular phrasal word template game.");

        while (true) {
            printMenu();
            String selection = scan.nextLine();
            if (selection.equalsIgnoreCase("e"))
                break;

            MadLibsStory story;
            try {
                int index = parseIndex(selection);
                story = chooseStory(index);
            }
            catch (InputException e) {
                out.println(e.getMessage());
                continue;
            }

            out.println(fillStory(story));
        }
    }

    private void printMenu() {
        out.println();
        out.println("******************************");
        out.println("SELECT A STORY (to pick a random story enter '0', to exit enter 'E')");
        listStories();
        out.print("Selection: ");
    }

    public void listStories() {
        int i = 1;
        for (MadLibsStory story: stories) {
            out.printf("%d. %s%n", i, story.title());
            i++;
        }
    }

    private static int parseIndex(String selection) throws InputException {
        try {
            return Integer.parseInt(selection);
        } catch (NumberFormatException fe) {
            throw new InputException(
                "Not a valid input, a number was expected. Please try again",
                fe);
        }
    }

    private MadLibsStory chooseStory(int index) throws InputException {
        if (index == 0)
            return getRandomStory();
        if (index > 0 && index <= stories.size())
            return stories.get(index - 1);
        throw new InputException(
            String.format(
                "Please choose a number 1-%d%n", stories.size()
            )
        );
    }

    public MadLibsStory getRandomStory() {
        int index = rand.nextInt(stories.size());
        return stories.get(index);
    }

    private String fillPlaceholder(MadLibsPlaceholder placeholder) {
        out.printf("Give me a %s: ", placeholder.type());
        String newWord = scan.nextLine();
        return placeholder.fill(newWord);
    }

    private String fillStory(MadLibsStory story) {
        return story.placeholders()
            .stream()
            .map(this::fillPlaceholder)
            .collect(Collectors.joining());
    }
}
public record MadLibsPlaceholder(
    String prefix, String type, String suffix
) {
    public String fill(String word) {
        return prefix + word + suffix;
    }
}
import java.util.List;

public record MadLibsStory(
    String title,
    List<MadLibsPlaceholder> placeholders
) {}
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

// Class supplied to students. Shouldn't be modified.
public class MadLibsStoryBuilder {
    private final Pattern placeholderPat = Pattern.compile(
        """
(?xs)            # Enable these comments, dot matches newlines
(?<prefix>       # Named capturing group
    [^\\[\\]]*   # As many non-brackets as possible
)
\\[              # Open bracket
(?<type>         # Named capturing group
    [^\\[\\]]*   # As many non-brackets as possible
)
\\]              # Close bracket
(?<suffix>       # Named capturing group
    [^\\[\\]]*   # As many non-brackets as possible
)
""");

    public List<MadLibsStory> makeStoriesFromDir(Path dir) throws IOException {
        return Files
            .walk(dir)
            .filter(path -> path.toFile().isFile())
            .map(this::makeStoryFromFile)
            .toList();
    }

    public MadLibsStory makeStoryFromFile(Path filePath) {
        String name = filePath.toFile().getName(),
            title = name.substring(0, name.lastIndexOf('.'));

        String content;
        try {
            content = Files.readString(filePath);
        }
        catch (IOException e) {
            throw new RuntimeException(e);
        }

        return new MadLibsStory(title, parseStory(content));
    }

    private List<MadLibsPlaceholder> parseStory(String content) {
        Matcher matcher = placeholderPat.matcher(content);
        return matcher
            .results()
            .map(result -> matchToPlaceholder(matcher))
            .toList();
    }

    private static MadLibsPlaceholder matchToPlaceholder(Matcher matcher) {
        return new MadLibsPlaceholder(
            matcher.group("prefix"),
            matcher.group("type"),
            matcher.group("suffix")
        );
    }
}
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;

// Class supplied to students. Shouldn't be modified.
public class Main {
    public static void main(String[] args) throws IOException {
        MadLibsStoryBuilder builder = new MadLibsStoryBuilder();
        List<MadLibsStory> stories = builder.makeStoriesFromDir(Path.of("stories"));
        MadLibsGame game = new MadLibsGame(stories);
        game.play();
    }
}
\$\endgroup\$
3
  • \$\begingroup\$ Thanks for the reply, I had a few questions about your revisions if you don't mind...In the 'listStories' method, you used a foreach in place of a for. Is that a preference or are foreach loops more efficient than for loops? Also, I see you created a new class to handle exceptions, is that something that is typically done for all programs? Lastly, could you explain what your returning in the 'fillStory' method? Or what to look up to read about that? \$\endgroup\$
    – Sean Riley
    Jun 16 at 13:37
  • 1
    \$\begingroup\$ Also, I really liked several of the suggestions like using 'Random' instead of Math.random. I know they aren't dumb, they just follow a state guided AP curriculum and there's stuff that it doesn't require, like immutable classes and Exception Handling classes. Though that does give me an idea what to go over with them for their Senior year, so I really appreciated it. That constants class was a bit silly, wasn't it? I almost got rid of it, but I wanted to show them that they can make additional helper classes. The Try/Catch class is a great replacement though. \$\endgroup\$
    – Sean Riley
    Jun 16 at 13:47
  • 1
    \$\begingroup\$ @SeanRiley Edited for your questions; feel free to ask others. \$\endgroup\$
    – Reinderien
    Jun 16 at 14:36

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.