This simple project uses custom 6 button keyboard(4 direction, Enter and Cancel), arduino and display - easiest HW possibility. SW development went really fast. User interface is in Slovak(example: "Body" means score), cause it is for children.
It is my first published project - if it will be usable for somebody, tell me and I will spend some time for adding comments and translation of messades to EN.
In next step I want to add Li-Po battery and add more games, but as ussuall with using adafruit gfx lib I am out of RAM. So I am thinking of one more old-timer thing - cartridges. But my cartridge will be complete arduino pro micro board.
Right now I am building case.
//#define ShowXY
#include <EEPROM.h>
#define TASKER_MAX_TASKS 5
#include "Tasker.h"
Tasker Tasking;
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH1106.h>
#define OLED_RESET -1
Adafruit_SH1106 display(OLED_RESET);
#include <EasyButton.h>
EasyButton btnEnter(4);
EasyButton btnCancel(7);
EasyButton btnUp(8);
EasyButton btnDown(9);
EasyButton btnLeft(16);
EasyButton btnRight(5);
//#include "progmem.h"
#define BLACK 0
#define WHITE 1
#define MSGBOX_STRING_LEN_MAX 27
#define MSGBOX_STRING_ROW_SIZE 9
#define MSGBOX_STRING_ROWS 3
// MsgBox(F("TbCdEfGhGjKlMnOpRsAuVxYz012"));
void MsgBox(const __FlashStringHelper* conditionalPrint){
// display.clearDisplay();
display.fillRoundRect( 20, 18, 65, 29, 3, BLACK);
display.drawRoundRect( 20, 18, 65, 29, 3, WHITE);
const char PROGMEM *p = (const char PROGMEM *)conditionalPrint;
for(byte i = 0; i < MSGBOX_STRING_LEN_MAX; i++){
display.setCursor( 22 + (i % MSGBOX_STRING_ROW_SIZE) * 7, 20 + (i / MSGBOX_STRING_ROW_SIZE) * 9);
char j = pgm_read_byte(p++);
if(j == 0){break;}
display.print(j);
}
display.display();
}
void MsgBox_Clear(){
//DshBrdWithoutMsgbox = true;
//Screens[ScreenActual].Redraw();
}
void MsgBox(const __FlashStringHelper* conditionalPrint, byte TimeToShow){
MsgBox(conditionalPrint);
Tasking.setTimeout(MsgBox_Clear, TimeToShow * 1000);
}
void MsgBox_RedrawNum(byte Num){
display.setCursor( 46, 29);
display.setTextSize(2);
display.print(Num);
display.setTextSize(1);
display.display();
}
#define PLAYGROUND_WIDTH 100
#define PLAYGROUND_HEIGH 64
#define SNAKE_LEN_MAX 100
#define GEMS_MAX 10
extern byte SnakeHeadPosition_X, SnakeHeadPosition_Y, SnakeLen;
extern byte GameLevel;
extern unsigned long GameScore;
extern byte SnakeBodyPositions_X[];
extern byte SnakeBodyPositions_Y[];
extern byte SnakeBodyPositions_Pointer;
extern byte GemsCount;
extern byte GemPositions_X[];
extern byte GemPositions_Y[];
void PaintCanvas(){
display.clearDisplay();
display.drawRect( 1, 1, PLAYGROUND_WIDTH - 1, PLAYGROUND_HEIGH - 1, WHITE);
display.setCursor( 102, 1);
display.print("Body");
display.setCursor( 103, 38);
display.print("C:");
display.setCursor( 103, 47);
display.print("U");
display.setCursor( 103, 56);
display.print("D");
//display.display();
}
void PaintGameLevel(){
display.setCursor( 109, 47);
if(GameLevel < 100){display.print(":");}
display.print(GameLevel);
}
void PaintGameScore(){
if(GameScore >= 1000){
unsigned short ToDisplay;
ToDisplay = GameScore / 1000;
display.setCursor( 106, 10);
if(ToDisplay < 100){display.print(" ");}
if(ToDisplay < 10){display.print(" ");}
display.print(ToDisplay);
ToDisplay = GameScore % 1000;
display.setCursor( 106, 18);
if(ToDisplay < 100){display.print("0");}
if(ToDisplay < 10){display.print("0");}
display.print(ToDisplay);
}else{
display.setCursor( 106, 11);
if(GameScore < 100){display.print(" ");}
if(GameScore < 10){display.print(" ");}
display.print(GameScore);
}
}
void PaintSnakeLen(){
display.setCursor( 109, 56);
if(SnakeLen < 100){display.print(":");}
display.print(SnakeLen);
}
void PaintRemainTime(){
display.setCursor( 114, 38);
unsigned long RemainTime = Tasking.scheduledIn(LevelEnd) / 1000;
display.print(RemainTime);
if(RemainTime < 10){display.print(" ");}
}
void PaintXY(){
display.setCursor( 3, 3);
display.print("X=");
display.print(SnakeHeadPosition_X);
if(SnakeHeadPosition_X < 10){display.print(" ");}
display.setCursor( 3, 11);
display.print("Y=");
display.print(SnakeHeadPosition_Y);
if(SnakeHeadPosition_Y < 10){display.print(" ");}
for(byte i = 0; i < GemsCount; i++){
display.setCursor( PLAYGROUND_WIDTH - 25, 3 + i * 16);
display.print("X=");
display.print(GemPositions_X[i]);
if(GemPositions_X[i] < 10){display.print(" ");}
display.setCursor( PLAYGROUND_WIDTH - 25, 11 + i * 16);
display.print("Y=");
display.print(GemPositions_Y[i]);
if(GemPositions_Y[i] < 10){display.print(" ");}
}
}
void PaintHead(){
display.fillRect(SnakeBodyPositions_X[SnakeBodyPositions_Pointer] - 1, SnakeBodyPositions_Y[SnakeBodyPositions_Pointer] - 1, 3, 3, BLACK);
display.fillRect(SnakeHeadPosition_X - 1, SnakeHeadPosition_Y - 1, 3, 3, WHITE);
}
void PaintBody(){
if(SnakeLen > 0){
display.fillCircle(SnakeBodyPositions_X[SnakeBodyPositions_Pointer], SnakeBodyPositions_Y[SnakeBodyPositions_Pointer], 1, WHITE);
byte LastBodyPointer;
if(SnakeLen > SnakeBodyPositions_Pointer){
LastBodyPointer = SNAKE_LEN_MAX + SnakeBodyPositions_Pointer - SnakeLen;
}else{
LastBodyPointer = SnakeBodyPositions_Pointer - SnakeLen;
}
display.fillCircle(SnakeBodyPositions_X[LastBodyPointer], SnakeBodyPositions_Y[LastBodyPointer], 1, BLACK);
}
}
void PaintGems(){
if(GemsCount == 1){
display.drawCircle(GemPositions_X[0], GemPositions_Y[0], 1, WHITE);
}else{
bool IsDoubled;
for(byte i = 0; i < GemsCount; i++){
IsDoubled = false;
for(byte j = i + 1; j < GemsCount; j++){
if((GemPositions_X[i] == GemPositions_X[j]) &&(GemPositions_Y[i] == GemPositions_Y[j])){
IsDoubled = true;
}
}
if(IsDoubled){
display.drawRect(GemPositions_X[i] - 1, GemPositions_Y[i] - 1, 3, 3, WHITE);
}else{
display.drawCircle(GemPositions_X[i], GemPositions_Y[i], 1, WHITE);
}
}
}
}
enum GameStates{StartUp, Running, LevelFinished, GameOverS};
enum GameModes{Levels, Continual};
GameStates GameState = StartUp;
GameModes GameMode = Levels;
byte GameLevel;
unsigned long GameScore;
byte SnakeLen;
byte SnakeBodyPositions_X[SNAKE_LEN_MAX];
byte SnakeBodyPositions_Y[SNAKE_LEN_MAX];
byte SnakeBodyPositions_Pointer;
byte GemsCount;
byte GemPositions_X[GEMS_MAX];
byte GemPositions_Y[GEMS_MAX];
byte SnakeHeadPosition_X, SnakeHeadPosition_Y;
enum Direction{None, Left, Right, Up, Down};
Direction SnakeDirection;
void GameNew(){
SnakeHeadPosition_X = PLAYGROUND_WIDTH / 2;
SnakeHeadPosition_Y = PLAYGROUND_HEIGH / 2;
SnakeBodyPositions_X[0] = SnakeHeadPosition_X;
SnakeBodyPositions_Y[0] = SnakeHeadPosition_Y;
SnakeLen = 0;
SnakeBodyPositions_Pointer = 0;
SnakeDirection = None;
GameState = Running;
switch(GameMode){
case Levels: GameNew_ModeL(); break;
case Continual: GameNew_ModeC(); break;
}
}
void GameNew_ModeL(){
GameLevel++;
GemsCount = GameLevel / 10 + 1;
if(GemsCount > GEMS_MAX){GemsCount == GEMS_MAX;}
for(byte i = 0; i < GemsCount; i++){
GemsRandomize2(i);
}
Tasking.setInterval(DoMove, 500 - (GameLevel - 1) * 5);
Tasking.setTimeout(LevelEnd, 60000);
}
void GameNew_ModeC(){
GemsCount = 1;
GemsRandomize2(0);
Tasking.setInterval(DoMove, 400);
Tasking.setInterval(LevelEnd, 30000);
}
void GameOver(){
Tasking.cancel(DoMove);
Tasking.cancel(LevelEnd);
GameState = GameOverS;
MsgBox(F("Game over"));
display.setCursor( 22, 21 + 8);
display.setTextSize(2);
if(GameScore < 1000){display.print(" ");}
if(GameScore < 100){display.print(" ");}
if(GameScore < 10){display.print(" ");}
display.print(GameScore);
display.setCursor( 4, 47);
display.print("Top:");
switch(GameMode){
case Levels:
display.print(LoadTopScore_ModeL());
SaveTopScore_ModeL();
break;
case Continual:
display.print(LoadTopScore_ModeC());
SaveTopScore_ModeC();
}
display.setTextSize(1);
display.display();
}
void LevelEnd(){
if(GameMode == Levels){
Tasking.cancel(DoMove);
Tasking.cancel(LevelEnd);
GameState = LevelFinished;
GameScore += GameLevel * 10;
PaintGameScore();
MsgBox(F(" Uroven zvladnutaStlac OK"));
}else if(GameMode == Continual){
GameLevel++;
PaintGameLevel();
Tasking.setInterval(DoMove, 400);
}
}
void StartNewLevel(){
PaintCanvas();
GameNew();
#ifdef ShowXY
PaintXY();
#endif
PaintGameScore();
PaintRemainTime();
PaintGameLevel();
PaintSnakeLen();
PaintGems();
PaintHead();
display.display();
}
void DoMove(){
if(GameState != Running){
Tasking.cancel(DoMove);
Tasking.cancel(LevelEnd);
MsgBox(F("Chyba 1, restartujte zariadenie"));
return;
}
PaintRemainTime();
if(SnakeDirection == None){return;}
SnakeBodyPositions_Pointer++;
if(SnakeBodyPositions_Pointer == SNAKE_LEN_MAX){SnakeBodyPositions_Pointer = 0;}
SnakeBodyPositions_X[SnakeBodyPositions_Pointer] = SnakeHeadPosition_X;
SnakeBodyPositions_Y[SnakeBodyPositions_Pointer] = SnakeHeadPosition_Y;
switch(SnakeDirection){
case Up: SnakeHeadPosition_Y -= 3; break;
case Down: SnakeHeadPosition_Y += 3; break;
case Left: SnakeHeadPosition_X -= 3; break;
case Right: SnakeHeadPosition_X += 3;
}
for(byte i = 0; i < GemsCount; i++){
if(SnakeHeadPosition_X == GemPositions_X[i] && SnakeHeadPosition_Y == GemPositions_Y[i]){
if(SnakeLen < SNAKE_LEN_MAX){SnakeLen++;}
GameScore += 1 + (SnakeLen >> 1);
PaintGameScore();
PaintSnakeLen();
GemsRandomize2(i);
PaintGems();
}
}
#ifdef ShowXY
PaintXY();
#endif
PaintHead();
PaintBody();
display.display();
if(SnakeHeadPosition_X < 3 || SnakeHeadPosition_X > (PLAYGROUND_WIDTH - 3)){
GameOver();
return;
}
if(SnakeHeadPosition_Y < 3 || SnakeHeadPosition_Y > (PLAYGROUND_HEIGH - 3)){
GameOver();
return;
}
for(byte i = 0; i < SnakeLen; i++){
if(SnakeHeadPosition_X == SnakeBodyPositions_X[SnakeBodyPositions_Pointer - i] && SnakeHeadPosition_Y == SnakeBodyPositions_Y[SnakeBodyPositions_Pointer - i]){
GameOver();
return;
}
}
}
void GemsRandomize2(byte Index){
bool IsOk;
randomSeed(analogRead(0));
do{
IsOk = true;
GemPositions_X[Index] = random(1, 32) * 3 + 2;
GemPositions_Y[Index] = random(1, 20) * 3 + 2;
char Pointer = SnakeBodyPositions_Pointer;
for(byte i = 0; i <= SnakeLen; i++){
if(Pointer + i >= SNAKE_LEN_MAX){Pointer -= SNAKE_LEN_MAX;}
if(i == 0){
if((GemPositions_X[Index] == SnakeHeadPosition_X) && (GemPositions_Y[Index] == SnakeHeadPosition_Y)){
IsOk = false;
break;
}
}else{
if((GemPositions_X[Index] == SnakeBodyPositions_X[SnakeBodyPositions_Pointer - (i - 1)]) && (GemPositions_Y[Index] == SnakeBodyPositions_Y[SnakeBodyPositions_Pointer - (i - 1)])){
IsOk = false;
break;
}
}
}
}while(!IsOk);
}
/*
void GemsRandomize(byte Index){
byte MaxY;
randomSeed(analogRead(0));
do{
GemPositions_X[Index] = random(1, 32) * 3 + 2;
MaxY = 19;
for(byte i = 0; i < SnakeLen; i++){
if(i == 0){
if(GemPositions_X[Index] == SnakeHeadPosition_X){MaxY--;}
}else{
if(GemPositions_X[Index] == SnakeBodyPositions_X[SnakeBodyPositions_Pointer - (i - 1)]){MaxY--;}
}
}
}while(MaxY == 0);
randomSeed(analogRead(1));
//GemPositions_Y[Index] = random(1, 20) * 3 + 2;
GemPositions_Y[Index] = random(1, (MaxY + 1)) * 3 + 2;
for(byte i = 0; i < SnakeLen; i++){
if(i == 0){
if(GemPositions_Y[Index] == SnakeHeadPosition_Y){GemPositions_Y[Index] += 3;}
}else{
if(GemPositions_Y[Index] == SnakeBodyPositions_Y[SnakeBodyPositions_Pointer - (i - 1)]){GemPositions_Y[Index] += 3;}
}
}
}
*/
void StartSnake(){
PaintCanvas();
GameLevel = 0;
GameScore = 0;
GameNew();
#ifdef ShowXY
PaintXY();
#endif
PaintGameScore();
PaintRemainTime();
PaintGameLevel();
PaintSnakeLen();
PaintGems();
PaintHead();
display.display();
}
#define EEPROM_INITED_VALUE 36
#define EEPROM_ADR_TOP_SCORE 10
#define EEPROM_ADR_TOP_SCORE_C 4
byte Stored_GameLevel = 0;
unsigned long Stored_GameScore = 0;
#define EEPROM_STORAGE_SIZE 9
#define EEPROM_ADR_STORAGE 20
#define EEPROM_STORAGE_CELL_SIZE 5
void StoreActualGameToEEPROM(byte Position){
EEPROM.write( EEPROM_ADR_STORAGE + Position * EEPROM_STORAGE_CELL_SIZE, GameLevel);
EEPROM_writelong( EEPROM_ADR_STORAGE + Position * EEPROM_STORAGE_CELL_SIZE + 1, GameScore);
}
void LoadGameFromEEPROM(byte Position){
GameLevel = EEPROM.read( EEPROM_ADR_STORAGE + Position * EEPROM_STORAGE_CELL_SIZE);
GameScore = EEPROM_readlong(EEPROM_ADR_STORAGE + Position * EEPROM_STORAGE_CELL_SIZE + 1);
}
void SaveActualGame(){
Stored_GameLevel = GameLevel;
Stored_GameScore = GameScore;
}
void StartStoredGame(){
GameLevel = Stored_GameLevel;
GameScore = Stored_GameScore;
StartNewLevel();
}
void SaveTopScore_ModeL(){
unsigned long ActualStored = EEPROM_readlong(EEPROM_ADR_TOP_SCORE);
if(GameScore > ActualStored){
EEPROM_writelong(EEPROM_ADR_TOP_SCORE, GameScore);
}
}
unsigned long LoadTopScore_ModeL(){
return EEPROM_readlong(EEPROM_ADR_TOP_SCORE);
}
void SaveTopScore_ModeC(){
unsigned long ActualStored = EEPROM_readlong(EEPROM_ADR_TOP_SCORE_C);
if(GameScore > ActualStored){
EEPROM_writelong(EEPROM_ADR_TOP_SCORE_C, GameScore);
}
}
unsigned long LoadTopScore_ModeC(){
return EEPROM_readlong(EEPROM_ADR_TOP_SCORE_C);
}
void EEPROM_writelong(byte Address, unsigned long Value){
EEPROM.write(Address + 2,highByte(word(Value)));
EEPROM.write(Address + 3,lowByte( word(Value)));
Value = Value >> 16;
EEPROM.write(Address + 0,highByte(word(Value)));
EEPROM.write(Address + 1,lowByte( word(Value)));
}
unsigned long EEPROM_readlong(byte Address){
unsigned long Ret = word(EEPROM.read(Address), EEPROM.read(Address + 1));
Ret = Ret << 16;
Ret = Ret | word(EEPROM.read(Address + 2), EEPROM.read(Address + 3));
return Ret;
}
bool SavingFlag = false;
byte SaveNum = 1;
void LeftPressed() {SnakeDirection = Left;}
void RightPressed() {SnakeDirection = Right;}
void UpPressed(){
if(!SavingFlag){
SnakeDirection = Up;
}else{
if(SaveNum < EEPROM_STORAGE_SIZE){SaveNum++;}
MsgBox_RedrawNum(SaveNum);
}
}
void DownPressed(){
if(!SavingFlag){
SnakeDirection = Down;
}else{
if(SaveNum > 1){SaveNum--;}
MsgBox_RedrawNum(SaveNum);
}
}
void EnterPressed() {
if(SavingFlag){return;}
switch(GameState){
case StartUp:
case GameOverS: StartSnake(); break;
case LevelFinished: StartNewLevel();// break;
}
}
void CancelPressed(){
if(SavingFlag){return;}
if(GameMode != Levels){return;}
switch(GameState){
case LevelFinished: SaveActualGame(); break;
case GameOverS: StartStoredGame();// break;
}
}
void EnterHeld(){
if(SavingFlag){
switch(GameState){
case LevelFinished:
StoreActualGameToEEPROM(SaveNum);
SavingFlag = false;
MsgBox(F("Ulozene Stlac OK"));
break;
case StartUp:
case GameOverS:
LoadGameFromEEPROM(SaveNum);
SavingFlag = false;
GameState = LevelFinished;
MsgBox(F("Nahrane Stlac OK"));
}
}else if(GameState == StartUp || GameState == GameOverS){
if(GameMode == Levels){
GameMode = Continual;
MsgBox(F("Mod bez urovni"));
}else{
GameMode = Levels;
MsgBox(F("Mod s urovnami"));
}
}
}
void CancelHeld(){
if(GameMode != Levels){return;}
if(GameState == LevelFinished){
if(!SavingFlag){
MsgBox(F("Uloz ako:"));
MsgBox_RedrawNum(SaveNum);
SavingFlag = true;
}else{
SavingFlag = false;
MsgBox(F(" Uroven zvladnutaStlac OK"));
}
}else if((GameState == GameOverS) || (GameState == StartUp)){
if(!SavingFlag){
MsgBox(F("Nahrat :"));
MsgBox_RedrawNum(SaveNum);
SavingFlag = true;
}else{
SavingFlag = false;
MsgBox(F("Game over"));
}
}
}
void InitDisplay(){
display.begin(SH1106_SWITCHCAPVCC, 0x3C);
display.clearDisplay();
//display.setRotation(1);
display.setTextWrap(false);
display.setTextColor(WHITE, BLACK);
display.setTextSize(1);
display.setFont();
MsgBox(F("Hra Snake Ready Stlac OK"));
display.display();
}
void InitButtons(){
btnEnter.begin();
btnEnter.onPressed(EnterPressed);
btnEnter.onPressedFor(1000, EnterHeld);
btnCancel.begin();
btnCancel.onPressed(CancelPressed);
btnCancel.onPressedFor(1000, CancelHeld);
btnUp.begin();
btnUp.onPressed(UpPressed);
btnDown.begin();
btnDown.onPressed(DownPressed);
btnLeft.begin();
btnLeft.onPressed(LeftPressed);
btnRight.begin();
btnRight.onPressed(RightPressed);
}
void InitEEPROM(){
if(EEPROM.read(1) != EEPROM_INITED_VALUE){
EEPROM_writelong(EEPROM_ADR_TOP_SCORE, 1);
EEPROM_writelong(EEPROM_ADR_TOP_SCORE_C, 1);
EEPROM.write(1, EEPROM_INITED_VALUE);
}
}
Comments