Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
Two ultra-simple adventure games for use in Codea on the iPad.
--# Main
-- Main
saveProjectInfo( "Description", "Two-choice Adventure Games by Rosie and Charlotte." )
This project creates two simple adventure games that my daughters made up.
The only project-specific code is in the Main and ImageSet tabs.
All other code can be repurposed without needing direct modification.
The tabs explained in order:
1. Main sets everything up--instantiating and configuring all the other classes.
2. ImageSets contains tables with the names of all the images to be used.
3. GameMC is a menu screen for launching different TwoChoiceAdventures.
4. TwoChoiceAdventure is a class that can be made into a simple adventure game.
5. Screen is a basic interactive screen, the building block of the previous two classes.
6. Inventory is a class for tracking, modifying, and displaying inventory.
7. StyleTable is a utility class for storing and applying style settings at will.
8. Utilities contains miscellaneous handy functions with self-explanatory names.
A note on my code conventions: I have attempted wherever possible to stick to the "golden path" convention, which means sticking to only two indentations maximum. I have also attempted to keep every statement to one line of text (as measured in Codea's portrait mode). And as a reaction to the very compelling points made in "Programming Sucks" (, I aspired to live by the slogan "the only good code is obsessively commented code."
--set basic display settings
--setting the icon
function (gotImage) saveImage("Project:Icon", gotImage) end, function (error) print("fail with error: "..error) end)
function setup()
--a flag for controlling whether or not custom sounds play
customSounds = false
--set the style for the initial loading screen text
fill(184, 100, 18, 255)
--make a boolean to track if the games have been created
gamesCreated = false
--make a counter to track how many http image requests have been sent
httpImagesLoading = 0
--load the image sets (as defined in the ImageSets tab)
--note: the draw() method will detect when all images are loaded and then create the games
function loadImages(tableWithImageNamesAsKeys)
--go through the keys in the table
for key, value in pairs(tableWithImageNamesAsKeys) do
--set the image from either a local file or from the web
getLocalOrWebImage(key, tableWithImageNamesAsKeys)
function getLocalOrWebImage(name, tableForImages)
--attempt to get image locally--if image doesn't exist, this will return nil
local imageGot = readImage("Dropbox:"
--if the local attempt does not come up nil, assign the image to the name given
if imageGot ~= nil then
tableForImages[name] = imageGot
--prepare a function that can assign an image (used if it has to be retrieved from the web)
local assignImage = function (thisImage)
--here, assign the image
tableForImages[name] = thisImage
--here, decrement the counter for loading images (this is incremented every http call)
httpImagesLoading = httpImagesLoading - 1
--if the local attempt does come up nil, load the image from the web
if imageGot == nil then
--increment the httpImagesLoading tracker
httpImagesLoading = httpImagesLoading + 1
--send the http call for the image, using the function defined above to assign it properly
assignImage, function (error) print("fail to assign " end)
function draw()
--make sure all images are loaded and games created, then pass draw() to the mainScreen
if httpImagesLoading == 0 and gamesCreated == true then
--mainScreen gets defined in createGames()
--if everything isn't done, check if there are still images to load
elseif httpImagesLoading > 0 then
--if there are images left to load, put up the "loading images" text while we wait
text("loading images", WIDTH / 2, HEIGHT / 2)
--if everything isn't done but all images are loaded, make the games
function touched(touch)
--send the touch to the main screen, which redirects it from there
--note: mainScreen gets defined in createGames()
function createGames()
--since we have two separate games, we need a GameMC as a launch screen
choicesMC = GameMC()
--set the choicesMC to be the main screen
mainScreen = choicesMC
--create our two adventure games
rosiesChoices = makeRosiesChoices()
charlottesChoices = makeCharlottesChoices()
--load the games into the GameMC
choicesMC:addGame("Rosie's Choices", rosiesChoices)
choicesMC:addGame("Charlotte's Choices", charlottesChoices)
--cutomize the GameMC's game buttons
choicesMC:customGameButton("Rosie's Choices", gameMCImages.choicesMenuRosie, 300, 307)
choicesMC:customGameButton("Charlotte's Choices", gameMCImages.choicesMenuCharlotte, 727, 387)
--add a custom background to the menu
choicesMC.gameSelectionScreen.background = gameMCImages.choicesMenuBackground
--cutomize the tint color that shows the game buttons have been pressed
choicesMC.gameSelectionScreen.touchedTint = color(213, 207, 207, 245)
--make a custom action to play a sound when Charlotte's game starts
local charlotteButtonAction = function ()
if customSounds == true then
choicesMC:runGame("Charlotte's Choices")
--make a custom action to play a sound when Charlotte's game starts
local rosieButtonAction = function ()
if customSounds == true then
choicesMC:runGame("Rosie's Choices")
--add those actions to the gameMC
choicesMC:customGameButtonAction("Charlotte's Choices", charlotteButtonAction)
choicesMC:customGameButtonAction("Rosie's Choices", rosieButtonAction)
--set a fill and stroke style for the rect of a custom exit button
fill(87, 90, 109, 129)
--make a StyleTable (which captures the current style upon initializing)
local exitBoxStyle = StyleTable()
--set a style for the text of thecustom exit button
fill(255, 255, 255, 255)
fontSize(HEIGHT / 28)
local exitTextStyle = StyleTable()
--use those styles to create the image for the button
exitLabel = Utilities:label("back to menu", 200, 50, nil, nil, exitTextStyle, exitBoxStyle)
--replace the GameMC's stored exit button image and location with custom values
choicesMC.exitButton.image = exitLabel
choicesMC.exitButton.x = exitLabel.width / 2
choicesMC.exitButton.y = 108
--flip the boolean that tracks if the games have been created
gamesCreated = true
--lastly, play a little mood music
if customSounds == true then
music("Dropbox:Choices_rumble-2", true)
function makeRosiesChoices()
--create the game object
local rosiesChoices = TwoChoiceAdventure("Rosie's Choices")
--create the individual screens (the format for these is explained in TwoChoiceAdventure)
{name = "firstScreen",
background = rcImages.rcFirst,
images = {},
narration = "Do you want to have an adventure?",
choices = {
{choiceText = "adventure", resultScreen = "theAdventureIs"},
{choiceText = "stay in bed", resultScreen ="sleepForDays" } }
{name = "theAdventureIs",
background = rcImages.theAdventureIs,
images = {},
narration = "The adventure is: help everybody!",
choices = {
{choiceText = "first go see your boyfriend", resultScreen ="boyfriendAsks" } }
{name = "sleepForDays",
background = rcImages.rcSleepForDays,
images = {},
narration = "You sleep for days and days and end up hated by everybody.",
choices = {
{choiceText = "The End", resultScreen ="firstScreen"} }
{name = "boyfriendAsks",
background = rcImages.rcBoyfriendAsks,
images = {},
narration = "Your boyfriend says \"do you want to know my secret or do you want to break up?\"",
choices = {
{choiceText = "break up", resultScreen ="breakUp" },
{choiceText = "secret", resultScreen = "boyfriendSecret"} }
{name = "boyfriendSecret",
background = rcImages.rcBoyfriendWithNote,
images = {},
narration = "My secret is this note I need to get to my sister somehow.",
choices = {
{choiceText = "send note for him", resultScreen ="helpEverybody" } }
{name = "breakUp",
background = rcImages.rcBreakup,
images = {},
narration = "You break up and he gets a girlfriend prettier than you and you cry.",
choices = {
{choiceText = "The End", resultScreen ="firstScreen"} }
{name = "helpEverybody",
background = rcImages.rcHelpEverybody,
images = {},
narration = "First, you send your boyfriend's note.\n\rThen you help everybody!",
choices = {
{choiceText = "good job", resultScreen ="queenAsks" } }
{name = "queenAsks",
background = rcImages.rcQueenAsksYouToDeliver,
images = {},
narration = "For doing such a good job, the Queen says you should be Queen, but only if you deliver some birthday invitations for her.",
choices = {
{choiceText = "don't deliver", resultScreen ="everybodyDisappointed" },
{choiceText = "deliver", resultScreen = "deliverInvitations"} }
{name = "deliverInvitations",
background = rcImages.rcDeliverAllInvitations,
images = {},
narration = "You deliver all the invitations!",
choices = {
{choiceText = "go back to Queen", resultScreen ="doYouHaveASecret"} }
{name = "everybodyDisappointed",
background = rcImages.rcEverybodysDisappointed,
images = {},
narration = "You never get famous and everybody's disappointed because they wanted you to be queen.",
choices = {
{choiceText = "The End", resultScreen ="firstScreen"} }
{name = "doYouHaveASecret",
background = rcImages.rcSecretOrQueen,
images = {},
narration = "The Queen asks:\n\rdo you have a secret or do you want to be queen?",
choices = {
{choiceText = "reveal your secret", resultScreen = "revealYourSecret"},
{choiceText = "become Queen", resultScreen ="becomeQueen" } }
{name = "revealYourSecret",
background = rcImages.rcTellYourSecret,
images = {},
narration = "You tell your secret: \"I didn't really want to help anybody.\"\n\rThe people kick you out and you lose.",
choices = {
{choiceText = "The End", resultScreen ="firstScreen"} }
{name = "becomeQueen",
background = rcImages.rcYoureQueen,
images = {},
narration = "You're Queen!",
choices = {
{choiceText = "The End", resultScreen ="firstScreen"} }
return rosiesChoices
function makeCharlottesChoices()
--create the game object
local charlottesChoices = TwoChoiceAdventure("Charlotte's Choices")
--create the individual screens (the format for these is explained in TwoChoiceAdventure)
{name = "firstScreen",
background = ccImages.ccFirst,
images = {
{"heroine", ccImages.ccHeroine, 235, 442},
{"coffeeSmall", ccImages.ccCoffeeSmall, 468, 439} },
narration = "You woke up.\n\rWhat do you want to do with your coffee?",
choices = {
{choiceText = "drink it", resultScreen = "drankCoffee"},
{choiceText = "save it for later", resultScreen ="savedCoffee",
inventoryAdd = {name = "coffee", icon = ccImages.ccCoffeeBig} } }
{name = "drankCoffee",
background = ccImages.ccFirst,
images = {
{"heroine", ccImages.ccHeroine, 235, 442} },
narration = "It tastes good.",
choices = {
{choiceText = "go outside", resultScreen = "boyfriendTellsAboutQueen"} },
{name = "savedCoffee",
background = ccImages.ccFirst,
images = {
{"heroine", ccImages.ccHeroine, 235, 442} },
narration = "You keep it with you for later.",
choices = {
{choiceText = "go outside", resultScreen = "boyfriendTellsAboutQueen"} },
{name = "boyfriendTellsAboutQueen",
background = ccImages.ccGenericOutside,
images = {
{"heroine", ccImages.ccHeroine, 319, 442},
{"boyfriend", ccImages.ccBoyfriend, 553, 450} },
narration = "Your boyfriend tells you the queen is bored.",
choices = {
{choiceText = "go to see the queen", resultScreen = "knightScreen"} }
{name = "knightScreen",
background = ccImages.ccKnightScreen,
images = {
{"heroine", ccImages.ccHeroine, 274, 442} },
narration = "You see a knight at the gate.\n\rWhat do you say to him?",
choices = {
{choiceText = "Just let me in to the castle.", resultScreen ="knightScolds" },
{choiceText = "Hi, having a nice day?", resultScreen = "knightGivesHeart",
inventoryAdd = {name = "heartBox", icon = ccImages.ccHeartBox} } }
{name = "knightGivesHeart",
background = ccImages.ccKnightScreen,
images = {
{"heroine", ccImages.ccHeroine, 274, 442} },
narration = "The knight likes you and gives you a heart box with chocolates in it.",
choices = {
{choiceText = "go in to castle", resultScreen ="boredQueen" } }
{name = "knightScolds",
background = ccImages.ccKnightScreen,
images = {
{"heroine", ccImages.ccHeroine, 274, 442} },
narration = "The knight tells you that you have a bad attitude.",
choices = {
{choiceText = "go home", resultScreen ="homeAfterKnight" } }
{name = "homeAfterKnight",
background = ccImages.ccFirst,
images = {
{"heroineFlipped", ccImages.ccHeroineFlipped, 400, 433} },
narration = "You say to yourself, \"He doesn't like me anyway.\"",
choices = {
{choiceText = "start over", resultScreen ="firstScreen",
inventoryRemove = "allItems" } }
{name = "boredQueen",
background = ccImages.ccQueenBored,
images = {
{"heroine", ccImages.ccHeroine, 225, 442} },
narration = "The queen tells you she's really bored.",
choices = {
{choiceText = "tell her you know a puppet show", resultScreen = "queenSaysShowMe" } }
{name = "queenSaysShowMe",
background = ccImages.ccQueenBored,
images = {
{"heroine", ccImages.ccHeroine, 230, 442} },
narration = "You tell the queen you know how to put on a puppet show.\n\rShe says, \"Show me as soon as you can!\"",
choices = {
{choiceText = "leave", resultScreen = "grouchyPuppeteer" } }
{name = "grouchyPuppeteer",
background = ccImages.ccGenericOutside,
images = {
{"heroineFlipped", ccImages.ccHeroineFlipped, 700, 458},
{"puppeteerGrouchy", ccImages.ccPuppeteerGrumpy, 325, 458} },
narration = "On your way home you see a grouchy puppeteer.",
choices = {
{onlyIfInInventory = "coffee", choiceText = "give him your coffee",
resultScreen = "happyPuppeteer", inventoryRemove = "coffee" },
{choiceText = "go home and practice", resultScreen ="queenNotLike" } }
{name = "happyPuppeteer",
background = ccImages.ccGenericOutside,
images = {
{"heroineFlipped", ccImages.ccHeroineFlipped, 693, 458},
{"puppeteerHappy", ccImages.ccPuppeteerHappy, 325, 458} },
narration = "He teaches you a new puppet show.",
choices = {
{choiceText = "go home and give up", resultScreen ="homeAndSleep" },
{choiceText = "go show the queen", resultScreen = "queenLovesShow"} }
{name = "queenNotLike",
background = ccImages.ccQueenNotLike,
images = {
{"heroineWithSockPuppets", ccImages.ccHeroineWithSockPuppets, 271, 430} },
narration = "You practice, but the queen doesn't like your show, and you're embarrassed.",
choices = {
{choiceText = "start over", resultScreen ="firstScreen",
inventoryRemove = "allItems" } }
{name = "homeAndSleep",
background = ccImages.ccHomeAndSleep,
narration = "You go back home and go to sleep.",
choices = {
{choiceText = "start over", resultScreen ="firstScreen",
inventoryRemove = "allItems" } }
{name = "queenLovesShow",
background = ccImages.ccQueenLoves,
images = {
{"heroineWithMarionettes", ccImages.ccHeroineWithMarionettes, 271, 440} },
narration = "The queen loves the show!\n\rShe tells you a secret. If you give the princess a heart box with chocolates in it, you will become the new queen.",
choices = {
{choiceText = "choose who to give the heart to", resultScreen ="chooseHeart" } }
{name = "chooseHeart",
background = ccImages.ccGenericCastleInterior,
images = {
{"princess", ccImages.ccPrincess, 512, 495},
{"boyfriend", ccImages.ccBoyfriend, 243, 452} },
narration = "Do you give the heart to the princess or your boyfriend?",
choices = {
{choiceText = "princess", resultScreen = "youBecomeQueen",
inventoryRemove = "heartBox"},
{choiceText = "boyfriend", resultScreen ="happyInForest",
inventoryRemove = "heartBox" } }
{name = "youBecomeQueen",
background = ccImages.ccGenericCastleInterior,
images = {
{"heroineQueened", ccImages.ccHeroineQueened, 319, 452} },
narration = "You become the queen!",
choices = {
{choiceText = "The End", resultScreen ="firstScreen",
inventoryRemove = "allItems" } }
{name = "happyInForest",
background = ccImages.ccHappyInForest,
images = {
{"happyFamily", ccImages.ccHappyFamily, 326, 323} },
narration = "You and your boyfriend end up living in a shack in the forest with your kids.\n\rAnd you are happy.",
choices = {
{choiceText = "The End", resultScreen ="firstScreen",
inventoryRemove = "allItems" } }
return charlottesChoices
--# ImageSets
ImageSets is not a class, just a tab for holding tables that have image names as keys and all values set to the placeholder value 0. During loadImages in setup, these keys are used to load images from the local Dropbox folder, or, if the images aren't there, to load them remotely from the web.
--image keys for the game selection screen buttons
gameMCImages = {}
gameMCImages.choicesMenuRosie = 0
gameMCImages.choicesMenuCharlotte = 0
--get this on the web!
gameMCImages.choicesMenuBackground = 0
--image keys for Charlotte's game
ccImages = {}
ccImages.ccFirst = 0
ccImages.ccHeroine = 0
ccImages.ccHeroineFlipped = 0
ccImages.ccCoffeeSmall = 0
ccImages.ccCoffeeBig = 0
ccImages.ccGenericOutside = 0
ccImages.ccBoyfriend = 0
ccImages.ccKnightScreen = 0
ccImages.ccHeartBox = 0
ccImages.ccQueenBored = 0
ccImages.ccPuppeteerGrumpy = 0
ccImages.ccPuppeteerHappy = 0
ccImages.ccQueenNotLike = 0
ccImages.ccHeroineWithSockPuppets = 0
ccImages.ccHomeAndSleep = 0
ccImages.ccQueenLoves = 0
ccImages.ccHeroineWithMarionettes = 0
ccImages.ccGenericCastleInterior = 0
ccImages.ccPrincess = 0
ccImages.ccHeroineQueened = 0
ccImages.ccHappyInForest = 0
ccImages.ccHappyFamily = 0
--image keys for Rosie's game
rcImages = {}
rcImages.rcFirst = 0
rcImages.theAdventureIs = 0
rcImages.rosiePlaceholder = 0
rcImages.rcSleepForDays = 0
rcImages.rcBoyfriendAsks = 0
rcImages.rcBoyfriendWithNote = 0
rcImages.rcBreakup = 0
rcImages.rcHelpEverybody = 0
rcImages.rcQueenAsksYouToDeliver = 0
rcImages.rcDeliverAllInvitations = 0
rcImages.rcEverybodysDisappointed = 0
rcImages.rcSecretOrQueen = 0
rcImages.rcTellYourSecret = 0
rcImages.rcYoureQueen = 0
--# GameMC
GameMC = class()
GameMC creates a menu screen for launching TwoChoiceAdventure games. It will automatically generate menu buttons for every game added by the addGame method. The automatically-generated screen is hideous, though, so there are also a couple methods included for customizing it. This class also adds an "exit game" button to the first screen of every game it runs.
function GameMC:init()
--make the games table = {}
--create an instance of the Screen class to become the selection screen
self.gameSelectionScreen = Screen()
--for setup purposes, instantiate a placeholder game
self.currentGame = TwoChoiceAdventure("placeholder game")
--add the placeholder to self, so an unconfigured selection menu can at least show one button
self:addGame("placeholder game", self.currentGame)
--make a boolean to track if a game is underway
self.gameUnderway = false
--create a table for the "exit game" button (overlaid on the first screen of any running game)
self.exitButton = {}
self.exitButton.image = Utilities:label("exit game", 200, 50)
--set the exit button location to be the middle of the screen (you'll want to customize that)
self.exitButton.x = WIDTH / 2
self.exitButton.y = HEIGHT / 10
function GameMC:draw()
--if a game is not underway, use the draw() method of the selection screen
if self.gameUnderway == false then
--otherwise use the current game's draw() method
function GameMC:touched(touch)
--if a game is not underway, use the touched(touch) method of the selection screen
if self.gameUnderway == false then
--otherwise use the current game's touched(touch) method
function GameMC:addGame(gameName, gameObject)
--if the placeholder game exists, remove it
if Utilities:doesTableHaveStringKey(, "placeholder game") then
self:removeGame("placeholder game")
--add the new game under the given name[gameName] = gameObject
--a count of the current games for measuring button placement
local gameCount = Utilities:numberOfKeyValueElements(
--the height to use for each button
local buttonHeight = (HEIGHT / gameCount)
--create new buttons for each game
for key, value in pairs( do
--make a name for the current game button
local buttonName = key.." Button"
--erase any old one by that name
Utilities:removeValueByStringKey(self.gameSelectionScreen.touchables, buttonName)
--make a new button image
local buttonImage = Utilities:label(key, WIDTH, buttonHeight)
--calculate the y value to use
local buttonY = (gameCount * buttonHeight) - (buttonHeight / 2)
--add it to the game selection screen
self.gameSelectionScreen:addTouchableAt(buttonName, buttonImage, WIDTH / 2, buttonY)
--decrement the game counter so the next label will be placed under the current one
gameCount = gameCount - 1
--make an action for it
local buttonAction = function () self:runGame(key) end
--make a name for the action
local actionName = buttonName.." Action"
--assign the action to the button
self.gameSelectionScreen:assignActionToTouchable(actionName, buttonAction, buttonName)
function GameMC:customGameButton(gameName, newImage, x, y)
--change the button of the given game to use the given image and given x, y coordinates
self.gameSelectionScreen.touchables[gameName.." Button"].image = newImage
self.gameSelectionScreen.touchables[gameName.." Button"].x = x
self.gameSelectionScreen.touchables[gameName.." Button"].y = y
function GameMC:customGameButtonAction(gameName, action)
--a reference to the button name for the given game
local buttonName = gameName.." Button"
--a reference to that button's action's name
local actionName = buttonName.." Action"
--use that action name to assign the passed-in action to that button
self.gameSelectionScreen:assignActionToTouchable(actionName, action, buttonName)
--note: be sure to include runGame(gameName) in the new action!
function GameMC:removeGame(gameName)
--if the game to remove is the currentGame, set currentGame to nil to remove the reference
if self.currentGame ==[gameName] then
self.currentGame = nil
--use a utility function to remove the game from the games table
Utilities:removeValueByStringKey(, gameName)
--lastly, remove the button and button action for the given game on the selection screen
self.gameSelectionScreen:removeTouchableNamed(gameName.." Button")
self.gameSelectionScreen:removeActionNamed(gameName.." Button Action")
function GameMC:runGame(gameName)
--set the internal flag that shows a game is underway
self.gameUnderway = true
--designate the given game as the current game
self.currentGame =[gameName]
--make shorter references to the position where the exit button will be placed
local buttonX = self.exitButton.x
local buttonY = self.exitButton.y
--use them to create a table holding the arguments for adding the exit button
local touchableArgs = {"exit button", self.exitButton.image, buttonX, buttonY}
--use that table to add the exit button to the first screen of the game about to be run
--make a function that runs this class's endGame() function
local exitGame = function () self:endGame() end
--add that function to the given game as the action for the exit button to run
self.currentGame.firstScreen:assignActionToTouchable("exit", exitGame, "exit button")
--set the first screen of the given game to be the current screen, and we're off and running!
function GameMC:endGame()
--set the internal flag that shows that no game is underway
self.gameUnderway = false
--remove the exit button and its action from the first screen of the game
self.currentGame.firstScreen:removeTouchableNamed("exit button")
--# TwoChoiceAdventure
TwoChoiceAdventure = class()
TwoChoiceAdventure is a class that can be made into a simple adventure game. The game consists of a series of screens with a small amount of narration and up to two choices of response. The choices can either change the current screen to a new screen or modify the inventory. Choices can be displayed or hidden dynamically based on inventory state. Screens are defined using the addScreen(...) method. The very first screen must be named "firstScreen".
function TwoChoiceAdventure:init(name)
--check if a name was given
if name == nil then
--if no name was given, print an error message
print("Error: game didn't init with name. TwoChoiceAdventure initialization failed.")
--and exit without initializing
--a default first screen, in case one is never set
local first = Screen()
--a label for it
local firstLabel = Utilities:label(name..": default screen", WIDTH / 2, HEIGHT / 2)
--add the label to the screen
first:addImageAt("default art", firstLabel, WIDTH / 2, HEIGHT / 2)
--declare the set of tables for saving screen definitions
self.screenTables = {}
--store the first screen as a special variable
self.firstScreen = first
--change the current screen to be the firstScreen
--make an Inventory object for tracking inventory
self.inventory = Inventory()
--make a style table to preserve the desired style for drawn text
self.textStyle = StyleTable()
--set the desired style settings
fontSize(HEIGHT / 28)
textWrapWidth(WIDTH / 2.4)
fill(251, 251, 251, 255)
--capture the current style settings the style table
--specify the size and location of the narration box
self.narrationW = WIDTH / 2.15
self.narrationH = HEIGHT / 3.4
self.narrationX = WIDTH - (self.narrationW / 2) - 35
self.narrationY = HEIGHT / 2.9
--specify the size of the choice buttons
self.choiceW = self.narrationW
self.choiceH = HEIGHT / 13.9
--align the horizontal position of the buttons with the narration box
self.choice1X = self.narrationX
self.choice2X = self.choice1X
--specify the gap between choice buttons
self.gapBetweenChoices = HEIGHT / 100
--find the bottom of the narration box
local bottomOfNarration = self.narrationY - (self.narrationH / 2)
--set the vertical positions of the choice buttons
self.choice1Y = bottomOfNarration - (self.choiceH / 2) - self.gapBetweenChoices
self.choice2Y = self.choice1Y - self.gapBetweenChoices - self.choiceH
function TwoChoiceAdventure:addScreens(...)
This method can take any number of arguments, each of which must be a properly constructed screen table. The tables are stored in self.screenTables and used to assemble screens when needed. Screen tables are key-value tables, which use these keys:
name = screen name
background = background image
images (optional) = a table of imageTables, which have the format: {name, image, x, y}
narration = narration text
choices = table of up to two choiceTables, key-value tables with these keys & values:
choice = choice text that appears on the choice button
resultScreen = name of screen shown after button is pressed
inventoryAdd (optional) = table with the keys & values needed for Inventory:addItem
inventoryRemove (optional) = name of inventory item to remove
onlyIfInInventory (optional) = name of item necessary for this choice to be displayed
--iterate through the arguments
for index, screenTable in ipairs(arg) do
--add each argument to self.screenTables using its own "name" value
self.screenTables[] = screenTable
--if the screen is named "firstScreen", assemble it and keep it around
function TwoChoiceAdventure:assembleIfNameIsFirstScreen(screenTable)
--the screen named "firstScreen" has to be kept in memory, so check if this is it
if == "firstScreen" then
--if it is, make it, and assign it to the firstScreen instance variable
self.firstScreen = self:assembleScreen(screenTable)
function TwoChoiceAdventure:assembleScreen(screenTable)
--create a Screen object to be configured
local newScreen = Screen()
--configure it using the values in the given screenTable
self:placeScreenImages(newScreen, screenTable)
self:placeScreenNarration(newScreen, screenTable)
self:createScreenChoices(newScreen, screenTable)
--return it
return newScreen
function TwoChoiceAdventure:changeScreen(newScreenName)
--if returning to the firstScreen, which is kept in memory, nothing has to be assembled
if newScreenName == "firstScreen" then
--set the first screen to be the current screen
self.currentScreen = self.firstScreen
--if going to any other screen, first assemble it using the given name
local assembledScreen = self:assembleScreen(self.screenTables[newScreenName])
--then check if anything needs to happen to it based on inventory state
if assembledScreen.inventoryCheck ~= nil then
--if so, run that function
--replace the current screen with the new screen (allowing the old one to deallocate)
self.currentScreen = assembledScreen
function TwoChoiceAdventure:placeScreenImages(screen, screenTable)
--if no images are defined, bail
if screenTable.images == nil then
--otherwise iterate through each of the image tables in the screenTable
for index, imageTable in ipairs(screenTable.images) do
--and add each image by unpacking its image table directly into addImageAt
function TwoChoiceAdventure:placeScreenNarration(screen, screenTable)
--if no narration is present, bail
if screenTable.narration == nil then
--create a style table for the narration box's fill and stroke
local boxStyle = StyleTable()
--set the desired fill and stroke (which is no stroke at all)
fill(13, 132, 209, 239)
--capture those settings in the style table
--a shorter variable name for covenience
local narration = screenTable.narration
--for readability, make shorter local variables for the needed stored values
local w, h, x, y = self.narrationW, self.narrationH, self.narrationX, self.narrationY
--create an image using the narration settings
local narrationBox = Utilities:label(narration, w, h, nil, nil, self.textStyle, boxStyle)
--place the image on the given screen
screen:addImageAt("narration", narrationBox, x, y)
--designate the narration as the last image to be drawn
function TwoChoiceAdventure:createScreenChoices(screen, screenTable)
--if no choices are present, bail
if screenTable.choices == nil then
--for readability, make shorter local variables for the needed stored values
local textStyle, height, width = self.textStyle, self.choiceH, self.choiceW
--make the style table for the choice button's colors
local buttonStyle = StyleTable()
--set the desired fill and stroke (which is no stroke at all)
fill(197, 17, 20, 239)
--capture those settings in the style table
--make buttons from the screenTable's stored choice settings
for index, choiceTable in ipairs(screenTable.choices) do
--shorter variable name for readability
local choiceText, width, height = choiceTable.choiceText, self.choiceW, self.choiceH
--a name for the current button based on which choice it is ("choice1", "choice2", etc)
local buttonName = "choice"..index
--capture the x and y at which to draw this button
local choiceX, choiceY = self[buttonName.."X"], self[buttonName.."Y"]
--make the button image for the current choice
local buttn = Utilities:label(choiceText, width, height, nil, nil, textStyle, buttonStyle)
--add that image as a touchable
screen:addTouchableAt(buttonName, buttn, choiceX, choiceY)
--create the action function for this button
local actionFunction = function() self:performChoiceTableResults(choiceTable) end
--make a name for the action by appending "action" to the button name
local actionName = buttonName.." action"
--assign the action to the button
screen:assignActionToTouchable(actionName, actionFunction, buttonName)
--if this button depends on an inventory state, make the action that checks for that
self:createInventoryCheckIfNeeded(screen, choiceTable, buttonName)
function TwoChoiceAdventure:performChoiceTableResults(choiceTable)
--play button-pushing sound
sound("Game Sounds One:Menu Back", 0.2)
--check if a result screen was specified
if choiceTable.resultScreen ~= nil then
--if so, make it the current screen
--check if an inventory item should be added
if choiceTable.inventoryAdd ~= nil then
--if so, add it
self.inventory:addItem(, choiceTable.inventoryAdd.icon)
--play fanfare sound
sound("Game Sounds One:Bell 2", 0.2)
--check if an inventory item should be removed
if choiceTable.inventoryRemove ~= nil then
--if so, remove it (note: using the name "allItems" will remove all items)
--play fanfare sound
sound("Game Sounds One:Bell 2", 0.2)
function TwoChoiceAdventure:createInventoryCheckIfNeeded(screen, choiceTable, buttonName)
--check if the given choiceTable has a value for "onlyIfInInventory"
if choiceTable.onlyIfInInventory == nil then
--if not, return
--compose a function that checks for the item and hides or reveals the button appropriately
local inventoryCheck = function ()
--store a boolean for whether or not the necessary inventory item is present
local itemPresent = self.inventory:checkForItem(choiceTable.onlyIfInInventory)
--store a boolean for whether or not the screen currently has a hidden button
local visibleButton = screen.hiddenButton == nil
--store a boolean that tells if the item and button are both showing
local correctShownState = itemPresent and visibleButton
--store a boolean that tells if neither the item nor the button are showing
local correctHiddenState = (not itemPresent) and (not visibleButton)
--if either both-showing or both-not-showing is true, things are the way they should be
if correctShownState or correctHiddenState then
--otherwise, check if the state is button-hidden-but-item-present
elseif not visibleButton then
--in which case, assume the inventory is in the correct state, and show the button
self:restoreHiddenButtonUsingName(screen, buttonName)
--and conversely, if item is not present, also obey inventory state, and hide button
self:hideButton(screen, buttonName)
--assign the function thus created to the inventoryCheck value of the screen given
screen.inventoryCheck = inventoryCheck
function TwoChoiceAdventure:hideButton(screen, buttonName)
--store the touchable with the given buttonName under the hiddenButton value
screen.hiddenButton = screen.touchables[buttonName]
--erase that touchable from the touchables table
screen.touchables[buttonName] = nil
--if the button that was hidden is choice 1 (the bottom button) we're all done, so return
if buttonName == "choice2" then
--if the button that was hidden is choice 1, move choice 2 up to take its place
elseif buttonName == "choice1" then
screen.touchables.choice1 = screen.touchables.choice2
screen.touchables.choice1.x = self.choice1X
screen.touchables.choice1.y = self.choice1Y
function TwoChoiceAdventure:restoreHiddenButtonUsingName(screen, buttonName)
--if there is no hiddenButton key, return
if screen.hiddenButton == nil then
--if the hidden button is choice 2, then put it back on the screen as choice 2
elseif buttonName == "choice2" then
screen.choice2 = screen.hiddenButton
--if the hidden button is choice 1, move the existing choice 1 down to choice 2
elseif buttonName == "choice1" then
screen.touchables.choice2 = screen.touchables.choice1
screen.touchables.choice2.x = self.choice2X
screen.touchables.choice2.y = self.choice2Y
--then return the hidden button to choice 1
screen.touchables.choice1 = screen.hiddenButton
--erase anything that's still assigned to the hiddenButton key
screen.hiddenButton = nil
function TwoChoiceAdventure:draw()
--draw the current screen, being sure to draw the narration last
--draw the inventory (if any)
function TwoChoiceAdventure:touched(touch)
--pass touches to the current screen
--# Screen
Screen = class()
Screen is a basic interactive screen, with methods for adding and removing images, touchable images ("touchables"), actions, and text.
function Screen:init()
--default font specs
--note: the text functions of this class haven't been tested
self.screenFont = "SourceSansPro-Regular"
self.screenFontSize = 34
self.screenFontColor = color(236, 211, 63, 255)
--blank background image & white background color:
self.background = image(WIDTH,HEIGHT)
self.backgroundColor = color(255, 255, 255, 255)
--empty table for images
self.images = {}
--empty table for things that can be touched
self.touchables = {}
--empty table for texts to print
self.texts = {}
--empty table for actions that can be triggered
self.actions = {}
--variable for designating a touchable that has been touched
self.touchedButton = "none"
--color to use for tinting a touched touchable
self.touchedTint = color(103, 108, 134, 255)
function Screen:setFontSpecs(font, size, color)
--define font specs to use for all text
--note: the text functions of this class haven't been tested
self.screenFont = font
self.screenFontSize = size
self.screenFontColor = color
function Screen:setBackground(thisImage)
--store background image
self.background = thisImage
function Screen:setBackgroundColor(thisColor)
--store background color
self.backgroundColor = thisColor
function Screen:addImageAt(imageName, imageToAdd, positionX, positionY)
--if the name given is unique, use it as a key for a table holding the image and position
if Utilities:nameIsUniqueAndNotNil(imageName, self.images, "image") == true then
self.images[imageName] = {}
self.images[imageName].image = imageToAdd
self.images[imageName].x = positionX
self.images[imageName].y = positionY
function Screen:setLastImageToDraw(imageName)
--define the last image to be drawn (not including touchables)
self.lastDrawnImage = self.images[imageName]
function Screen:addTouchableAt(name, touchableImage, positionX, positionY)
--if the name given is unique, use it as a key for a table holding the image and position
if Utilities:nameIsUniqueAndNotNil(name, self.touchables, "touchable") == true then
self.touchables[name] = {}
self.touchables[name].image = touchableImage
self.touchables[name].x = positionX
self.touchables[name].y = positionY
--flag the touchable as active, meaning it responds to touches
self.touchables[name].state = "active"
function Screen:addTextAt(textName, textString, positionX, positionY)
--if the name given is unique, use it as a key for a table holding the text and position
--note: the text functions of this class haven't been tested
if Utilities:nameIsUniqueAndNotNil(textName, self.texts, "text") == true then
self.texts[textName] = {}
self.texts[textName].text = textString
self.texts[textName].x = positionX
self.texts[textName].y = positionY
function Screen:addAction(actionName, actionFunction)
--if the name given is unique, use it as a key for storing the action function
if Utilities:nameIsUniqueAndNotNil(actionName, self.images, "action") == true then
self.actions[actionName] = actionFunction
function Screen:assignActionToTouchable(actionName, actionFunction, touchableName)
--add the given action function under the given action name
self:addAction(actionName, actionFunction)
--define the action of the given touchable using the given action name
self.touchables[touchableName].action = self.actions[actionName]
function Screen:draw()
--draw the preset background color and background image
sprite(self.background, WIDTH/2, HEIGHT/2, WIDTH, HEIGHT)
--draw all the imageTables unless they're the designated lastDrawnImage
for key, imageTable in pairs(self.images) do
--draw the lastDrawnImage, if there is one
if self.lastDrawnImage ~= nil then
sprite(self.lastDrawnImage.image, self.lastDrawnImage.x, self.lastDrawnImage.y)
--draw all the touchables
for key, touchable in pairs(self.touchables) do
--draw the text
function Screen:drawText()
--set aside the current style for use later
--note: the text functions of this class haven't been tested
--set the style according to the stored custom values
--go through the text tables and draw each text at its stored location
for key, textTable in pairs(self.texts) do
text(textTable.text, textTable.x, textTable.y)
--restore the style that was set aside
function Screen:drawUnlessLastImage(imageTable)
--if there is a lastDrawnImage and this is it, return without drawing anything
if self.lastDrawnImage ~= nil and imageTable == self.lastDrawnImage then
--otherwise go ahead and draw this thing
sprite(imageTable.image, imageTable.x, imageTable.y)
function Screen:drawTouchable(touchable)
--if the touchable's state is "active", draw it normally
if touchable.state == "active" then
sprite(touchable.image, touchable.x, touchable.y)
--if the touchable's state is "pressed", apply a tint before drawing it
elseif touchable.state == "pressed" then
sprite(touchable.image, touchable.x, touchable.y)
function Screen:touched(touch)
--iterate through the touchables
for index, touchable in pairs(self.touchables) do
--react if they were touched
self:reactIfTouched(touchable, touch)
function Screen:reactIfTouched(touchable, touch)
--define the touchable's rect
local tWidth, tHeight = touchable.image.width, touchable.image.height
local tX = touchable.x - (touchable.image.width / 2)
local tY = touchable.y - (touchable.image.height / 2)
--check if the touch is outside the touchable's rect
if Utilities:isPointInRect(touch, tX, tY, tWidth, tHeight) == false then
--if so, set the touchable to the "active" state
touchable.state = "active"
--and quit
--otherwise, if the touch has just started, flip the touchable to its "pressed" state
if CurrentTouch.state == BEGAN then
touchable.state = "pressed"
--if the touch has ended, put the touchable back in the "active" state and fire its action
if CurrentTouch.state == ENDED then
touchable.state = "active"
function Screen:removeImageNamed(imageName)
--find the named value in self.images and remove it
Utilities:removeValueByStringKey(self.images, imageName)
function Screen:removeTouchableNamed(touchableName)
--find the named value in self.touchables and remove it
Utilities:removeValueByStringKey(self.touchables, touchableName)
function Screen:removeActionNamed(actionName)
--find the named value in self.actions and remove it
Utilities:removeValueByStringKey(self.actions, actionName)
function Screen:removeTextNamed(textName)
--find the named value in self.texts and remove it
--note: the text functions of this class haven't been tested
Utilities:removeValueByStringKey(self.texts, textName)
function Screen:clearAllImages()
--iterate through all images (not counting images in touchables) and remove them
for index, imageTable in ipairs(self.images) do
table.remove(self.images, index)
--# Inventory
Inventory = class()
Inventory maintains a set of items and can draw their icons to the screen. Currently it is only configured to display these icons in one specific area of the screen, namely the lower third.
function Inventory:init()
--the set of items (must be an ordered table so icons display in consistent order)
self.items = {}
--the arbitrary setting for space between icons
self.spaceBetweenIcons = WIDTH / 9
function Inventory:addItem(name, icon)
--prevent an item from being named the reserved term "allItems"
if name == "allItems" then
print("Error: cannot name inventory item \"allItems\". Item not added.")
--make a new table for this item
local itemTable = {}
--store the name and the icon in it = name
itemTable.icon = icon
--add the table to self.items
table.insert(self.items, itemTable)
function Inventory:checkForItem(itemName)
--booleans for the current search result and the ultimate result
local currentResult, ultimateResult = false, false
--iterate through self.items
for index, itemTable in ipairs(self.items) do
--set the currentResult to true if the names match
currentResult = Utilities:thisIfThisIsThisElseThis(true, itemName,, false)
--if currentResult is true, keep ultimateResult true through all subsequent loops
ultimateResult = ultimateResult or currentResult
--send back the result
return ultimateResult
function Inventory:removeItem(nameOfItem)
--if sent the special term "allItems", remove all items
if nameOfItem == "allItems" then
--otherwise set up a variable to hold the index of the item to remove
local removeAt = 0
--go through all the items
for index, item in ipairs(self.items) do
--set removeAt to the current index if this item has the right name
removeAt = Utilities:thisIfThisIsThisElseThis(index,, nameOfItem, removeAt)
--if removeAt has not been set, the given nameOfItem isn't in the table, so return
if removeAt == 0 then
--if removeAt has been set, remove the item at that index
table.remove(self.items, removeAt)
function Inventory:clearInventory()
--clear the inventory by setting it to an empty table
self.items = {}
function Inventory:draw()
--quit if there are no items to draw
if #self.items == 0 then
--position for first item to draw
local itemY = 103
local itemX = 160
--go through all the items
for index, item in ipairs(self.items) do
--draw item at the given position
sprite(item.icon, itemX, itemY)
--scoot the position to the right for the next item
itemX = itemX + self.spaceBetweenIcons + (item.icon.width / 2)
--# StyleTable
StyleTable = class()
A StyleTable can preserve the current style settings and restore them when requested.
function StyleTable:init()
--create an empty style table = {}
--initialize with whatever the current style values are
function StyleTable:apply()
--go through all the style table's keys and values
for key, value in pairs( do
--use a key as a command for setting its value
function StyleTable:captureCurrentStyle()
--store all the current style settings = {stroke()} = {strokeWidth()} = {fill()} = {font()} = {fontSize()} = {textWrapWidth()} = {textAlign()} = {textMode()}
--(manually set any variable to nil to prevent it from being changed when apply() is called)
--# Utilities
Utilities = class()
Utilities contains various handy tools. It is not meant to be instantiated. Its functions are meant to be accessed as class methods. The functions have very self-explanatory names.
function Utilities:doesTableHaveStringKey(tableToCheck, stringKey)
--make a boolean for the given string being in the given table
local tableHasKey = false
--iterate through the table
for thisKey, thisValue in pairs(tableToCheck) do
--set the boolean to true if the key is found
tableHasKey = Utilities:thisIfThisIsThisElseThis(true, thisKey, stringKey, tableHasKey)
--return the boolean
return tableHasKey
function Utilities:stringKeyInThisTableForThisValue(tableToCheck, valueToFind)
--make a variable to hold the string key once found, set to an error message to start with
local stringKey = "string key not found"
--iterate through the table
for thisKey, thisValue in pairs(tableToCheck) do
--grab the key if it is found
stringKey = Utilities:thisIfThisIsThisElseThis(thisKey, thisValue, valueToFind, stringKey)
--return the key/error message
return stringKey
function Utilities:removeValueByStringKey(tableToCheck, keyString)
--find out if the key exists (without this method, if key doesn't exist you'll get an error)
if Utilities:doesTableHaveStringKey(tableToCheck, keyString) then
--if it does, nil it out
tableToCheck[keyString] = nil
--if not, report on it (this isn't necessarily an error)
print("can't remove element because nonexistent key: "..keyString)
function Utilities:arbitraryValueFromKeyValueTable(keyValueTable)
--a variable to hold the arbitrary value
local returnMe = nil
--iterate through the table
for key, value in pairs(keyValueTable) do
--grab the first value (unpredictable: pairs(table) doesn't guarantee consistent sorting)
returnMe = value
--stop after first iteration
--send the value back
return returnMen
function Utilities:numberOfKeyValueElements(tableToInspect)
--a variable with which to count the number of key-value elements in a table
local count = 0
--iterate through the table and count the variables
for key, value in pairs(tableToInspect) do
count = count + 1
--return the total
return count
function Utilities:nameIsUniqueAndNotNil(mightBeAName, givenTable, elementType)
--a boolean to hold whether or not the name is unique and not nil
local uniqueAndNotNil = true
--verify that a name has been given
if mightBeAName == nil then
print("error: can't check for name because mightBeAName is nil")
uniqueAndNotNil = false
--make sure name isn't used yet
if uniqueAndNotNil and Utilities:doesTableHaveStringKey(givenTable, mightBeAName) then
print("error: "..mightBeAName.." is a name already in use")
uniqueAndNotNil = false
--return the result
return uniqueAndNotNil
--label-creation function: only the first three parameters are mandatory
function Utilities:label(lText, width, height, textX, textY, textStyleTable, labelStyleTable)
--save current style settings
--if a StyleTable object has been passed in for the label, use it, otherwise use defaults
if labelStyleTable ~= nil then
stroke(126, 127, 185, 255)
fill(55, 70, 110, 255)
--make a blank image for this label
local thisLabel = image(width, height)
--make all drawing happen on the image
--set an x value for the text if specified, otherwise center it
local actualX
if textX ~= nil then
actualX = textX
actualX = width / 2
--set an x value for the text if specified, otherwise center it
local actualY
if textY ~= nil then
actualY = textY
actualY = height / 2
--draw the label rect
rect(0, 0, width, height)
--if a StyleTable object has been passed in for the text, use it, otherwise use defaults
if textStyleTable ~= nil then
fill(192, 192, 218, 255)
--draw the text to the image
text(lText, width / 2, height / 2)
--set drawing back to happening on the device screen
--restore the current style settings
--return the label
return thisLabel
--check if a given point is inside a rect
function Utilities:isPointInRect(point, rectX, rectY, rectWidth, rectHeight)
--a variable to return, initially stating the point to be outside the rect
local pointIsInRect = false
--if the point is inside the rect's values, make the variable true
if point.x > rectX and point.x < rectX + rectWidth and point.y > rectY
and point.y < rectY + rectHeight then
pointIsInRect = true
--send back the result
return pointIsInRect
function Utilities:playSoundAndWaitUntilFinished(soundName)
--start the sound playing and assign it to a variable
local soundMonitor = sound(soundName)
--run an empty loop until the sound is over
while soundMonitor.playing == true do
function Utilities:thisIfThisIsThisElseThis(this, ifThis, isThis, elseThis)
--if the two middle args are equal, return the first arg, otherwise return the last arg
if ifThis == isThis then
return this
return elseThis
--note: this is basically the equivalent of the C syntax "a == b ? c : d"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.