In this task we will create the necessary data structures for a choose your own adventure story.
Background
Choose your own adventure (CYOA) is a genre of storytelling where the reader is able to make their own choices about how to proceed through the story. Instead of progressing linearly from beginning to end, a CYOA book contains a tree-like structure of branching paths that the reader must navigate. Typically there are many paths that lead to unhappy endings, and only few that lead to a satisfactory conclusion. It is up to the reader to choose their path wisely.
It is common for CYOA stories to include an inventory system, where readers acquire or lose items (swords, shields, allies, etc) or status effects (like ailments, wounds, etc) as they progress through the story. Some paths will only be available to the reader if they are able to acquire certain items or status effects (e.g. it may be impossible to win the final battle without first acquiring allies to fight along side you). Conversely, the reader may be prevented from taking certain paths if they have certain items or status effects (e.g. it may not be possible to fight effectively while fatigued or poisoned).
Task Details
In this task you need to implement the following classes:
- Choice, for storing a single choice that a reader can make.
- Page, for storing a single page in a book.
- Book, for storing all of the pages for a single book.
The Choice class
Implement the Choice class so that it has the following public methods:
- A constructor that accepts the following parameters (in the following order):
- An String containing the id of the page that the reader will be taken to if they make this choice.
- A String containing a text of the choice (e.g. “Draw your sword” or “Make a hasty retreat“).
- A getText method, which takes no parameters and returns the text of the choice (e.g. “Draw your sword” or “Make a hasty retreat“).
- A getDestinationId method, which takes no parameters and returns the id (as a String) of the page that the reader will navigate to if they make this choice.
All of these methods should be public and not static. There should not be any other public methods or properties, but you are welcome to add any private properties and methods that you feel are necessary/helpful.
The Page class
Implement the Page class so that it has the following public methods:
- A constructor that accepts the following parameters (in the following order):
- a unique id for the page (as a String)
- the text of the page (as a String)
- A getId method, which accepts no input, and returns the unique id of the page (as a String).
- A getText method, which accepts no input, and returns the text of the page (as a String).
- A withChoice method that accepts an instance of Choice. Calling this method will cause the given choice to be added as a possible choice that the reader can make when they reach this page (i.e. so that it would be included in the results returned by the getChoices method described below). This method should return the current Page.
- An adds method, that accepts an item/state (as a String) that should be added to the reader when they reach this page (see the getUpdatedState method described below). This method may be called multiple times, in which case all of the given items/states will be added to the reader when they reach this page. This method should return the current Page.
- A removes method, that accepts an item/state (as a String) that should be removed from the reader when they reach this page (see the getUpdatedState method described below). This method may be called multiple times, in which case all of the given items/states will be removed from the reader when they reach this page. This method should return the current Page.
- A requires method, that accepts an item/state (as a String) that the reader should have before they can access this page (see the isAvailable method described below). This method may be called multiple times, in which case the reader will need to have all of the given items/states before they can access the page. This method should return the current Page.
- An excludedBy method, that accepts a state (as a String) that the reader should not have in their inventory if they want to access this page (see the isAvailable method described below). This method may be called multiple times, in which case the reader will be excluded from the page if they have any of the given items/states. This method should return the current Page.
- An isAvailable method, which takes a List of Strings containing the current items/states that the reader possesses, and returns true if the page is available to the reader or false if not. The availability of the page depends on prior calls to the requires and excludedBy methods (see above).
- AngetUpdatedStates method, which takes a List of Strings containing the items/states that the reader possesses before navigating to this page, and returns a new List of Strings containing the items/states that the reader possesses after navigating to this page. Check the description of the adds and removes methods above for more information about how navigating to a page should update the items/states that a reader possesses. Please note:
- The list that is returned by this method should be a completely seperate variable from the list that is passed into it. The list that is passed into it should remain unchanged after calling this method (i.e. this method should not have any side-effects).
- The list that is returned by this method should not contain any duplicate items or statuses; every item in the list should be unique. In other words, calling this method should not add items/states to the list if those items/states are already in the list.
All of these methods should be public and not static. There should not be any other public methods or properties, but you are welcome to add any private properties and methods that you feel are necessary/helpful.
The Book class
Implement the Book class so that it has the following methods:
- A constructor that accepts no parameters, and results in a book that has no pages.
- An addPage method, that takes an instance of Page as the only parameter, and returns true if the given page has been added to the book, or false if not. It should not attempt to add a page if the book already contains a page that has the same id.
- A getPage method, that takes a page id (i.e. a String) and returns the corresponding Page. If there is no page with the given id then the method should return null.
- A getPageCount method, that takes no parameters, and returns an int representing the number of pages in the book.
- An isAvailable method, that accepts a page id (as a String) and a list of items/states that the reader currently has (as a List of String). The method should return true if the page corresponding to the given id is available to the reader, or false if not. If the book does not contain a page with the given id, then this method should return false. If the book does contain a page with the given id, then it’s availability will depend on what items/states the reader has, and what items/states the page requires or is excluded by.
All of these methods should be public and not static. There should not be any other public methods or properties, but you are welcome to add any private properties and methods that you feel are necessary/helpful.
Examples
Below is an example of what the output of the Runner should look like if you edit is that the reader is navigating to the “wanderingHub” without any items/states:
You are just barely able to see your way forward. Around the corner you hear someone hissing furiously. “My presccciouusss! My preccousss is losssstt!”
Based on the reader’s items/states, the available choices are:
– Turn back and look for another way (goes to stream)
– Sneak forwards, towards the voice (goes to meetGollum)
After navigating to this page, the reader’s items/states are now []
This is the expected output if the reader navigates to the “wanderingHub” page with the “sting” item/state
You are just barely able to see your way forward. Around the corner you hear someone hissing furiously. “My presccciouusss! My preccousss is losssstt!”
Based on the reader’s items/states, the available choices are:
– Turn back and look for another way (goes to stream)
– Sneak forwards, towards the voice (goes to meetGollum)
– Draw your sword before proceeding (goes to confrontGollum)
After navigating to this page, the reader’s items/states are now [sting]
This is the expected output if the reader navigates to the “wanderingHub” page with the “cold” item/state.
You are just barely able to see your way forward. Around the corner you hear someone hissing furiously. “My presccciouusss! My preccousss is losssstt!”
Based on the reader’s items/states, the available choices are:
– Sneak forwards, towards the voice (goes to meetGollum)
After navigating to this page, the reader’s items/states are now [cold]
This is the expected output if the reader navigates to the “pickUpRing” page with no items/states.
Your numb fingers fish through the water, and you clumsily pick up what appears to be a small golden ring. It feels oddly warm and inviting.
Based on the reader’s items/states, the available choices are:
– Put the ring on (goes to putOnRing)
– Pocket the ring and get out of the stream (goes to moveOnFromRing1)
After navigating to this page, the reader’s items/states are now [ringOfPower, freezing]
This is the expected output if the reader navigates to the “moveOnFromRing1” page with the “freezing” and “invisible” states.
You reluctantly pocket the ring.
Based on the reader’s items/states, the available choices are:
– Stumble onwards (goes to wanderingHub)
After navigating to this page, the reader’s items/states are now [freezing]
Hints
Expand
In the Page class, the withChoice, adds, removes, requires and excludedBy methods should all return the current Page. This is as easy as ensuring the method has the right return type, and ensuring the method body ends with the statement return this ;
The idea here is that since each method returns the current page, you can chain together multiple method calls to efficently build a page. You can see this in action in the loadBook method of the Runner class, which contains code like:
book.addPage(
new Page(“stream”, “You stumble into a cold stream. It is shockingly cold, but a glint below you catches your eye.”)
.adds(“cold”)
.excludedBy(“cold”)
.withChoice(new Choice(“pickUpRing”, “Ignore the freezing cold and look closer”))
.withChoice(new Choice(“wanderingHub”, “Get out of the stream before you freeze”))
);
Neat, huh?
Expand
A Page can have any number of choices, can add or remove any number of items/states, and can require or be excluded by any number of items/states. So these all need to be stored in resizable lists. The lists should all initially be empty when the page is first created, and only grow as calls to withChoice, adds, removes etc are made.
Expand
In the Book class it makes sense to put all of the pages into a big array or list. When it comes time to retrieve a page (i.e. with getPage) you’ll need to scroll through this list to find the page that has the right id. Remember you can’t reliably use == to compare strings, but you should instead use string1.equals(string2).
Expand
Your code does not need to make any distinction between items (like weapons or potions) or states (like fatigued or inspired). Each item or state is simply a unique string (e.g. “kiteShield”, “magicSword”, “wounded”, “invisible”) that will be in the reader’s inventory if they have that particular item or state, and removed from their inventory if not.
Expand
The List interface (and consequently also ArrayList and LinkedList) defines several methods that you will find useful for this task: contains, indexOf, and remove. Make sure you carefully read the lessons on collection classes so you know how to use these methods.
In this task the Runner is give:
import java.util.List ;
import java.util.ArrayList ;
public class Runner {
public static void main(String[] args) {
Book book = loadBook() ;
List<String> states = new ArrayList<String>() ;
//Try altering what items/states are in the reader’s possession.
//states.add(“cold”) ;
//states.add(“sting”);
//states.add(“freezing”);
//states.add(“invisible”);
//Try also altering what page you load up
Page page = book.getPage(“wanderingHub”) ;
//Page page = book.getPage(“pickUpRing”) ;
//Page page = book.getPage(“moveOnFromRing1”) ;
System.out.println(page.getText()) ;
List<Choice> validChoices = new ArrayList<>();
for (Choice choice: page.getChoices()) {
if (book.isAvailable(choice.getDestinationId(), states)) {
validChoices.add(choice) ;
}
}
if (validChoices.size() == 0) {
System.out.println(“THE END”) ;
} else {
System.out.println(“Based on the reader’s items/states, the available choices are: “);
for (Choice choice: validChoices) {
System.out.println(” – ” + choice.getText() + ” (goes to ” + choice.getDestinationId() + “)”) ;
}
}
states = page.getUpdatedStates(states) ;
System.out.println(“After navigating to this page, the reader’s items/states are now ” + states) ;
}
private static Book loadBook() {
Book book = new Book() ;
book.addPage(
new Page(“start”, “You have been seperated from your companions and find yourself lost and in the dark.”)
.withChoice(
new Choice(“wanderingHub”, “Continue on”)
)
.adds(“sting”)
);
book.addPage(
new Page(“wanderingHub”, “You are just barely able to see your way forward. Around the corner you hear someone hissing furiously. \”My presccciouusss! My preccousss is losssstt!\””)
.withChoice(new Choice(“stream”, “Turn back and look for another way”))
.withChoice(new Choice(“meetGollum”, “Sneak forwards, towards the voice”))
.withChoice(new Choice(“confrontGollum”, “Draw your sword before proceeding”))
.withChoice(new Choice(“evadeGollum1”, “Put the ring on before proceeding”))
);
book.addPage(
new Page(“stream”, “You stumble into a cold stream. It is shockingly cold, but a glint below you catches your eye.”)
.adds(“cold”)
.excludedBy(“cold”)
.withChoice(new Choice(“pickUpRing”, “Ignore the freezing cold and look closer”))
.withChoice(new Choice(“wanderingHub”, “Get out of the stream before you freeze”))
);
book.addPage(
new Page(“pickUpRing”, “Your numb fingers fish through the water, and you clumsily pick up what appears to be a small golden ring. It feels oddly warm and inviting.”)
.adds(“ringOfPower”)
.adds(“freezing”)
.withChoice(new Choice(“putOnRing”, “Put the ring on”))
.withChoice(new Choice(“moveOnFromRing1”, “Pocket the ring and get out of the stream”))
);
book.addPage(
new Page(“putOnRing”, “You slip the ring on your finger, and a sensation of overwhelming power envelops you. As you look down you suddenly realise that you cannot see your hands, even when you bring them close to your face in the darkness”)
.adds(“invisible”)
.withChoice(new Choice(“putOnRing2”, “Am I dead? Am I a ghost?”))
);
book.addPage(
new Page(“putOnRing2”, “You frantically stumble out of the stream, only to trip over a rock. The feel of gravel in your face comes as a relief as you realise that you are very much still alive. With a chuckle you realise that you are no ghost, but have become invisible!”)
.withChoice(new Choice(“putOnRing3”, “Continue to marvel at your newfound power”))
.withChoice(new Choice(“wanderingHub”, “Pocket the ring and move on”))
);
book.addPage(
new Page(“putOnRing3”, “A huge eye enters your vision and a booming voice invades your skull. \”I SEE YOU!\” it roars.”)
.adds(“knownToSauron”)
.withChoice(new Choice(“moveOnFromRing2”, “Rip the ring from your finger”))
);
book.addPage(
new Page(“moveOnFromRing1”, “You reluctantly pocket the ring.”)
.removes(“invisible”)
.withChoice(new Choice(“wanderingHub”, “Stumble onwards”))
);
book.addPage(
new Page(“moveOnFromRing2”, “The eye disapears from view immediately. It feels like an age before you panic receeds, and you feel ready to move on.”)
.withChoice(new Choice(“wanderingHub”, “Stumble onwards”))
);
book.addPage(
new Page(“meetGollum”, “You turn the corner to find a gaunt, feral creature dressed in pathetic rags. Without hesitating, it leaps towards your throat, teeth bared.”)
.withChoice(new Choice(“defeatedByGollum”, “Defend yourself”))
);
book.addPage(
new Page(“confrontGollum”, “You turn the corner to find a gaunt, feral creature dressed in pathetic rags. Without hesitating, it leaps towards your throat, teeth bared.”)
.requires(“sting”).adds(“armed”)
.withChoice(new Choice(“fightGollum”, “Defend yourself”))
.withChoice(new Choice(“fightGollumFrozen”, “Defend yourself”))
);
book.addPage(
new Page(“evadeGollum1”, “You place the ring on your finger and immediately disappear from view. “)
.requires(“ringOfPower”)
.adds(“invisible”)
.adds(“knownToSauron”)
.withChoice(new Choice(“evadeGollum2”, “Sneak forwards”))
) ;
book.addPage(
new Page(“evadeGollum2”, “You turn the corner to find a gaunt, feral creature dressed in pathetic rags. It’s head snaps up and it sniffs the air suspiciously, but it does not appear to be able to see you. “)
.requires(“invisible”)
.withChoice(new Choice(“toBeContinued”, “Sneak carefully around it”))
) ;
book.addPage(
new Page(“fightGollum”, “The creature stops it’s charge, it’s eyes narrowing at the sight of your glowing blade.”)
.withChoice(new Choice(“toBeContinued”, “You circle each other warily.”))
.excludedBy(“freezing”)
);
book.addPage(
new Page(“fightGollumFrozen”, “Your sword tumbles from your frozen fingers. The creature grins and leaps towards your throat, teeth bared.”)
.withChoice(new Choice(“defeatedByGollum”, “Push the creature away”))
.requires(“freezing”)
);
book.addPage(
new Page(“defeatedByGollum”, “Your feeble attempts barely slow the creature down. Sadly, your adventure ends here, in the dark.”)
) ;
book.addPage(
new Page(“toBeContinued”, “To be continued….”)
);
return book ;
}
}