
You need an Arduino (I use a Leonardo in the demo), an Adafruit 128x64 OLED (the 1.3" is my new favorite display!) and a single pushbutton.
I have a video of the action up here: SFCave for Arduino at Vimeo
And you can check out my blog writeup here: SFCave for Arduino at PlanetStephanie
Here's the sketch - try it out, modify it, have fun!
- Code: Select all
/* This is a game based on an old Palm-pilot game that I used to enjoy a long long time ago.
The game was called SFCave - it was simple to learn and not so easy to master, only required
a single button, and I quite enjoyed it. The arduino version is perhaps a little more limited
but I think it's still enjoyable.
The premis is that you are 'flying' through a cave which progressively gets narrower. You cannot
adjust your speed (and it gradually increases as you go), you can only influence your altitude.
Pressing the button sends you upwards, releasing the button lets gravity take you downwards.
There is a little inertia that you have to allow for, and the gravity / antigravity has a tiny bit
of acceleration (it is not constant). The game goes until you crash into the ceiling or floor.
There is no end, the cave will go 'forever'.
Inspired by Sunflat SFCave for Palm http://www2.sunflat.net/en/games/palmcave.html
Written for Arduino by Stephanie Maks http://planetstephanie.net/
Requires an Arduino, a pushbutton, and a 128x64 OLED display from Adafruit http://adafruit.com/
Note: written assuming version 1.0.1 or later of Arduino IDE
*/
#define OLED_DC 11
#define OLED_CS 12
#define OLED_CLK 10
#define OLED_MOSI 9
#define OLED_RESET 13
#define SFC_INERTIA 3 // higher for more inertia, lower for less inertia, 0 for no inertia
#define SFC_MIN_WIDTH 4 // minimum width of cave
#define SFC_MAX_WIDTH 22 // maximum width of cave (total screen width is only 32)
#define SFC_LEVEL_RATE 75 // every N points the cave gets narrower, every 5N points the screen speeds up
#define BUTTON_PIN 7 // pushbutton wired to pin 7 and ground
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
unsigned int sfcScore = 0;
unsigned int sfcHighScore = 0;
char sfcWalls[32][2];
char sfcRibbon[16];
char sfcTrend = 0;
char sfcVelocity = 0;
char sfcWidth = 0;
char sfcMode = 0;
boolean sfcWinner = false;
boolean buttonPressed = false;
Adafruit_SSD1306 oled(OLED_MOSI, OLED_CLK, OLED_DC, OLED_RESET, OLED_CS);
void sfcDrawPixel(int x, int y, boolean n) {
if((sfcMode < 2) && ((y < 4) || (y > 27))) return;
x=x*4;
y=y*2;
oled.drawPixel(x, y, WHITE);
oled.drawPixel(x+1, y+1, WHITE);
oled.drawPixel(x+2, y, WHITE);
oled.drawPixel(x+3, y+1, WHITE);
if(n) {
oled.drawPixel(x, y+1, WHITE);
oled.drawPixel(x+1, y, WHITE);
oled.drawPixel(x+2, y+1, WHITE);
oled.drawPixel(x+3, y, WHITE);
} else {
if(y <= 0) {
oled.drawPixel(x, 0, WHITE);
oled.drawPixel(x+2, 0, WHITE);
} else if(y >= 63) {
oled.drawPixel(x, 63, WHITE);
oled.drawPixel(x+2, 63, WHITE);
}
}
}
void sfcPaintScreen() {
char x, y, z, q;
// the following 5 delays may need to be tweaked to get a desirable / playable speed
// depending on the speed of your Arduino and if you're using hardware spi, software spi
// or i2c for the oled display
// this worked ok for me on 16MHz Leonardo with software SPI for the display
if(sfcScore > (SFC_LEVEL_RATE * 20)) {
q=1;
// delay(0);
} else if(sfcScore > (SFC_LEVEL_RATE * 15)) {
q=2;
delay(10);
} else if(sfcScore > (SFC_LEVEL_RATE*10)) {
q=3;
delay(20);
} else if(sfcScore > (SFC_LEVEL_RATE * 5)) {
q=4;
delay(30);
} else {
q=5;
delay(40);
}
oled.clearDisplay();
for(x=0; x<32; x++) {
// draw the top half of the screen
z = 16 - sfcWalls[x][0];
for(y=(z-q); y<z; y++) {
sfcDrawPixel(x, y, false);
}
// draw the bottom half of the screen
z = 16 - sfcWalls[x][1];
for(y=z; y<(z+q); y++) {
sfcDrawPixel(x, y, false);
}
// draw the ribbon
if(x < 16) {
y = 16 - sfcRibbon[x];
sfcDrawPixel(x, y, true);
}
}
oled.setTextColor(WHITE, BLACK);
oled.setCursor(0, 0);
if(sfcMode != 2) {
if(sfcScore < 10000) oled.write(48); // zero
if(sfcScore < 1000) oled.write(48); // zero
if(sfcScore < 100) oled.write(48); // zero
if(sfcScore < 10) oled.write(48); // zero
oled.print(sfcScore, DEC);
if(sfcWinner) {
oled.setTextColor(BLACK, WHITE);
oled.setCursor(0, 0);
oled.write(32); // space
oled.write(32); // space
oled.print(F("High Score!"));
oled.write(32); // space
oled.write(32); // space
oled.write(32); // space
oled.write(32); // space
oled.write(32); // space
}
oled.setCursor(98, 0);
if(sfcHighScore < 10000) oled.write(48); // zero
if(sfcHighScore < 1000) oled.write(48); // zero
if(sfcHighScore < 100) oled.write(48); // zero
if(sfcHighScore < 10) oled.write(48); // zero
oled.print(sfcHighScore, DEC);
oled.setTextColor(WHITE, BLACK);
} else {
oled.print(sfcScore, DEC);
}
if(sfcMode==0) {
oled.setCursor(37, 56);
oled.print(F("Game Over"));
} else if(sfcMode==1) {
oled.setCursor(10, 57);
oled.print(F("Press Btn to Start"));
}
oled.display();
}
char sfcStepWalls(char lastValue) {
if(random(100) < 30) sfcTrend = sfcTrend * -1;
char newValue = lastValue + sfcTrend;
if(newValue < (-14 + sfcWidth)) {
newValue = (-14 + sfcWidth);
sfcTrend = 1;
}
if(newValue > 14) {
newValue = 14;
sfcTrend = -1;
}
return newValue;
}
void sfcWallsInit() {
char x;
// initialize the ribbon
for(x=0; x<16; x++) {
sfcRibbon[x] = 0;
}
// initialize the screen at max width
sfcWidth = SFC_MAX_WIDTH;
sfcTrend=1;
for(x=0; x<32; x++) {
if(x==0) {
sfcWalls[x][0] = SFC_MAX_WIDTH / 2;
sfcWalls[x][1] = sfcWalls[x][0] - SFC_MAX_WIDTH;
} else {
sfcWalls[x][0] = sfcStepWalls(sfcWalls[x-1][0]);
sfcWalls[x][1] = sfcWalls[x][0] - sfcWidth;
}
}
sfcWinner = false;
sfcMode = 1;
// sfcScore = 0;
sfcVelocity = 2; // start off with a bit of velocity
sfcPaintScreen();
}
void sfcGamePlay() {
char x, y;
char newRibbon = sfcRibbon[15];
boolean testButton = !digitalRead(BUTTON_PIN);
// set velocity
if(testButton) {
sfcVelocity++;
} else {
sfcVelocity--;
}
if(sfcVelocity > SFC_INERTIA) {
sfcVelocity = SFC_INERTIA;
newRibbon++; // increases difficulty!
}
if(sfcVelocity < -SFC_INERTIA) {
sfcVelocity = -SFC_INERTIA;
newRibbon--; // increases difficulty!
}
// get next ribbon position
if(sfcVelocity > 0) newRibbon++;
if(sfcVelocity < 0) newRibbon--;
if(newRibbon > 16) newRibbon = 16;
if(newRibbon < -15) newRibbon = -15;
// get next screen column
char newRoof = sfcStepWalls(sfcWalls[31][0]);
char newFloor = newRoof - sfcWidth;
// advance the screen one column
for(x=0; x<31; x++) {
sfcWalls[x][0] = sfcWalls[x+1][0];
sfcWalls[x][1] = sfcWalls[x+1][1];
if(x<15) sfcRibbon[x] = sfcRibbon[x+1];
}
sfcWalls[31][0] = newRoof;
sfcWalls[31][1] = newFloor;
sfcRibbon[15] = newRibbon;
// test for game over
if(newRibbon >= sfcWalls[15][0]) sfcMode = 0; // crashed!
if(newRibbon <= sfcWalls[15][1]) sfcMode = 0; // crashed!
if(sfcMode == 0) {
buttonPressed=false; // fixes button glitches when game is over
if(sfcScore > sfcHighScore) {
sfcHighScore = sfcScore;
sfcWinner = true;
}
sfcPaintScreen();
delay(500); // little pause to relax and stop button mashing
} else {
sfcPaintScreen();
sfcScore++;
if((sfcWidth > SFC_MIN_WIDTH) && ((sfcScore % SFC_LEVEL_RATE) == 0)) sfcWidth--;
}
}
void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP); // command button
oled.begin(SSD1306_SWITCHCAPVCC);
oled.display();
oled.setTextWrap(false);
randomSeed(analogRead(0)); // set random seed to whatever is on A0
delay(500);
sfcWallsInit();
}
void loop() {
if(sfcMode == 2) {
sfcGamePlay();
} else {
boolean testButton = !digitalRead(BUTTON_PIN);
if(testButton != buttonPressed) {
if(buttonPressed) {
if(sfcMode == 0) {
sfcWallsInit();
} else if(sfcMode == 1) {
sfcMode = 2;
sfcScore = 0;
}
}
buttonPressed = testButton;
}
}
}
Cheers!

