/*
ArduBomber — Bomberman-inspired game for Pro Micro with Arduboy2
Author: RetroSketch2025 (Mehrad) concept; implemented modularly for remixing
Hardware: Pro Micro (ATmega32U4) + Arduboy-compatible 128x64 OLED & buttons
Library: Arduboy2
*/
#include <Arduboy2.h>
#include <ArduboyTones.h>
#include <EEPROM.h>
Arduboy2 arduboy;
ArduboyTones tones(arduboy.audio.enabled);
// ------------------------ Constants ------------------------
enum GameState {
STATE_BOOT,
STATE_MENU,
STATE_OPTIONS,
STATE_LEVEL_INTRO,
STATE_PLAY,
STATE_GAMEOVER,
STATE_SCORES
};
const uint8_t GRID_W = 18; // 18 columns (clips on 128px width)
const uint8_t GRID_H = 6; // 6 rows
const uint8_t TILE = 8; // 8x8 tiles
// Map codes
const uint8_t TILE_EMPTY = 0;
const uint8_t TILE_SOLID = 2; // Non-destroyable (iron/main)
const uint8_t TILE_SOFT = 6; // Destroyable
const uint8_t TILE_ENEMY = 3; // Enemy marker in level layout
// Player tracked separately (code 4 reserved for reference)
const uint8_t PLAYER_CODE = 4;
// Bomb settings
const uint16_t BOMB_FUSE_MS = 4000; // 4 seconds fuse
const uint8_t BLAST_W = 4; // 4x4 blast area width
const uint8_t BLAST_H = 4; // 4x4 blast area height
// Levels
const uint8_t MAX_LEVELS = 8;
const uint8_t BASE_ENEMIES = 2; // Level 1 has 2 enemies
// Level N has BASE_ENEMIES + (N-1) enemies
// EEPROM addresses (simple fixed layout)
const int EEPROM_ADDR_MAGIC = 0;
const int EEPROM_ADDR_SOUND = 1;
const int EEPROM_ADDR_SCORE_A = 2;
const int EEPROM_ADDR_SCORE_B = 6;
const int EEPROM_ADDR_SCORE_C = 10;
const int EEPROM_ADDR_SCORE_E = 14;
// Magic value for save validation
const uint8_t EEPROM_MAGIC = 0xA7;
// Auto-return to menu after scores timeout
const uint16_t SCORES_TIMEOUT_MS = 5000;
// ------------------------ Data structures ------------------------
struct Bomb {
bool active = false;
int8_t gx = 0; // grid x
int8_t gy = 0; // grid y
uint32_t placedMs = 0; // timestamp
};
struct Enemy {
bool alive = false;
int8_t gx = 0;
int8_t gy = 0;
int8_t dir = 0; // 0=left,1=right,2=up,3=down
uint32_t moveTickMs = 0;
};
struct Player {
int8_t gx = 1;
int8_t gy = 1;
};
// ------------------------ Globals ------------------------
GameState state = STATE_BOOT;
uint8_t currentLevel = 1;
bool soundOn = true;
// Scores categories as requested (A:, B:, C:, E:)
uint32_t scoreA = 0;
uint32_t scoreB = 0;
uint32_t scoreC = 0;
uint32_t scoreE = 0;
uint32_t stateEnterMs = 0;
// Map: 18x6 bytes
uint8_t mapGrid[GRID_W * GRID_H];
// Entities
Player player;
Bomb bomb;
const uint8_t MAX_ENEMIES = 12; // safety cap
Enemy enemies[MAX_ENEMIES];
uint8_t enemyCount = 0;
// ------------------------ Sprites (8x8) ------------------------
// Hand-coded bytes (1-bit per pixel, top-left origin)
const uint8_t PROGMEM SPR_PLAYER[8] = {
0b01110000,
0b00111100,
0b01101010,
0b01101010,
0b01111110,
0b10111101,
0b01100110,
0b01110111
};
const uint8_t PROGMEM SPR_ENEMY[8] = {
0b11110010,
0b01011101,
0b01101011,
0b01101010,
0b01111110,
0b10111101,
0b01100110,
0b01110111
};
const uint8_t PROGMEM SPR_BOMB[8] = {
0b01000000,
0b10100000,
0b01011000,
0b00100100,
0b01011010,
0b00100100,
0b00011000,
0b00000000
};
const uint8_t PROGMEM SPR_SOLID[8] = {
0b11111111,
0b10000001,
0b10111101,
0b10000001,
0b10111101,
0b10000001,
0b10111101,
0b11111111
};
const uint8_t PROGMEM SPR_SOFT[8] = {
0b01010101,
0b10101010,
0b01010101,
0b10101010,
0b01010101,
0b10101010,
0b01010101,
0b10101010
};
// ------------------------ Menu Aseprite placeholders ------------------------
// Replace with your exported 128x64 bitmaps from Aseprite.
// For now, we draw text over these dummy images.
const uint8_t PROGMEM BMP_MENU_PLAY[1024] = { /* 128x64 placeholder: all zero */ };
const uint8_t PROGMEM BMP_MENU_OPTIONS[1024] = { /* 128x64 placeholder: all zero */ };
bool menuOnPlay = true; // toggle between PLAY and OPTIONS with up/down or left/right
// ------------------------ Utility ------------------------
uint8_t clampU8(int v, uint8_t lo, uint8_t hi) {
if (v < (int)lo) return lo;
if (v > (int)hi) return hi;
return (uint8_t)v;
}
uint8_t idx(int x, int y) {
return (uint8_t)(y * GRID_W + x);
}
bool inBounds(int x, int y) {
return x >= 0 && y >= 0 && x < GRID_W && y < GRID_H;
}
// ------------------------ EEPROM ------------------------
void saveEEPROM() {
EEPROM.update(EEPROM_ADDR_MAGIC, EEPROM_MAGIC);
EEPROM.put(EEPROM_ADDR_SOUND, soundOn);
EEPROM.put(EEPROM_ADDR_SCORE_A, scoreA);
EEPROM.put(EEPROM_ADDR_SCORE_B, scoreB);
EEPROM.put(EEPROM_ADDR_SCORE_C, scoreC);
EEPROM.put(EEPROM_ADDR_SCORE_E, scoreE);
}
void loadEEPROM() {
uint8_t magic = EEPROM.read(EEPROM_ADDR_MAGIC);
if (magic == EEPROM_MAGIC) {
EEPROM.get(EEPROM_ADDR_SOUND, soundOn);
EEPROM.get(EEPROM_ADDR_SCORE_A, scoreA);
EEPROM.get(EEPROM_ADDR_SCORE_B, scoreB);
EEPROM.get(EEPROM_ADDR_SCORE_C, scoreC);
EEPROM.get(EEPROM_ADDR_SCORE_E, scoreE);
} else {
soundOn = true;
scoreA = scoreB = scoreC = scoreE = 0;
saveEEPROM();
}
}
// ------------------------ Sound helpers ------------------------
void beepPlaceBomb() {
if (!soundOn) return;
tones.tone(600, 60);
}
void beepExplosion() {
if (!soundOn) return;
tones.tone(140, 120);
tones.tone(90, 80);
}
void beepEnemyPop() {
if (!soundOn) return;
tones.tone(440, 60);
}
// ------------------------ Level generation ------------------------
void clearMap() {
for (uint8_t y = 0; y < GRID_H; y++) {
for (uint8_t x = 0; x < GRID_W; x++) {
mapGrid[idx(x,y)] = TILE_EMPTY;
}
}
}
void addBorderSolids() {
for (uint8_t x = 0; x < GRID_W; x++) {
mapGrid[idx(x,0)] = TILE_SOLID;
mapGrid[idx(x,GRID_H-1)] = TILE_SOLID;
}
for (uint8_t y = 0; y < GRID_H; y++) {
mapGrid[idx(0,y)] = TILE_SOLID;
mapGrid[idx(GRID_W-1,y)] = TILE_SOLID;
}
}
// Simple pattern: checker soft blocks leaving player start free
void addSoftBlocksPattern() {
for (uint8_t y = 1; y < GRID_H-1; y++) {
for (uint8_t x = 1; x < GRID_W-1; x++) {
if ((x + y) % 2 == 0) mapGrid[idx(x,y)] = TILE_SOFT;
}
}
// Player start area clear
mapGrid[idx(1,1)] = TILE_EMPTY;
mapGrid[idx(2,1)] = TILE_EMPTY;
mapGrid[idx(3,1)] = TILE_EMPTY;
mapGrid[idx(1,2)] = TILE_EMPTY;
mapGrid[idx(2,2)] = TILE_EMPTY;
mapGrid[idx(1,3)] = TILE_EMPTY;
}
void spawnEnemiesForLevel(uint8_t level) {
enemyCount = clampU8(BASE_ENEMIES + (int)(level - 1), 0, MAX_ENEMIES);
uint8_t placed = 0;
for (uint8_t i = 0; i < MAX_ENEMIES; i++) {
enemies[i].alive = false;
}
// Place enemies in empty tiles away from player start
for (uint8_t y = 1; y < GRID_H-1 && placed < enemyCount; y++) {
for (uint8_t x = GRID_W-2; x >= 1 && placed < enemyCount; x--) {
if (mapGrid[idx(x,y)] == TILE_EMPTY && !(x <= 2 && y <= 2)) {
enemies[placed].alive = true;
enemies[placed].gx = x;
enemies[placed].gy = y;
enemies[placed].dir = (placed % 4);
enemies[placed].moveTickMs = millis();
placed++;
}
if (x == 1) break; // avoid underflow for uint8_t loop
}
}
}
// ------------------------ State management ------------------------
void enterState(GameState s) {
state = s;
stateEnterMs = millis();
}
void startLevel(uint8_t level) {
currentLevel = level;
clearMap();
addBorderSolids();
addSoftBlocksPattern();
player.gx = 1; player.gy = 1;
bomb.active = false;
spawnEnemiesForLevel(level);
enterState(STATE_LEVEL_INTRO);
}
// ------------------------ Movement and collisions ------------------------
bool isWalkable(int gx, int gy) {
if (!inBounds(gx, gy)) return false;
uint8_t t = mapGrid[idx(gx,gy)];
return (t == TILE_EMPTY || t == TILE_SOFT); // walking over soft block allowed? Typically no; set empty only if you want strict.
}
bool isSolidBlock(int gx, int gy) {
if (!inBounds(gx, gy)) return true;
return mapGrid[idx(gx,gy)] == TILE_SOLID;
}
// Enemies move stepwise at intervals
void updateEnemies() {
const uint16_t MOVE_INTERVAL_MS = 300;
for (uint8_t i = 0; i < enemyCount; i++) {
if (!enemies[i].alive) continue;
if (millis() - enemies[i].moveTickMs < MOVE_INTERVAL_MS) continue;
enemies[i].moveTickMs = millis();
int nx = enemies[i].gx;
int ny = enemies[i].gy;
switch (enemies[i].dir) {
case 0: nx--; break;
case 1: nx++; break;
case 2: ny--; break;
case 3: ny++; break;
}
// Bounce if blocked
if (!inBounds(nx, ny) || mapGrid[idx(nx,ny)] == TILE_SOLID || mapGrid[idx(nx,ny)] == TILE_SOFT) {
enemies[i].dir = (enemies[i].dir + 1) % 4;
} else {
enemies[i].gx = nx;
enemies[i].gy = ny;
}
}
}
// If enemy touches player => game over
bool checkPlayerEnemyCollision() {
for (uint8_t i = 0; i < enemyCount; i++) {
if (!enemies[i].alive) continue;
if (enemies[i].gx == player.gx && enemies[i].gy == player.gy) {
return true;
}
}
return false;
}
// ------------------------ Bomb and explosion ------------------------
void placeBomb() {
if (bomb.active) return;
bomb.active = true;
bomb.gx = player.gx;
bomb.gy = player.gy;
bomb.placedMs = millis();
beepPlaceBomb();
}
void explodeBomb() {
if (!bomb.active) return;
// Center blast — destroy the bomb tile itself
uint8_t ¢erTile = mapGrid[idx(bomb.gx, bomb.gy)];
if (centerTile == TILE_SOFT) {
centerTile = TILE_EMPTY;
scoreB += 10;
}
// Check if player is on bomb tile
if (player.gx == bomb.gx && player.gy == bomb.gy) {
enterState(STATE_GAMEOVER);
bomb.active = false;
beepExplosion();
return;
}
// Check if any enemy is on bomb tile
for (uint8_t i = 0; i < enemyCount; i++) {
if (enemies[i].alive && enemies[i].gx == bomb.gx && enemies[i].gy == bomb.gy) {
enemies[i].alive = false;
scoreE += 25;
}
}
// Directions: left, right, up, down
const int dirs[4][2] = { {-1,0}, {1,0}, {0,-1}, {0,1} };
const uint8_t blastLength = 3; // blast range in each direction
for (uint8_t d = 0; d < 4; d++) {
int x = bomb.gx;
int y = bomb.gy;
int dx = dirs[d][0];
int dy = dirs[d][1];
for (uint8_t step = 0; step < blastLength; step++) {
x += dx;
y += dy;
if (!inBounds(x, y)) break;
uint8_t &tile = mapGrid[idx(x, y)];
// Stop at iron wall
if (tile == TILE_SOLID) break;
// Destroy soft block
if (tile == TILE_SOFT) {
tile = TILE_EMPTY;
scoreB += 10;
break; // blast stops after destroying soft block
}
// Check for enemy
for (uint8_t i = 0; i < enemyCount; i++) {
if (enemies[i].alive && enemies[i].gx == x && enemies[i].gy == y) {
enemies[i].alive = false;
scoreE += 25;
}
}
// Check for player
if (player.gx == x && player.gy == y) {
enterState(STATE_GAMEOVER);
bomb.active = false;
beepExplosion();
return;
}
}
}
bomb.active = false;
beepExplosion();
// Check if all enemies are dead
bool anyAlive = false;
for (uint8_t i = 0; i < enemyCount; i++) {
if (enemies[i].alive) {
anyAlive = true;
break;
}
}
if (!anyAlive) {
scoreA += 100;
scoreC += 50;
if (currentLevel < MAX_LEVELS) {
startLevel(currentLevel + 1);
} else {
enterState(STATE_SCORES);
saveEEPROM();
}
}
}
void updateBomb() {
if (!bomb.active) return;
if (millis() - bomb.placedMs >= BOMB_FUSE_MS) {
explodeBomb();
}
}
// ------------------------ Input ------------------------
void handlePlayInput() {
if (arduboy.pressed(LEFT_BUTTON)) {
int nx = player.gx - 1;
if (nx >= 0 && mapGrid[idx(nx, player.gy)] == TILE_EMPTY) player.gx = nx;
}
if (arduboy.pressed(RIGHT_BUTTON)) {
int nx = player.gx + 1;
if (nx < GRID_W && mapGrid[idx(nx, player.gy)] == TILE_EMPTY) player.gx = nx;
}
if (arduboy.pressed(UP_BUTTON)) {
int ny = player.gy - 1;
if (ny >= 0 && mapGrid[idx(player.gx, ny)] == TILE_EMPTY) player.gy = ny;
}
if (arduboy.pressed(DOWN_BUTTON)) {
int ny = player.gy + 1;
if (ny < GRID_H && mapGrid[idx(player.gx, ny)] == TILE_EMPTY) player.gy = ny;
}
if (arduboy.justPressed(A_BUTTON)) {
placeBomb();
}
}
// ------------------------ Rendering ------------------------
void drawTile8(int x, int y, const uint8_t *spr) {
// x,y in pixels
for (uint8_t row = 0; row < 8; row++) {
uint8_t bits = pgm_read_byte(spr + row);
for (uint8_t col = 0; col < 8; col++) {
if (bits & (0x80 >> col)) {
arduboy.drawPixel(x + col, y + row, 1);
}
}
}
}
void drawMap() {
// Clip 18x6 to 128x64; only columns that fit
uint8_t maxCols = min(GRID_W, (uint8_t)(WIDTH / TILE)); // 16 columns visible
uint8_t maxRows = min(GRID_H, (uint8_t)(HEIGHT / TILE)); // 8 rows visible
// Center horizontally if fewer than GRID_W draw
int xOffsetTiles = 0;
if (maxCols < GRID_W) {
// We’ll show leftmost portion; could scroll or center. For simplicity: left aligned.
xOffsetTiles = 0;
}
for (uint8_t gy = 0; gy < maxRows; gy++) {
for (uint8_t gx = 0; gx < maxCols; gx++) {
uint8_t t = mapGrid[idx(gx + xOffsetTiles, gy)];
int px = gx * TILE;
int py = gy * TILE;
if (t == TILE_SOLID) {
drawTile8(px, py, SPR_SOLID);
} else if (t == TILE_SOFT) {
drawTile8(px, py, SPR_SOFT);
}
}
}
// Draw enemies
for (uint8_t i = 0; i < enemyCount; i++) {
if (!enemies[i].alive) continue;
int gx = enemies[i].gx - xOffsetTiles;
if (gx < 0 || gx >= (int)maxCols) continue;
drawTile8(gx * TILE, enemies[i].gy * TILE, SPR_ENEMY);
}
// Draw player
int pgx = player.gx - xOffsetTiles;
if (pgx >= 0 && pgx < (int)maxCols) {
drawTile8(pgx * TILE, player.gy * TILE, SPR_PLAYER);
}
// Draw bomb
if (bomb.active) {
int bgx = bomb.gx - xOffsetTiles;
if (bgx >= 0 && bgx < (int)maxCols) {
drawTile8(bgx * TILE, bomb.gy * TILE, SPR_BOMB);
}
}
}
void drawLevelIntro() {
arduboy.setCursor(30, 28);
arduboy.print(F("LEVEL "));
arduboy.print(currentLevel);
}
void drawMenu() {
arduboy.setCursor(24, 16);
arduboy.print(F("ARDUBOMBER"));
arduboy.setCursor(40, 32);
arduboy.print(menuOnPlay ? F("> PLAY") : F(" PLAY"));
arduboy.setCursor(40, 42);
arduboy.print(!menuOnPlay ? F("> OPTIONS") : F(" OPTIONS"));
}
void drawOptions() {
arduboy.setCursor(36, 20);
arduboy.print(F("OPTIONS"));
arduboy.setCursor(24, 36);
arduboy.print(F("SOUND: "));
arduboy.print(soundOn ? F("ON") : F("OFF"));
arduboy.setCursor(24, 48);
arduboy.print(F("(B to Menu)"));
}
void drawGameOver() {
arduboy.setCursor(32, 26);
arduboy.print(F("GAME OVER"));
}
void drawScores() {
arduboy.setCursor(6, 8);
arduboy.print(F("SCORES:"));
arduboy.setCursor(6, 22);
arduboy.print(F("A: "));
arduboy.print(scoreA);
arduboy.setCursor(6, 32);
arduboy.print(F("B: "));
arduboy.print(scoreB);
arduboy.setCursor(6, 42);
arduboy.print(F("C: "));
arduboy.print(scoreC);
arduboy.setCursor(6, 52);
arduboy.print(F("E: "));
arduboy.print(scoreE);
arduboy.setCursor(74, 54);
arduboy.print(F("L+R reset"));
}
// ------------------------ State update ------------------------
void updateMenu() {
// Toggle selection with up/down or left/right
if (arduboy.justPressed(UP_BUTTON) || arduboy.justPressed(DOWN_BUTTON) ||
arduboy.justPressed(LEFT_BUTTON) || arduboy.justPressed(RIGHT_BUTTON)) {
menuOnPlay = !menuOnPlay;
}
if (arduboy.justPressed(A_BUTTON)) {
if (menuOnPlay) {
startLevel(1);
} else {
enterState(STATE_OPTIONS);
}
}
}
void updateOptions() {
// Toggle sound with A
if (arduboy.justPressed(A_BUTTON)) {
soundOn = !soundOn;
saveEEPROM();
}
// Back to menu with B
if (arduboy.justPressed(B_BUTTON)) {
enterState(STATE_MENU);
}
}
void updateLevelIntro() {
if (millis() - stateEnterMs > 1000) {
enterState(STATE_PLAY);
}
}
void updatePlay() {
handlePlayInput();
updateEnemies();
updateBomb();
if (checkPlayerEnemyCollision()) {
enterState(STATE_GAMEOVER);
}
}
void updateGameOver() {
// Short pause then go to scores
if (millis() - stateEnterMs > 1000) {
enterState(STATE_SCORES);
saveEEPROM();
}
}
void updateScores() {
// Reset scores if LEFT+RIGHT pressed simultaneously
if (arduboy.pressed(LEFT_BUTTON) && arduboy.pressed(RIGHT_BUTTON)) {
scoreA = scoreB = scoreC = scoreE = 0;
saveEEPROM();
}
// Auto-return after timeout
if (millis() - stateEnterMs > SCORES_TIMEOUT_MS) {
enterState(STATE_MENU);
}
}
// ------------------------ Setup & loop ------------------------
void setup() {
arduboy.begin();
arduboy.bootLogo();
arduboy.setFrameRate(60);
loadEEPROM();
enterState(STATE_MENU);
}
void loop() {
if (!arduboy.nextFrame()) return;
arduboy.pollButtons();
arduboy.clear();
switch (state) {
case STATE_MENU:
updateMenu();
drawMenu();
break;
case STATE_OPTIONS:
updateOptions();
drawOptions();
break;
case STATE_LEVEL_INTRO:
updateLevelIntro();
drawLevelIntro();
break;
case STATE_PLAY:
updatePlay();
drawMap();
break;
case STATE_GAMEOVER:
updateGameOver();
drawGameOver();
break;
case STATE_SCORES:
updateScores();
drawScores();
break;
default:
enterState(STATE_MENU);
break;
}
arduboy.display();
}
Comments