Last active
February 27, 2021 21:45
-
-
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/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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