// Written by Alonso Canales for the following Instructable: https://www.instructables.com/Create-a-Choice-based-Text-Adventure-Game-With-Tin/
/********
CONSTANTS
*********/
/* Set these to your liking! */
const int MESSAGES_PER_PASSAGE = 5; // Number of messages before choices are presented.
const int CHOICES_PER_PASSAGE = 2; // Choice that are presented to the player at each fork.
const int TYPEWRITER_DELAY = 50; // Pause in milliseconds inbetween characters appearing on screen.
/* These depend on the LCD screen you are using. */
const int LINES_PER_MESSAGE = 2; // Number of rows on your LCD screen.
const int CHARACTERS_PER_LINE = 16; // Number of columns on your LCD screen.
/*********
STRUCTURES
**********/
/* Message that will be presented to a player on screen. */
typedef struct {
char lines[LINES_PER_MESSAGE][CHARACTERS_PER_LINE + 1];
int action;
} Message;
/* A choice that will be presented once all messages have been read. */
typedef struct {
char lines[LINES_PER_MESSAGE][CHARACTERS_PER_LINE + 1];
int target;
} Choice;
/* Structure that contains a collection of messages and choices. */
typedef struct {
Message messages[MESSAGES_PER_PASSAGE];
Choice choices[CHOICES_PER_PASSAGE];
} Passage;
/*********
STORY
**********/
/*
Use the following sheet to write your story:
https://docs.google.com/spreadsheets/d/1C74b5puLM0qTp43sT1uaNC4YvTEe7cJvJ_NguqkdF1k/copy
Once you've finished writing, hit the "Generate Story" button and replace this story with the output text.
Note that:
- If you wish to change the title card, you must change it in setup.
- If you wish to change the end card, you must change it in the printEnding() function.
*/
const Passage story[4] PROGMEM = {{{{{"As you sipped on","today's coffee"},-1},{{"you decided to","write an"},-1},{{"instructable.","But about what?"},-1},{{"You pondered and","pondered,"},-1},{{"and got a","great idea!"},-1}},{{{"1-Write about ","text adventures"},1},{{"2-Actually,let's","do other stuff"},3}}},{{{{"Excited, you sit","down at your PC"},3},{{"and begin to","write. The code"},-1},{{"flows naturally","but you reach a"},-1},{{"problem. What","should your"},-1},{{"adventure be","about?"},0}},{{{"1-About writing","Instructables"},2},{{"2-About a ","colossal cave"},2}}},{{{{"You write and","write, drinking"},-1},{{"a lot of coffee","inbetween, but"},-1},{{"eventually,","you've finished!"},2},{{"You celebrate","with a green LED"},-1},{{"Thanks for","reading!"},0}},{{{"",""},-1},{{"",""},-1}}},{{{{"You decided to","leave your great"},1},{{"idea for another","day. Yet, years"},-1},{{"passed and you","eventualy forgot"},-1},{{"about it.","Well, there'll"},-1},{{"always be other","projects to do!"},0}},{{{"",""},-1},{{"",""},-1}}}};
/*********
CODE
**********/
#include <LiquidCrystal.h>
const int NEXT_BUTTON_PIN = 12;
const int CONFIRM_BUTTON_PIN = 13;
// To demostrate the use of actions, we'll add a RGB LED.
const int RED_LED_PIN = 9;
const int BLUE_LED_PIN = 10;
const int GREEN_LED_PIN = 11;
LiquidCrystal lcd(7, 6, 5, 4, 3, 2);
void setup()
{
lcd.begin(CHARACTERS_PER_LINE, LINES_PER_MESSAGE);
// Define your title screen here!
lcd.setCursor(0, 0);
lcd.print(F(" -- A STORY -- "));
lcd.setCursor(0, 1);
lcd.print(F(" by aecanales "));
pinMode(NEXT_BUTTON_PIN, OUTPUT);
pinMode(CONFIRM_BUTTON_PIN, OUTPUT);
pinMode(RED_LED_PIN, OUTPUT);
pinMode(BLUE_LED_PIN, OUTPUT);
pinMode(GREEN_LED_PIN, OUTPUT);
}
// State of each button (HIGH or LOW).
int nextPressed;
int confirmPressed;
// Current index of our position in the story.
int currentPassage = 0;
int currentMessage = -1;
int currentChoice = -1;
// Containers used to hold each message/choice as we load them from memory.
Message message;
Choice choice;
void loop()
{
readButtons();
// If we've pressed the next button...
if (nextPressed)
{
// If we haven't read all messages, advance to the next message in the passage.
if (currentMessage < MESSAGES_PER_PASSAGE - 1)
{
advancePassage();
}
// Otherwise, cycle to the next available choice.
else
{
cycleChoice();
}
}
// If we've reached the choices and press the confirm button, jump to the corresponding passage.
if (confirmPressed && currentMessage == MESSAGES_PER_PASSAGE - 1)
{
currentPassage = choice.target;
currentMessage = -1;
currentChoice = -1;
advancePassage();
}
// This delay drastically improves perfomance in Tinkercad Circuits but may not be necessary on a real Arduino.
delay(100);
}
/* Prints a message or choice with a stylish delay between characters. */
void printLines(char lines[LINES_PER_MESSAGE][CHARACTERS_PER_LINE + 1])
{
lcd.clear();
for (int row = 0; row < LINES_PER_MESSAGE; row++)
{
for (int column = 0; column < CHARACTERS_PER_LINE; column++)
{
char letter = lines[row][column];
if (letter != NULL)
{
lcd.setCursor(column, row);
lcd.print(lines[row][column]);
delay(TYPEWRITER_DELAY);
}
}
}
}
/* Reads the state of two buttons that are connected to our project. */
void readButtons()
{
nextPressed = digitalRead(NEXT_BUTTON_PIN);
confirmPressed = digitalRead(CONFIRM_BUTTON_PIN);
}
/* Prints the next message in the current passage and executes and action if there's any.*/
void advancePassage()
{
currentMessage++;
readMessageFromPROGMEM();
if (message.action != -1)
{
activateAction(message.action);
}
printLines(message.lines);
}
/* Reads the current message from PROGMEM and saves it to 'message'.*/
void readMessageFromPROGMEM()
{
memcpy_P(&message, &story[currentPassage].messages[currentMessage], sizeof message);
}
/* Cycles and prints the next choice in the current passage, or jumps to the end if the passage is terminal. */
void cycleChoice()
{
incrementCurrentChoice();
readChoiceFromPROGMEM();
if (choice.target != -1)
{
printLines(choice.lines);
}
else
{
printEnding();
}
}
/* Prints our ending screen. */
void printEnding()
{
lcd.setCursor(0, 0);
lcd.print(F(" -- THE -- "));
lcd.setCursor(0, 1);
lcd.print(F(" -- END -- "));
}
/* Selects the next choice, returning to the first one once we've seen them all. */
void incrementCurrentChoice()
{
if (currentChoice < CHOICES_PER_PASSAGE - 1)
{
currentChoice++;
}
else
{
currentChoice = 0;
}
}
/* Reads the current choice from PROGMEM and saves it to 'choice'. */
void readChoiceFromPROGMEM()
{
memcpy_P(&choice, &story[currentPassage].choices[currentChoice], sizeof choice);
}
/***********
ACTIONS
************/
/* Calls an action corresponding to the number you put in the message. Can be anything you want! */
void activateAction(int action)
{
switch(action)
{
case 0:
lightRGBLED(0, 0, 0);
break;
case 1:
lightRGBLED(255, 0, 0);
break;
case 2:
lightRGBLED(0, 255, 0);
break;
case 3:
lightRGBLED(0, 0, 255);
break;
}
}
void lightRGBLED(int r, int g, int b)
{
analogWrite(RED_LED_PIN, r);
analogWrite(GREEN_LED_PIN, g);
analogWrite(BLUE_LED_PIN, b);
}