5
\$\begingroup\$

I am creating a choose your own story type of game in Java. I am new to object oriented design and Java, and a text adventure game sounded like a fun way to try to implement some of the programming that I have been learning. The game tells a story through sections of text, and continues the story based on what choices the player makes.

For now, I am just making this for the console so that I can just worry about the code, but I hope to convert it over to an Android app at some point in the future as I think that would also be good experience. I am trying to implement object oriented design as best I can.

My two main concerns are:

  1. Whether I am implementing object oriented design well enough.
  2. Making the main loop for the game better. I feel like it could be done in a more efficient, concise way.

I am planning on putting the text portions of the game into JSON files in the next few days, but as of right now they are just in the code. I also plan to implement a way to save where the player is at, and I assume the main loop will need to be changed to allow for both of those.

Story class:

public class Story {
   String storyName;
   String storyText;

   public Story (String name, String text) {
       this.storyName = name;
       this.storyText = text;
   }

   public void printStory() {
       System.out.println(this.storyText);
       }

}

Choices class:

public class Choices {
        String storyBlock;
        String option;
        String choice;
        String result;

        public Choices (String storyBlock, String option, String choice, String result) {
            this.storyBlock = storyBlock;
            this.option = option;
            this.choice = choice;
            this.result = result;
        }

        public void printChoices () {
            System.out.println(this.option + ": " + this.choice);
            }

}

Game class, which is where I have the loop that handles the game:

import java.util.Collections;
import java.util.Scanner;
import java.util.ArrayList;

public class Game {

    public static void main(String[] args) {
        Scanner reader = new Scanner(System.in);
        String storySection = "one";
        String option;
        ArrayList<Choices> choiceRepo = new ArrayList<>();
        ArrayList<Story> storyRepo = new ArrayList<>();

        Story one = new Story("one", "The many rolling fields within Argonia are ideal for\n" +
                "raising sheep, which is what my family has done for generations. In my grandfather's\n" +
                "day, our village was known all round for having some of the best sheep in the land.\n" +
                "It's not anymore. For years we've barely had enough sheep to help keep our family fe\n " +
                "and clothed. It's been tough ever since the Ostians invaded our land and pillaged our towns\n" +
                "and villages. After years of teetering on the edge of warfare, our countries finally clashed.\n" +
                "We lost, obviously. Ostia is a much rougher country than ours, and it didn't take long for our\n" +
                "peace seeking king to surrender his country. Though naive, I have to respect him for doing\n" +
                "what he thought would save the most lives. And maybe it did save the most lives, but at the\n" +
                "steep price of our freedom and well being. My village has struggled, but it hasn't been as\n" +
                "bad as some of the villages that aren't as far out in the hills as we are. I've heard rumors that\n" +
                "the Dark Hordes have taken to ravaging the countryside again, though, so it's only a matter\n" +
                "of time until something very bad happens.\n" +
                "The thought of them coming to my village and having to defend it makes me...\n");

        Story twoA = new Story("twoA", "...frightened. The Dark Horde is notorious for their savagery,\n" +
                " and the king lets them loose to keep the populous of our country in fear of him. Their warriors\n" +
                "are cold blooded killers, and their mages are even worse. Just thinking of coming into contact\n" +
                "with them makes me stir from the place I have been sitting on the hill. I look up and after a\n" +
                "quick count I realize that Pud has gone missing. That fluffer, always going over one hill or\n" +
                "another. I just hope he hasn't gotten lost in the woods again. I might have to chase off another\n" +
                "bear.");

        Story twoB = new Story("twoB", "...faintly excited at the notion of getting to fight. Though\n" +
                " the Dark Horde is notorious for their savagery, I would almost welcome a chance to fight some\n" +
                "of them. The king lets them loose to keep the populous of our country in fear of him.  Their\n" +
                "warriors are cold blooded killers, and their mages are even worse. This makes me concerned for\n" +
                "my family and friends, but inside myself I can feel a craving for the adventure they would\n" +
                "bring. While tending the flock I have fought bears, cougars, and even run into some roving hob\n" +
                "goblins. My job has been to keep the flock safe, and I am very capable. And it also would give\n" +
                "me a chance to practice the sword skills my father has been teaching me since he came back from\n" +
                "the war that we lost. After a quick count realize that Pud has gone missing. That fluffer,\n" +
                "always going over one hill or another. I just hope he hasn't gotten lost in the woods again.");

        Choices ch1ChoiceA = new Choices("one", "A",
                "frightened", "twoA");
        Choices ch1ChoiceB = new Choices("one", "B",
                "faintly excited at the notion of getting to fight", "twoB");
        Choices ch2ChoiceA = new Choices("twoA", "A", "Continue", "three");
        Choices ch2ChoiceB = new Choices("twoB", "A", "Continue","three");

        Collections.addAll(storyRepo, one, twoA, twoB);
        Collections.addAll(choiceRepo, ch1ChoiceA, ch1ChoiceB, ch2ChoiceA,ch2ChoiceB);




        // main loop for the game
        while (true) {
            ArrayList<Choices> specificChoices = new ArrayList<>();

            for (Story story : storyRepo) {
                if (story.storyName.equals(storySection)) {
                    story.printStory();
                    }
                }

            for (Choices choice : choiceRepo) {
                if (choice.storyBlock.equals(storySection)) {
                    choice.printChoices();
                    specificChoices.add(choice);
                }
            }

            System.out.println("Choose a response: ");
            option = reader.nextLine();
            while (!option.equals("A") &&
                    !option.equals("B") &&
                    !option.equals("Exit")) {
                System.out.println("Invalid input. Enter A, B or Exit");
                option = reader.nextLine();
            }

            if (option.equals("Exit")) {
                break;
            }

            for (Choices specificChoice : specificChoices) {
                if (specificChoice.option.equals(option)) {
                    storySection = specificChoice.result;
                }
            }

        }
    }

}
\$\endgroup\$
  • 2
    \$\begingroup\$ Welcome to Code Review! I hope you get great answers. \$\endgroup\$ – Phrancis Nov 1 '17 at 3:04
4
\$\begingroup\$

I somewhat like your objects, but I don't like your implementations.

Ignoring your code, you have a Story made up of a series of parts. You might call the parts Pages or Sections.

Each of those parts has one or more possible successors. You will want to balance showing a huge blob of text versus having single-successor parts ("page breaks").

You have a class called Choices. This is IMO a mistake. Your parts have a variable number of attached optional transitions. These are your choices. But each transition can and should stand alone. These are links to the next page, each of which is a Link (or Choice if you insist, but that's not much of a noun -- it sounds too verb-ish).

I would suggest that you practice a little inversion of control and write your Story class to accept a decision-maker that will determine which links get followed. This can be as simple as a list of values.

The other half of the equation is how to store the story. I would suggest that you invest a little time and effort into a simple text parser. Something like this:

start: It was a dark       # Leader: text. A tag + colon starts text
and stormy night ...       # Text continues until line of "====="
=====
clifford: Paul Clifford    # target: Prompt text. Each line is a link.
wrinkle: A Wrinkle in Time # as many as you need
snoopy: Snoopy's Novel     # Until line of =====
=====
clifford: ... the rain fell in torrents — except at occasional
intervals, when it was checked by a violent gust of wind which swept
up the streets (for it is in London that our scene lies), rattling
along the housetops, and fiercely agitating the scanty flame of the
lamps that struggled against the darkness.
=====
END                        # Special link "END" means end-of-text
=====
wrinkle: In her attic bedroom Margaret Murry, wrapped in an old
patchwork quilt, sat on the foot of her bed and watched the trees
tossing in the frenzied lashing of the wind. Behind the trees clouds
scudded frantically across the sky. Every few moments the moon ripped
through them, creating wraithlike shadows that raced along the ground.
=====
END
=====
snoopy: Suddenly, a shot rang out! A door slammed. The maid screamed.
Suddenly, a pirate ship appeared on the horizon! While millions of
people were starving, the king lived in luxury. Meanwhile, on a small
farm in Kansas, a boy was growing up.
=====
END
=====

With that text structure in mind, it seems obvious that each part will have an isEnd() boolean method, and some mechanism for selecting the tag of the next part. Something like chooseLink("Snoopy's Novel") which can return a tag. The tag (string or integer) can be used by the Story to select the next part.

Thus, I can pass in a simple class that has a getChoice() method that returns some appropriate value (single letter of the alphabet? integer?). Creating a new Story(LinkChooser) and calling story.printOn(PrintWriter) will cause something like:

tag = this.start_tag;
page = this.pageFromTag(tag);

while (!page.isEnd()) {
    page.printOn(print_writer);
    tag = this.link_chooser.getChoice();
    page = this.pageFromTag(tag);
}

In this design, your Story has-a collection of Page objects (or Sections, or Parts, or whatever) that are created while parsing the input file. The Story knows the starting tag either because it was the first in the file, or because it was explicitly specified in the file, or because it is a parameter to some method.

The Story knows how to convert a tag into a Page by some kind of lookup: could be an array, could be a map, could be ... whatever you like.

Finally, the Story knows how to print itself on an output by choosing links and telling the various pages to print themselves. I have suggested the LinkChooser as a constructor parameter, but you could certainly configure it separately, or pass it as a parameter to the printOn method. (I don't recommend this.)

The Page objects hold text, and they hold links to other Page objects using some kind of tag type, as mentioned above. They hold text labels to be used when choosing which link to follow. They know how to translate an input choice into a tag, although this may be a coupling flaw (you could refactor in an input-to-page-link translator, but for now a string to tag method seems easy).

As a special case, the Page objects know if they are a valid END page. (If you wish to continue past an optional end, that seems doable without too much hassle.)

Finally, the Page knows how to print its text on an output stream.

\$\endgroup\$
  • \$\begingroup\$ Wow, thanks for the detailed response! You gave me a lot to think about and figure out how to implement. The flow that you proposed makes sense. A few questions: would the decision maker be a class of it's own? Are you referring to this when you say LinkChooser, or is that something that would be in the Story class? Also, do you recommend I store the text that way or would something like JSON be better? Thanks for your response! \$\endgroup\$ – Jacobra Nov 1 '17 at 15:08
  • \$\begingroup\$ Yes, the decision maker would stand alone. That way, you could have one decision maker that interacted with the user, and a different decision maker that gave pre-programmed responses for testing. And yes, that's what a LinkChooser is - some class that chooses links. As for how to store the text, I recommend a text format just because it will be easier for you to edit. JSON is fundamentally a "programming language oriented" text format. For writing a story, you need a "written word oriented" text format. Think about markdown versus HTML. Markdown is easier to compose and edit. \$\endgroup\$ – Austin Hastings Nov 1 '17 at 23:38

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.