Skip to content

Instantly share code, notes, and snippets.

@aecanales
Last active February 27, 2021 21:45
Show Gist options
  • Save aecanales/b542c99f2877bd9f59548b0447f2bd75 to your computer and use it in GitHub Desktop.
Save aecanales/b542c99f2877bd9f59548b0447f2bd75 to your computer and use it in GitHub Desktop.
Sketch for multi-choice text adventure Instructable. https://www.instructables.com/Create-a-Choice-based-Text-Adventure-Game-With-Tin/
let messages = 0;
let choices = 0;
let lines = 0;
let characters = 0;
function readConstants(sheet)
{
messages = parseInt(sheet.getRange("B2").getDisplayValues());
choices = parseInt(sheet.getRange("B3").getDisplayValues());
lines = parseInt(sheet.getRange("B4").getDisplayValues());
characters = parseInt(sheet.getRange("B5").getDisplayValues());
}
function AddPassages()
{
const sheet = SpreadsheetApp.getActiveSheet();
readConstants(sheet);
const passages = parseInt(sheet.getRange("B8").getDisplayValues());
const activeSpreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const numberOfPassages = activeSpreadsheet.getNumSheets() - 1;
for (i=numberOfPassages; i < numberOfPassages + passages; i++)
{
const newSheet = activeSpreadsheet.insertSheet(i.toString(), activeSpreadsheet.getNumSheets());
formatSection(newSheet, 1, 1, 1, 3, `PASSAGE ${i}`);
newSheet.getRange(1, 1, 1, 3).setBorder(true, true, true, true, true, true);
createSequence(newSheet, messages, 2, "Message", "Action", -1);
createSequence(newSheet, choices, 2 + messages * (lines + 1), "Choice", "Target", 0);
newSheet.autoResizeColumn(2);
newSheet.setColumnWidth(3, 10 * characters);
newSheet.getRange(2, 2, messages * (lines + 1) + choices * (lines + 1), 1).setFontStyle("italic");
}
}
function formatSection(sheet, row, column, height, width, text)
{
const range = sheet.getRange(row, column);
range.setValue(text);
range.setHorizontalAlignment("center");
range.setVerticalAlignment("middle");
range.setFontWeight("bold");
sheet.getRange(row, column, height, width).merge();
}
function createSequence(sheet, amount, start, name, final, default_final)
{
for (j=0; j < amount; j++)
{
const row = start + j * (lines + 1);
formatSection(sheet, row, 1, lines + 1, 1, `${name} ${j}`);
for (k=0; k < lines; k++)
{
sheet.getRange(row + k, 2).setValue(`Line ${k}`);
const range = sheet.getRange(row + k, 3);
range.setHorizontalAlignment("right");
range.setDataValidation(createLengthRule(range));
sheet.getRange(row + k, 4).setFormula(`=LEN(${range.getA1Notation()})`);
}
sheet.getRange(row + lines, 2).setValue(`${final}`);
sheet.getRange(row + lines, 3).setValue(default_final);
}
}
function createLengthRule(range)
{
return SpreadsheetApp.newDataValidation()
.setHelpText(`Text cannot have more than ${characters} characters.`)
.requireFormulaSatisfied(`=len(${range.getA1Notation()})<${characters}`)
.build();
}
function GenerateStory()
{
readConstants(SpreadsheetApp.getActiveSheet());
const activeSpreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const numberOfPassages = activeSpreadsheet.getNumSheets() - 1;
let story = `const Passage story[${numberOfPassages}] PROGMEM = {`
for (i=0; i < numberOfPassages; i++)
story += formPassage(activeSpreadsheet.getSheetByName(i)) + ",";
story = story.slice(0, -1);
story += "};"
SpreadsheetApp.getActiveSheet().getRange("B9").setValue(story);
}
function formPassage(sheet)
{
let passage = "{{";
for (j=0; j < messages; j++)
passage += formMessageOrChoice(sheet, 2 + j * (lines + 1)) + ",";
passage = passage.slice(0, -1);
passage += "},{";
for (l=0; l < choices; l++)
passage += formMessageOrChoice(sheet, 2 + messages * (lines + 1) + l * (lines + 1)) + ",";
passage = passage.slice(0, -1);
passage += "}}";
return passage;
}
function formMessageOrChoice(sheet, starting_row)
{
let message = "{{";
for (k=0; k < lines; k++)
message += `"${sheet.getRange(starting_row + k, 3).getDisplayValues()}",`;
message = message.slice(0, -1);
message += "},";
message += sheet.getRange(starting_row + lines, 3).getDisplayValues();
message += "}"
return message;
}
// 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 pressend 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);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment