Optional Questions & Branching
Only Prompting Some Interactions
There are situations where a question should be asked some times, but not all the time. Or, a message should be displayed some times.
For example, a program that collects information about a user's
pets should not ask the user for the dog's name and breed if the
user said they do not have a dog. If the user has a dog, the program
may want to display a special message. The should_ask
argument that
is present on each interaction provides a way to achieve this functionality.
Similarly, should_ask
can be used to provide branching paths to the user. An example of these branching paths is a
Choose Your Own Adventure story. The story provides the reader with choices during the
adventure. These choices introduce diverging paths of interactions that may or may not join at the end.
Warning
By default when columbo
skips over a question, the Answers
dictionary will NOT contain a key-value pair for the
skipped question. If you want a skipped question to have a specific answer when skipped, you can do so using the
value_if_not_asked
kwarg as detailed in the section below.
Optional Questions
The following is a basic example that has two optional questions that are not asked based on the answer to the first question. It also has an optional message that is only displayed based on the answer to the first question.
import columbo
def user_has_dog(answers: columbo.Answers) -> bool:
return answers["has_dog"]
interactions = [
columbo.Confirm("has_dog", "Do you have a dog?", default=True),
columbo.Echo(
"Because you have have a dog, we want to ask you some more questions.",
should_ask=user_has_dog,
),
columbo.BasicQuestion(
"dog_name",
"What is the name of the dog?",
should_ask=user_has_dog,
default="Kaylee",
),
columbo.BasicQuestion(
"dog_breed",
"What is the breed of the dog?",
should_ask=user_has_dog,
default="Basset Hound",
),
]
user_answers = columbo.get_answers(interactions)
print(user_answers)
If the user accepts the default answers for each of these questions, the output will be:
{"has_dog": True, "dog_name": "Kaylee", "dog_breed": "Basset Hound"}
However, when the user answers the first question with "no", the output will be:
{"has_dog": False}
Note that the Answers
dictionary in the previous example has an answer to only the first question (there are no answers for the skipped questions).
The next section shows how to provide an answer for a skipped question.
Providing an Answer for Skipped Questions
To provide a specific answer used when a user skips a question, use the value_if_not_asked
kwarg:
import columbo
def user_has_dog(answers: columbo.Answers) -> bool:
return answers["has_dog"]
interactions = [
columbo.Confirm("has_dog", "Do you have a dog?", default=True),
columbo.Echo(
"Because you have have a dog, we want to ask you some more questions.",
should_ask=user_has_dog,
),
columbo.BasicQuestion(
"dog_name",
"What is the name of the dog?",
should_ask=user_has_dog,
value_if_not_asked="n/a",
default="Kaylee",
),
columbo.BasicQuestion(
"dog_breed",
"What is the breed of the dog?",
should_ask=user_has_dog,
value_if_not_asked="n/a",
default="Basset Hound",
),
]
user_answers = columbo.get_answers(interactions)
print(user_answers)
If the user answers the first question with "no", the output will now be:
{"has_dog": False, "dog_name": "n/a", "dog_breed": "n/a"}
Columbo will not ask the user for a dog name or breed, but the answers will have the values provided with the value_if_not_asked
kwarg.
Branching Paths
A question that is part of a branching path is very similar to an optional question. It is still a question where
the should_ask
function was provided to determine if the question should be skipped or not. The branching aspect comes from there being at
least two sets of optional questions. Each set has a should_ask
argument that checks for a different state for a
single answer. In this way, only one of the sets of optional questions will ever be asked.
The following is an example of a short story that has two divergent paths that join at the end. Each individual question
isn't different from the optional questions demonstrated above. The program achieves the
branching paths by supplying different should_ask
values that will never both evaluate to True
.
import columbo
def went_left(answers: columbo.Answers) -> bool:
return answers["which_door"] == "left"
def went_right(answers: columbo.Answers) -> bool:
return answers["which_door"] == "right"
def outcome(answers: columbo.Answers) -> str:
if answers.get("has_key", False):
return "You try the the key on the lock. With a little jiggling, it finally opens. You open the gate and leave."
if answers.get("has_hammer", False):
return "You hit the lock with the hammer and it falls to the ground. You open the gate and leave."
return (
"Unable to open the gate yourself, you yell for help. A farmer in the nearby field hears you. "
"He reaches into his pocket and pulls out a key to unlock the gate and open it. "
"As you walk through the archway he says, "
'"What I don\'t understand is how you got in there. This is the only key."'
)
interactions = [
columbo.Echo(
"You wake up in a room that you do not recognize. "
"In the dim light, you can see a large door to the left and a small door to the right."
),
columbo.Choice(
"which_door",
"Which door do you walk through?",
options=["left", "right"],
default="left",
),
columbo.Echo(
"You step into a short hallway and the door closes behind you, refusing to open again. "
"As you walk down the hallway, there is a small side table with a key on it.",
should_ask=went_left,
),
columbo.Confirm(
"has_key",
"Do you pick up the key before going through the door at the other end?",
should_ask=went_left,
default=True,
),
columbo.Echo(
"You step into smaller room and the door closes behind, refusing to open again. "
"The room has a single door on the opposite side of the room and a work bench with a hammer on it.",
should_ask=went_right,
),
columbo.Confirm(
"has_hammer",
"Do you pick up the hammer before going through the door at the other side?",
should_ask=went_right,
default=True,
),
columbo.Echo(
"You enter a small courtyard with high walls. There is an archway that would allow you to go free, "
"but the gate is locked."
),
columbo.Echo(outcome),
]
user_answers = columbo.get_answers(interactions)
print(user_answers)
The import thing to note in the example above is that the Answers
dictionary can have a key-value pair for
has_key
or has_hammer
, not both.
Complicated Situations
While should_ask
is capable of supporting complex combinations of optional questions and branching paths,
there are times where only using that functionality can make the code harder to read and understand. There
are alternate strategies that can be used in order to make the code easier to follow.