//////////////////////////////////////////////////////////////////////////////////
// Adventure Game Framework
//
// Authors: Morrison, J & Wheeler, T
//
// Revision History:
// November 10, 2022 -- Moved state table to FLASH to free up RAM, updated locateState() logic to match
//
//
// Synopsis: This project uses a LCD and switch buttons to implement an adventure
// game using a finite-state implementation.
//
//
// Origination Date: November 1, 2022
// Project: Adventure Game
// Platform: ATMEGA 328P on Arduino Red Board
//
// DEPENDENCIES
//
// Wire.h -- The Arduino I2C driver library
// LiquidCrystalI2C by Frank deBrabander -- The driver to talk to I2C LCDs
//
// BE SURE YOU HAVE USE THE TOOLS->MANAGE LIBRARIES COMMAND TO INSTALL BOTH OF
// THESE LIBRARIES, OR YOUR PROJECT WILL NOT COMPILE.
//
//
//
// PIN USAGE:
//
// A4 & A5 are the I2C interface as follows:
// A4 -- SDA signal to LCD
// A5 -- SCL signal to LCD
//
// D2 -- Key switch A
// D3 -- Key switch B
// D4 -- Key switch C
// D5 -- Key switch D
//
//////////////////////////////////////////////////////////////////////////////////
#include <EEPROM.h>
#include <LiquidCrystal_I2C.h>
#define I2C_ADDR 0x27
#define LCD_COLUMNS 20
#define LCD_LINES 4
LiquidCrystal_I2C m_LCD(I2C_ADDR, LCD_COLUMNS, LCD_LINES);
// Declaration of pin for pushbuttons
#define NUM_KEYS 4
#define KS_A 2
#define KS_B 3
#define KS_C 4
#define KS_D 5
// Key codes
#define KEY_A 1
#define KEY_B 2
#define KEY_C 3
#define KEY_D 4
// Global buffer for text messages
char szBuf[128];
//
// Definition of the SavedGame structure
//
struct SavedGame
{
int m_nLastStateID;
// Various Metas
int m_nTicket;
unsigned m_nRandomID;
// Reserved for CRC check -- MUST be last 2 bytes of structure
unsigned m_CRC;
};
struct SavedGame m_SavedGame;
//
// Calculate the CRC for an object.
//
// The CRC is embedded into the LAST TWO bytes of the object, by definition.
// Data structures should have an INT type reserved as the last element for
// storing this information.
//
//
void CalculateObjectCRC(void* pD, int nSize)
{
int i;
char* pData = (char*) pD;
int* pnCRC;
int nCRC;
for(i=0, nCRC=0 ; i<(nSize-2) ; i++)
{
nCRC = UpdateCRC(*pData, nCRC);
pData++;
}
pnCRC = (int*) pData;
*pnCRC = nCRC;
}
//
// Check the CRC of an object.
//
// The object must be formatted as described under "CalculateObjectCRC" above.
//
// Returns TRUE (1) if the CRC is good, FALSE (0) if it's bad.
//
int CheckObjectCRC(void* pD, int nSize)
{
int i;
char* pData = (char*) pD;
int* pnCRC;
int nCRC;
for(i=0, nCRC=0 ; i<(nSize-2) ; i++)
{
nCRC = UpdateCRC(*pData, nCRC);
pData++;
}
pnCRC = (int*) pData;
if(*pnCRC == nCRC) return 1;
return 0;
}
//
// CCITT-16 CRC calculation routine
//
int UpdateCRC(char c, int crc)
{
int i;
crc = crc ^ c << 8;
for (i = 0; i < 8; i++)
{
if (crc & 0x8000)
crc = crc << 1 ^ 0x1021;
else crc <<= 1;
}
return(crc);
}
//
// Check to see if saved game in EEPROM is valid
// Returns TRUE if valid, FALSE if not
int checkSavedGame()
{
int nResult;
nResult = CheckObjectCRC(&m_SavedGame, sizeof(SavedGame));
return nResult;
}
//
// Save current game state to EEPROM @ location 0
//
void SaveGame()
{
CalculateObjectCRC(&m_SavedGame, sizeof(SavedGame));
EEPROM.put(0, m_SavedGame);
}
//
// Definition of the state structure. This structure is replicated for every possible dimensional location in the game.
//
struct GameState
{
int m_nStateID; // Unique identifier for this particular state, needed to "jump" to this state. Should be zero if not defined.
int (*onEnterState)(int nStateID); // Optional function called on entry into any state, if NULL it is not called.
// The return value is the new StateID, which allows the function to dynamically move
// system into a new state based on its computations. This return value should be
// set to zero if the state is to remain unchanged.
int (*onLeaveState)(int nStateID); // Optional function called on exit from a state, if NULL it is not called.
// The return value is the new StateID, which allows the function to dynamically move
// system into a new state based on its computations. This return value should be
// set to 0 to allow the framework to use nLeaveNextStateID[] to determine the next
// state, or non-zero to force the next state ID.
char m_szLine1Text[32]; // Text to display on each LCD line as the state is entered. Note that
char m_szLine2Text[32]; // this text is displayed first, THEN onEnterState() is called, which will allow
char m_szLine3Text[32]; // dynamic updating of screen text on the fly as computations allow.
char m_szLine4Text[32]; // Empty lines are not displayed.
int nLeaveNextStateID[NUM_KEYS]; // Based on the key button pressed (KEY_A ~ KEY_D), the specified state is dispatched
// by the framework. onLeaveState() is called first -- if onLeaveState() specifies
// a new state, that new state value overrides the default next state ID from this table.
// A value of ZERO for any table entry means that there is no next state ID for that
// button choice.
};
GameState m_CurrentState;
void populateLCD(char* szLines[4] )
{
int i;
m_LCD.clear();
for(i=0;i<4;i++)
{
m_LCD.setCursor(0,i);
m_LCD.print(szLines[i]);
}
}
int checkTicket(int nStateID)
{
if (m_SavedGame.m_nTicket == 0)
{
return 5; // Dummy! No ticket, go get one!
}
return 0;
}
int setTicket(int nStateID)
{
char* szGoodText[4] = {"Great, you now","have a ticket!","",""};
char* szBadText[4] = {"Dummy, you already","have a ticket!","",""};
if (m_SavedGame.m_nTicket == 0)
{
m_SavedGame.m_nTicket = 1;
populateLCD( szGoodText );
delay(1000);
return 2; // Move player back to foyer
}
populateLCD( szBadText );
delay(1000);
return 2; // Move player back to foyer
}
int doRegistration(int nStateID)
{
char* szText[4] = {"Registration done.","","","Returning to foyer."};
char* szBadText[4] = {"Are you trying","to scam us?","I'm calling the","cops on you!"};
if (m_SavedGame.m_nRandomID != 0)
{
populateLCD( szBadText );
delay(2000);
return 12;
}
m_SavedGame.m_nRandomID = random(9999);
sprintf(szBuf,"Registration# %u", m_SavedGame.m_nRandomID);
populateLCD( szText );
m_LCD.setCursor(0,2);
m_LCD.print(szBuf);
delay(2000);
return 2; // Move player back to foyer
}
int checkDone(int nStateID)
{
if (m_SavedGame.m_nRandomID) return 13;
return 0;
}
//
// Definition of a simple game with four states. State 4 is the "Winner."
//
// Note: State 1 is ALWAYS the first condition when the state machine starts running.
//
const GameState m_States[] PROGMEM =
{
// State 1 - standing at front door
{1, NULL,NULL,
"You are at the door",
"of the DMV. Lotsa",
"people here!",
"Choose?",
0,0,2,999},
// State 2 - in the foyer
{2, NULL, NULL,
"Standing in foyer.",
"Left is registration,",
"Ticket machine at",
"Right.",
3,4,6,1},
// State 4 - getting your ticket
{4, setTicket, NULL,
"Standing in the foyer.",
"Left is registration,",
"Ticket machine at",
"Right.",
3,4,6,1},
// State 6 - Dept of mental health
{6, NULL, NULL,
"You entered the men-",
"tal health center.",
"FREE PROBE TODAY",
"the sign says.",
0,0,7,2},
// State 6 - Dept of mental health getting the probe treatment
{7, NULL, NULL,
"OW! That hurts, you",
"yell as the probe",
"is activated.",
"You try to run.",
12,12,12,12},
// State 3 - in line to get registered
{3, checkTicket ,NULL,
"You are now in line",
"to renew your car",
"registration.",
"Choose?",
0,0,8,2},
// State 8 - registration
{8, doRegistration, NULL,
"",
"",
"",
"",
0,0,2,2},
// State 5 - DUMMY, no ticket!
{5, NULL,NULL,
"DUMMY! You need to",
"get a ticket first.",
"",
"(Press any button)",
2,2,2,2},
// State 6 - getting a ticket
{6, setTicket,NULL,
"",
"",
"",
"",
2,2,2,2},
// State 12 - going to jail
{12, NULL,NULL,
"You're in jail.",
"It's cold in here.",
"Gonna be here a",
"while dude!",
0,0,0,0},
// State 999 - Back to parking lot
{999,checkDone,NULL,
"Leaving DMV? Hope",
"the cops don't give",
"you a ticket for",
"expired tags!",
0,0,0,0},
// State 13 - Win game
{13,NULL,NULL,
"Success!",
"Hope you drive",
"safely and watch",
"out for JoCo drivers",
0,0,0,0},
// State 9999 - Dummy state to mark end of table.
{9999,NULL,NULL,
"",
"",
"",
"",
0,0,0,0}
};
//
// scanKeyboard()
//
// Reads the key switches (A~D) and returns zero if no switch pressed, or KEY_A~KEY_D indicating the button pressed.
//
// Notes: 1) This method blocks as long as ANY keys are pressed. A key must be released before its value is returned.
// 2) In the event of multiple, simultaneous key closures, the highest valued key will be returned.
//
char scanKeyboard()
{
char cRetVal;
static unsigned int nRandomSeed;
cRetVal = 0;
nRandomSeed++;
randomSeed(nRandomSeed);
if (digitalRead(KS_A) == LOW)
cRetVal = KEY_A;
if (digitalRead(KS_B) == LOW)
cRetVal = KEY_B;
if (digitalRead(KS_C) == LOW)
cRetVal = KEY_C;
if (digitalRead(KS_D) == LOW)
cRetVal = KEY_D;
delay(50);
// Debounce: User must release ALL keys before a value can be returned.
if (cRetVal)
{
for(;;)
{
if ( (digitalRead(KS_A)==LOW) || (digitalRead(KS_B)==LOW) || (digitalRead(KS_C)==LOW) || (digitalRead(KS_D)==LOW) )
{
delay(50);
continue;
}
Serial.println(int(cRetVal));
return cRetVal;
}
}
return 0;
}
//
// Locate state in table, return NULL if not found, otherwise return pointer
// to GameState structure from the table
//
// 11-10-2022
// Function has been modified to pull table data from FLASH memory.
// This should give you plenty of breathing room on the UNO to implement
// your project.
//
GameState* locateState(int nStateID, GameState* pTable=m_States)
{
GameState* pItem;
int nID;
int nByte;
byte* pRAM_Data;
byte* pFLASH_Data;
if (pTable==NULL) return NULL;
for(pItem=pTable; ;pItem++)
{
pFLASH_Data = (byte*) pItem;
pRAM_Data = (byte*) &m_CurrentState;
// Move one GameState structure to the global variable m_CurrentState
for(nByte = 0; nByte < sizeof(GameState); nByte++, pRAM_Data++, pFLASH_Data++)
{
*pRAM_Data = pgm_read_byte_near(pFLASH_Data);
}
if (m_CurrentState.m_nStateID == 9999)
return NULL; // end of table, so no such state
if (m_CurrentState.m_nStateID == nStateID)
return &m_CurrentState;
}
}
void initializeSavedGame()
{
m_SavedGame.m_nLastStateID = 1;
// Various Metas
m_SavedGame.m_nTicket = 0;
m_SavedGame.m_nRandomID = 0;
// Reserved for CRC check -- MUST be last 2 bytes of structure
m_SavedGame.m_CRC = 0;
}
//
// The one and only stateMachine()
//
//
int nCurrentState;
int nNextState;
GameState* pCurrentItem;
int nResult;
int (*onEnterState)(int nStateID);
int (*onLeaveState)(int nStateID);
char* lcdLines[4];
void stateMachine()
{
char c;
Serial.println("Enter state machine");
EEPROM.get(0,m_SavedGame);
if (CheckObjectCRC(&m_SavedGame,sizeof(SavedGame))==true)
{
Serial.println("Previous game detected: Startup");
m_LCD.clear();
m_LCD.print("Continue previous");
m_LCD.setCursor(0,1);
m_LCD.print("game?");
m_LCD.setCursor(0,3);
m_LCD.print("LEFT:NO RIGHT:YES");
for(;;)
{
c=scanKeyboard();
if (c==KEY_A)
{
initializeSavedGame();
break;
}
if (c==KEY_B)
{
break;
}
}
}
else
{
Serial.println("Initialized game: Startup");
initializeSavedGame();
}
nCurrentState = m_SavedGame.m_nLastStateID;
for(pCurrentItem=locateState(nCurrentState);;)
{
Serial.print("Begin state ");Serial.println(nCurrentState);
m_SavedGame.m_nLastStateID = nCurrentState;
SaveGame(); // Keep game status in EEPROM
if (pCurrentItem == NULL)
{
Serial.println("ERROR - non existent state, aborting.");
return; // ERROR, this state does not exist
}
// Perform actions for current item
nCurrentState = pCurrentItem->m_nStateID;
onEnterState = pCurrentItem->onEnterState;
onLeaveState = pCurrentItem->onLeaveState;
// For this item, push text to LCD
lcdLines[0] = pCurrentItem->m_szLine1Text;
lcdLines[1] = pCurrentItem->m_szLine2Text;
lcdLines[2] = pCurrentItem->m_szLine3Text;
lcdLines[3] = pCurrentItem->m_szLine4Text;
populateLCD(lcdLines);
// Allow onEnterState to act if it is specified
if (onEnterState != NULL)
{
nResult = onEnterState(nCurrentState);
if (nResult)
{
pCurrentItem=locateState(nResult);
continue;
}
}
for(;;)
{
c=scanKeyboard();
//Serial.print(int(c));
if (c) break;
}
//Serial.print("Got key code ");Serial.println(int(c));
// Call onLeaveState first to determine if anything is being overriden
nResult = 0;
if (onLeaveState != NULL)
nResult = onLeaveState(nCurrentState);
if (nResult == 0)
{
// Determine next state, if any, from key pressed, no override
if ((c==KEY_A) && (pCurrentItem->nLeaveNextStateID[0]))
nNextState = pCurrentItem->nLeaveNextStateID[0];
if ((c==KEY_B) && (pCurrentItem->nLeaveNextStateID[1]))
nNextState = pCurrentItem->nLeaveNextStateID[1];
if ((c==KEY_C) && (pCurrentItem->nLeaveNextStateID[2]))
nNextState = pCurrentItem->nLeaveNextStateID[2];
if ((c==KEY_D) && (pCurrentItem->nLeaveNextStateID[3]))
nNextState = pCurrentItem->nLeaveNextStateID[3];
}
else
{
nNextState = nResult;
}
// override from onLeaveState, so go to that new state
//Serial.print("Next state should be ");Serial.println(nNextState);
nCurrentState = nNextState;
// Only try to move to next state if non-zero!
//Serial.println(nCurrentState);
if (nCurrentState != 0)
{
pCurrentItem = locateState(nNextState);
//Serial.println("Located new item.");
//Serial.print("pCurrentItem = ");Serial.println(unsigned(pCurrentItem),HEX);
}
}
}
//
// Setup routine
//
// 1. Starts the LCD display
// 2. Sets up the pushbutton input pins as input_pullup
// 3. Initializes the state machine
//
void setup()
{
Serial.begin(9600);
Serial.println("Begin simulation");
Serial.print("m_States = ");Serial.println(unsigned(m_States),HEX);
// Initialize the I2C LCD display
m_LCD.init(); // NOTE that this code is DIFFERENT than an LCD with a parallel interface
m_LCD.backlight(); // Turn on LCD backlight
m_LCD.backlight(); // Turn on LCD backlight
// Set up pushbutton
pinMode(KS_A, INPUT_PULLUP);
pinMode(KS_B, INPUT_PULLUP);
pinMode(KS_C, INPUT_PULLUP);
pinMode(KS_D, INPUT_PULLUP);
GameState* temp;
temp = locateState(1);
}
//
// Main body of program
//
void loop()
{
// Just light up the state machine...
stateMachine();
}