/* Fortune teller program -- PM Wiegand Copyright (C) 2024
Based loosely on the "Open 8 ball" game: https://github.com/omiq/open8ball/blob/master/open8ball.ino
Hardware needed:
Required: oled display, button
Optional: two LED lights, one buzzer
Gameplay:
The title screen displays and prompts user to ask a question (prompt the user to really think hard...)
Once the user has asked the question (get them to say it out loud, it is more fun) they press the button
If lights are activated the LEDs will blink
If buzzer is activated the buzzer will emit random tones
Eventually the response will be displayed. After few seconds (definable below) the title screen displays
and the user is able to ask another question. The timeout period can be skipped by pressing the button.
Modeled after the old magic 8 ball game, the responses are just random, but they don't have to know that!
*/
// oled display
#include <U8g2lib.h> // Include file for the U8g2 library. If wire.h is needed, U6g2lib will include it automatically
//next line uses _2_ meaning paged buffer and use of firstpage/nextpage. Pins are SCK=A5 and SDA=A4 as defined in include file
U8G2_SH1106_128X64_NONAME_2_HW_I2C oled(U8G2_R0, /* reset=*/U8X8_PIN_NONE); //this matches the display being used see include file. we'll call the display 'oled'
//you can change the next statement to reflect the pin your button is actually connected to.
//The other side of the button should be connected to ground.
#define BUTPIN 8
#define BUZACTIVE 1 //change to zero if you don't want noise or don't want to install buzzer
#define BUZPIN 10 //set to any unused pin if buzzer not active
#define LEDACTIVE 1 //change to zero if you don't want to install leds
#define LEDONE 9 //likewise set to any unused pin if leds are not active
#define LEDTWO 12 //ditto
#define RESPDELAY 5000 //milliseconds to show the reponse before returning to title screen
// Fortune Teller Responses (you can add your own if you like)
char *responses[] =
{"It is certain.",
"Decidedly so.",
"Without a doubt.",
"Definitely.",
"Rely on it.",
"As I see it, yes",
"Most likely.",
"Outlook good.",
"Yes.",
"Signs point yes.",
"Reply hazy.",
"Ask again later.",
"Better not say.",
"Cannot predict.",
"Concentrate!",
"Dont count on it",
"My reply is no.",
"Sources say no.",
"Outlook not good",
"Very doubtful.",
"Good question!"} ;
long numResponses = sizeof(responses) / sizeof(responses[0]);
//Helper routines*******************************************
#define myU8G2Display oled //put the name of your display here (match constructor name above)
unsigned int drawCenteredString(unsigned int y, char* myString, bool invert = false, bool underline = false, bool rJustified = false) {
//provides horizontal centering (or right justified) on the display. Returns the starting x-position, which can be used to align other text to the centered text
//to provide black on white lettering set invert to true. To underline set underline to true, rJustified = true means RJ instead of center
//Note because of defaults, this function must be placed above any code that calls it.
unsigned int centered_x = (myU8G2Display.getDisplayWidth() - myU8G2Display.getStrWidth(myString)) / 2; //calculation for centered text
if (rJustified == true) {
centered_x = (myU8G2Display.getDisplayWidth() - myU8G2Display.getStrWidth(myString)); //calculation for right justified text
}
unsigned int boxWidth = myU8G2Display.getStrWidth(myString);
unsigned int boxHeight = myU8G2Display.getMaxCharHeight();
boxHeight = boxHeight + 3;
if (invert == true) {
myU8G2Display.setDrawColor(1);
myU8G2Display.drawBox(centered_x, y, boxWidth, boxHeight);
myU8G2Display.setDrawColor(0);
myU8G2Display.drawStr(centered_x, y, myString);
myU8G2Display.setDrawColor(1);
} else {
myU8G2Display.setDrawColor(1);
myU8G2Display.drawStr(centered_x, y, myString);
}
if (underline == true) {
myU8G2Display.setDrawColor(1);
myU8G2Display.drawHLine(centered_x, y + oled.getAscent() + 2, boxWidth);
}
return centered_x; //return the starting x position of centered string
}
void drawFancyString(unsigned int x, unsigned int y, char* myString, bool invert = false, bool underline = false) {
//provides for inverted or underlined text at any x and y position on the display
//set invert to true for black on white lettering. Set underline to true for white on black underlined text
//because of defaults, this must be placed above any code that calls it.
unsigned int boxWidth = myU8G2Display.getStrWidth(myString);
unsigned int boxHeight = myU8G2Display.getMaxCharHeight();
boxHeight = boxHeight + 3; //adjust this if desired: puts box boundaries a bit outside of text
if (invert == true) {
myU8G2Display.setDrawColor(1); //pixel is on
myU8G2Display.drawBox(x, y, boxWidth, boxHeight);
myU8G2Display.setDrawColor(0); //now set pixel to dark
myU8G2Display.drawStr(x, y, myString); //draw dark on light box background
myU8G2Display.setDrawColor(1); //be nice and set color back to normal
} else {
myU8G2Display.setDrawColor(1); //not inverted so don't need to draw box. Just normal print
myU8G2Display.drawStr(x, y, myString);
}
if (underline == true) {
myU8G2Display.setDrawColor(1);
myU8G2Display.drawHLine(x, y + oled.getAscent() + 2, boxWidth); //adjust if desired, puts underline one pixel below bottom of non-descended text
}
}
unsigned int newLine(unsigned int oldYPos, unsigned int sep = 1) {
//calculates the y position in pixels for a new line, given the position of the old line
unsigned int lineHeight = myU8G2Display.getMaxCharHeight() + sep; // gives a small separation between ines if invert is used, 1 is good for normal text
unsigned int newYPos = oldYPos + lineHeight;
return newYPos;
}
unsigned int centerVertical(unsigned int numLines) {
//allows centering 1 or more lines vertically on the display. Provide the number of lines to center.
//returns the y position for the first line in the group. Use newLine to get the y positions of the other lines
unsigned int lineHeight = myU8G2Display.getMaxCharHeight() + 3;
unsigned int dispUsed = lineHeight * numLines;
unsigned int dispLeft = myU8G2Display.getDisplayHeight() - dispUsed;
unsigned int startYPos = dispLeft / 2;
return startYPos;
}
void timeoutPress(unsigned long msTimeout, byte butPin){
// waits for a button press but automatically proceeds after a specified timeout
unsigned long msStart = millis();
unsigned long msEnd = msStart + msTimeout;
int butPress = HIGH;
do{
butPress = digitalRead(butPin);
} while((butPress == HIGH) && (millis() < msEnd));
butPress = HIGH;
delay(300);
}
// end helper routines**************************************
void setup() {
//Serial.begin(9600);
oled.begin(); // Initialize our display (referred to by our name we gave it, 'oled')
oled.setFont(u8g2_font_t0_11b_te); // choose a suitable font
oled.setFontPosTop(); //needed to use helper routines
pinMode(BUTPIN, INPUT_PULLUP);
if (LEDACTIVE == 1){pinMode(LEDONE, OUTPUT);}
if (LEDACTIVE == 1){pinMode(LEDTWO, OUTPUT);}
unsigned long seed = analogRead(A0);
randomSeed(seed); //comment out this line to have the same sequence every time...
}
void loop() {
int butPress = digitalRead(BUTPIN);
unsigned int yPos;
if(butPress == LOW){
long randResp = random(0,numResponses);
blinkLights();
yPos = centerVertical(1);
oled.firstPage();
do{
oled.drawStr(0,yPos,responses[randResp]);
} while(oled.nextPage());
timeoutPress(RESPDELAY, BUTPIN);
} else {
oled.firstPage();
do{
drawCenteredString(0,"Fortune Teller",true);
yPos = oled.getDisplayHeight() - (2 * oled.getMaxCharHeight());
drawCenteredString(yPos, "Ask a question and");
yPos = newLine(yPos);
drawCenteredString(yPos, "press button.");
} while(oled.nextPage());
}
}
void blinkLights(){
//user distraction...
unsigned int yPos = oled.getDisplayHeight() - oled.getMaxCharHeight();
oled.firstPage();
do{
drawFancyString(0,yPos,"Thinking...");
} while(oled.nextPage());
for (int i=0; i < 6; i++){
if(LEDACTIVE == 1){
digitalWrite(LEDONE, HIGH);
digitalWrite(LEDTWO, LOW);
}
unsigned int myTone = random(1500, 5000);
if (BUZACTIVE == 1){tone(BUZPIN, myTone, 100);}
delay(200);
myTone = random(1500, 5000);
if (BUZACTIVE == 1){tone(BUZPIN, myTone, 100);}
if (LEDACTIVE == 1){
digitalWrite(LEDONE, LOW);
digitalWrite(LEDTWO, HIGH);
}
delay(200);
}
if (LEDACTIVE == 1){
digitalWrite(LEDONE, LOW);
digitalWrite(LEDTWO, LOW);
}
}