/*
Hello! This is a game console! If you want to add a game, follow these 4 steps:
1. Add your game to the GAME class.
2. Add your game to the gameList array.
3. Change the size of the gameList array.
4. Call an object of your game class and invoke the main method.
The program already includes the NewGame class, which you can remove. It serves as an example of how to correctly add a game.
*/
#include <Arduino.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
// Define screen width and height
#define OLED_WIDTH 128
#define OLED_HEIGHT 64
// Define pins
#define SIO 2
#define SJO 3
#define IO 7
// Initialize the OLED display object
Adafruit_SSD1306 display(OLED_WIDTH, OLED_HEIGHT, &Wire, -1);
// Control class to handle all input devices
class Control {
// Define joystick range
#define JOSTICK_MIN 123
#define JOSTICK_MAX 900
// Define joystick directions
#define UP 1
#define DOWN 2
#define RIGHT 1
#define LEFT 2
// Define pins for horizontal and vertical directions
#define HOR A0
#define VER A1
public:
// Function to read joystick input and determine direction
uint8_t Jostick(uint8_t pin) {
if (analogRead(pin) >= JOSTICK_MAX)
return 2;
if (analogRead(pin) <= JOSTICK_MIN)
return 1;
else
return 0;
}
// Function to check the state of a switch
bool Switch(uint8_t pin) {
if (digitalRead(pin) == HIGH)
return false;
if (digitalRead(pin) == LOW)
return true;
}
// Function to check the state of a switch and handle debouncing
bool SwitchIO(uint8_t pin, uint8_t time) {
if (Switch(pin)) {
delay(time);
if (Switch(pin))
return true;
else
return false;
}
}
};
// 1. GAME Class: Add your games here!
class Game {
public:
class SnakeGame {
Control contr; // Instance of the Control class to handle input
// Structure defining the properties of the Snake game
struct Snake {
// Constants for game logic
#define MAX_LENGTH 25
#define SNAKE_SIZE 7
#define APPLE_SIZE 4
int snake[MAX_LENGTH][2]; // Array to store snake segments
int length; // Length of the snake
int vectorX; // X-component of the snake's movement vector
int vectorY; // Y-component of the snake's movement vector
int vector; // Current direction of the snake (1: left, 2: right, 3: down, 4: up)
int apple[2]; // Array to store the position of the apple
int speed; // Speed of the snake
int move; // Move increment for the snake
bool gameOver; // Flag to indicate if the game is over
// Constructor to initialize game state
Snake() : length(2), vectorX(0), vectorY(-8), gameOver(true), move(9), vector(3), speed(200) {
// Initialize snake positions
for(int i = 0; i < MAX_LENGTH; i++) {
snake[i][0] = 0;
snake[i][1] = 0;
}
for (int i = 0; i < length; i++) {
snake[i][0] = OLED_WIDTH / 2;
snake[i][1] = OLED_HEIGHT / 2 + i * SNAKE_SIZE;
}
// Generate a random apple position
do {
apple[0] = random(3, OLED_WIDTH - 3);
apple[1] = random(3, OLED_HEIGHT - 3);
} while(apple[0] % 10 != 0 || apple[1] % 10 != 0);
}
};
public:
// Main function to run the Snake game
void main() {
Snake snake; // Instance of the Snake structure
while(snake.gameOver) {
display.clearDisplay();
delay(snake.speed);
gameControl(snake);
if(!snake.gameOver) break;
gameDraw(snake);
display.display();
}
}
private:
// Function to draw the game on the display
void gameDraw(Snake& snake) {
display.fillRect(snake.snake[0][0] + snake.vectorX, snake.snake[0][1] + snake.vectorY, SNAKE_SIZE, SNAKE_SIZE, WHITE);
for (int i = 0; i < snake.length; i++) {
display.fillRect(snake.snake[i][0], snake.snake[i][1], SNAKE_SIZE, SNAKE_SIZE, WHITE);
}
display.drawCircle(snake.apple[0], snake.apple[1], APPLE_SIZE, WHITE);
}
// Function to control the game logic
void gameControl(Snake& snake) {
// Move the head of the snake
snake.snake[0][0] += snake.vectorX;
snake.snake[0][1] += snake.vectorY;
// Wrap around the screen if the snake goes beyond the edges
snake.snake[0][0] = (snake.snake[0][0] > OLED_WIDTH - 8)
? 0 : (snake.snake[0][0] < 0) ? OLED_WIDTH - SNAKE_SIZE : snake.snake[0][0];
snake.snake[0][1] = (snake.snake[0][1] > OLED_HEIGHT - 8)
? 0 : (snake.snake[0][1] < 0) ? OLED_HEIGHT - SNAKE_SIZE : snake.snake[0][1];
// Read analog joystick input for controlling the snake's direction
if (contr.Jostick(HOR) == LEFT && snake.vector != 2) {
snake.vectorX = snake.move;
snake.vectorY = 0;
snake.vector = 1;
} else if (contr.Jostick(HOR) == RIGHT && snake.vector != 1) {
snake.vectorX = -snake.move;
snake.vectorY = 0;
snake.vector = 2;
}
if (contr.Jostick(VER) == DOWN && snake.vector != 4) {
snake.vectorY = snake.move;
snake.vectorX = 0;
snake.vector = 3;
} else if (contr.Jostick(VER) == UP && snake.vector != 3) {
snake.vectorY = -snake.move;
snake.vectorX = 0;
snake.vector = 4;
}
// Check if the snake has eaten the apple
if (snake.snake[0][0] + SNAKE_SIZE > snake.apple[0] &&
snake.snake[0][0] < snake.apple[0] + APPLE_SIZE * 2 &&
snake.snake[0][1] + SNAKE_SIZE > snake.apple[1] &&
snake.snake[0][1] < snake.apple[1] + APPLE_SIZE * 2) {
snake.length++;
// Generate a new apple location
do {
snake.apple[0] = random(3, OLED_WIDTH - 3);
snake.apple[1] = random(3, OLED_HEIGHT - 3);
} while (snake.apple[0] % 10 != 0 || snake.apple[1] % 10 != 0);
}
// Check for collision with the snake's own body
for (int i = 1; i < snake.length; i++) {
if (snake.snake[i][0] == snake.snake[0][0] && snake.snake[i][1] == snake.snake[0][1])
gameOver(snake);
}
// Update the positions of the snake's body segments
for (int i = snake.length - 1; i > 0; i--) {
snake.snake[i][0] = snake.snake[i - 1][0];
snake.snake[i][1] = snake.snake[i - 1][1];
}
}
// Function to reset the game state
void reset(Snake& snake) {
snake.length = 2;
snake.vectorX = 0;
snake.vectorY = -8;
snake.gameOver = true;
snake.move = 9;
snake.speed = 200;
snake.vector = 3;
for (int i = 0; i < MAX_LENGTH; i++) {
snake.snake[i][0] = 0;
snake.snake[i][1] = 0;
}
for (int i = 0; i < snake.length; i++) {
snake.snake[i][0] = OLED_WIDTH / 2;
snake.snake[i][1] = OLED_HEIGHT / 2 + i * SNAKE_SIZE;
}
// Generate a new apple location
do {
snake.apple[0] = random(3, OLED_WIDTH - 3);
snake.apple[1] = random(3, OLED_HEIGHT - 3);
} while (snake.apple[0] % 10 != 0 || snake.apple[1] % 10 != 0);
}
// Function to handle game over state
void gameOver(Snake& snake) {
int cursor=2, size=50, id=1;
bool exit=true;
while(exit) {
display.display();
display.clearDisplay();
display.setTextColor(WHITE);
display.setTextSize(2,2);
display.setCursor(22, 15);
display.print("Score:");
display.println(snake.length-2);
display.setTextSize(1,2);
display.setCursor(5, 45);
display.print("Exit");
display.print(" Try again");
display.drawRect(cursor, 42, size, 20, WHITE);
if(contr.Jostick(HOR) == RIGHT) {
cursor=2;
size=50;
id=1;
}
if(contr.Jostick(HOR) == LEFT) {
cursor=64;
size=62;
id=2;
}
if(contr.Switch(SJO)) {
if(id==1) snake.gameOver=false, exit=false;
delay(10);
if(id==2) reset(snake), exit=false;
}
}
}
};
class ArkanoidGame {
Control contr; // Instance of the Control class for input handling
// Structure defining properties of the Arkanoid game
struct Arkanoid {
// Constants for game logic
#define BALL_SIZE 4
#define BLOCK_NUMBER 16
#define PAD_WEIGHT 30
#define PAD_HEIGHT 5
int ball[2]; // positionX, positionY
int vector[2]; // vectorX, vectorY
int block[BLOCK_NUMBER][3]; // positionX, positionY, Size
int pad; // Paddle position
int points; // Player's score
bool gameOver; // Flag indicating game over state
// Constructor for initializing the game state
Arkanoid() : pad(49), points(0), gameOver(true) {
ball[0] = 60;
ball[1] = 40;
vector[0] = 1;
vector[1] = 1;
// Initialize block positions and sizes
for (int i = 0; i < BLOCK_NUMBER; i++) {
block[i][0] = i * 10;
block[i][1] = 0;
block[i][2] = 8;
}
}
};
public:
// Main function to run the Arkanoid game
void main() {
Arkanoid arkanoid;
while (arkanoid.gameOver) {
display.clearDisplay();
gameControl(arkanoid);
display.display();
}
}
private:
// Function to control the game logic
void gameControl(Arkanoid& arkanoid) {
// Draw the ball, paddle, and blocks on the display
display.drawCircle(arkanoid.ball[0], arkanoid.ball[1], BALL_SIZE, WHITE);
display.fillRect(arkanoid.pad, 55, 30, 5, WHITE);
for (int i = 0; i < BLOCK_NUMBER; i++) {
display.fillRect(arkanoid.block[i][0], arkanoid.block[i][1], arkanoid.block[i][2], arkanoid.block[i][2], WHITE);
}
// Check for collisions with blocks
for (int i = 0; i < BLOCK_NUMBER; i++) {
if (arkanoid.ball[0] + BALL_SIZE > arkanoid.block[i][0] &&
arkanoid.ball[0] < arkanoid.block[i][0] + arkanoid.block[i][2] * 2 &&
arkanoid.ball[1] + BALL_SIZE > arkanoid.block[0][1] &&
arkanoid.ball[1] < arkanoid.block[i][1] + arkanoid.block[i][2] * 2) {
arkanoid.vector[1] = -arkanoid.vector[1];
arkanoid.block[i][2] = 0; // Set block size to 0 to indicate it's destroyed
}
}
// Move the ball
arkanoid.ball[0] = arkanoid.ball[0] + arkanoid.vector[0];
// Bounce off the walls
if (arkanoid.ball[0] > 123 || arkanoid.ball[0] < 5) {
arkanoid.vector[0] = -arkanoid.vector[0];
}
arkanoid.ball[1] = arkanoid.ball[1] + arkanoid.vector[1];
// Bounce off the ceiling
if (arkanoid.ball[1] < 5) {
arkanoid.vector[1] = -arkanoid.vector[1];
}
// Check if the ball hits the paddle
if (arkanoid.ball[1] == 50 && arkanoid.ball[0] > arkanoid.pad && arkanoid.ball[0] < arkanoid.pad + PAD_WEIGHT) {
arkanoid.vector[1] = -arkanoid.vector[1];
arkanoid.points++;
}
// Check if the ball goes below the paddle (game over condition)
if (arkanoid.ball[1] > 64) {
gameOver(arkanoid);
}
// Move the paddle based on joystick input
if (contr.Jostick(HOR) == LEFT) {
arkanoid.pad += 3;
if (arkanoid.pad > 97) {
arkanoid.pad = 97;
}
}
if (contr.Jostick(HOR) == RIGHT) {
arkanoid.pad -= 3;
if (arkanoid.pad < 1) {
arkanoid.pad = 1;
}
}
}
// Function to reset the game state
void reset(Arkanoid& arkanoid) {
arkanoid.pad = 49;
arkanoid.points = 0;
arkanoid.gameOver = true;
arkanoid.ball[0] = 60;
arkanoid.ball[1] = 40;
arkanoid.vector[0] = 1;
arkanoid.vector[1] = 1;
// Reset block positions and sizes
for (int i = 0; i < BLOCK_NUMBER; i++) {
arkanoid.block[i][0] = i * 10;
arkanoid.block[i][1] = 0;
arkanoid.block[i][2] = 8;
}
}
// Function to handle the game over state
void gameOver(Arkanoid& arkanoid) {
int cursor = 2, size = 50, id = 1;
bool exit = true;
while (exit) {
display.display();
display.clearDisplay();
display.setTextColor(WHITE);
display.setTextSize(2, 2);
display.setCursor(22, 15);
display.print("Score:");
display.println(arkanoid.points);
display.setTextSize(1, 2);
display.setCursor(5, 45);
display.print("Exit");
display.print(" Try again");
display.drawRect(cursor, 42, size, 20, WHITE);
// Handle joystick input for menu navigation
if (contr.Jostick(HOR) == RIGHT) {
cursor = 2;
size = 50;
id = 1;
}
if (contr.Jostick(HOR) == LEFT) {
cursor = 64;
size = 62;
id = 2;
}
// Handle button press to select menu option
if (contr.Switch(SJO)) {
if (id == 1) {
arkanoid.gameOver = false;
exit = false;
}
delay(10);
if (id == 2) {
reset(arkanoid);
exit = false;
}
}
}
}
};
};
// System class to manage the main menu and system operations
class System {
Control &contr;
#define CURSOR_HEIGHT 20
#define CURSOR_MARGIN 2
String gameList[2]; // 3. Don't forget to change the size of the array, and remember it is zero-based!
uint8_t id;
uint8_t gameListSize;
uint8_t cursorLimit;
int8_t cursorPos;
int8_t bgPos;
bool exitMenu;
public:
System() : id(1), cursorPos(2),
cursorLimit(gameListSize * CURSOR_HEIGHT + CURSOR_MARGIN - CURSOR_HEIGHT),
gameListSize(sizeof(gameList) / 6), bgPos(5), exitMenu(true) {
// 2. Add your game to the menu. Remember to change the size of the array!
gameList[0] = "Snake";
gameList[1] = "Arkanoid";
}
// Main function for system operation
void main() {
while (exitMenu) {
delay(100);
pointer();
displayDisplays(bgPos);
turnOFF();
}
}
// Function to turn on the system
void turnON() {
if (contr.SwitchIO(SIO, 20)) {
digitalWrite(IO, HIGH);
delay(200);
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
}
}
// Function to turn off the system
void turnOFF() {
if (contr.SwitchIO(SIO, 20)) {
display.clearDisplay();
display.display();
digitalWrite(IO, LOW);
delay(200);
void (*resetFunc)(void) = 0;
resetFunc();
delay(200);
}
}
// Function to display menu options on the OLED screen
void displayDisplays(uint8_t setElements) {
display.clearDisplay();
for (String name : gameList) {
display.setTextColor(WHITE);
display.setTextSize(1, 2);
display.setCursor(5, setElements);
display.println(name);
setElements += CURSOR_HEIGHT;
}
display.drawRect(2, cursorPos, 120, CURSOR_HEIGHT, WHITE);
display.display();
}
// Function to handle joystick input for menu navigation
void pointer() {
if (contr.Switch(SJO)) {
display.clearDisplay();
menu();
}
if (contr.Jostick(VER) == DOWN) {
cursorPos += CURSOR_HEIGHT;
id++;
if (cursorPos > cursorLimit)
cursorPos = cursorLimit, id = gameListSize;
if (cursorPos > 42)
bgPos -= 60, cursorPos = CURSOR_MARGIN, cursorLimit = ((gameListSize + 1) * CURSOR_HEIGHT + CURSOR_MARGIN) - 100;
}
if (contr.Jostick(VER) == UP) {
cursorPos -= CURSOR_HEIGHT;
id--;
if (cursorPos < CURSOR_MARGIN && bgPos == 5)
cursorPos = CURSOR_MARGIN, id = 1;
if (cursorPos < CURSOR_MARGIN)
bgPos += 60, cursorPos = 42, cursorLimit = gameListSize * CURSOR_HEIGHT + CURSOR_MARGIN - CURSOR_HEIGHT;
}
}
// 4. Function to handle menu selection and navigation. Call your game object here!
void menu() {
Game Game;
switch (id) {
case 1: {
Game::SnakeGame snake;
snake.main();
break;
}
case 2: {
Game::ArkanoidGame arkanoid;
arkanoid.main();
break;
}
default:
Serial.println("Error");
break;
}
}
};
Control contr;
// Setup function to initialize pins and serial communication
void setup() {
Serial.begin(9600);
pinMode(SIO, INPUT_PULLUP);
pinMode(SJO, INPUT_PULLUP);
pinMode(IO, OUTPUT);
pinMode(HOR, INPUT);
pinMode(VER, INPUT);
}
// Main loop function
void loop() {
if (contr.SwitchIO(SIO, 20)) {
// Create a System object, turn on the system, and run the main loop
System system;
system.turnON();
system.main();
}
}