Writing an Adventure

An adventure is made up of a collection of interconnected cards. Each card represents a single screen on the PyPortal and single point in the adventure.

An adventure is defined by a JSON file that contains an array of objects, each one defining a card.

Cards

Each card defines a screen and point in the adventure. The core structure of a card is made up of several items:

card_id (required)

This is the unique (with the file) string id of the card. It is used to refer to the card from others.

background_image (optional, defaults to None)

This is the name of an image file to use as the background image of the card.

text (optional, defaults to None)

This is the text that is displayed on the card.

text_color (optional, defaults to black)

The color in which to display the text.

sound (optional, defaults to None)

This is the name of an audio file that is played when the card is displayed. 

sound_repeat (optional, defaults to False)

A True value for this will have the sound (if specified) continue to loop while this card is displayed. Otherwise the sound will play once and stop.

Download: file
{
  "card_id": "startup",
  "background_image": "startup.bmp",
  "sound": "startup.wav",
}

Linking Cards

A single card doesn't make for a very compelling adventure. To do that we need several, even many, cards. We also need a way to move from one card to another. Since this is a interactive adventure system, we need to give the user some agency in how they move between cards.

Auto-advance

This way of moving to another card doesn't give the user any agency, it just happens after a specified amount of time. The card advanced to is the next one defined in the adventure file.

In the example below, When the startup card is displayed and will stay for 5 seconds before automatically moving to the home card.

Download: file
{
  "card_id": "startup",
  "background_image": "startup.bmp",
  "sound": "startup.wav",
  "auto_advance": "5"
},
{
  "card_id": "home",
  "background_image": "home.bmp",
  "sound": "home.wav",
  "sound_repeat": "True",
}

Single Button

You can provide a single button for the user to press to advance to another card. Unlike auto_advance, this does not have to be the next card in the file; you use the card_id of the card to be moved to. The example below shows using the button01 fields to specify the button text as well as the target card.

Download: file
{
  "card_id": "happy_ending",
  "background_image": "happyending.bmp",
  "sound": "happy_ending.wav",
  "sound_repeat": "True",
  "button01_text": "Home",
  "button01_goto_card_id": "home"
}

Two Buttons

The final option for moving is to provide the user with a choice. This choice has to have two, and only two, options. The simplest case of this is a yes/no question.

Similar to the single button case, the button01 fields are used to specify the left button and a set of button02 fields are used for the right button.

Below is a typical yes/no choice.

Download: file
{
  "card_id": "1",
  "background_image": "page01.bmp",
  "text": "You do not have any friends so you decide that it might be a good idea to build a robot friend. You're unsure if you want to do this, so now is the time to decide. Do you want to build a robot friend?",
  "text_color": "0x000001",
  "sound": "sound_01.wav",
  "button01_text": "Yes",
  "button01_goto_card_id": "2",
  "button02_text": "No",
  "button02_goto_card_id": "4"
},

And here is an example of a two option choice that isn't a yes/no question. Notice that the only difference is the button text.

Download: file
{
  "card_id": "home",
  "background_image": "home.bmp",
  "sound": "home.wav",
  "sound_repeat": "True",
  "button01_text": "Help",
  "button01_goto_card_id": "help",
  "button02_text": "Start",
  "button02_goto_card_id": "1"
},

An Example

Here is a simple, yet complete, example. This is the file corresponding to the diagram on the previous page.

Clicking on download Project zip below will give you a zip file that you will need to dig into to get the cyoa directory. Copy that to your CIRCUITPY drive.

[
  {
    "card_id": "startup",
    "background_image": "startup.bmp",
    "sound": "startup.wav",
    "auto_advance": "5"
  },
  {
    "card_id": "home",
    "background_image": "home.bmp",
    "sound": "home.wav",
    "sound_repeat": "True",
    "button01_text": "Help",
    "button01_goto_card_id": "help",
    "button02_text": "Start",
    "button02_goto_card_id": "want to build?"
  },

  {
    "card_id": "want to build?",
    "background_image": "page01.bmp",
    "text": "You do not have any friends so you decide that it might be a good idea to build a robot friend. You're unsure if you want to do this, so now is the time to decide. Do you want to build a robot friend?",
    "text_color": "0x000001",
    "sound": "sound_01.wav",
    "button01_text": "Yes",
    "button01_goto_card_id": "continue?",
    "button02_text": "No",
    "button02_goto_card_id": "lazy"
  },
  {
    "card_id": "continue?",
    "background_image": "page02.bmp",
    "text": "You spend all day, then all week, then all month building a robot, everyone stops talking to you, however a lot of progress has been made. Do you want to keep making the robots?",
    "text_color": "0xFFFFFF",
    "button01_text": "Yes",
    "button01_goto_card_id": "robot friend",
    "button02_text": "No",
    "button02_goto_card_id": "lazy"
  },
  {
    "card_id": "robot friend",
    "background_image": "page03.bmp",
    "text": "The robot is now you're friend, everyone else wishes they had a robot, this is the best thing ever. Good work!",
    "text_color": "0xFFFFFF",
    "sound": "Mystery.wav",
    "button01_text": "Next",
    "button01_goto_card_id": "happy ending"
  },
  {
    "card_id": "lazy",
    "background_image": "page04.bmp",
    "sound": "sound_04.wav",
    "text": "Welp, not only will you not have any friends, you are lazy. What's the point of playing? Try again.",
    "text_color": "0xFFFFFF",
    "button01_text": "Start Over",
    "button01_goto_card_id": "home"
  },
  {
    "card_id": "help",
    "background_image": "help.bmp",
    "text": "All you need to do is click the buttons, that's it.\nThis is a new line.",
    "text_color": "0xFFFFFF",
    "button01_text": "Home",
    "button01_goto_card_id": "home"
  },
 {
    "card_id": "happy ending",
    "background_image": "happyending.bmp",
    "sound": "happy_ending.wav",
    "sound_repeat": "True",
    "button01_text": "Home",
    "button01_goto_card_id": "home"
  }
]

Driving it

adafruit_pyoa is a library and framework that let's you easily create adventures. You'll need to write a small code.py to set it up and make it work. Here's the example from the repo. Copy this file to CIRCUITPY/code.py.

import board
import digitalio
import adafruit_sdcard
import storage
from adafruit_pyoa import PYOA_Graphics

try:
    sdcard = adafruit_sdcard.SDCard(board.SPI(), digitalio.DigitalInOut(board.SD_CS))
    vfs = storage.VfsFat(sdcard)
    storage.mount(vfs, "/sd")
    print("SD card found") # no biggie
except OSError:
    print("No SD card found") # no biggie

gfx = PYOA_Graphics()

gfx.load_game("/cyoa")
current_card = 0   # start with first card

while True:
    print("Current card:", current_card)
    current_card = gfx.display_card(current_card)

This shows how to hook up to the SD card interface so you can put the adventure JSON file along with images and sounds on an SD card. The example puts it on the flash file system for simplicity.

There are three key things that this code does to make the adventure run:

  1. creates an instance to the PYOA_Graphics class that does the work,
  2. loads the game by telling the framework where the adventure data is located (note that the JSON file must currently be called cyoa.json) and sets the index of the start card
  3. goes into an infinite loop that calls display_card(current_card) in the framework which returns a new value for current_card.
This guide was first published on Jun 22, 2019. It was last updated on Jun 22, 2019. This page (Writing an Adventure) was last updated on Nov 13, 2019.