|
/* *** Note: please use marble_disorder_v2.pde instead *** |
|
*** (see in readme file how to modify the hardware) *** |
|
*** This file is only kept here for historical reasons *** |
|
|
|
Marble Disorder - a borderline case of Marble Madness :) |
|
(if you're not MM-aware, see http://bit.ly/marblemadnessvid). |
|
|
|
It's a marble maze for a 16x2 LCD display, controled by 2 tilt switches. |
|
There's also a potentiometer to control the marble's "spin": |
|
since tilt switches only have 2 options each (N/S and E/W), i.e. there's |
|
no "stay put" option for an axis, sometimes there are 2 paths for the |
|
marble to chose from. In that case - the spin is the tie-breaker between |
|
these options. In other cases it has no effect. |
|
|
|
You can use any Hitachi HD44780 compatible LCD |
|
(Pins are explained at http://www.arduino.cc/en/Tutorial/LiquidCrystal): |
|
rs on pin 2, rw on 3, enable on 4, data on pins 5-8. |
|
Tilt switches are on pins 11 and 12 |
|
(with a 10k ohm pull-up resistors between the pin and gnd). |
|
There's a button on pin 10 (with the same pull-resistor trick), |
|
and a potentiometer on analog pin 1 (outer legs go to +5v and gnd). |
|
|
|
Optional: |
|
You can put a piezo speaker between pin 9 and gnd. |
|
|
|
It beeps whenever there's a spin (i.e. - you can't avoid beeps by |
|
playing better). You can also mute it from code by setting MUTESOUND |
|
to 1 (which is the default :)). |
|
|
|
Why have the speaker and mute it? |
|
Glad you asked: |
|
This box can also run Ariadne (a less physical [more mental?] 1st person |
|
maze - http://bit.ly/lcdmaze). Ariadne only beeps when you hit a wall |
|
(something that can be avoided). |
|
|
|
Enjoy, |
|
@TheRealDod, Dec 1, 2010 |
|
*/ |
|
#include <LiquidCrystal.h> |
|
// LiquidCrystal display |
|
// You can use any Hitachi HD44780 compatible. Wiring explained at |
|
// http://www.arduino.cc/en/Tutorial/LiquidCrystal |
|
LiquidCrystal lcd(2, 3, 4, 5, 6, 7, 8); |
|
|
|
// Tilt switches. Origin at bottom right |
|
// (x is high when you tilt left, y - when you tilt forward) |
|
// You can reverse these assumptions at movePlayer() |
|
const int XTILTPIN = 11; |
|
const int YTILTPIN = 12; |
|
|
|
const int SPEAKERPIN = 9; |
|
|
|
// Uncomment one of the next 2 lines |
|
//const int MUTESOUND = 0; // Beep on spins |
|
const int MUTESOUND = 1; // Peace and quiet |
|
|
|
const int CLOCKWISETONE = 1760; // in Hz |
|
const int COUNTERCLOCKWISETONE = 440; // 2 octaves lower |
|
const int SPINBEEPDURATION = 100; // should be less than MINSTEPDELAY |
|
|
|
const int BUTTONPIN = 10; |
|
const int BUTTONPRESSED = HIGH; // depends on whether button is normally-open/closed |
|
|
|
const int SPINCONTROLPIN = 1; // potentiometer on analog 1 form marble's spin |
|
|
|
const int RANDSEEDPIN = 0; // analog pin o shouldn't be connected to anything |
|
|
|
// Maze can be much larger than this (makeMaze() is pretty fast), |
|
// but this seems to be the right size for a fun game (at least for me). |
|
const int MAZEROWS = 8; |
|
const int MAZECOLS = 16; |
|
|
|
const int DISPLAYROWS = 2; |
|
const int DISPLAYCOLS = 16; |
|
const int PLAYERDISPLAYROW = 1; |
|
const int PLAYERDISPLAYCOL = 8; |
|
|
|
const int MAXSTEPDELAY = 500; // First level is the slowest |
|
const int LEVELDELAYDIFF = 75; // each level, we subtract this from step_delay |
|
const int MINSTEPDELAY = 200; // That's as fast as we go |
|
int step_delay; |
|
|
|
char line_buff[DISPLAYCOLS+1]; // extra char for \0 |
|
|
|
#define BITMASK(x) (1<<(x)) |
|
const byte LEFT=0; |
|
const byte BOTTOM=1; |
|
const byte PLAYER=2; |
|
|
|
char *intro_message[DISPLAYROWS] = { |
|
"Marble Disorder.", |
|
"Click to start."}; |
|
|
|
char *noob_score[DISPLAYROWS] = { |
|
" sec. Click", |
|
"for next level."}; |
|
|
|
char *pro_score[DISPLAYROWS] = { |
|
"s average.", |
|
"Another round?"}; |
|
|
|
// glyphs |
|
char BLANK = ' '; // no need to waste a glyph on that :) |
|
char EXIT = '*'; |
|
char SPIN = '\0'; |
|
|
|
const int NGLYPHS = 8; |
|
// note that glyph 0 can't be used in |
|
// lcd.print() of null-terminated strings |
|
byte glyphs[NGLYPHS][8] = { |
|
// 0: SPIN |
|
{B01101, |
|
B10011, |
|
B00111, |
|
B00000, |
|
B00000, |
|
B00111, |
|
B10011, |
|
B01101} |
|
// 1: BITMASK(LEFT) |
|
,{B10000, |
|
B10000, |
|
B10000, |
|
B10000, |
|
B10000, |
|
B10000, |
|
B10000, |
|
B10000} |
|
// 2: BITMASK(BOTTOM) |
|
,{B00000, |
|
B00000, |
|
B00000, |
|
B00000, |
|
B00000, |
|
B00000, |
|
B00000, |
|
B11111} |
|
// 3: BITMASK(LEFT)|BITMASK(BOTTOM) |
|
,{B10000, |
|
B10000, |
|
B10000, |
|
B10000, |
|
B10000, |
|
B10000, |
|
B10000, |
|
B11111} |
|
// 4: BITMASK(PLAYER) |
|
,{B00000, |
|
B00000, |
|
B00110, |
|
B01011, |
|
B01111, |
|
B00110, |
|
B00000, |
|
B00000} |
|
// 5: BITMASK(PLAYER)|BITMASK(LEFT) |
|
,{B10000, |
|
B10000, |
|
B10110, |
|
B11011, |
|
B11111, |
|
B10110, |
|
B10000, |
|
B10000} |
|
// 6: BITMASK(PLAYER)|BITMASK(BOTTOM) |
|
,{B00000, |
|
B00000, |
|
B00110, |
|
B01011, |
|
B01111, |
|
B00110, |
|
B00000, |
|
B11111} |
|
// 7: BITMASK(PLAYER)|BITMASK(LEFT)|BITMASK(BOTTOM) |
|
,{B10000, |
|
B10000, |
|
B10110, |
|
B11011, |
|
B11111, |
|
B10110, |
|
B10000, |
|
B11111} |
|
}; |
|
|
|
const byte NORTH = 0; |
|
const byte EAST = 1; |
|
const byte SOUTH = 2; |
|
const byte WEST = 3; |
|
|
|
byte maze[MAZEROWS][MAZECOLS]; |
|
int groups[MAZEROWS][MAZECOLS]; |
|
|
|
int player_row; |
|
int player_col; |
|
|
|
// score |
|
const int NOSCORE = -1; // for intro message |
|
unsigned long game_start; // millis |
|
int n_games; |
|
int total_time; |
|
void setup() |
|
{ |
|
//Serial.begin(9600); |
|
pinMode(SPEAKERPIN,OUTPUT); |
|
n_games = total_time = 0; |
|
step_delay = MAXSTEPDELAY; // start slow |
|
randomSeed(analogRead(RANDSEEDPIN)); |
|
for (int i=0; i<NGLYPHS; i++) { |
|
lcd.createChar(i,glyphs[i]); |
|
} |
|
lcd.begin(DISPLAYCOLS,DISPLAYROWS); |
|
initGame(NOSCORE,intro_message); |
|
} |
|
|
|
void loop() { |
|
// Next 2 lines assume origin is at bottom right |
|
// Adjust if you rig the tilt switches in a different way |
|
int xheading = digitalRead(XTILTPIN) ? WEST : EAST; |
|
int yheading = digitalRead(YTILTPIN) ? NORTH : SOUTH; |
|
// Uncomment one of the following lines according to how outer legs go to gnd/+5v |
|
// (look at the game's spin indicator on the right and see if it looks right). |
|
int is_clockwise = analogRead(SPINCONTROLPIN)>511; |
|
//int is_clockwise = analogRead(SPINCONTROLPIN)<512; |
|
int did_spin = movePlayer(xheading, yheading, is_clockwise); |
|
if (did_spin && !MUTESOUND) { |
|
tone(SPEAKERPIN, is_clockwise ? CLOCKWISETONE : COUNTERCLOCKWISETONE, SPINBEEPDURATION); |
|
} |
|
if (!(player_row || player_col)) { // Yay! We won! |
|
int duration = gameDuration()/1000; |
|
total_time += duration; |
|
n_games++; |
|
if (step_delay>MINSTEPDELAY) { |
|
step_delay -= LEVELDELAYDIFF; |
|
initGame(duration,noob_score); |
|
} |
|
else { // "pro" playa, show average |
|
initGame(total_time/n_games,pro_score); |
|
} |
|
} |
|
drawMaze(xheading, yheading, is_clockwise); |
|
delay((did_spin && !MUTESOUND) ? step_delay-SPINBEEPDURATION : step_delay); |
|
} |
|
|
|
void initGame(int score, char *msg[]) { |
|
// displays a message, calls makeMaze(), and waits for a tilt |
|
int xtilt=digitalRead(XTILTPIN); |
|
int ytilt=digitalRead(YTILTPIN); |
|
lcd.clear(); |
|
for (int r=0; r<2; r++) { |
|
lcd.setCursor(0,r); |
|
if (!r && score!=NOSCORE) { |
|
lcd.print(score); |
|
} |
|
lcd.print(msg[r]); |
|
} |
|
makeMaze(); |
|
debugMaze(); |
|
while (digitalRead(BUTTONPIN)!=BUTTONPRESSED) { |
|
delay(50); // wait for button |
|
} |
|
lcd.clear(); |
|
game_start = millis(); |
|
} |
|
|
|
unsigned long gameDuration() { |
|
return millis()-game_start; |
|
} |
|
|
|
int movePlayer(int xheading, int yheading, int is_clockwise) { |
|
int xfree = !(maze[player_row][player_col]&BITMASK(xheading)); |
|
int yfree = !(maze[player_row][player_col]&BITMASK(yheading)); |
|
int is_spin = 0; |
|
if (xfree && yfree) { |
|
is_spin = 1; |
|
// is_clockwise is the tie-breaker. Sets desired modulo diff. |
|
// Assumption: (xheading-yheading)%4 can only be 1 or 3. |
|
int diff_of_spin = is_clockwise ? 1 : 3; |
|
if ((xheading-yheading)%4 == diff_of_spin) { |
|
yfree = 0; // spin prefers xheading |
|
} |
|
else { |
|
xfree = 0; // spin prefers yheading |
|
} |
|
} |
|
if (xfree) { |
|
player_col += (xheading==EAST) ? 1 : -1; |
|
return is_spin; |
|
} |
|
if (yfree) { |
|
player_row += (yheading==SOUTH) ? 1 : -1; |
|
return is_spin; |
|
} |
|
} |
|
|
|
void drawMaze(int xheading, int yheading, int is_clockwise) { |
|
// leave last 2 cols for spin and tilt indication |
|
line_buff[DISPLAYCOLS-2]='\0'; |
|
for (int r=0; r<DISPLAYROWS; r++) { |
|
for (int c=0; c<DISPLAYCOLS-2; c++) { |
|
int absrow=r+player_row-PLAYERDISPLAYROW; |
|
int abscol=c+player_col-PLAYERDISPLAYCOL; |
|
line_buff[c]=rowColChar(absrow,abscol); |
|
} |
|
lcd.setCursor(0,r); |
|
lcd.print(line_buff); |
|
} |
|
// spin indication |
|
lcd.setCursor(DISPLAYCOLS-2,0); |
|
lcd.write(SPIN); |
|
lcd.write(is_clockwise ? '>' : '<'); |
|
// heading indication |
|
lcd.setCursor(DISPLAYCOLS-2,1); |
|
lcd.write(yheading==NORTH ? '^' : 'v'); |
|
lcd.write(xheading==EAST ? '>' : '<'); |
|
} |
|
|
|
//===== aux functions |
|
|
|
|
|
//---- for initGame() |
|
|
|
void makeMaze() { |
|
// initialize |
|
for (int r=0; r<MAZEROWS; r++) { |
|
for (int c=0; c<MAZECOLS; c++) { |
|
maze[r][c] = 15; // walls in all directions |
|
groups[r][c] = r*MAZECOLS+c; // each cell is in a different group |
|
} |
|
} |
|
// break walls |
|
int walls_to_break = MAZEROWS*MAZECOLS-1; |
|
while (walls_to_break--) { |
|
breakWall(); |
|
} |
|
// put player at bottom right |
|
player_row = MAZEROWS-1; |
|
player_col = MAZECOLS-1; |
|
} |
|
|
|
|
|
void breakWall() { |
|
int breaking_west,row1,col1,row2,col2,joinfrom,jointo; |
|
while (1) { |
|
breaking_west=random(2); // decide whether it's a western or a southern wall |
|
if (breaking_west) { |
|
row1 = row2 = random(MAZEROWS); |
|
col1 = 1+random(MAZECOLS-1); // can't break west from 1st col |
|
col2 = col1-1; |
|
if (!(maze[row1][col1]&BITMASK(WEST))) { |
|
continue; |
|
} // no wall to break |
|
joinfrom = groups[row1][col1]; |
|
jointo = groups[row2][col2]; |
|
if (joinfrom==jointo) { |
|
continue; |
|
} // no reason to break this wall |
|
maze[row1][col1] &= ~BITMASK(WEST); |
|
maze[row2][col2] &= ~BITMASK(EAST); |
|
} |
|
else { // breaking south |
|
row1 = random(MAZEROWS-1); // can't break south from last row |
|
row2 = row1+1; |
|
col1 = col2 = random(MAZECOLS); |
|
if (!(maze[row1][col1]&BITMASK(SOUTH))) { |
|
continue; |
|
} // no wall to break |
|
joinfrom = groups[row1][col1]; |
|
jointo = groups[row2][col2]; |
|
if (joinfrom==jointo) { |
|
continue; |
|
} // no reason to break this wall |
|
maze[row1][col1] &= ~BITMASK(SOUTH); |
|
maze[row2][col2] &= ~BITMASK(NORTH); |
|
} |
|
// join the groups |
|
for (int r=0; r<MAZEROWS; r++) { |
|
for (int c=0; c<MAZECOLS; c++) { |
|
if (groups[r][c]==joinfrom) { |
|
groups[r][c] = jointo; |
|
} |
|
} |
|
} |
|
break; |
|
} |
|
} |
|
|
|
//---- for drawMaze() |
|
|
|
// rowColChar deals with all special cases, or calls cellChar otherwise |
|
char rowColChar(int row, int col) { |
|
if (row==-1 && col==0) { // the exit |
|
return EXIT; |
|
} |
|
if (row<-1 || row>MAZEROWS || col<-1 || col>MAZECOLS) { // limbo: unconstructed mazespace ;) |
|
return BLANK; |
|
} |
|
if ((row==-1 || row==MAZEROWS) && (col==-1 || col==MAZECOLS)) { // corners are limbo too |
|
return BLANK; |
|
} |
|
if (row==-1) { // northern wall |
|
return cellChar(BITMASK(SOUTH),0); |
|
} |
|
if (col==MAZECOLS) { // eastern wall |
|
return cellChar(BITMASK(WEST),0); |
|
} |
|
if (row==MAZEROWS) { // southern wall |
|
return cellChar(BITMASK(NORTH),0); |
|
} |
|
if (col==-1) { // western wall |
|
return cellChar(BITMASK(EAST),0); |
|
} |
|
// A real cell (at least in this reality) |
|
return cellChar(maze[row][col],row==player_row&&col==player_col); // a real maze cell |
|
} |
|
|
|
char cellChar(byte walls, byte is_player) { |
|
// looks a bit trivial - now that it doesn't do rotation |
|
byte result=is_player ? BITMASK(PLAYER) : 0; |
|
if (walls & BITMASK(WEST)) { |
|
result |= BITMASK(LEFT); |
|
} |
|
if (walls & BITMASK(SOUTH)) { |
|
result |= BITMASK(BOTTOM); |
|
} |
|
if (result) { |
|
return result; |
|
} |
|
return BLANK; |
|
} |
|
|
|
//---- cheat mode ;) |
|
|
|
void debugMaze() { |
|
for (int r=0; r<MAZEROWS; r++) { |
|
for (int c=0; c<MAZECOLS; c++) { |
|
if (maze[r][c]&BITMASK(WEST)) { |
|
Serial.print("|"); |
|
} |
|
else { |
|
Serial.print(" "); |
|
} |
|
if (maze[r][c]&BITMASK(SOUTH)) { |
|
Serial.print("_"); |
|
} |
|
else { |
|
Serial.print(" "); |
|
} |
|
} |
|
Serial.println('|'); |
|
} |
|
} |