Week 08 – Electronics Production#
Week 8 is where everything from the previous weeks starts to become real. We stop designing PCBs and actually make them — mill the copper, solder the components, program the board, and test that it works. The goal was to take the PCB I designed back in Week 6 and produce a fully functional board out of it.
This week was guided by Saheen, Sibin, and Ravisankar.
Group Assignment#
- Characterise the design rules of our in-house PCB production process
- Submit a PCB design to a professional board house
Individual Assignment#
- Make and test an embedded microcontroller system that you designed
Extra Credit Goals
- Make it with another process (e.g., vinyl cutting, etching)
What I Learned#
This week was a big reality check — going from a KiCad file to a physical, working board taught me a lot about the gap between digital design and physical making.
Key things I picked up:
- How the Gerber2PNG → Mods → Roland MDX-20 milling pipeline works end to end
- Why design rule constraints (trace width, clearance, drill size) actually matter in fabrication, not just on paper
- How V-bit tip geometry translates directly to what you configure in Mods
- Why drill hole diameters and tool diameters need a tolerance margin — exact matches cause the toolpath to disappear
- How to use FabStash to request components from the lab inventory
- SMD soldering workflow — thumb rule of smallest component first, working up in height
- How to use a multimeter for continuity checks and polarity verification before powering anything up
- How to set up a new board (XIAO ESP32-C6) in Arduino IDE from scratch using the manufacturer’s wiki
Software Used#
- KiCad — schematic and PCB design (from Week 6)
- Gerber2PNG (FabLab Kerala) — converting Gerber files to PNG for milling
- Mods Project (offline, lab version) — generating toolpaths (RML) for the CNC mill
- Arduino IDE — programming the XIAO ESP32-C6
Machines Used#
- Roland Modela MDX-20 — PCB milling machine
Weekly Schedule#
| Day | What I Did |
|---|---|
| WED | Lecture on electronics production by Saheen Neil |
| THU | Group assignment — characterising mill design rules , Opened PCB design from Week 6, checked trace widths and clearances |
| FRI | Exported Gerber files, ran Gerber2PNG, set up Mods toolpaths , Milled traces, drilled holes, cut outline on Roland MDX-20 |
| SAT | Created the Arduino sketch that is required for testing the PCB |
| SUN | Created the bill of materials (BOM) for the components needed to populate the PCB, used FabStash to request components from the lab inventory |
| MON | Soldering the PCB, tested the Arduino sketch |
| TUE | Regional review |
Group Assignment – Design Rule Characterisation#
For the group assignment, we used KiCad to design a test board that characterised the minimum feature sizes our lab’s milling process can reliably produce. We tested the V-bit, 1/64" end mill, and 1/32" end mill — each one gives you different trace resolutions and clearance limits.
The key values we established from the group assignment:
| Parameter | Value |
|---|---|
| Minimum Trace Width | 0.4 mm |
| Minimum Clearance | 0.4 mm |

These numbers became the constraints I used when checking my own PCB design before fabrication.
Check out the full group documentation here: 🔗 https://fabacademy.org/2026/labs/kochi/group_assignmetns/week08/
Part 1 – Preparing Fabrication Files#
📂 Opening the Week 6 PCB Design#
With the group assignment done and our design rule limits understood, I opened up the PCB I had designed in KiCad during Week 6.
First thing I did was verify that my design was actually within the limits we just characterised:
- Trace width: 0.4 mm ✅ — set this way during Week 6 as instructed
- Clearance: 0.4 mm ✅ — same
Since both values matched the minimum viable specs from the group assignment, the design was good to go for fabrication without any modifications.
📤 Exporting Gerber Files from KiCad#
The first fabrication step is exporting Gerber files — the industry-standard format that encodes every layer of your PCB design.
Layers I exported:#
| Layer | What It Contains |
|---|---|
| F.Cu | Front copper traces |
| B.Cu | Back copper traces |
| Edge.Cuts | Board outline |
| Drill file (.drl) | Hole positions and diameters |
Steps:
Steps to Export Gerber Files:#
- In the PCB Editor, go to File → Fabrication Outputs → Gerbers (.gbr)
- In the Plot dialog:
- Set the Output Directory — I used a
gerber/subfolder in my project - Select the layers you need:
- F.Cu — Front copper (your traces)
- B.Cu — Back copper (if doing a 2-layer board)
- F.Silkscreen — Front silkscreen labels
- F.Mask — Front solder mask
- Edge.Cuts — Board outline
- Set the Output Directory — I used a
- Click Plot to generate the Gerber files


Exporting the Drill File:#
- Still in the Plot dialog, click Generate Drill Files
- In the Drill File dialog:
- Set the same output directory as the Gerbers
- Click Generate Drill File

Creating Zip Files:#
- Navigate to your
gerber/output folder
- Select all the generated files (
.gbr,.drl, etc.) - Right-click and Compress to ZIP
🖼️ Converting Gerbers to PNG Using Gerber2PNG#
The milling pipeline in our lab doesn’t read Gerber files directly — Mods Project requires PNG images. FabLab Kerala built a dedicated tool for this conversion: gerber2png.fablabkerala.in .
There’s also a KiCad plugin version of Gerber2PNG — I installed that as well so I can generate PNGs directly from inside KiCad without going to the browser. The plugin repo is linked in the references at the bottom of this page.

Files generated:#
| PNG File | Purpose |
|---|---|
| Front Traces (Top Layer) | Copper trace milling — the V-bit follows these paths |
| Drill Holes (Top Layer) | Hole positions for the drill pass |
| Outline (Top Layer) | Board boundary — the final cutout pass |
| Back Traces | Same as above but for the back copper layer |
| Back Drill Holes | Hole positions from the back side |
| Back Outline | Back boundary cutout |
Steps:
- Go to gerber2png.fablabkerala.in
- Upload the Gerber zip file.
- Click “Generate all PNGs”
- Download the generated PNGs


🗺️ Generating Toolpaths in Mods Project#
With the PNGs ready, the next step was to generate the actual machine instructions — toolpaths — using Mods Project .
Mods is a modular, browser-based toolpath generation environment developed at MIT. It’s the standard tool across Fab Labs for converting image files into RML or G-code that milling machines can execute.
Our lab runs an offline version of Mods locally.
The Mods program for PCB milling loads three core modules:
- Read PNG — imports the trace/drill/outline image
- Set PCB Defaults — defines tool parameters and milling settings
- Mill Raster 2D — generates the raster toolpath from the image
This is a double-sided PCB, so the sequence was:#
Top side:
- Front traces → V-bit pass
- Front drill holes → 1/32" end mill
- Front outline → 1/32" end mill
Flip the board, then:
- Back traces → V-bit pass
- Back drill holes → 1/32" end mill
- Back outline → 1/32" end mill
V-bit settings in Mods:#
| Parameter | Value |
|---|---|
| V-bit Tip Diameter | 0.2 mm |
| V-bit Point Angle | 60° |
| Cut Depth | 0.9 mm |

Part 2 – PCB Milling#
⚙️ Machine Setup — Roland Modela MDX-20#
The Roland Modela MDX-20 is the PCB milling machine we use in the lab — a small desktop CNC that’s accurate enough for 0.4 mm trace widths.
Board Fixturing#
The PCB blank (copper-clad FR4) was fixed onto the machine bed using double-sided tape. Since this was a double-sided board, I shared the blank with my classmate Ashitami — she handled the taping and fixturing, and we both used the same board stock.

Setting the Origin#
Before sending any file, the machine origin needs to be set:
- Jog the machine to the desired XY start position on the blank
- Set this as the origin in the Mods interface
- Press Change Tool to move the spindle to the tool change position
- Insert the V-bit and tighten the grub screw with an Allen key

- Jog back to the origin XY position
- Manually lower the Z-axis until the bit just touches the copper surface
- Lock the Z — this is your Z zero reference

🔪 Milling Sequence#
Pass 1 — Front Traces (V-bit)#
Loaded the front traces PNG into Mods, configured the V-bit parameters (tip 0.2 mm, 60°, cut depth 0.9 mm), calculated the toolpath, opened the socket, and clicked Send File.
The machine started milling the copper traces. The V-bit cuts away the copper around the traces — what’s left standing is your circuit.

Pass 2 — Drill Holes (1/32" End Mill)#
Changed the bit to the 1/32" flat end mill. Reset Z zero with the new bit, loaded the drill holes PNG into Mods, calculated, sent file.
Pass 3 — Outline Cut (1/32" End Mill)#
Kept the same 1/32" bit, loaded the outline PNG, calculated and sent. This cuts the board free from the stock material.
Flipping the Board#
After the front side was done, I carefully lifted the board, flipped it, re-aligned it on the bed, and re-taped it down. Then repeated passes 1, 2, and 3 for the back traces, back drill holes, and back outline.
🧹 Post-Milling Cleanup#
After all passes were complete:
- Used a scraper to carefully peel the PCB off the machine bed
- Cleaned the back of the board with IPA (isopropyl alcohol) to remove double-sided tape residue
- Vacuumed the machine bed to clean up copper dust and FR4 chips — leaving it ready for the next person
Part 3 – Component Collection#
📦 Using FabStash#
To get the components needed for assembly, I used FabStash — Fab Lab Kerala’s digital inventory management system. It lets you search the lab inventory, find what’s in stock, and submit a request that the instructors approve.
Steps:
- Log in at inventory.fablabkerala.in using lab credentials
- Search for each component by name or category
- Click + to add it to your request list
- Submit the request
- Wait for instructor approval
- Collect components from their listed storage locations
Most components were available in FabStash, but the button and JST connector weren’t listed in the system — even though they were physically in the lab. I noted these manually on the printed request sheet.
Components Requested#
| Reference | Part | Quantity |
|---|---|---|
| C1, C2 | 100nf | 2 |
| R1 | 1k | 1 |
| D1 | LED_1206 | 1 |
| SW1-6 | Push Button | 6 |
| M1 | Module_XIAO-ESP32C6 | 1 |
| J2, J3, J9, J10 | S4B-PH-SM4-TB | 4 |
| J7, J8 | Conn_01x07_Socket | 2 |
| J1 | PinSocket_01x04_P2.54mm_Vertical_SMD | 1 |
| J5 | Screw_Terminal_01x02_P5mm | 1 |
| P1 | S3B-PH-SM4-TB | 1 |
After the request was approved and printed, I collected everything from the storage locations. All the tiny components were stuck onto a strip of double-sided tape on the printed request sheet — an easy way to keep everything organised and stop 0402s from disappearing.

Part 4 – Soldering#
🔧 Soldering Station Setup#
Before starting, a brief orientation on the soldering workstation:
| Tool | Purpose |
|---|---|
| Soldering Iron | Heating pads and melting solder |
| Hot Air Blower | Reflow soldering / component removal |
| Fume Extractor | Pulling solder fumes away from your face |
| Tip Cleaner | Keeping the iron tip clean for good heat transfer |
| Silicone Work Mat | Heat-resistant surface, keeps small parts from rolling away |
| Tweezers | Placing SMD components accurately |
| Digital Microscope | Inspecting solder joint quality up close |

Safety first: Turned on the fume extractor and wore a mask before starting. Solder fumes are not something you want to breathe in session after session.
🪡 Creating Vias#
Before soldering any components, any required vias were made and soldered first — in my case, I had 32 vias connecting the front and back copper layers. I used a small piece of wire to create the via, soldering it on both sides to ensure a good electrical connection.

📐 Soldering Order — Smallest to Tallest#
The golden rule for PCB assembly: start with the smallest/flattest components and work up to the tallest. This keeps the board stable and makes placement easier.
My order:
- Resistors (1206 SMD — lowest profile)
- Capacitors (1206 SMD)
- XIAO ESP32-C6 (the microcontroller)
- Header pins
- JST connectors
- Terminal blocks / tall connectors
For each SMD component:
- Tin one pad first — add a small blob of solder
- Pick up the component with tweezers, hold it in position
- Touch the iron to the tinned pad — the component sinks into place
- Solder the remaining pads
- Go back and reflow the first joint to clean it up


Xiao ESP32-C6 taped as the footprint in design was for Xiao RP2040
🔬 Inspection with Digital Microscope#
After each section of components, I used the digital microscope at the station to check solder joint quality — looking for:
- Cold joints (dull, grainy surface — not enough heat or movement during cooling)
- Solder bridges (blobs connecting two adjacent pads — will short your circuit)
- Lifted pads (component slightly off the pad — weak mechanical connection)
- Correct polarity on LEDs and other polarised components
The microscope makes problems obvious that you’d completely miss with the naked eye at these component sizes.
🔌 Continuity Testing with Multimeter#
After soldering the LEDs, I used a multimeter in continuity mode to verify polarity. LEDs are polarised — flip them around and they won’t light up.
The multimeter also helped catch any solder bridges across traces before powering anything up.
Issue I hit: When I checked the XIAO ESP32-C6 after soldering, it wasn’t connecting correctly. Turned out I had placed it in the wrong orientation — rotated 180°. Had to desolder it, clean the pads with solder wick, and resolder it in the correct position. After that, continuity checks all passed.

Part 5 – Programming and Testing#
💻 Setting Up Arduino IDE for XIAO ESP32-C6#
This board used the Seeed Studio XIAO ESP32-C6 — which I hadn’t set up in Arduino IDE before. Different board package from the RP2040 I used in earlier weeks.
- Open Arduino IDE
- Go to File → Preferences
- Add the Seeed board manager URL to Additional Boards Manager URLs
- Go to Tools → Board → Boards Manager, search ESP32, install the Seeed package
- Select XIAO ESP32-C6 as the board
- Select the correct COM port
- Upload
Setup reference: wiki.seeedstudio.com/xiao_esp32c6_getting_started

Arduino Code#
/*
* ╔══════════════════════════════════════════════════════════╗
* ║ DUAL-BOOT CONSOLE — XIAO ESP32C6 ║
* ║ Boot screen selects OS on every power-on: ║
* ║ 1. NB6_BOY — 8×8 NeoPixel matrix, WiFi/Firebase, ║
* ║ buzzer, 18 games ║
* ║ 2. QPAD — OLED-only, 10 classic games ║
* ╠══════════════════════════════════════════════════════════╣
* ║ SHARED PIN MAP: ║
* ║ D3=UP D9=DOWN D2=LEFT D7=RIGHT ║
* ║ D1=MODE/SELECT D6=ACT/PAUSE ║
* ║ D8=Power LED D10=NeoPixel Matrix ║
* ║ D4=I2C SDA D5=I2C SCL ║
* ║ D0=Buzzer (NB6_BOY only) ║
* ╠══════════════════════════════════════════════════════════╣
* ║ BOOT: UP/DOWN navigate, ACT(D6) to confirm ║
* ╚══════════════════════════════════════════════════════════╝
*
* Libraries: Adafruit SSD1306, Adafruit GFX, Adafruit NeoPixel,
* ArduinoJson, WiFi, HTTPClient, Preferences
*/
// ─────────────────────────────────────────────────────────
// INCLUDES (deduplicated)
// ─────────────────────────────────────────────────────────
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_NeoPixel.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <Preferences.h>
#include <math.h>
// ─────────────────────────────────────────────────────────
// SHARED DISPLAY (used by both OS and boot screen)
// ─────────────────────────────────────────────────────────
#define OLED_ADDR 0x3C
#define SW 128
#define SH 64
Adafruit_SSD1306 display(SW, SH, &Wire, -1);
// ─────────────────────────────────────────────────────────
// OS SELECTION STATE
// ─────────────────────────────────────────────────────────
enum BootOS { OS_NONE, OS_NB6BOY, OS_QPAD };
static BootOS gSelectedOS = OS_NONE;
// Forward declarations for OS entry points (defined later in file)
void nb_setup();
void nb_loop();
void qpad_setup();
void qpad_loop();
// ─────────────────────────────────────────────────────────
// BOOT SCREEN
// ─────────────────────────────────────────────────────────
#define BOOT_PIN_UP D3
#define BOOT_PIN_DOWN D9
#define BOOT_PIN_ACT D6
#define BOOT_PIN_LED D8
static void drawBootScreen(int sel) {
display.clearDisplay();
display.drawRect(0, 0, 128, 64, SSD1306_WHITE);
display.drawLine(0, 10, 128, 10, SSD1306_WHITE);
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(22, 2);
display.print(F("[ DUAL-BOOT CONSOLE ]"));
// Option 1
if (sel == 0) {
display.fillRect(3, 14, 122, 18, SSD1306_WHITE);
display.setTextColor(SSD1306_BLACK);
} else {
display.setTextColor(SSD1306_WHITE);
}
display.setCursor(7, 18);
display.print(F("1. NB6_BOY [matrix+wifi]"));
// Option 2
if (sel == 1) {
display.fillRect(3, 36, 122, 18, SSD1306_WHITE);
display.setTextColor(SSD1306_BLACK);
} else {
display.setTextColor(SSD1306_WHITE);
}
display.setCursor(7, 40);
display.print(F("2. QPAD [oled games] "));
display.setTextColor(SSD1306_WHITE);
display.setCursor(12, 56);
display.print(F("UP/DN=select ACT=boot"));
display.display();
}
static BootOS runBootSelector() {
int sel = 0;
bool upHeld = false, downHeld = false, actHeld = false;
drawBootScreen(sel);
while (true) {
bool upNow = (digitalRead(BOOT_PIN_UP) == LOW);
bool dnNow = (digitalRead(BOOT_PIN_DOWN) == LOW);
bool actNow = (digitalRead(BOOT_PIN_ACT) == LOW);
if (upNow && !upHeld) { sel = (sel+1)%2; drawBootScreen(sel); delay(150); }
if (dnNow && !downHeld){ sel = (sel+1)%2; drawBootScreen(sel); delay(150); }
if (actNow && !actHeld){ delay(40); return (sel==0) ? OS_NB6BOY : OS_QPAD; }
upHeld = upNow; downHeld = dnNow; actHeld = actNow;
delay(10);
}
}
// ─────────────────────────────────────────────────────────
// MASTER SETUP / LOOP
// ─────────────────────────────────────────────────────────
void setup() {
pinMode(BOOT_PIN_LED, OUTPUT);
digitalWrite(BOOT_PIN_LED, HIGH);
// All button pins as pull-up
const uint8_t btnPins[] = {D1,D2,D3,D6,D7,D9};
for (auto p : btnPins) pinMode(p, INPUT_PULLUP);
// I2C + OLED
Wire.begin(D4, D5);
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
while (true) { digitalWrite(BOOT_PIN_LED, !digitalRead(BOOT_PIN_LED)); delay(300); }
}
display.clearDisplay(); display.display();
gSelectedOS = runBootSelector();
if (gSelectedOS == OS_NB6BOY) nb_setup();
else qpad_setup();
}
void loop() {
if (gSelectedOS == OS_NB6BOY) nb_loop();
else qpad_loop();
}
// ═════════════════════════════════════════════════════════
// ███╗ ██╗██████╗ ██████╗ ██████╗ ██████╗ ██╗ ██╗
// ████╗ ██║██╔══██╗██╔════╝ ██╔══██╗██╔═══██╗╚██╗ ██╔╝
// ██╔██╗ ██║██████╔╝███████╗ ██████╔╝██║ ██║ ╚████╔╝
// ██║╚██╗██║██╔══██╗██╔═══██╗ ██╔══██╗██║ ██║ ╚██╔╝
// ██║ ╚████║██████╔╝╚██████╔╝ ██████╔╝╚██████╔╝ ██║
// ╚═╝ ╚═══╝╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝
//
// NB6_BOY OS — 18 games, 8×8 NeoPixel matrix, WiFi/Firebase, buzzer
// Entry points: nb_setup() / nb_loop()
// ═════════════════════════════════════════════════════════
// ========================================
// Multi-Game Console for RP2040
// Fixed and Optimized Version
// WITH FIREBASE LEADERBOARD SUPPORT
// ========================================
// ========================================
// WiFi & Firebase Configuration
// ========================================
const char* WIFI_SSID = "FABLAB Guest";
const char* WIFI_PASSWORD = "sharedsecret";
const char* FIREBASE_HOST = "https://nb6boy-scores-default-rtdb.asia-southeast1.firebasedatabase.app";
// ========================================
// Pin Definitions
// ========================================
#define MATRIX_PIN D10
#define BTN_UP D3
#define BTN_DOWN D9
#define BTN_LEFT D2
#define BTN_RIGHT D7
#define BTN_PAUSE D6
#define BUZZER D0
#define GAME_SELECTOR_BTN D1
// ========================================
// Hardware Constants
// ========================================
constexpr uint8_t MATRIX_W = 8;
constexpr uint8_t MATRIX_H = 8;
constexpr uint8_t MATRIX_COUNT = 64;
// ========================================
// Timing Constants
// ========================================
namespace Timing {
constexpr uint16_t BTN_DEBOUNCE_MS = 30;
constexpr uint16_t MATRIX_FRAME_MS = 33; // ~30 FPS
constexpr uint16_t OLED_FRAME_MS = 100; // ~10 FPS
constexpr uint16_t GAME_OVER_DELAY_MS = 2000; // Auto-return to menu after 2s
// Game-specific timing
constexpr uint16_t SNAKE_INIT_MS = 280;
constexpr uint16_t TETRIS_INIT_MS = 640;
constexpr uint16_t FLAPPY_HARD_MS = 125;
constexpr uint16_t FLAPPY_EASY_MS = 200;
}
// ========================================
// Hardware Objects
// ========================================
Adafruit_NeoPixel nb_matrix(MATRIX_COUNT, MATRIX_PIN, NEO_GRB + NEO_KHZ800);
// display object shared - declared in boot header
// ========================================
// System State
// ========================================
enum SystemState : uint8_t {
STATE_MENU,
STATE_PLAYING,
STATE_GAME_OVER
};
enum GameId : uint8_t {
GAME_SNAKE_WALL,
GAME_SNAKE_WRAP,
GAME_TETRIS,
GAME_FLAPPY_EASY, // Fixed typo
GAME_FLAPPY_HARD,
GAME_ASTEROIDS_HARD,
GAME_PACMAN_EASY,
GAME_PACMAN_HARD,
GAME_SPACE_SHOOTER,
GAME_BREAKOUT,
GAME_TTT_AI,
GAME_TTT_2P,
GAME_PONG,
GAME_TUG,
GAME_CHECKERS_AI, // NEW!
GAME_CHECKERS_2P, // Renamed from GAME_CHECKERS
GAME_MINESWEEPER, // Minesweeper!
GAME_DINO, // NEW! Dino Run
GAME_COUNT
};
const char *GAME_NAMES[GAME_COUNT] = {
"Snake (Wall)",
"Snake (Wrap)",
"Tetris",
"Flappy Bird Easy", // Fixed typo
"Flappy Bird Hard",
"Asteroids Hard",
"Pac-Man Easy",
"Pac-Man Hard",
"Space Shooter",
"Breakout",
"TicTacToe AI",
"TicTacToe 2P",
"Pong",
"Tug of War 2P",
"Checkers AI", // NEW!
"Checkers 2P",
"Minesweeper",
"Dino Run"
};
// Firebase game keys (lowercase, no spaces) - must match HTML
const char* FIREBASE_KEYS[GAME_COUNT] = {
"snake_wall",
"snake_wrap",
"tetris",
"flappy_easy",
"flappy_hard",
"asteroids",
"pacman_easy",
"pacman_hard",
"space_shooter",
"breakout",
"tictactoe_ai",
"tictactoe_2p",
"pong",
"tug_of_war",
"checkers_ai",
"checkers_2p",
"minesweeper",
"dino_run"
};
// ========================================
// Button System
// ========================================
enum ButtonId : uint8_t {
B_UP,
B_DOWN,
B_LEFT,
B_RIGHT,
B_PAUSE,
B_SELECT,
BUTTON_COUNT
};
const uint8_t BUTTON_PINS[BUTTON_COUNT] = {
BTN_UP, BTN_DOWN, BTN_LEFT, BTN_RIGHT, BTN_PAUSE, GAME_SELECTOR_BTN
};
struct Button {
uint8_t pin;
bool stable;
bool last;
bool press;
bool release;
uint32_t changedAt;
};
Button buttons[BUTTON_COUNT];
// ========================================
// Sound System
// ========================================
struct ToneEvent {
uint16_t freq;
uint16_t duration;
};
constexpr uint8_t TONE_QUEUE_SIZE = 24;
ToneEvent toneQueue[TONE_QUEUE_SIZE];
uint8_t toneHead = 0;
uint8_t toneTail = 0;
bool toneActive = false;
uint32_t toneEndTime = 0;
bool soundMuted = false;
// ========================================
// Firebase State
// ========================================
bool wifiConnected = false;
uint32_t lastFirebasePush = 0;
constexpr uint32_t FIREBASE_COOLDOWN_MS = 3000; // Wait 3s between pushes
// ========================================
// Persistent Storage (ESP32 Preferences)
// ========================================
Preferences preferences;
// ========================================
// Global Game State
// ========================================
SystemState systemState = STATE_MENU;
GameId currentGame = GAME_SNAKE_WALL;
int8_t menuIndex = 0;
int16_t score = 0;
int16_t highScore[GAME_COUNT] = {0};
bool lastWin = false;
char lastMsg[24] = "Game over";
bool gamePaused = false;
bool menuMuteComboLatched = false;
uint32_t gameOverStartTime = 0; // NEW: Track when game over started
// Frame buffer
uint32_t frameBuffer[MATRIX_COUNT];
uint32_t lastMatrixMs = 0;
uint32_t lastOledMs = 0;
// ========================================
// Game State Structures
// ========================================
struct Bullet {
int8_t x;
int8_t y;
bool on;
};
struct Snake {
int8_t x[64];
int8_t y[64];
uint8_t length;
int8_t dx;
int8_t dy;
int8_t nextDx;
int8_t nextDy;
int8_t foodX;
int8_t foodY;
bool wrapMode;
uint16_t moveInterval;
uint32_t lastMove;
};
struct Tetris {
uint8_t board[8][8];
int8_t pieceX;
int8_t pieceY;
uint8_t pieceType;
uint8_t rotation;
uint16_t dropInterval;
uint32_t lastDrop;
uint32_t softDropTime;
};
struct Flappy {
int16_t y8; // Y position * 8 for sub-pixel movement
int16_t velocity8;
int8_t pipeX;
int8_t gapY;
bool scoredPipe;
uint16_t moveInterval;
uint32_t lastMove;
};
struct FlappyEasy {
int8_t birdY;
int8_t velocity;
int8_t pipeX;
int8_t gapY;
uint8_t gapSize;
uint16_t moveInterval;
uint32_t lastMove;
};
struct AstRock {
int8_t x;
int8_t y;
bool active;
uint16_t moveInterval;
uint32_t lastMove;
};
struct AsteroidsHard {
int8_t shipX;
Bullet bullets[5];
AstRock rocks[8];
uint16_t spawnInterval;
uint32_t lastMove;
uint32_t lastBulletUpdate;
uint32_t lastSpawn;
uint32_t lastShot;
};
struct AstEasyBullet {
int8_t x;
int8_t y;
int8_t dx;
int8_t dy;
bool active;
};
struct AstEasyRock {
int8_t x;
int8_t y;
bool active;
};
struct AsteroidsEasy {
int8_t shipX;
int8_t shipY;
AstEasyBullet bullets[3];
AstEasyRock rocks[5];
uint32_t lastMove;
uint32_t lastSpawn;
uint32_t lastBulletUpdate;
uint32_t lastInput;
uint32_t lastShot;
};
struct PacEasy {
int8_t pacX;
int8_t pacY;
int8_t direction;
int8_t ghostX;
int8_t ghostY;
uint8_t pellets[8][8];
int16_t pelletsLeft;
uint32_t lastPacMove;
uint32_t lastGhostMove;
};
struct Pacman {
uint8_t cells[8][8];
int8_t pacX;
int8_t pacY;
int8_t ghostX;
int8_t ghostY;
int8_t dx;
int8_t dy;
int8_t nextDx;
int8_t nextDy;
uint16_t pelletsLeft;
uint16_t pacInterval;
uint16_t ghostInterval;
uint32_t lastPacMove;
uint32_t lastGhostMove;
uint8_t worldIndex;
};
struct Enemy {
int8_t x;
int8_t y;
bool active;
};
struct SpaceShooter {
int8_t shipX;
Bullet playerBullets[6];
Bullet enemyBullets[5];
Enemy enemies[8];
uint16_t enemyInterval;
uint16_t spawnInterval;
uint32_t lastMove;
uint32_t lastPlayerBullet;
uint32_t lastEnemyBullet;
uint32_t lastEnemyStep;
uint32_t lastSpawn;
uint32_t lastShot;
uint32_t lastEnemyShoot;
};
struct Breakout {
bool bricks[3][8];
int8_t paddleX;
int8_t ballX;
int8_t ballY;
int8_t velocityX;
int8_t velocityY;
uint16_t moveInterval;
uint32_t lastMove;
uint32_t lastPaddleMove;
};
struct TicTacToe {
uint8_t board[3][3];
uint8_t cursorX;
uint8_t cursorY;
uint8_t turn;
bool aiMode;
bool waitingForAI;
uint32_t aiMoveTime;
};
struct Pong {
int8_t playerX; // Player paddle X position (bottom side, y=7)
int8_t aiX; // AI paddle X position (top side, y=0)
int8_t ballX;
int8_t ballY;
int8_t velocityX;
int8_t velocityY;
uint8_t paddleSize; // Paddle width
uint16_t moveInterval;
uint32_t lastMove;
uint32_t lastPaddleMove;
uint32_t lastAIMove;
};
struct TugOfWar {
int8_t position;
uint32_t player1LastPress;
uint32_t player2LastPress;
};
struct Checkers {
uint8_t board[8][8]; // 0=empty, 1=P1, 2=P1 king, 3=P2, 4=P2 king
int8_t cursorX;
int8_t cursorY;
int8_t selectedX; // -1 if no piece selected
int8_t selectedY;
uint8_t currentPlayer; // 1 or 2
bool mustCapture; // If true, must make a capture move
uint8_t validMoves[12][2]; // Valid destination squares for selected piece
uint8_t validMoveCount;
bool aiMode; // NEW: true if playing against AI
bool waitingForAI; // NEW: true when AI is thinking
uint32_t aiMoveTime; // NEW: when AI should make its move
int8_t aiLastFromX; // Last AI move origin
int8_t aiLastFromY;
int8_t aiLastToX; // Last AI move destination
int8_t aiLastToY;
bool aiBlinkVisible;
uint32_t aiBlinkNextToggleAt;
uint32_t aiBlinkEndsAt;
};
struct Minesweeper {
uint8_t board[8][8]; // 0-8 = number of adjacent mines, 9 = mine
uint8_t revealed[8][8]; // 0 = hidden, 1 = revealed, 2 = flagged
int8_t cursorX;
int8_t cursorY;
uint8_t minesLeft; // Flags remaining
uint8_t cellsToReveal; // Non-mine cells left to reveal
bool firstClick; // First click can't be a mine
};
struct Dino {
int16_t playerY8; // Player Y position * 8 for sub-pixel movement
int16_t velocityY8; // Player Y velocity * 8
bool isCrouching; // NEW: For dodging
int8_t obstacleX; // X position of the current obstacle
int8_t obstacleY; // Y position of obstacle (for flying ones)
uint8_t obstacleH; // Height of the obstacle
uint8_t obstacleW; // Width of the obstacle
uint8_t obstacleType; // NEW: 0 for ground, 1 for flying
bool passedObstacle; // If score was given for this obstacle
uint16_t moveInterval; // How fast the game scrolls
uint32_t lastMove; // Time of last game tick
};
// ========================================
// Memory-Optimized Game State Union
// ========================================
union GameState {
Snake snake;
Tetris tetris;
Flappy flappyHard;
FlappyEasy flappyEasy;
AsteroidsHard asteroidsHard;
AsteroidsEasy asteroidsEasy;
PacEasy pacEasy;
Pacman pacman;
SpaceShooter shooter;
Breakout breakout;
TicTacToe ttt;
Pong pong;
TugOfWar tug;
Checkers checkers; // NEW!
Minesweeper minesweeper;
Dino dino;
};
GameState gameState;
// ========================================
// Forward Declarations
// ========================================
static void initGame(GameId id);
static void updateGame(GameId id, uint32_t now);
static void drawGame(GameId id);
// Dino Game forward declarations
void dinoInit();
void dinoUpdate(uint32_t now);
void dinoDraw();
// ========================================
// Utility Functions
// ========================================
// Creates an RGB color value for NeoPixel LEDs
// Parameters: r, g, b (0-255 each)
// Returns: 32-bit packed color value
static inline uint32_t Color(uint8_t r, uint8_t g, uint8_t b) {
return nb_matrix.Color(r, g, b);
}
// Converts (x,y) grid coordinates to physical LED index
// The LED strip is arranged in a serpentine (snake) pattern:
// Row 0: left-to-right, Row 1: right-to-left, Row 2: left-to-right, etc.
// This function handles the conversion automatically
uint8_t ledIndex(uint8_t x, uint8_t y) {
return (y % 2 == 0) ? // Even rows (0,2,4,6) go left-to-right
static_cast<uint8_t>(y * MATRIX_W + x) :
static_cast<uint8_t>(y * MATRIX_W + (MATRIX_W - 1 - x)); // Odd rows reversed
}
// Clears the entire frame buffer to a single color (default: black/off)
// Call this at the start of each game's draw function
void clearFrame(uint32_t color = 0) {
for (uint8_t i = 0; i < MATRIX_COUNT; ++i) {
frameBuffer[i] = color;
}
}
// Sets a single pixel in the frame buffer
// Bounds checking prevents drawing outside the 8x8 grid
// Changes are not visible until showFrame() is called
void setPixel(int8_t x, int8_t y, uint32_t color) {
if (x < 0 || x >= MATRIX_W || y < 0 || y >= MATRIX_H) return; // Ignore out-of-bounds
frameBuffer[ledIndex(static_cast<uint8_t>(x), static_cast<uint8_t>(y))] = color;
}
// Pushes the frame buffer to the physical LEDs
// Call once per frame after all setPixel() calls
// This is when the display actually updates
void showFrame() {
for (uint8_t i = 0; i < MATRIX_COUNT; ++i) {
nb_matrix.setPixelColor(i, frameBuffer[i]);
}
nb_matrix.show(); // Send data to NeoPixels (takes ~2ms)
}
void drawFlashingPauseBorder(uint32_t now) {
if (!gamePaused) return;
if (((now / 200) % 2) != 0) return;
uint32_t borderColor = Color(50, 50, 0);
for (uint8_t i = 0; i < 8; ++i) {
setPixel(0, i, borderColor);
setPixel(7, i, borderColor);
setPixel(i, 0, borderColor);
setPixel(i, 7, borderColor);
}
}
void setSoundMuted(bool muted) {
soundMuted = muted;
if (soundMuted) {
noTone(BUZZER);
toneActive = false;
toneHead = 0;
toneTail = 0;
}
}
// ========================================
// Sound Functions
// ========================================
// Adds a tone to the non-blocking sound queue
// Traditional tone() blocks code - this queues sounds for later playback
// Queue is circular buffer with head/tail pointers
void enqueueTone(uint16_t freq, uint16_t duration) {
if (soundMuted) return;
uint8_t nextTail = static_cast<uint8_t>((toneTail + 1) % TONE_QUEUE_SIZE);
if (nextTail == toneHead) return; // Queue full - discard sound
toneQueue[toneTail] = {freq, duration};
toneTail = nextTail;
}
// Predefined sound effects for common game events
// These can be chained by calling multiple times - they'll play in sequence
void beepButton() { enqueueTone(1200, 26); } // UI feedback - short high beep
void beepScore() { enqueueTone(1600, 35); } // Scoring - higher pitched
void beepHit() { enqueueTone(240, 90); } // Damage - low rumble
// Multi-tone sound sequences for game state changes
void beepStart() {
enqueueTone(500, 45); // Ascending
enqueueTone(850, 45); // three-tone
enqueueTone(1200, 65); // fanfare
}
void beepLose() {
enqueueTone(1000, 60); // Descending
enqueueTone(700, 70); // three-tone
enqueueTone(430, 90); // sad sound
}
void beepWin() {
enqueueTone(800, 50); // Ascending
enqueueTone(1050, 50); // three-tone
enqueueTone(1400, 80); // victory sound
}
void updateSound(uint32_t now) {
if (soundMuted) {
if (toneActive) {
noTone(BUZZER);
toneActive = false;
}
toneHead = toneTail;
return;
}
if (toneActive && now >= toneEndTime) {
noTone(BUZZER);
toneActive = false;
}
if (!toneActive && toneHead != toneTail) {
ToneEvent event = toneQueue[toneHead];
toneHead = (toneHead + 1) % TONE_QUEUE_SIZE;
tone(BUZZER, event.freq);
toneEndTime = now + event.duration;
toneActive = true;
}
}
// ========================================
// Button Functions
// ========================================
void initButtons() {
for (uint8_t i = 0; i < BUTTON_COUNT; ++i) {
pinMode(BUTTON_PINS[i], INPUT_PULLUP);
bool pressed = digitalRead(BUTTON_PINS[i]) == LOW;
buttons[i] = {BUTTON_PINS[i], pressed, pressed, false, false, millis()};
}
}
void clearButtonEdges() {
for (uint8_t i = 0; i < BUTTON_COUNT; ++i) {
buttons[i].press = buttons[i].release = false;
}
}
void updateButtons(uint32_t now) {
for (uint8_t i = 0; i < BUTTON_COUNT; ++i) {
bool raw = digitalRead(buttons[i].pin) == LOW;
if (raw != buttons[i].last) {
buttons[i].last = raw;
buttons[i].changedAt = now;
}
if (now - buttons[i].changedAt >= Timing::BTN_DEBOUNCE_MS && raw != buttons[i].stable) {
buttons[i].stable = raw;
if (raw) {
buttons[i].press = true;
} else {
buttons[i].release = true;
}
}
}
}
static bool takePress(ButtonId id) {
if (!buttons[id].press) return false;
buttons[id].press = false;
return true;
}
static bool isDown(ButtonId id) {
return buttons[id].stable;
}
// ========================================
// State Management
// ========================================
void returnToMenu() {
systemState = STATE_MENU;
gamePaused = false;
menuMuteComboLatched = false;
clearButtonEdges();
}
// ========================================
// WiFi & Firebase Functions
// ========================================
void saveHighScores() {
preferences.begin("nb6boy", false); // Open in read-write mode
for (uint8_t i = 0; i < GAME_COUNT; i++) {
String key = "hs_" + String(i); // High score key: hs_0, hs_1, etc.
preferences.putInt(key.c_str(), highScore[i]);
}
preferences.end();
}
void loadHighScores() {
preferences.begin("nb6boy", true); // Open in read-only mode
for (uint8_t i = 0; i < GAME_COUNT; i++) {
String key = "hs_" + String(i);
highScore[i] = preferences.getInt(key.c_str(), 0); // Default to 0 if not found
}
preferences.end();
}
void connectWiFi() {
if (wifiConnected) return;
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
uint8_t attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
wifiConnected = true;
}
}
static void pushScoreToFirebase(GameId gameId, int16_t scoreValue) {
// Prevent spam pushes
if (millis() - lastFirebasePush < FIREBASE_COOLDOWN_MS) return;
if (!wifiConnected) return;
if (scoreValue <= 0) return; // Don't push zero scores
HTTPClient http;
// Build Firebase URL
String url = String(FIREBASE_HOST) + "/scores/" + FIREBASE_KEYS[gameId] + ".json";
http.begin(url);
http.addHeader("Content-Type", "application/json");
// Build JSON payload (without player name as requested)
StaticJsonDocument<128> doc;
doc["score"] = scoreValue;
doc["timestamp"] = millis();
String jsonString;
serializeJson(doc, jsonString);
// Push to Firebase
int httpCode = http.POST(jsonString);
if (httpCode == 200 || httpCode == 201) {
lastFirebasePush = millis();
// Successfully pushed!
}
http.end();
}
static void finishGame(bool win, const char *message) {
if (systemState != STATE_PLAYING) return;
if (score > highScore[currentGame]) {
highScore[currentGame] = score;
// Save high scores to persistent storage
saveHighScores();
// Push new high score to Firebase
pushScoreToFirebase(currentGame, score);
}
lastWin = win;
if (message) {
strncpy(lastMsg, message, sizeof(lastMsg) - 1);
} else {
strncpy(lastMsg, win ? "Win" : "Game over", sizeof(lastMsg) - 1);
}
lastMsg[sizeof(lastMsg) - 1] = '\0'; // Ensure null termination
systemState = STATE_GAME_OVER;
gameOverStartTime = millis(); // NEW: Record when game over started
clearButtonEdges();
if (win) {
beepWin();
} else {
beepLose();
}
}
static void startGame(GameId id) {
currentGame = id;
score = 0;
lastWin = false;
gamePaused = false;
menuMuteComboLatched = false;
strncpy(lastMsg, "Game over", sizeof(lastMsg) - 1);
lastMsg[sizeof(lastMsg) - 1] = '\0';
systemState = STATE_PLAYING;
clearButtonEdges();
initGame(id);
beepStart();
}
// ========================================
// Boot Animation
// ========================================
const uint8_t GLYPH_N[8] = {
0b10000001, 0b11000001, 0b10100001, 0b10010001,
0b10001001, 0b10000101, 0b10000011, 0b10000001,
};
const uint8_t GLYPH_B[8] = {
0b11111100, 0b10000010, 0b10000010, 0b11111100,
0b10000010, 0b10000010, 0b11111100, 0b00000000,
};
const uint8_t GLYPH_6[8] = {
0b00111110, 0b01000000, 0b10000000, 0b11111100,
0b10000010, 0b10000010, 0b01111100, 0b00000000,
};
const uint8_t GLYPH_HEART[8] = {
0b00000000, 0b01100110, 0b11111111, 0b11111111,
0b11111111, 0b01111110, 0b00111100, 0b00011000,
};
// Pac-Man sprites (mouth open and closed)
// Mouth facing LEFT (for entering from left side)
const uint8_t GLYPH_PACMAN_OPEN[8] = {
0b00111100, // Center (symmetric)
0b00011110, // Mouth opens to the left
0b00001111,
0b00000111,
0b00000111,
0b00001111,
0b00011110,
0b00111100,
};
const uint8_t GLYPH_PACMAN_CLOSED[8] = {
0b00111100,
0b01111110,
0b11111111,
0b11111111,
0b11111111,
0b11111111,
0b01111110,
0b00111100,
};
void drawGlyph(const uint8_t rows[8], uint32_t color) {
clearFrame();
for (uint8_t y = 0; y < 8; ++y) {
for (uint8_t x = 0; x < 8; ++x) {
if (rows[y] & static_cast<uint8_t>(1 << x)) {
setPixel(static_cast<int8_t>(x), static_cast<int8_t>(y), color);
}
}
}
showFrame();
}
void drawPacmanAt(int8_t xPos, bool mouthOpen, uint32_t color) {
clearFrame();
const uint8_t* sprite = mouthOpen ? GLYPH_PACMAN_OPEN : GLYPH_PACMAN_CLOSED;
for (uint8_t y = 0; y < 8; ++y) {
for (uint8_t x = 0; x < 8; ++x) {
if (sprite[y] & static_cast<uint8_t>(1 << x)) {
int8_t drawX = xPos + static_cast<int8_t>(x);
if (drawX >= 0 && drawX < 8) {
setPixel(drawX, static_cast<int8_t>(y), color);
}
}
}
}
showFrame();
}
void drawPacmanAndHeart(int8_t pacX, bool mouthOpen, bool showHeart, uint32_t pacColor, uint32_t heartColor) {
clearFrame();
// Draw heart first (if visible)
if (showHeart) {
for (uint8_t y = 0; y < 8; ++y) {
for (uint8_t x = 0; x < 8; ++x) {
if (GLYPH_HEART[y] & static_cast<uint8_t>(1 << x)) {
setPixel(static_cast<int8_t>(x), static_cast<int8_t>(y), heartColor);
}
}
}
}
// Draw Pac-Man on top
const uint8_t* sprite = mouthOpen ? GLYPH_PACMAN_OPEN : GLYPH_PACMAN_CLOSED;
for (uint8_t y = 0; y < 8; ++y) {
for (uint8_t x = 0; x < 8; ++x) {
if (sprite[y] & static_cast<uint8_t>(1 << x)) {
int8_t drawX = pacX + static_cast<int8_t>(x);
if (drawX >= 0 && drawX < 8) {
setPixel(drawX, static_cast<int8_t>(y), pacColor);
}
}
}
}
showFrame();
}
void playBootSequence() {
constexpr uint16_t frameMs = 1333;
drawGlyph(GLYPH_N, Color(0, 80, 255));
delay(frameMs);
drawGlyph(GLYPH_B, Color(255, 85, 0));
delay(frameMs);
drawGlyph(GLYPH_6, Color(80, 255, 80));
delay(frameMs);
// Heartbeat animation for 2 seconds
const uint8_t levels[8] = {30, 60, 95, 140, 200, 140, 95, 60};
uint32_t start = millis();
uint8_t i = 0;
while (millis() - start < 2000) { // Changed from 4000 to 2000
uint8_t intensity = levels[i % 8];
drawGlyph(GLYPH_HEART, Color(intensity, 0, 0));
delay(125);
++i;
}
// Pac-Man eating animation
const uint32_t pacColor = Color(255, 200, 0); // Yellow Pac-Man
const uint32_t heartColor = Color(200, 0, 0); // Red heart
// Pac-Man approaches from the left
for (int8_t x = -8; x <= 0; x++) {
bool mouthOpen = ((x / 2) % 2) == 0; // Animate mouth
drawPacmanAndHeart(x, mouthOpen, true, pacColor, heartColor);
delay(60);
}
// Pac-Man "eats" the heart (heart disappears)
drawPacmanAndHeart(0, true, true, pacColor, heartColor);
delay(150);
drawPacmanAndHeart(0, false, false, pacColor, heartColor); // Mouth closes, heart gone
delay(100);
// Pac-Man continues off screen to the right
for (int8_t x = 1; x <= 8; x++) {
bool mouthOpen = ((x / 2) % 2) == 0;
drawPacmanAt(x, mouthOpen, pacColor);
delay(60);
}
clearFrame();
showFrame();
}
// ========================================
// SNAKE GAME
// ========================================
bool snakeHasSegment(int8_t x, int8_t y, uint8_t length) {
for (uint8_t i = 0; i < length; ++i) {
if (gameState.snake.x[i] == x && gameState.snake.y[i] == y) {
return true;
}
}
return false;
}
void snakePlaceFood() {
for (uint8_t attempts = 0; attempts < 100; ++attempts) {
int8_t x = static_cast<int8_t>(random(0, 8));
int8_t y = static_cast<int8_t>(random(0, 8));
if (!snakeHasSegment(x, y, gameState.snake.length)) {
gameState.snake.foodX = x;
gameState.snake.foodY = y;
return;
}
}
gameState.snake.foodX = 0;
gameState.snake.foodY = 0;
}
void snakeInit(bool wrapMode) {
gameState.snake.length = 3;
gameState.snake.x[0] = 4;
gameState.snake.y[0] = 4;
gameState.snake.x[1] = 3;
gameState.snake.y[1] = 4;
gameState.snake.x[2] = 2;
gameState.snake.y[2] = 4;
gameState.snake.dx = 1;
gameState.snake.dy = 0;
gameState.snake.nextDx = 1;
gameState.snake.nextDy = 0;
gameState.snake.wrapMode = wrapMode;
gameState.snake.moveInterval = Timing::SNAKE_INIT_MS;
gameState.snake.lastMove = millis();
snakePlaceFood();
}
void snakeUpdate(uint32_t now) {
if (takePress(B_PAUSE)) {
gamePaused = !gamePaused;
beepButton();
}
if (gamePaused) return;
if (takePress(B_UP) && gameState.snake.dy != 1) {
gameState.snake.nextDx = 0;
gameState.snake.nextDy = -1;
beepButton();
}
if (takePress(B_DOWN) && gameState.snake.dy != -1) {
gameState.snake.nextDx = 0;
gameState.snake.nextDy = 1;
beepButton();
}
if (takePress(B_LEFT) && gameState.snake.dx != 1) {
gameState.snake.nextDx = -1;
gameState.snake.nextDy = 0;
beepButton();
}
if (takePress(B_RIGHT) && gameState.snake.dx != -1) {
gameState.snake.nextDx = 1;
gameState.snake.nextDy = 0;
beepButton();
}
if (now - gameState.snake.lastMove < gameState.snake.moveInterval) return;
gameState.snake.lastMove = now;
gameState.snake.dx = gameState.snake.nextDx;
gameState.snake.dy = gameState.snake.nextDy;
int8_t newX = static_cast<int8_t>(gameState.snake.x[0] + gameState.snake.dx);
int8_t newY = static_cast<int8_t>(gameState.snake.y[0] + gameState.snake.dy);
if (gameState.snake.wrapMode) {
if (newX < 0) newX = 7;
if (newX > 7) newX = 0;
if (newY < 0) newY = 7;
if (newY > 7) newY = 0;
} else if (newX < 0 || newX > 7 || newY < 0 || newY > 7) {
beepHit();
finishGame(false, "Hit wall");
return;
}
if (snakeHasSegment(newX, newY, gameState.snake.length)) {
beepHit();
finishGame(false, "Hit body");
return;
}
bool ateFood = (newX == gameState.snake.foodX && newY == gameState.snake.foodY);
if (ateFood && gameState.snake.length < 64) {
gameState.snake.length++;
}
for (int8_t i = static_cast<int8_t>(gameState.snake.length - 1); i > 0; --i) {
gameState.snake.x[i] = gameState.snake.x[i - 1];
gameState.snake.y[i] = gameState.snake.y[i - 1];
}
gameState.snake.x[0] = newX;
gameState.snake.y[0] = newY;
if (ateFood) {
score += 10;
beepScore();
if (gameState.snake.length == 64) {
finishGame(true, "Board full");
return;
}
snakePlaceFood();
if (gameState.snake.moveInterval > 100) {
gameState.snake.moveInterval = static_cast<uint16_t>(gameState.snake.moveInterval - 10);
}
}
}
void snakeDraw() {
clearFrame();
uint32_t headColor = gameState.snake.wrapMode ? Color(150, 20, 150) : Color(30, 130, 30);
uint32_t bodyColor = gameState.snake.wrapMode ? Color(90, 35, 90) : Color(20, 70, 20);
uint32_t foodColor = gameState.snake.wrapMode ? Color(0, 85, 85) : Color(85, 12, 12);
setPixel(gameState.snake.foodX, gameState.snake.foodY, foodColor);
for (int8_t i = static_cast<int8_t>(gameState.snake.length - 1); i >= 0; --i) {
setPixel(gameState.snake.x[i], gameState.snake.y[i], i == 0 ? headColor : bodyColor);
}
}
// ========================================
// TETRIS GAME
// ========================================
struct TetrisRotation {
int8_t x[4];
int8_t y[4];
};
const TetrisRotation TETRIS_SHAPES[5][4] = {
// I piece
{{{0, 1, 2, 3}, {1, 1, 1, 1}},
{{2, 2, 2, 2}, {0, 1, 2, 3}},
{{0, 1, 2, 3}, {2, 2, 2, 2}},
{{1, 1, 1, 1}, {0, 1, 2, 3}}},
// O piece
{{{1, 2, 1, 2}, {0, 0, 1, 1}},
{{1, 2, 1, 2}, {0, 0, 1, 1}},
{{1, 2, 1, 2}, {0, 0, 1, 1}},
{{1, 2, 1, 2}, {0, 0, 1, 1}}},
// T piece
{{{1, 0, 1, 2}, {0, 1, 1, 1}},
{{1, 1, 2, 1}, {0, 1, 1, 2}},
{{0, 1, 2, 1}, {1, 1, 1, 2}},
{{1, 0, 1, 1}, {0, 1, 1, 2}}},
// L piece
{{{0, 0, 0, 1}, {0, 1, 2, 2}},
{{0, 1, 2, 2}, {1, 1, 1, 0}},
{{0, 1, 1, 1}, {0, 0, 1, 2}},
{{0, 0, 1, 2}, {2, 1, 1, 1}}},
// Z piece
{{{1, 2, 0, 1}, {0, 0, 1, 1}},
{{1, 1, 2, 2}, {0, 1, 1, 2}},
{{1, 2, 0, 1}, {1, 1, 2, 2}},
{{0, 0, 1, 1}, {0, 1, 1, 2}}}
};
bool tetrisCanPlace(int8_t x, int8_t y, uint8_t type, uint8_t rotation) {
for (uint8_t i = 0; i < 4; ++i) {
int8_t blockX = static_cast<int8_t>(x + TETRIS_SHAPES[type][rotation].x[i]);
int8_t blockY = static_cast<int8_t>(y + TETRIS_SHAPES[type][rotation].y[i]);
if (blockX < 0 || blockX > 7 || blockY < 0 || blockY > 7) return false;
if (gameState.tetris.board[blockY][blockX]) return false;
}
return true;
}
void tetrisSpawnPiece() {
gameState.tetris.pieceType = static_cast<uint8_t>(random(0, 5));
gameState.tetris.rotation = 0;
gameState.tetris.pieceX = 2;
gameState.tetris.pieceY = 0;
if (!tetrisCanPlace(gameState.tetris.pieceX, gameState.tetris.pieceY,
gameState.tetris.pieceType, gameState.tetris.rotation)) {
beepHit();
finishGame(false, "Stacked out");
}
}
void tetrisLockPiece() {
for (uint8_t i = 0; i < 4; ++i) {
int8_t x = static_cast<int8_t>(gameState.tetris.pieceX + TETRIS_SHAPES[gameState.tetris.pieceType][gameState.tetris.rotation].x[i]);
int8_t y = static_cast<int8_t>(gameState.tetris.pieceY + TETRIS_SHAPES[gameState.tetris.pieceType][gameState.tetris.rotation].y[i]);
if (x >= 0 && x < 8 && y >= 0 && y < 8) {
gameState.tetris.board[y][x] = static_cast<uint8_t>(gameState.tetris.pieceType + 1);
}
}
}
void tetrisClearLines() {
uint8_t linesCleared = 0;
for (int8_t y = 7; y >= 0; --y) {
bool full = true;
for (uint8_t x = 0; x < 8; ++x) {
if (!gameState.tetris.board[y][x]) {
full = false;
break;
}
}
if (!full) continue;
linesCleared++;
for (int8_t row = y; row > 0; --row) {
for (uint8_t x = 0; x < 8; ++x) {
gameState.tetris.board[row][x] = gameState.tetris.board[row - 1][x];
}
}
for (uint8_t x = 0; x < 8; ++x) {
gameState.tetris.board[0][x] = 0;
}
y++; // Recheck this row
}
if (linesCleared) {
score += static_cast<int16_t>(linesCleared * 10);
beepScore();
}
}
void tetrisDropPiece() {
if (tetrisCanPlace(gameState.tetris.pieceX,
static_cast<int8_t>(gameState.tetris.pieceY + 1),
gameState.tetris.pieceType,
gameState.tetris.rotation)) {
gameState.tetris.pieceY++;
return;
}
tetrisLockPiece();
tetrisClearLines();
tetrisSpawnPiece();
}
void tetrisInit() {
memset(gameState.tetris.board, 0, sizeof(gameState.tetris.board));
gameState.tetris.dropInterval = Timing::TETRIS_INIT_MS;
gameState.tetris.lastDrop = millis();
gameState.tetris.softDropTime = millis();
tetrisSpawnPiece();
}
void tetrisUpdate(uint32_t now) {
if (takePress(B_LEFT) && tetrisCanPlace(static_cast<int8_t>(gameState.tetris.pieceX - 1),
gameState.tetris.pieceY,
gameState.tetris.pieceType,
gameState.tetris.rotation)) {
gameState.tetris.pieceX--;
beepButton();
}
if (takePress(B_RIGHT) && tetrisCanPlace(static_cast<int8_t>(gameState.tetris.pieceX + 1),
gameState.tetris.pieceY,
gameState.tetris.pieceType,
gameState.tetris.rotation)) {
gameState.tetris.pieceX++;
beepButton();
}
if (takePress(B_PAUSE)) {
uint8_t newRotation = static_cast<uint8_t>((gameState.tetris.rotation + 1) % 4);
if (tetrisCanPlace(gameState.tetris.pieceX, gameState.tetris.pieceY,
gameState.tetris.pieceType, newRotation)) {
gameState.tetris.rotation = newRotation;
beepButton();
}
}
if (takePress(B_DOWN) || (isDown(B_DOWN) && now - gameState.tetris.softDropTime >= 90)) {
gameState.tetris.softDropTime = now;
tetrisDropPiece();
}
if (now - gameState.tetris.lastDrop >= gameState.tetris.dropInterval) {
gameState.tetris.lastDrop = now;
tetrisDropPiece();
}
int16_t newInterval = static_cast<int16_t>(640 - score * 3);
if (newInterval < 120) newInterval = 120;
gameState.tetris.dropInterval = static_cast<uint16_t>(newInterval);
}
void tetrisDraw() {
clearFrame();
const uint32_t colors[6] = {
0,
0x000022,
0x221100,
0x002200,
0x220000,
0x001122
};
for (uint8_t y = 0; y < 8; ++y) {
for (uint8_t x = 0; x < 8; ++x) {
if (gameState.tetris.board[y][x]) {
uint32_t c = colors[gameState.tetris.board[y][x]];
setPixel(x, y, Color(
static_cast<uint8_t>((c >> 16) & 0xFF),
static_cast<uint8_t>((c >> 8) & 0xFF),
static_cast<uint8_t>(c & 0xFF)
));
}
}
}
for (uint8_t i = 0; i < 4; ++i) {
setPixel(
static_cast<int8_t>(gameState.tetris.pieceX + TETRIS_SHAPES[gameState.tetris.pieceType][gameState.tetris.rotation].x[i]),
static_cast<int8_t>(gameState.tetris.pieceY + TETRIS_SHAPES[gameState.tetris.pieceType][gameState.tetris.rotation].y[i]),
Color(60, 60, 60)
);
}
}
// ========================================
// FLAPPY BIRD (HARD) GAME
// ========================================
void flappyHardNewPipe() {
gameState.flappyHard.pipeX = 8;
gameState.flappyHard.gapY = static_cast<int8_t>(random(1, 5));
gameState.flappyHard.scoredPipe = false;
}
void flappyHardInit() {
gameState.flappyHard.y8 = 28;
gameState.flappyHard.velocity8 = 0;
gameState.flappyHard.moveInterval = Timing::FLAPPY_HARD_MS;
gameState.flappyHard.lastMove = millis();
flappyHardNewPipe();
}
void flappyHardUpdate(uint32_t now) {
if (takePress(B_UP) || takePress(B_PAUSE)) {
gameState.flappyHard.velocity8 = -14;
beepButton();
}
if (now - gameState.flappyHard.lastMove < gameState.flappyHard.moveInterval) return;
gameState.flappyHard.lastMove = now;
gameState.flappyHard.velocity8 += 4;
if (gameState.flappyHard.velocity8 > 18) {
gameState.flappyHard.velocity8 = 18;
}
gameState.flappyHard.y8 = static_cast<int16_t>(gameState.flappyHard.y8 + gameState.flappyHard.velocity8);
int8_t birdY = static_cast<int8_t>(gameState.flappyHard.y8 / 8);
gameState.flappyHard.pipeX--;
if (gameState.flappyHard.pipeX < -2) {
flappyHardNewPipe();
}
if (!gameState.flappyHard.scoredPipe && gameState.flappyHard.pipeX + 1 < 2) {
gameState.flappyHard.scoredPipe = true;
score++;
beepScore();
}
if (gameState.flappyHard.y8 < 0 || birdY > 7) {
beepHit();
finishGame(false, "Crashed");
return;
}
if (2 >= gameState.flappyHard.pipeX && 2 < gameState.flappyHard.pipeX + 2) {
if (birdY < gameState.flappyHard.gapY || birdY >= gameState.flappyHard.gapY + 3) {
beepHit();
finishGame(false, "Hit pipe");
return;
}
}
int16_t newInterval = static_cast<int16_t>(125 - score * 2);
if (newInterval < 65) newInterval = 65;
gameState.flappyHard.moveInterval = static_cast<uint16_t>(newInterval);
}
void flappyHardDraw() {
clearFrame();
for (int8_t x = gameState.flappyHard.pipeX; x < gameState.flappyHard.pipeX + 2; ++x) {
for (uint8_t y = 0; y < 8; ++y) {
if (y < gameState.flappyHard.gapY || y >= gameState.flappyHard.gapY + 3) {
setPixel(x, y, Color(0, 35, 8));
}
}
}
setPixel(2, static_cast<int8_t>(gameState.flappyHard.y8 / 8), Color(70, 55, 0));
}
// ========================================
// FLAPPY BIRD (EASY) GAME
// ========================================
void flappyEasyInit() {
gameState.flappyEasy.birdY = 4;
gameState.flappyEasy.velocity = 0;
gameState.flappyEasy.pipeX = 7;
gameState.flappyEasy.gapY = static_cast<int8_t>(random(1, 5));
gameState.flappyEasy.gapSize = 3;
gameState.flappyEasy.moveInterval = Timing::FLAPPY_EASY_MS;
gameState.flappyEasy.lastMove = millis();
}
void flappyEasyUpdate(uint32_t now) {
if (takePress(B_UP) || takePress(B_PAUSE)) {
gameState.flappyEasy.velocity = -2;
beepButton();
}
if (now - gameState.flappyEasy.lastMove < gameState.flappyEasy.moveInterval) return;
gameState.flappyEasy.lastMove = now;
gameState.flappyEasy.velocity++;
gameState.flappyEasy.birdY = static_cast<int8_t>(gameState.flappyEasy.birdY + gameState.flappyEasy.velocity);
if (gameState.flappyEasy.birdY < 0 || gameState.flappyEasy.birdY >= 8) {
beepHit();
finishGame(false, "Crashed");
return;
}
gameState.flappyEasy.pipeX--;
if (gameState.flappyEasy.pipeX < 0) {
gameState.flappyEasy.pipeX = 7;
gameState.flappyEasy.gapY = static_cast<int8_t>(random(1, 5));
score += 10;
beepScore();
}
if (gameState.flappyEasy.pipeX == 0) {
if (gameState.flappyEasy.birdY < gameState.flappyEasy.gapY ||
gameState.flappyEasy.birdY >= gameState.flappyEasy.gapY + gameState.flappyEasy.gapSize) {
beepHit();
finishGame(false, "Hit pipe");
return;
}
}
}
void flappyEasyDraw() {
clearFrame();
setPixel(0, gameState.flappyEasy.birdY, Color(70, 55, 0));
for (uint8_t y = 0; y < 8; ++y) {
if (y < gameState.flappyEasy.gapY || y >= gameState.flappyEasy.gapY + gameState.flappyEasy.gapSize) {
setPixel(gameState.flappyEasy.pipeX, static_cast<int8_t>(y), Color(0, 55, 0));
}
}
}
// ========================================
// ASTEROIDS (HARD) GAME
// ========================================
void asteroidsHardInit() {
gameState.asteroidsHard.shipX = 3;
gameState.asteroidsHard.spawnInterval = 900;
gameState.asteroidsHard.lastMove = millis();
gameState.asteroidsHard.lastBulletUpdate = millis();
gameState.asteroidsHard.lastSpawn = millis();
gameState.asteroidsHard.lastShot = millis();
for (uint8_t i = 0; i < 5; ++i) {
gameState.asteroidsHard.bullets[i].on = false;
}
for (uint8_t i = 0; i < 8; ++i) {
gameState.asteroidsHard.rocks[i].active = false;
}
}
void asteroidsHardShoot() {
for (uint8_t i = 0; i < 5; ++i) {
if (!gameState.asteroidsHard.bullets[i].on) {
gameState.asteroidsHard.bullets[i] = {gameState.asteroidsHard.shipX, 6, true};
return;
}
}
}
void asteroidsHardSpawnRock() {
for (uint8_t i = 0; i < 8; ++i) {
if (!gameState.asteroidsHard.rocks[i].active) {
int16_t interval = static_cast<int16_t>(random(180, 320) - score * 2);
if (interval < 90) interval = 90;
gameState.asteroidsHard.rocks[i] = {
static_cast<int8_t>(random(0, 8)),
0,
true,
static_cast<uint16_t>(interval),
millis()
};
return;
}
}
}
void asteroidsHardUpdate(uint32_t now) {
if (isDown(B_LEFT) && now - gameState.asteroidsHard.lastMove >= 90) {
gameState.asteroidsHard.lastMove = now;
if (gameState.asteroidsHard.shipX > 0) {
gameState.asteroidsHard.shipX--;
beepButton();
}
}
if (isDown(B_RIGHT) && now - gameState.asteroidsHard.lastMove >= 90) {
gameState.asteroidsHard.lastMove = now;
if (gameState.asteroidsHard.shipX < 7) {
gameState.asteroidsHard.shipX++;
beepButton();
}
}
if ((takePress(B_UP) || takePress(B_PAUSE)) && now - gameState.asteroidsHard.lastShot >= 170) {
gameState.asteroidsHard.lastShot = now;
asteroidsHardShoot();
beepButton();
}
if (now - gameState.asteroidsHard.lastBulletUpdate >= 85) {
gameState.asteroidsHard.lastBulletUpdate = now;
for (uint8_t i = 0; i < 5; ++i) {
if (gameState.asteroidsHard.bullets[i].on) {
gameState.asteroidsHard.bullets[i].y--;
if (gameState.asteroidsHard.bullets[i].y < 0) {
gameState.asteroidsHard.bullets[i].on = false;
}
}
}
}
if (now - gameState.asteroidsHard.lastSpawn >= gameState.asteroidsHard.spawnInterval) {
gameState.asteroidsHard.lastSpawn = now;
asteroidsHardSpawnRock();
}
for (uint8_t i = 0; i < 8; ++i) {
if (gameState.asteroidsHard.rocks[i].active &&
now - gameState.asteroidsHard.rocks[i].lastMove >= gameState.asteroidsHard.rocks[i].moveInterval) {
gameState.asteroidsHard.rocks[i].lastMove = now;
gameState.asteroidsHard.rocks[i].y++;
if (gameState.asteroidsHard.rocks[i].y > 7) {
gameState.asteroidsHard.rocks[i].active = false;
}
}
}
for (uint8_t bi = 0; bi < 5; ++bi) {
if (gameState.asteroidsHard.bullets[bi].on) {
for (uint8_t ri = 0; ri < 8; ++ri) {
if (gameState.asteroidsHard.rocks[ri].active &&
gameState.asteroidsHard.bullets[bi].x == gameState.asteroidsHard.rocks[ri].x &&
gameState.asteroidsHard.bullets[bi].y == gameState.asteroidsHard.rocks[ri].y) {
gameState.asteroidsHard.bullets[bi].on = false;
gameState.asteroidsHard.rocks[ri].active = false;
score++;
beepScore();
break;
}
}
}
}
for (uint8_t i = 0; i < 8; ++i) {
if (gameState.asteroidsHard.rocks[i].active &&
gameState.asteroidsHard.rocks[i].x == gameState.asteroidsHard.shipX &&
gameState.asteroidsHard.rocks[i].y == 7) {
beepHit();
finishGame(false, "Ship hit");
return;
}
}
int16_t newInterval = static_cast<int16_t>(900 - score * 16);
if (newInterval < 220) newInterval = 220;
gameState.asteroidsHard.spawnInterval = static_cast<uint16_t>(newInterval);
}
void asteroidsHardDraw() {
clearFrame();
for (uint8_t i = 0; i < 8; ++i) {
if (gameState.asteroidsHard.rocks[i].active) {
setPixel(gameState.asteroidsHard.rocks[i].x, gameState.asteroidsHard.rocks[i].y, Color(40, 18, 0));
}
}
for (uint8_t i = 0; i < 5; ++i) {
if (gameState.asteroidsHard.bullets[i].on) {
setPixel(gameState.asteroidsHard.bullets[i].x, gameState.asteroidsHard.bullets[i].y, Color(65, 65, 65));
}
}
setPixel(gameState.asteroidsHard.shipX, 7, Color(0, 40, 45));
}
// ========================================
// PAC-MAN (EASY) GAME
// ========================================
void pacEasyRefillPellets() {
gameState.pacEasy.pelletsLeft = 0;
for (uint8_t y = 0; y < 8; ++y) {
for (uint8_t x = 0; x < 8; ++x) {
gameState.pacEasy.pellets[y][x] = 1;
gameState.pacEasy.pelletsLeft++;
}
}
if (gameState.pacEasy.pellets[gameState.pacEasy.pacY][gameState.pacEasy.pacX]) {
gameState.pacEasy.pellets[gameState.pacEasy.pacY][gameState.pacEasy.pacX] = 0;
gameState.pacEasy.pelletsLeft--;
}
}
void pacEasyInit() {
gameState.pacEasy.pacX = 4;
gameState.pacEasy.pacY = 4;
gameState.pacEasy.direction = 0;
gameState.pacEasy.ghostX = 0;
gameState.pacEasy.ghostY = 0;
gameState.pacEasy.lastPacMove = millis();
gameState.pacEasy.lastGhostMove = millis();
pacEasyRefillPellets();
}
void pacEasyUpdate(uint32_t now) {
// Handle pause
if (takePress(B_PAUSE)) {
gamePaused = !gamePaused;
beepButton();
return;
}
if (gamePaused) return;
if (takePress(B_UP)) gameState.pacEasy.direction = 3;
else if (takePress(B_DOWN)) gameState.pacEasy.direction = 1;
else if (takePress(B_LEFT)) gameState.pacEasy.direction = 2;
else if (takePress(B_RIGHT)) gameState.pacEasy.direction = 0;
if (now - gameState.pacEasy.lastPacMove > 250) {
gameState.pacEasy.lastPacMove = now;
int8_t newX = gameState.pacEasy.pacX;
int8_t newY = gameState.pacEasy.pacY;
switch (gameState.pacEasy.direction) {
case 0: newX++; break;
case 1: newY++; break;
case 2: newX--; break;
default: newY--; break;
}
if (newX < 0) newX = 7;
if (newX >= 8) newX = 0;
if (newY < 0) newY = 7;
if (newY >= 8) newY = 0;
gameState.pacEasy.pacX = newX;
gameState.pacEasy.pacY = newY;
if (gameState.pacEasy.pellets[gameState.pacEasy.pacY][gameState.pacEasy.pacX]) {
gameState.pacEasy.pellets[gameState.pacEasy.pacY][gameState.pacEasy.pacX] = 0;
gameState.pacEasy.pelletsLeft--;
score += 10;
beepScore();
if (gameState.pacEasy.pelletsLeft == 0) {
score += 100;
pacEasyRefillPellets();
}
}
}
if (now - gameState.pacEasy.lastGhostMove > 400) {
gameState.pacEasy.lastGhostMove = now;
if (abs(gameState.pacEasy.ghostX - gameState.pacEasy.pacX) >
abs(gameState.pacEasy.ghostY - gameState.pacEasy.pacY)) {
gameState.pacEasy.ghostX += (gameState.pacEasy.ghostX < gameState.pacEasy.pacX) ? 1 : -1;
} else {
gameState.pacEasy.ghostY += (gameState.pacEasy.ghostY < gameState.pacEasy.pacY) ? 1 : -1;
}
if (gameState.pacEasy.ghostX < 0) gameState.pacEasy.ghostX = 7;
if (gameState.pacEasy.ghostX >= 8) gameState.pacEasy.ghostX = 0;
if (gameState.pacEasy.ghostY < 0) gameState.pacEasy.ghostY = 7;
if (gameState.pacEasy.ghostY >= 8) gameState.pacEasy.ghostY = 0;
}
if (gameState.pacEasy.pacX == gameState.pacEasy.ghostX &&
gameState.pacEasy.pacY == gameState.pacEasy.ghostY) {
beepHit();
finishGame(false, "Caught");
}
}
void pacEasyDraw() {
clearFrame();
for (uint8_t y = 0; y < 8; ++y) {
for (uint8_t x = 0; x < 8; ++x) {
if (gameState.pacEasy.pellets[y][x]) {
setPixel(static_cast<int8_t>(x), static_cast<int8_t>(y), Color(0, 28, 45));
}
}
}
setPixel(gameState.pacEasy.pacX, gameState.pacEasy.pacY, Color(130, 110, 0));
setPixel(gameState.pacEasy.ghostX, gameState.pacEasy.ghostY, Color(65, 0, 0));
}
// ========================================
// PAC-MAN (HARD) GAME
// ========================================
const uint8_t PACMAN_WORLDS[4][8][8] = {
{
{0, 0, 0, 0, 0, 0, 0, 0},
{0, 1, 1, 1, 1, 1, 1, 0},
{0, 1, 0, 0, 1, 0, 1, 0},
{0, 1, 1, 1, 1, 0, 1, 0},
{0, 1, 0, 1, 1, 1, 1, 0},
{0, 1, 1, 0, 0, 0, 1, 0},
{0, 1, 1, 1, 1, 1, 1, 0},
{0, 0, 0, 0, 0, 0, 0, 0},
},
{
{0, 0, 0, 0, 0, 0, 0, 0},
{0, 1, 1, 1, 1, 1, 1, 0},
{0, 1, 0, 1, 0, 1, 1, 0},
{0, 1, 1, 1, 0, 1, 1, 0},
{0, 1, 0, 1, 1, 1, 0, 0},
{0, 1, 1, 0, 1, 1, 1, 0},
{0, 1, 1, 1, 1, 0, 1, 0},
{0, 0, 0, 0, 0, 0, 0, 0},
},
{
{0, 0, 0, 0, 0, 0, 0, 0},
{0, 1, 1, 1, 0, 1, 1, 0},
{0, 1, 0, 1, 1, 1, 0, 0},
{0, 1, 1, 0, 1, 0, 1, 0},
{0, 0, 1, 1, 1, 1, 1, 0},
{0, 1, 0, 1, 0, 1, 1, 0},
{0, 1, 1, 1, 1, 1, 1, 0},
{0, 0, 0, 0, 0, 0, 0, 0},
},
{
{0, 0, 0, 0, 0, 0, 0, 0},
{0, 1, 1, 1, 1, 1, 1, 0},
{0, 1, 0, 0, 1, 0, 1, 0},
{0, 1, 1, 1, 1, 1, 1, 0},
{0, 1, 0, 1, 0, 1, 0, 0},
{0, 1, 1, 1, 1, 1, 1, 0},
{0, 1, 1, 0, 1, 0, 1, 0},
{0, 0, 0, 0, 0, 0, 0, 0},
},
};
bool pacmanIsWall(int8_t x, int8_t y) {
if (x < 0 || x > 7 || y < 0 || y > 7) return true;
return gameState.pacman.cells[y][x] == 0;
}
void pacmanLoadWorld() {
const uint8_t (*map)[8] = PACMAN_WORLDS[gameState.pacman.worldIndex];
gameState.pacman.pelletsLeft = 0;
for (uint8_t y = 0; y < 8; ++y) {
for (uint8_t x = 0; x < 8; ++x) {
if (map[y][x] == 0) {
gameState.pacman.cells[y][x] = 0;
} else {
gameState.pacman.cells[y][x] = 1;
gameState.pacman.pelletsLeft++;
}
}
}
if (gameState.pacman.cells[gameState.pacman.pacY][gameState.pacman.pacX] == 1) {
gameState.pacman.cells[gameState.pacman.pacY][gameState.pacman.pacX] = 2;
gameState.pacman.pelletsLeft--;
}
if (gameState.pacman.cells[gameState.pacman.ghostY][gameState.pacman.ghostX] == 1) {
gameState.pacman.cells[gameState.pacman.ghostY][gameState.pacman.ghostX] = 2;
gameState.pacman.pelletsLeft--;
}
}
void pacmanRespawn() {
const uint8_t (*map)[8] = PACMAN_WORLDS[gameState.pacman.worldIndex];
for (uint8_t y = 0; y < 8; ++y) {
for (uint8_t x = 0; x < 8; ++x) {
if (map[y][x] != 0) {
gameState.pacman.cells[y][x] = 1;
}
}
}
gameState.pacman.cells[gameState.pacman.pacY][gameState.pacman.pacX] = 2;
gameState.pacman.cells[gameState.pacman.ghostY][gameState.pacman.ghostX] = 2;
gameState.pacman.pelletsLeft = 0;
for (uint8_t y = 0; y < 8; ++y) {
for (uint8_t x = 0; x < 8; ++x) {
if (gameState.pacman.cells[y][x] == 1) {
gameState.pacman.pelletsLeft++;
}
}
}
}
void pacmanMoveGhost() {
const int8_t directions[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
int8_t bestX = gameState.pacman.ghostX;
int8_t bestY = gameState.pacman.ghostY;
int8_t bestDist = 127;
for (uint8_t i = 0; i < 4; ++i) {
int8_t newX = static_cast<int8_t>(gameState.pacman.ghostX + directions[i][0]);
int8_t newY = static_cast<int8_t>(gameState.pacman.ghostY + directions[i][1]);
if (pacmanIsWall(newX, newY)) continue;
int8_t dist = static_cast<int8_t>(abs(newX - gameState.pacman.pacX) + abs(newY - gameState.pacman.pacY));
if (dist < bestDist || (dist == bestDist && random(0, 2) == 0)) {
bestDist = dist;
bestX = newX;
bestY = newY;
}
}
gameState.pacman.ghostX = bestX;
gameState.pacman.ghostY = bestY;
}
void pacmanInit() {
gameState.pacman.worldIndex = 0;
gameState.pacman.pacX = 1;
gameState.pacman.pacY = 1;
gameState.pacman.ghostX = 6;
gameState.pacman.ghostY = 6;
gameState.pacman.dx = 1;
gameState.pacman.dy = 0;
gameState.pacman.nextDx = 1;
gameState.pacman.nextDy = 0;
gameState.pacman.pacInterval = 210;
gameState.pacman.ghostInterval = 360;
gameState.pacman.lastPacMove = millis();
gameState.pacman.lastGhostMove = millis();
pacmanLoadWorld();
}
void pacmanUpdate(uint32_t now) {
// Handle pause
if (takePress(B_PAUSE)) {
gamePaused = !gamePaused;
beepButton();
return;
}
if (gamePaused) return;
if (takePress(B_UP)) {
gameState.pacman.nextDx = 0;
gameState.pacman.nextDy = -1;
beepButton();
}
if (takePress(B_DOWN)) {
gameState.pacman.nextDx = 0;
gameState.pacman.nextDy = 1;
beepButton();
}
if (takePress(B_LEFT)) {
gameState.pacman.nextDx = -1;
gameState.pacman.nextDy = 0;
beepButton();
}
if (takePress(B_RIGHT)) {
gameState.pacman.nextDx = 1;
gameState.pacman.nextDy = 0;
beepButton();
}
if (now - gameState.pacman.lastPacMove >= gameState.pacman.pacInterval) {
gameState.pacman.lastPacMove = now;
if (!pacmanIsWall(static_cast<int8_t>(gameState.pacman.pacX + gameState.pacman.nextDx),
static_cast<int8_t>(gameState.pacman.pacY + gameState.pacman.nextDy))) {
gameState.pacman.dx = gameState.pacman.nextDx;
gameState.pacman.dy = gameState.pacman.nextDy;
}
int8_t newX = static_cast<int8_t>(gameState.pacman.pacX + gameState.pacman.dx);
int8_t newY = static_cast<int8_t>(gameState.pacman.pacY + gameState.pacman.dy);
if (!pacmanIsWall(newX, newY)) {
gameState.pacman.pacX = newX;
gameState.pacman.pacY = newY;
}
if (gameState.pacman.cells[gameState.pacman.pacY][gameState.pacman.pacX] == 1) {
gameState.pacman.cells[gameState.pacman.pacY][gameState.pacman.pacX] = 2;
if (gameState.pacman.pelletsLeft) gameState.pacman.pelletsLeft--;
score++;
beepScore();
}
}
if (now - gameState.pacman.lastGhostMove >= gameState.pacman.ghostInterval) {
gameState.pacman.lastGhostMove = now;
pacmanMoveGhost();
}
if (gameState.pacman.pacX == gameState.pacman.ghostX &&
gameState.pacman.pacY == gameState.pacman.ghostY) {
beepHit();
finishGame(false, "Caught");
return;
}
if (gameState.pacman.pelletsLeft == 0) {
if (gameState.pacman.worldIndex < 3) {
gameState.pacman.worldIndex++;
score += 25;
beepScore();
gameState.pacman.pacX = 1;
gameState.pacman.pacY = 1;
gameState.pacman.ghostX = 6;
gameState.pacman.ghostY = 6;
gameState.pacman.dx = 1;
gameState.pacman.dy = 0;
gameState.pacman.nextDx = 1;
gameState.pacman.nextDy = 0;
if (gameState.pacman.ghostInterval > 120) {
gameState.pacman.ghostInterval = static_cast<uint16_t>(gameState.pacman.ghostInterval - 18);
}
if (gameState.pacman.pacInterval > 145) {
gameState.pacman.pacInterval = static_cast<uint16_t>(gameState.pacman.pacInterval - 10);
}
pacmanRespawn();
} else {
score += 100;
finishGame(true, "World 4 clear");
return;
}
}
}
void pacmanDraw() {
clearFrame();
for (uint8_t y = 0; y < 8; ++y) {
for (uint8_t x = 0; x < 8; ++x) {
if (gameState.pacman.cells[y][x] == 0) {
setPixel(x, y, Color(0, 0, 24));
} else if (gameState.pacman.cells[y][x] == 1) {
setPixel(x, y, Color(170, 150, 55));
}
}
}
setPixel(gameState.pacman.ghostX, gameState.pacman.ghostY, Color(45, 0, 0));
setPixel(gameState.pacman.pacX, gameState.pacman.pacY, Color(255, 95, 0));
}
// ========================================
// SPACE SHOOTER GAME
// ========================================
void spaceShooterInit() {
gameState.shooter.shipX = 3;
gameState.shooter.enemyInterval = 720;
gameState.shooter.spawnInterval = 1350;
gameState.shooter.lastMove = millis();
gameState.shooter.lastPlayerBullet = millis();
gameState.shooter.lastEnemyBullet = millis();
gameState.shooter.lastEnemyStep = millis();
gameState.shooter.lastSpawn = millis();
gameState.shooter.lastShot = millis();
gameState.shooter.lastEnemyShoot = millis();
for (uint8_t i = 0; i < 6; ++i) {
gameState.shooter.playerBullets[i].on = false;
}
for (uint8_t i = 0; i < 5; ++i) {
gameState.shooter.enemyBullets[i].on = false;
}
for (uint8_t i = 0; i < 8; ++i) {
gameState.shooter.enemies[i].active = false;
}
}
void spaceShooterFirePlayer() {
for (uint8_t i = 0; i < 6; ++i) {
if (!gameState.shooter.playerBullets[i].on) {
gameState.shooter.playerBullets[i] = {gameState.shooter.shipX, 6, true};
return;
}
}
}
void spaceShooterSpawnEnemy() {
for (uint8_t i = 0; i < 8; ++i) {
if (!gameState.shooter.enemies[i].active) {
gameState.shooter.enemies[i] = {static_cast<int8_t>(random(0, 8)), 0, true};
return;
}
}
}
void spaceShooterFireEnemy(int8_t x, int8_t y) {
for (uint8_t i = 0; i < 5; ++i) {
if (!gameState.shooter.enemyBullets[i].on) {
gameState.shooter.enemyBullets[i] = {x, y, true};
return;
}
}
}
void spaceShooterUpdate(uint32_t now) {
if (isDown(B_LEFT) && now - gameState.shooter.lastMove >= 125) {
gameState.shooter.lastMove = now;
if (gameState.shooter.shipX > 0) {
gameState.shooter.shipX--;
beepButton();
}
}
if (isDown(B_RIGHT) && now - gameState.shooter.lastMove >= 125) {
gameState.shooter.lastMove = now;
if (gameState.shooter.shipX < 7) {
gameState.shooter.shipX++;
beepButton();
}
}
if ((takePress(B_UP) || takePress(B_PAUSE)) && now - gameState.shooter.lastShot >= 320) {
gameState.shooter.lastShot = now;
spaceShooterFirePlayer();
beepButton();
}
if (now - gameState.shooter.lastSpawn >= gameState.shooter.spawnInterval) {
gameState.shooter.lastSpawn = now;
spaceShooterSpawnEnemy();
}
if (now - gameState.shooter.lastPlayerBullet >= 130) {
gameState.shooter.lastPlayerBullet = now;
for (uint8_t i = 0; i < 6; ++i) {
if (gameState.shooter.playerBullets[i].on) {
gameState.shooter.playerBullets[i].y--;
if (gameState.shooter.playerBullets[i].y < 0) {
gameState.shooter.playerBullets[i].on = false;
}
}
}
}
if (now - gameState.shooter.lastEnemyStep >= gameState.shooter.enemyInterval) {
gameState.shooter.lastEnemyStep = now;
for (uint8_t i = 0; i < 8; ++i) {
if (gameState.shooter.enemies[i].active) {
if (random(0, 3) == 0) {
int8_t newX = static_cast<int8_t>(gameState.shooter.enemies[i].x + random(-1, 2));
if (newX >= 0 && newX < 8) {
gameState.shooter.enemies[i].x = newX;
}
}
gameState.shooter.enemies[i].y++;
if (gameState.shooter.enemies[i].y >= 7) {
beepHit();
finishGame(false, "Line broken");
return;
}
}
}
}
if (now - gameState.shooter.lastEnemyShoot >= 850) {
gameState.shooter.lastEnemyShoot = now;
uint8_t start = static_cast<uint8_t>(random(0, 8));
for (uint8_t j = 0; j < 8; ++j) {
uint8_t i = static_cast<uint8_t>((start + j) % 8);
if (gameState.shooter.enemies[i].active) {
spaceShooterFireEnemy(gameState.shooter.enemies[i].x,
static_cast<int8_t>(gameState.shooter.enemies[i].y + 1));
break;
}
}
}
if (now - gameState.shooter.lastEnemyBullet >= 190) {
gameState.shooter.lastEnemyBullet = now;
for (uint8_t i = 0; i < 5; ++i) {
if (gameState.shooter.enemyBullets[i].on) {
gameState.shooter.enemyBullets[i].y++;
if (gameState.shooter.enemyBullets[i].y > 7) {
gameState.shooter.enemyBullets[i].on = false;
} else if (gameState.shooter.enemyBullets[i].y == 7 &&
gameState.shooter.enemyBullets[i].x == gameState.shooter.shipX) {
beepHit();
finishGame(false, "Shot down");
return;
}
}
}
}
for (uint8_t bi = 0; bi < 6; ++bi) {
if (gameState.shooter.playerBullets[bi].on) {
for (uint8_t ei = 0; ei < 8; ++ei) {
if (gameState.shooter.enemies[ei].active &&
gameState.shooter.playerBullets[bi].x == gameState.shooter.enemies[ei].x &&
gameState.shooter.playerBullets[bi].y == gameState.shooter.enemies[ei].y) {
gameState.shooter.playerBullets[bi].on = false;
gameState.shooter.enemies[ei].active = false;
score++;
beepScore();
break;
}
}
}
}
int16_t newEnemyInterval = static_cast<int16_t>(720 - score * 2);
if (newEnemyInterval < 300) newEnemyInterval = 300;
gameState.shooter.enemyInterval = static_cast<uint16_t>(newEnemyInterval);
int16_t newSpawnInterval = static_cast<int16_t>(1350 - score * 5);
if (newSpawnInterval < 500) newSpawnInterval = 500;
gameState.shooter.spawnInterval = static_cast<uint16_t>(newSpawnInterval);
}
void spaceShooterDraw() {
clearFrame();
for (uint8_t i = 0; i < 8; ++i) {
if (gameState.shooter.enemies[i].active) {
setPixel(gameState.shooter.enemies[i].x, gameState.shooter.enemies[i].y, Color(46, 0, 20));
}
}
for (uint8_t i = 0; i < 6; ++i) {
if (gameState.shooter.playerBullets[i].on) {
setPixel(gameState.shooter.playerBullets[i].x, gameState.shooter.playerBullets[i].y, Color(65, 65, 65));
}
}
for (uint8_t i = 0; i < 5; ++i) {
if (gameState.shooter.enemyBullets[i].on) {
setPixel(gameState.shooter.enemyBullets[i].x, gameState.shooter.enemyBullets[i].y, Color(55, 10, 0));
}
}
setPixel(gameState.shooter.shipX, 7, Color(0, 60, 10));
}
// ========================================
// BREAKOUT GAME
// ========================================
void breakoutResetBricks() {
for (uint8_t y = 0; y < 3; ++y) {
for (uint8_t x = 0; x < 8; ++x) {
gameState.breakout.bricks[y][x] = true;
}
}
}
void breakoutResetBall() {
gameState.breakout.ballX = 3;
gameState.breakout.ballY = 6;
gameState.breakout.velocityX = (random(0, 2) == 0) ? -1 : 1;
gameState.breakout.velocityY = -1;
}
bool breakoutAnyBricks() {
for (uint8_t y = 0; y < 3; ++y) {
for (uint8_t x = 0; x < 8; ++x) {
if (gameState.breakout.bricks[y][x]) return true;
}
}
return false;
}
void breakoutInit() {
breakoutResetBricks();
gameState.breakout.paddleX = 2;
breakoutResetBall();
gameState.breakout.moveInterval = 200;
gameState.breakout.lastMove = millis();
gameState.breakout.lastPaddleMove = millis();
}
void breakoutUpdate(uint32_t now) {
// Handle pause
if (takePress(B_PAUSE)) {
gamePaused = !gamePaused;
beepButton();
return;
}
if (gamePaused) return;
if (isDown(B_LEFT) && now - gameState.breakout.lastPaddleMove >= 80) {
gameState.breakout.lastPaddleMove = now;
if (gameState.breakout.paddleX > 0) {
gameState.breakout.paddleX--;
beepButton();
}
}
if (isDown(B_RIGHT) && now - gameState.breakout.lastPaddleMove >= 80) {
gameState.breakout.lastPaddleMove = now;
if (gameState.breakout.paddleX < 5) {
gameState.breakout.paddleX++;
beepButton();
}
}
if (now - gameState.breakout.lastMove < gameState.breakout.moveInterval) return;
gameState.breakout.lastMove = now;
int8_t newX = static_cast<int8_t>(gameState.breakout.ballX + gameState.breakout.velocityX);
int8_t newY = static_cast<int8_t>(gameState.breakout.ballY + gameState.breakout.velocityY);
if (newX < 0 || newX > 7) {
gameState.breakout.velocityX = static_cast<int8_t>(-gameState.breakout.velocityX);
newX = static_cast<int8_t>(gameState.breakout.ballX + gameState.breakout.velocityX);
}
if (newY < 0) {
gameState.breakout.velocityY = 1;
newY = static_cast<int8_t>(gameState.breakout.ballY + gameState.breakout.velocityY);
}
if (newY >= 0 && newY < 3 && gameState.breakout.bricks[newY][newX]) {
gameState.breakout.bricks[newY][newX] = false;
gameState.breakout.velocityY = static_cast<int8_t>(-gameState.breakout.velocityY);
newY = static_cast<int8_t>(gameState.breakout.ballY + gameState.breakout.velocityY);
score++;
beepScore();
}
if (newY >= 7) {
bool topHit = (newX >= gameState.breakout.paddleX && newX <= gameState.breakout.paddleX + 2);
bool leftSideHit = (newX == gameState.breakout.paddleX - 1 && gameState.breakout.velocityX > 0);
bool rightSideHit = (newX == gameState.breakout.paddleX + 3 && gameState.breakout.velocityX < 0);
if (topHit || leftSideHit || rightSideHit) {
gameState.breakout.velocityY = -1;
if (leftSideHit || rightSideHit) {
gameState.breakout.velocityX = static_cast<int8_t>(-gameState.breakout.velocityX);
} else {
int8_t hitPos = static_cast<int8_t>(newX - (gameState.breakout.paddleX + 1));
if (hitPos < 0) gameState.breakout.velocityX = -1;
if (hitPos > 0) gameState.breakout.velocityX = 1;
}
newX = static_cast<int8_t>(constrain(newX, 0, 7));
newY = 6;
beepButton();
} else {
beepHit();
finishGame(false, "Missed ball");
return;
}
}
gameState.breakout.ballX = newX;
gameState.breakout.ballY = newY;
if (!breakoutAnyBricks()) {
score += 5;
beepWin();
breakoutResetBricks();
breakoutResetBall();
if (gameState.breakout.moveInterval > 80) {
gameState.breakout.moveInterval = static_cast<uint16_t>(gameState.breakout.moveInterval - 12);
}
}
}
void breakoutDraw() {
clearFrame();
for (uint8_t y = 0; y < 3; ++y) {
for (uint8_t x = 0; x < 8; ++x) {
if (gameState.breakout.bricks[y][x]) {
setPixel(x, y, Color(
static_cast<uint8_t>(35 + y * 6),
static_cast<uint8_t>(10 + y * 4),
0
));
}
}
}
for (uint8_t x = 0; x < 3; ++x) {
setPixel(static_cast<int8_t>(gameState.breakout.paddleX + x), 7, Color(0, 48, 50));
}
setPixel(gameState.breakout.ballX, gameState.breakout.ballY, Color(65, 65, 65));
}
// ========================================
// TIC-TAC-TOE GAME
// ========================================
void tttInit(bool aiMode) {
memset(gameState.ttt.board, 0, sizeof(gameState.ttt.board));
gameState.ttt.cursorX = 1;
gameState.ttt.cursorY = 1;
gameState.ttt.turn = 1;
gameState.ttt.aiMode = aiMode;
gameState.ttt.waitingForAI = false;
gameState.ttt.aiMoveTime = 0;
}
uint8_t tttCheckWinner() {
for (uint8_t i = 0; i < 3; ++i) {
if (gameState.ttt.board[i][0] &&
gameState.ttt.board[i][0] == gameState.ttt.board[i][1] &&
gameState.ttt.board[i][1] == gameState.ttt.board[i][2]) {
return gameState.ttt.board[i][0];
}
if (gameState.ttt.board[0][i] &&
gameState.ttt.board[0][i] == gameState.ttt.board[1][i] &&
gameState.ttt.board[1][i] == gameState.ttt.board[2][i]) {
return gameState.ttt.board[0][i];
}
}
if (gameState.ttt.board[0][0] &&
gameState.ttt.board[0][0] == gameState.ttt.board[1][1] &&
gameState.ttt.board[1][1] == gameState.ttt.board[2][2]) {
return gameState.ttt.board[0][0];
}
if (gameState.ttt.board[0][2] &&
gameState.ttt.board[0][2] == gameState.ttt.board[1][1] &&
gameState.ttt.board[1][1] == gameState.ttt.board[2][0]) {
return gameState.ttt.board[0][2];
}
for (uint8_t y = 0; y < 3; ++y) {
for (uint8_t x = 0; x < 3; ++x) {
if (!gameState.ttt.board[y][x]) return 0;
}
}
return 3; // Draw
}
bool tttFindWinningMove(uint8_t player, uint8_t &outX, uint8_t &outY) {
for (uint8_t y = 0; y < 3; ++y) {
for (uint8_t x = 0; x < 3; ++x) {
if (!gameState.ttt.board[y][x]) {
gameState.ttt.board[y][x] = player;
bool wins = tttCheckWinner() == player;
gameState.ttt.board[y][x] = 0;
if (wins) {
outX = x;
outY = y;
return true;
}
}
}
}
return false;
}
void tttAIMove() {
uint8_t x = 0, y = 0;
if (tttFindWinningMove(2, x, y) || tttFindWinningMove(1, x, y)) {
gameState.ttt.board[y][x] = 2;
return;
}
if (!gameState.ttt.board[1][1]) {
gameState.ttt.board[1][1] = 2;
return;
}
const uint8_t priority[9][2] = {
{0, 0}, {2, 0}, {0, 2}, {2, 2},
{1, 0}, {0, 1}, {2, 1}, {1, 2}, {1, 1}
};
for (uint8_t i = 0; i < 9; ++i) {
uint8_t px = priority[i][0];
uint8_t py = priority[i][1];
if (!gameState.ttt.board[py][px]) {
gameState.ttt.board[py][px] = 2;
return;
}
}
}
void tttFinish() {
uint8_t result = tttCheckWinner();
if (!result) return;
if (result == 1) {
score += 5;
finishGame(true, gameState.ttt.aiMode ? "You win" : "X wins");
} else if (result == 2) {
finishGame(gameState.ttt.aiMode ? false : true, gameState.ttt.aiMode ? "AI wins" : "O wins");
} else {
finishGame(false, "Draw");
}
}
void tttUpdate(uint32_t now) {
if (gameState.ttt.aiMode && gameState.ttt.waitingForAI && now >= gameState.ttt.aiMoveTime) {
gameState.ttt.waitingForAI = false;
tttAIMove();
tttFinish();
if (systemState == STATE_PLAYING) {
gameState.ttt.turn = 1;
}
return;
}
if (gameState.ttt.aiMode && gameState.ttt.turn == 2) return;
if (takePress(B_UP) && gameState.ttt.cursorY > 0) {
gameState.ttt.cursorY--;
beepButton();
}
if (takePress(B_DOWN) && gameState.ttt.cursorY < 2) {
gameState.ttt.cursorY++;
beepButton();
}
if (takePress(B_LEFT) && gameState.ttt.cursorX > 0) {
gameState.ttt.cursorX--;
beepButton();
}
if (takePress(B_RIGHT) && gameState.ttt.cursorX < 2) {
gameState.ttt.cursorX++;
beepButton();
}
if (takePress(B_PAUSE)) {
if (gameState.ttt.board[gameState.ttt.cursorY][gameState.ttt.cursorX]) {
beepHit();
return;
}
gameState.ttt.board[gameState.ttt.cursorY][gameState.ttt.cursorX] = gameState.ttt.turn;
score++;
beepButton();
tttFinish();
if (systemState != STATE_PLAYING) return;
if (gameState.ttt.aiMode) {
gameState.ttt.turn = 2;
gameState.ttt.waitingForAI = true;
gameState.ttt.aiMoveTime = now + 230;
} else {
gameState.ttt.turn = (gameState.ttt.turn == 1) ? 2 : 1;
}
}
}
void tttDraw() {
clearFrame();
const int8_t cellX[3] = {1, 3, 6};
const int8_t cellY[3] = {1, 3, 6};
for (uint8_t i = 0; i < 8; ++i) {
setPixel(2, i, Color(8, 8, 8));
setPixel(5, i, Color(8, 8, 8));
setPixel(i, 2, Color(8, 8, 8));
setPixel(i, 5, Color(8, 8, 8));
}
for (uint8_t y = 0; y < 3; ++y) {
for (uint8_t x = 0; x < 3; ++x) {
int8_t gridX = cellX[x];
int8_t gridY = cellY[y];
if (gameState.ttt.board[y][x] == 1) {
setPixel(gridX, gridY, Color(0, 52, 0));
} else if (gameState.ttt.board[y][x] == 2) {
setPixel(gridX, gridY, Color(55, 0, 0));
} else {
setPixel(gridX, gridY, Color(10, 10, 10));
}
if (x == gameState.ttt.cursorX && y == gameState.ttt.cursorY &&
(!gameState.ttt.aiMode || gameState.ttt.turn == 1)) {
setPixel(gridX, gridY, gameState.ttt.board[y][x] ? Color(65, 65, 65) : Color(0, 0, 65));
}
}
}
}
// ========================================
// AIR HOCKEY (CLEAN REWRITE)
// ========================================
void pongInit() {
gameState.pong.playerX = 3;
gameState.pong.aiX = 3;
gameState.pong.paddleSize = 3;
gameState.pong.ballX = 4;
gameState.pong.ballY = 4;
gameState.pong.velocityX = (random(0, 2) == 0) ? -1 : 1;
gameState.pong.velocityY = 1; // AI serves first: ball starts toward player side
gameState.pong.moveInterval = 155;
gameState.pong.lastMove = millis();
gameState.pong.lastPaddleMove = millis();
gameState.pong.lastAIMove = millis();
score = 0;
}
void pongResetBall(int8_t directionY) {
gameState.pong.ballX = 4;
gameState.pong.ballY = 4;
gameState.pong.velocityX = (random(0, 2) == 0) ? -1 : 1;
gameState.pong.velocityY = directionY;
}
// ========================================
// UPDATE
// ========================================
void pongUpdate(uint32_t now) {
if (takePress(B_PAUSE)) {
gamePaused = !gamePaused;
beepButton();
return;
}
if (gamePaused) return;
if (isDown(B_LEFT) && now - gameState.pong.lastPaddleMove >= 60) {
gameState.pong.lastPaddleMove = now;
if (gameState.pong.playerX > 0) {
gameState.pong.playerX--;
}
}
if (isDown(B_RIGHT) && now - gameState.pong.lastPaddleMove >= 60) {
gameState.pong.lastPaddleMove = now;
if (gameState.pong.playerX < 8 - gameState.pong.paddleSize) {
gameState.pong.playerX++;
}
}
if (now - gameState.pong.lastAIMove >= 110) {
gameState.pong.lastAIMove = now;
int8_t targetX = static_cast<int8_t>(gameState.pong.ballX - 1);
targetX = static_cast<int8_t>(constrain(targetX, 0, 8 - gameState.pong.paddleSize));
if (random(0, 100) < 85) {
if (gameState.pong.aiX < targetX) {
gameState.pong.aiX++;
} else if (gameState.pong.aiX > targetX) {
gameState.pong.aiX--;
}
}
}
if (now - gameState.pong.lastMove < gameState.pong.moveInterval) return;
gameState.pong.lastMove = now;
int8_t newX = gameState.pong.ballX + gameState.pong.velocityX;
int8_t newY = gameState.pong.ballY + gameState.pong.velocityY;
if (newX < 0) {
newX = 0;
gameState.pong.velocityX = 1;
} else if (newX > 7) {
newX = 7;
gameState.pong.velocityX = -1;
}
if (newY >= 7 && gameState.pong.velocityY > 0) {
if (newX >= gameState.pong.playerX &&
newX < gameState.pong.playerX + gameState.pong.paddleSize) {
newY = 6;
gameState.pong.velocityY = -1;
int8_t hit = newX - (gameState.pong.playerX + 1);
if (hit < 0) {
gameState.pong.velocityX = -1;
} else if (hit > 0) {
gameState.pong.velocityX = 1;
}
score++;
beepButton();
} else {
beepHit();
finishGame(false, "Missed paddle");
return;
}
}
if (newY <= 0 && gameState.pong.velocityY < 0) {
if (newX >= gameState.pong.aiX &&
newX < gameState.pong.aiX + gameState.pong.paddleSize) {
newY = 1;
gameState.pong.velocityY = 1;
int8_t hit = newX - (gameState.pong.aiX + 1);
if (hit < 0) {
gameState.pong.velocityX = -1;
} else if (hit > 0) {
gameState.pong.velocityX = 1;
}
beepButton();
} else {
score += 20;
beepScore();
pongResetBall(1);
// Keep acceleration gentle so the ball stays readable like breakout.
if (gameState.pong.moveInterval > 120) {
gameState.pong.moveInterval = static_cast<uint16_t>(gameState.pong.moveInterval - 1);
}
return;
}
}
gameState.pong.ballX = constrain(newX, 0, 7);
gameState.pong.ballY = constrain(newY, 0, 7);
}
// ========================================
// DRAW
// ========================================
void pongDraw() {
clearFrame();
// Player paddle (bottom)
for (uint8_t i = 0; i < gameState.pong.paddleSize; i++) {
setPixel(gameState.pong.playerX + i, 7, Color(0, 80, 0));
}
// AI paddle (top)
for (uint8_t i = 0; i < gameState.pong.paddleSize; i++) {
setPixel(gameState.pong.aiX + i, 0, Color(80, 0, 0));
}
// Ball
setPixel(gameState.pong.ballX, gameState.pong.ballY, Color(80, 80, 80));
}
// ========================================
// TUG OF WAR GAME
// ========================================
void tugInit() {
gameState.tug.position = 4;
gameState.tug.player1LastPress = millis();
gameState.tug.player2LastPress = millis();
score = 0;
}
void tugUpdate(uint32_t now) {
if ((takePress(B_UP) || takePress(B_LEFT)) && now - gameState.tug.player1LastPress >= 80) {
gameState.tug.player1LastPress = now;
gameState.tug.position--;
score++;
beepButton();
}
if ((takePress(B_DOWN) || takePress(B_RIGHT) || takePress(B_PAUSE)) &&
now - gameState.tug.player2LastPress >= 80) {
gameState.tug.player2LastPress = now;
gameState.tug.position++;
score++;
beepButton();
}
if (gameState.tug.position <= 0) {
finishGame(true, "P1 wins");
return;
}
if (gameState.tug.position >= 7) {
finishGame(true, "P2 wins");
}
}
void tugDraw() {
clearFrame();
for (uint8_t x = 0; x < 8; ++x) {
setPixel(x, 3, Color(8, 8, 8));
}
setPixel(0, 3, Color(0, 50, 0));
setPixel(7, 3, Color(50, 0, 0));
setPixel(gameState.tug.position, 3, Color(65, 55, 0));
}
// ========================================
// CHECKERS GAME
// ========================================
//
// OFFICIAL RULES (American/English Checkers):
// 1. SETUP: 12 pieces per side on dark squares in first 3 rows
// 2. MOVEMENT (Regular pieces):
// - Move one square diagonally FORWARD only
// - CANNOT move backwards
// 3. JUMPING (Regular pieces in this implementation):
// - Can jump over opponent diagonally FORWARD only
// - Must jump if available (mandatory)
// - Can make multiple jumps in one turn
// - Jumped pieces are removed
// 4. KINGS:
// - Promoted when reaching opposite end
// - Can move AND jump in ALL diagonal directions
// - Move one square at a time (not multiple squares)
// 5. WINNING:
// - Capture all opponent pieces, OR
// - Block opponent from making any legal moves
//
// ========================================
bool checkersIsPlaySquare(int8_t x, int8_t y) {
// Only dark squares are playable in checkers
return (x + y) % 2 == 1;
}
bool checkersIsPlayer1Piece(uint8_t piece) {
return piece == 1 || piece == 2;
}
bool checkersIsPlayer2Piece(uint8_t piece) {
return piece == 3 || piece == 4;
}
bool checkersIsKing(uint8_t piece) {
return piece == 2 || piece == 4;
}
void checkersSetupBoard() {
// Clear board
memset(gameState.checkers.board, 0, sizeof(gameState.checkers.board));
// Place player 2 pieces (top, rows 0-2)
for (uint8_t y = 0; y < 3; ++y) {
for (uint8_t x = 0; x < 8; ++x) {
if (checkersIsPlaySquare(x, y)) {
gameState.checkers.board[y][x] = 3; // Player 2 piece
}
}
}
// Place player 1 pieces (bottom, rows 5-7)
for (uint8_t y = 5; y < 8; ++y) {
for (uint8_t x = 0; x < 8; ++x) {
if (checkersIsPlaySquare(x, y)) {
gameState.checkers.board[y][x] = 1; // Player 1 piece
}
}
}
}
bool checkersCanJump(int8_t fromX, int8_t fromY, int8_t dx, int8_t dy, int8_t &toX, int8_t &toY) {
uint8_t piece = gameState.checkers.board[fromY][fromX];
if (!piece) return false;
int8_t midX = fromX + dx;
int8_t midY = fromY + dy;
toX = fromX + dx * 2;
toY = fromY + dy * 2;
// Check bounds
if (toX < 0 || toX >= 8 || toY < 0 || toY >= 8) return false;
if (midX < 0 || midX >= 8 || midY < 0 || midY >= 8) return false;
// Destination must be empty and playable
if (gameState.checkers.board[toY][toX] != 0) return false;
if (!checkersIsPlaySquare(toX, toY)) return false;
uint8_t midPiece = gameState.checkers.board[midY][midX];
// Must jump over opponent piece
if (checkersIsPlayer1Piece(piece)) {
return checkersIsPlayer2Piece(midPiece);
} else {
return checkersIsPlayer1Piece(midPiece);
}
}
void checkersFindValidMoves() {
gameState.checkers.validMoveCount = 0;
if (gameState.checkers.selectedX < 0 || gameState.checkers.selectedY < 0) return;
int8_t x = gameState.checkers.selectedX;
int8_t y = gameState.checkers.selectedY;
uint8_t piece = gameState.checkers.board[y][x];
if (!piece) return;
bool isP1 = checkersIsPlayer1Piece(piece);
bool isKing = checkersIsKing(piece);
// PHASE 1: Check capture moves
// Non-king pieces may only capture forward to match the requested rules.
int8_t jumpX, jumpY;
bool canJump = false;
if ((isKing || isP1) && checkersCanJump(x, y, -1, -1, jumpX, jumpY)) {
gameState.checkers.validMoves[gameState.checkers.validMoveCount][0] = jumpX;
gameState.checkers.validMoves[gameState.checkers.validMoveCount][1] = jumpY;
gameState.checkers.validMoveCount++;
canJump = true;
}
if ((isKing || isP1) && checkersCanJump(x, y, 1, -1, jumpX, jumpY)) {
gameState.checkers.validMoves[gameState.checkers.validMoveCount][0] = jumpX;
gameState.checkers.validMoves[gameState.checkers.validMoveCount][1] = jumpY;
gameState.checkers.validMoveCount++;
canJump = true;
}
if ((isKing || !isP1) && checkersCanJump(x, y, -1, 1, jumpX, jumpY)) {
gameState.checkers.validMoves[gameState.checkers.validMoveCount][0] = jumpX;
gameState.checkers.validMoves[gameState.checkers.validMoveCount][1] = jumpY;
gameState.checkers.validMoveCount++;
canJump = true;
}
if ((isKing || !isP1) && checkersCanJump(x, y, 1, 1, jumpX, jumpY)) {
gameState.checkers.validMoves[gameState.checkers.validMoveCount][0] = jumpX;
gameState.checkers.validMoves[gameState.checkers.validMoveCount][1] = jumpY;
gameState.checkers.validMoveCount++;
canJump = true;
}
// If we found jumps, ONLY return jumps (captures are mandatory)
if (canJump) return;
// PHASE 2: Check REGULAR moves (non-captures)
// Regular pieces can ONLY move forward, kings can move any direction
const int8_t dirs[4][2] = {{-1, -1}, {1, -1}, {-1, 1}, {1, 1}};
for (uint8_t i = 0; i < 4; ++i) {
int8_t dx = dirs[i][0];
int8_t dy = dirs[i][1];
// *** FIX: Regular pieces can ONLY move forward (not jump, just move) ***
if (!isKing) {
// Player 1 (green, bottom) moves UP (negative dy)
if (isP1 && dy > 0) continue;
// Player 2 (red, top) moves DOWN (positive dy)
if (!isP1 && dy < 0) continue;
}
int8_t newX = x + dx;
int8_t newY = y + dy;
// Check bounds
if (newX < 0 || newX >= 8 || newY < 0 || newY >= 8) continue;
// Only dark squares are playable
if (!checkersIsPlaySquare(newX, newY)) continue;
// Destination must be empty
if (gameState.checkers.board[newY][newX] != 0) continue;
// Valid move!
gameState.checkers.validMoves[gameState.checkers.validMoveCount][0] = newX;
gameState.checkers.validMoves[gameState.checkers.validMoveCount][1] = newY;
gameState.checkers.validMoveCount++;
}
}
bool checkersIsValidMove(int8_t toX, int8_t toY) {
for (uint8_t i = 0; i < gameState.checkers.validMoveCount; ++i) {
if (gameState.checkers.validMoves[i][0] == toX &&
gameState.checkers.validMoves[i][1] == toY) {
return true;
}
}
return false;
}
void checkersExecuteMove(int8_t toX, int8_t toY) {
int8_t fromX = gameState.checkers.selectedX;
int8_t fromY = gameState.checkers.selectedY;
uint8_t piece = gameState.checkers.board[fromY][fromX];
// Check if this is a capture
int8_t dx = toX - fromX;
int8_t dy = toY - fromY;
if (abs(dx) == 2) {
// Remove captured piece
int8_t capX = fromX + dx / 2;
int8_t capY = fromY + dy / 2;
gameState.checkers.board[capY][capX] = 0;
score += 10;
beepScore();
}
// Move piece
gameState.checkers.board[toY][toX] = piece;
gameState.checkers.board[fromY][fromX] = 0;
// Check for king promotion
if (piece == 1 && toY == 0) {
gameState.checkers.board[toY][toX] = 2; // P1 becomes king
beepWin();
} else if (piece == 3 && toY == 7) {
gameState.checkers.board[toY][toX] = 4; // P2 becomes king
beepWin();
}
// Deselect piece
gameState.checkers.selectedX = -1;
gameState.checkers.selectedY = -1;
gameState.checkers.validMoveCount = 0;
// Switch players
gameState.checkers.currentPlayer = (gameState.checkers.currentPlayer == 1) ? 2 : 1;
beepButton();
}
bool checkersHasAnyMoves(uint8_t player) {
for (uint8_t y = 0; y < 8; ++y) {
for (uint8_t x = 0; x < 8; ++x) {
uint8_t piece = gameState.checkers.board[y][x];
if (!piece) continue;
bool isPlayerPiece = (player == 1) ? checkersIsPlayer1Piece(piece) : checkersIsPlayer2Piece(piece);
if (!isPlayerPiece) continue;
// Save current selection
int8_t oldSelX = gameState.checkers.selectedX;
int8_t oldSelY = gameState.checkers.selectedY;
// Temporarily select this piece
gameState.checkers.selectedX = x;
gameState.checkers.selectedY = y;
checkersFindValidMoves();
// Restore selection
gameState.checkers.selectedX = oldSelX;
gameState.checkers.selectedY = oldSelY;
if (gameState.checkers.validMoveCount > 0) {
checkersFindValidMoves(); // Restore valid moves for actual selection
return true;
}
}
}
checkersFindValidMoves(); // Restore valid moves for actual selection
return false;
}
bool checkersCheckGameOver() {
// Count pieces
uint8_t p1Count = 0, p2Count = 0;
for (uint8_t y = 0; y < 8; ++y) {
for (uint8_t x = 0; x < 8; ++x) {
uint8_t piece = gameState.checkers.board[y][x];
if (checkersIsPlayer1Piece(piece)) p1Count++;
if (checkersIsPlayer2Piece(piece)) p2Count++;
}
}
// Check if either player has no pieces
if (p1Count == 0) {
finishGame(false, "Player 2 wins");
return true;
}
if (p2Count == 0) {
finishGame(true, "Player 1 wins");
return true;
}
// Check if current player has no moves
if (!checkersHasAnyMoves(gameState.checkers.currentPlayer)) {
if (gameState.checkers.currentPlayer == 1) {
finishGame(false, "P2 wins (no moves)");
} else {
finishGame(true, "P1 wins (no moves)");
}
return true;
}
return false;
}
// ========================================
// CHECKERS AI
// ========================================
struct CheckersMove {
int8_t fromX;
int8_t fromY;
int8_t toX;
int8_t toY;
int16_t score;
};
int16_t checkersEvaluateMove(int8_t fromX, int8_t fromY, int8_t toX, int8_t toY) {
int16_t score = 0;
uint8_t piece = gameState.checkers.board[fromY][fromX];
bool isKing = checkersIsKing(piece);
// Check if this is a capture
int8_t dx = toX - fromX;
int8_t dy = toY - fromY;
bool isCapture = (abs(dx) == 2);
if (isCapture) {
// Captures are highly valuable
score += 100;
// Check if capturing a king
int8_t capX = fromX + dx / 2;
int8_t capY = fromY + dy / 2;
if (checkersIsKing(gameState.checkers.board[capY][capX])) {
score += 50; // Capturing a king is even better
}
}
// King promotion is very valuable
if (!isKing && toY == 7) {
score += 80;
}
// Moving toward promotion is good
if (!isKing) {
score += toY * 5; // Closer to promotion (row 7 for AI pieces) = higher score
}
// Central control is valuable
int8_t centerDist = abs(toX - 3) + abs(toX - 4);
score += (8 - centerDist) * 3;
// Edge squares are less desirable (easier to trap)
if (toX == 0 || toX == 7) score -= 10;
// Check if move leaves piece vulnerable
// (simplified: check if moving to a square where opponent can capture)
int8_t attackDirs[4][2] = {{-1, 1}, {1, 1}, {-1, -1}, {1, -1}};
for (uint8_t i = 0; i < 4; ++i) {
int8_t checkX = toX + attackDirs[i][0];
int8_t checkY = toY + attackDirs[i][1];
if (checkX >= 0 && checkX < 8 && checkY >= 0 && checkY < 8) {
uint8_t adjacentPiece = gameState.checkers.board[checkY][checkX];
if (checkersIsPlayer1Piece(adjacentPiece)) {
// Enemy piece diagonal - check if we'd be capturable
int8_t behindX = toX - attackDirs[i][0];
int8_t behindY = toY - attackDirs[i][1];
if (behindX >= 0 && behindX < 8 && behindY >= 0 && behindY < 8) {
if (gameState.checkers.board[behindY][behindX] == 0) {
score -= 25; // Vulnerable position
}
}
}
}
}
// Add small random factor to avoid predictability
score += random(-5, 6);
return score;
}
void checkersAIMove(uint32_t now) {
CheckersMove bestMove = {-1, -1, -1, -1, -32000};
// Find all possible moves for AI (Player 2)
for (uint8_t y = 0; y < 8; ++y) {
for (uint8_t x = 0; x < 8; ++x) {
uint8_t piece = gameState.checkers.board[y][x];
if (!checkersIsPlayer2Piece(piece)) continue;
// Temporarily select this piece to find its moves
int8_t oldSelX = gameState.checkers.selectedX;
int8_t oldSelY = gameState.checkers.selectedY;
uint8_t oldMoveCount = gameState.checkers.validMoveCount;
gameState.checkers.selectedX = x;
gameState.checkers.selectedY = y;
checkersFindValidMoves();
// Evaluate each valid move
for (uint8_t i = 0; i < gameState.checkers.validMoveCount; ++i) {
int8_t toX = gameState.checkers.validMoves[i][0];
int8_t toY = gameState.checkers.validMoves[i][1];
int16_t score = checkersEvaluateMove(x, y, toX, toY);
if (score > bestMove.score) {
bestMove.fromX = x;
bestMove.fromY = y;
bestMove.toX = toX;
bestMove.toY = toY;
bestMove.score = score;
}
}
// Restore selection
gameState.checkers.selectedX = oldSelX;
gameState.checkers.selectedY = oldSelY;
gameState.checkers.validMoveCount = oldMoveCount;
}
}
// If we found a valid move, execute it
if (bestMove.fromX >= 0) {
// Select the piece
gameState.checkers.selectedX = bestMove.fromX;
gameState.checkers.selectedY = bestMove.fromY;
checkersFindValidMoves();
// Execute the move
checkersExecuteMove(bestMove.toX, bestMove.toY);
// Keep AI move visible for a short blink window.
gameState.checkers.aiLastFromX = bestMove.fromX;
gameState.checkers.aiLastFromY = bestMove.fromY;
gameState.checkers.aiLastToX = bestMove.toX;
gameState.checkers.aiLastToY = bestMove.toY;
gameState.checkers.aiBlinkVisible = true;
gameState.checkers.aiBlinkNextToggleAt = now + 140;
gameState.checkers.aiBlinkEndsAt = now + 1100;
}
}
void checkersInit(bool aiMode) {
checkersSetupBoard();
gameState.checkers.cursorX = 0;
gameState.checkers.cursorY = 5;
gameState.checkers.selectedX = -1;
gameState.checkers.selectedY = -1;
gameState.checkers.currentPlayer = 1;
gameState.checkers.mustCapture = false;
gameState.checkers.validMoveCount = 0;
gameState.checkers.aiMode = aiMode;
gameState.checkers.waitingForAI = false;
gameState.checkers.aiMoveTime = 0;
gameState.checkers.aiLastFromX = -1;
gameState.checkers.aiLastFromY = -1;
gameState.checkers.aiLastToX = -1;
gameState.checkers.aiLastToY = -1;
gameState.checkers.aiBlinkVisible = false;
gameState.checkers.aiBlinkNextToggleAt = 0;
gameState.checkers.aiBlinkEndsAt = 0;
score = 0;
}
void checkersUpdate(uint32_t now) {
if (gameState.checkers.aiMode && gameState.checkers.aiBlinkEndsAt > 0) {
if (now >= gameState.checkers.aiBlinkEndsAt) {
gameState.checkers.aiBlinkEndsAt = 0;
gameState.checkers.aiBlinkVisible = false;
} else {
if (now >= gameState.checkers.aiBlinkNextToggleAt) {
gameState.checkers.aiBlinkVisible = !gameState.checkers.aiBlinkVisible;
gameState.checkers.aiBlinkNextToggleAt = now + 140;
}
return;
}
}
// Handle AI turn
if (gameState.checkers.aiMode && gameState.checkers.currentPlayer == 2) {
if (!gameState.checkers.waitingForAI) {
// Start AI thinking
gameState.checkers.waitingForAI = true;
gameState.checkers.aiMoveTime = now + 500; // AI thinks for 0.5 seconds
return;
}
if (now >= gameState.checkers.aiMoveTime) {
gameState.checkers.waitingForAI = false;
checkersAIMove(now);
checkersCheckGameOver();
return;
}
// Still waiting for AI
return;
}
// Human player controls
if (takePress(B_UP) && gameState.checkers.cursorY > 0) {
gameState.checkers.cursorY--;
beepButton();
}
if (takePress(B_DOWN) && gameState.checkers.cursorY < 7) {
gameState.checkers.cursorY++;
beepButton();
}
if (takePress(B_LEFT) && gameState.checkers.cursorX > 0) {
gameState.checkers.cursorX--;
beepButton();
}
if (takePress(B_RIGHT) && gameState.checkers.cursorX < 7) {
gameState.checkers.cursorX++;
beepButton();
}
if (takePress(B_PAUSE)) {
int8_t x = gameState.checkers.cursorX;
int8_t y = gameState.checkers.cursorY;
// If no piece selected, try to select one
if (gameState.checkers.selectedX < 0) {
uint8_t piece = gameState.checkers.board[y][x];
// Check if this is current player's piece
bool validSelection = false;
if (gameState.checkers.currentPlayer == 1 && checkersIsPlayer1Piece(piece)) {
validSelection = true;
} else if (gameState.checkers.currentPlayer == 2 && checkersIsPlayer2Piece(piece)) {
validSelection = true;
}
if (validSelection) {
gameState.checkers.selectedX = x;
gameState.checkers.selectedY = y;
checkersFindValidMoves();
if (gameState.checkers.validMoveCount > 0) {
beepButton();
} else {
// No valid moves, deselect
gameState.checkers.selectedX = -1;
gameState.checkers.selectedY = -1;
beepHit();
}
} else {
beepHit();
}
} else {
// Piece is selected, try to move or deselect
if (x == gameState.checkers.selectedX && y == gameState.checkers.selectedY) {
// Clicked same piece, deselect
gameState.checkers.selectedX = -1;
gameState.checkers.selectedY = -1;
gameState.checkers.validMoveCount = 0;
beepButton();
} else if (checkersIsValidMove(x, y)) {
// Valid move
checkersExecuteMove(x, y);
checkersCheckGameOver();
} else {
beepHit();
}
}
}
}
void checkersDraw() {
clearFrame();
// Draw checkerboard pattern and pieces
for (uint8_t y = 0; y < 8; ++y) {
for (uint8_t x = 0; x < 8; ++x) {
uint32_t squareColor;
// Checkerboard pattern
if (checkersIsPlaySquare(x, y)) {
squareColor = Color(15, 15, 15); // Dark squares
} else {
squareColor = Color(5, 5, 5); // Light squares
setPixel(x, y, squareColor);
continue;
}
uint8_t piece = gameState.checkers.board[y][x];
if (piece == 0) {
// Empty dark square
setPixel(x, y, squareColor);
} else if (checkersIsPlayer1Piece(piece)) {
// Player 1 piece (green)
if (checkersIsKing(piece)) {
setPixel(x, y, Color(50, 200, 50)); // MUCH lighter green king
} else {
setPixel(x, y, Color(0, 70, 0)); // Dark green regular
}
} else {
// Player 2 piece (red)
if (checkersIsKing(piece)) {
setPixel(x, y, Color(200, 50, 50)); // MUCH lighter red king
} else {
setPixel(x, y, Color(70, 0, 0)); // Dark red regular
}
}
}
}
// Show AI thinking indicator
if (gameState.checkers.aiMode && gameState.checkers.waitingForAI) {
// Pulsing red border to indicate AI is thinking
bool pulse = ((millis() / 200) % 2) == 0;
if (pulse) {
for (uint8_t i = 0; i < 8; ++i) {
setPixel(i, 0, Color(40, 0, 0));
setPixel(i, 7, Color(40, 0, 0));
setPixel(0, i, Color(40, 0, 0));
setPixel(7, i, Color(40, 0, 0));
}
}
return; // Don't show cursor or highlights while AI is thinking
}
if (gameState.checkers.aiMode && gameState.checkers.aiBlinkEndsAt > 0) {
if (gameState.checkers.aiBlinkVisible) {
setPixel(gameState.checkers.aiLastFromX, gameState.checkers.aiLastFromY, Color(95, 95, 0));
setPixel(gameState.checkers.aiLastToX, gameState.checkers.aiLastToY, Color(0, 95, 95));
}
return;
}
// Highlight valid moves
for (uint8_t i = 0; i < gameState.checkers.validMoveCount; ++i) {
int8_t mx = gameState.checkers.validMoves[i][0];
int8_t my = gameState.checkers.validMoves[i][1];
// Yellow highlight for valid moves
setPixel(mx, my, Color(50, 50, 0));
}
// Highlight selected piece
if (gameState.checkers.selectedX >= 0 && gameState.checkers.selectedY >= 0) {
int8_t sx = gameState.checkers.selectedX;
int8_t sy = gameState.checkers.selectedY;
// Bright yellow for selected piece
setPixel(sx, sy, Color(100, 100, 0));
}
// Draw cursor (only for human player's turn)
if (!gameState.checkers.aiMode || gameState.checkers.currentPlayer == 1) {
int8_t cx = gameState.checkers.cursorX;
int8_t cy = gameState.checkers.cursorY;
// Pulsing cursor
bool pulse = ((millis() / 300) % 2) == 0;
if (pulse) {
setPixel(cx, cy, Color(80, 80, 80));
}
}
}
// ========================================
// Minesweeper Game
// ========================================
void minesweeperInit() {
memset(&gameState.minesweeper, 0, sizeof(Minesweeper));
gameState.minesweeper.cursorX = 4;
gameState.minesweeper.cursorY = 4;
gameState.minesweeper.minesLeft = 10; // 10 mines on 8x8 board
gameState.minesweeper.cellsToReveal = 64 - 10; // All cells except mines
gameState.minesweeper.firstClick = true;
score = 0;
// All cells start hidden
for (uint8_t y = 0; y < 8; ++y) {
for (uint8_t x = 0; x < 8; ++x) {
gameState.minesweeper.revealed[y][x] = 0; // Hidden
gameState.minesweeper.board[y][x] = 0; // Will be set after first click
}
}
}
void minesweeperPlaceMines(int8_t safeX, int8_t safeY) {
// Place 10 mines randomly, but not on the first clicked cell
uint8_t minesPlaced = 0;
while (minesPlaced < 10) {
int8_t x = random(8);
int8_t y = random(8);
// Skip if already a mine or the safe cell
if (gameState.minesweeper.board[y][x] == 9 || (x == safeX && y == safeY)) {
continue;
}
gameState.minesweeper.board[y][x] = 9; // 9 = mine
minesPlaced++;
}
// Calculate numbers for non-mine cells
for (uint8_t y = 0; y < 8; ++y) {
for (uint8_t x = 0; x < 8; ++x) {
if (gameState.minesweeper.board[y][x] == 9) continue; // Skip mines
uint8_t count = 0;
// Check all 8 adjacent cells
for (int8_t dy = -1; dy <= 1; ++dy) {
for (int8_t dx = -1; dx <= 1; ++dx) {
if (dx == 0 && dy == 0) continue;
int8_t nx = x + dx;
int8_t ny = y + dy;
if (nx >= 0 && nx < 8 && ny >= 0 && ny < 8) {
if (gameState.minesweeper.board[ny][nx] == 9) count++;
}
}
}
gameState.minesweeper.board[y][x] = count;
}
}
}
void minesweeperFloodFill(int8_t x, int8_t y) {
// Reveal empty cells recursively (flood fill)
if (x < 0 || x >= 8 || y < 0 || y >= 8) return;
if (gameState.minesweeper.revealed[y][x] != 0) return; // Already revealed or flagged
gameState.minesweeper.revealed[y][x] = 1; // Reveal
gameState.minesweeper.cellsToReveal--;
score++;
// If this cell has no adjacent mines, reveal neighbors
if (gameState.minesweeper.board[y][x] == 0) {
for (int8_t dy = -1; dy <= 1; ++dy) {
for (int8_t dx = -1; dx <= 1; ++dx) {
if (dx == 0 && dy == 0) continue;
minesweeperFloodFill(x + dx, y + dy);
}
}
}
}
void minesweeperUpdate(uint32_t now) {
int8_t cx = gameState.minesweeper.cursorX;
int8_t cy = gameState.minesweeper.cursorY;
// Move cursor with UP/DOWN/LEFT/RIGHT
if (takePress(B_UP)) {
gameState.minesweeper.cursorY = (gameState.minesweeper.cursorY + 7) % 8;
beepButton();
}
if (takePress(B_DOWN)) {
gameState.minesweeper.cursorY = (gameState.minesweeper.cursorY + 1) % 8;
beepButton();
}
if (takePress(B_LEFT)) {
gameState.minesweeper.cursorX = (gameState.minesweeper.cursorX + 7) % 8;
beepButton();
}
if (takePress(B_RIGHT)) {
gameState.minesweeper.cursorX = (gameState.minesweeper.cursorX + 1) % 8;
beepButton();
}
// Update cursor position after movement
cx = gameState.minesweeper.cursorX;
cy = gameState.minesweeper.cursorY;
// PAUSE button handling with long press detection
static uint32_t pausePressStart = 0;
static bool pauseWasPressed = false;
static const uint32_t LONG_PRESS_MS = 500; // 500ms = long press
if (isDown(B_PAUSE)) {
if (!pauseWasPressed) {
// Just started pressing
pausePressStart = now;
pauseWasPressed = true;
} else if (now - pausePressStart >= LONG_PRESS_MS) {
// LONG PRESS = Reveal cell (only trigger once)
if (gameState.minesweeper.revealed[cy][cx] == 0) { // Hidden cell only
// First click - place mines avoiding this cell
if (gameState.minesweeper.firstClick) {
minesweeperPlaceMines(cx, cy);
gameState.minesweeper.firstClick = false;
}
// Check if mine
if (gameState.minesweeper.board[cy][cx] == 9) {
// Hit a mine - game over
// Reveal all mines
for (uint8_t y = 0; y < 8; ++y) {
for (uint8_t x = 0; x < 8; ++x) {
if (gameState.minesweeper.board[y][x] == 9) {
gameState.minesweeper.revealed[y][x] = 1;
}
}
}
finishGame(false, "Hit a mine!");
} else {
// Safe cell - reveal it (and neighbors if empty)
beepScore();
minesweeperFloodFill(cx, cy);
// Check win condition
if (gameState.minesweeper.cellsToReveal == 0) {
finishGame(true, "All clear!");
}
}
}
pauseWasPressed = false; // Reset to prevent repeat
}
} else if (pauseWasPressed) {
// Button released - check if it was a short press
if (now - pausePressStart < LONG_PRESS_MS) {
// SHORT PRESS = Toggle flag
if (gameState.minesweeper.revealed[cy][cx] == 0) {
// Hidden - add flag
beepButton();
if (gameState.minesweeper.minesLeft > 0) {
gameState.minesweeper.revealed[cy][cx] = 2; // Flag
gameState.minesweeper.minesLeft--;
}
} else if (gameState.minesweeper.revealed[cy][cx] == 2) {
// Flagged - remove flag
beepButton();
gameState.minesweeper.revealed[cy][cx] = 0; // Unflag
gameState.minesweeper.minesLeft++;
}
}
pauseWasPressed = false;
}
}
void minesweeperDraw() {
clearFrame();
for (uint8_t y = 0; y < 8; ++y) {
for (uint8_t x = 0; x < 8; ++x) {
uint8_t state = gameState.minesweeper.revealed[y][x];
if (state == 0) {
// Hidden cell - gray
setPixel(x, y, Color(15, 15, 15));
} else if (state == 2) {
// Flagged cell - blue
setPixel(x, y, Color(0, 0, 90));
} else {
// Revealed cell
uint8_t val = gameState.minesweeper.board[y][x];
if (val == 9) {
// Mine - bright red
setPixel(x, y, Color(100, 0, 0));
} else if (val == 0) {
// Empty safe zone - light purple/magenta
setPixel(x, y, Color(40, 0, 40));
} else {
// Number - color coded by danger
if (val == 1) setPixel(x, y, Color(0, 50, 0)); // Green
else if (val == 2) setPixel(x, y, Color(50, 50, 0)); // Yellow
else if (val == 3) setPixel(x, y, Color(70, 35, 0)); // Orange
else setPixel(x, y, Color(val * 15, 0, 0)); // Red
}
}
}
}
// Draw cursor
bool pulse = ((millis() / 250) % 2) == 0;
if (pulse) {
int8_t cx = gameState.minesweeper.cursorX;
int8_t cy = gameState.minesweeper.cursorY;
setPixel(cx, cy, Color(100, 100, 100));
}
}
// ========================================
// Dino Run Game
// ========================================
void dinoNewObstacle() {
gameState.dino.obstacleType = static_cast<uint8_t>(random(0, 2)); // 0=ground, 1=flying
gameState.dino.obstacleW = static_cast<uint8_t>(random(1, 3)); // 1 or 2 wide
gameState.dino.obstacleX = -gameState.dino.obstacleW; // Start off-screen left
if (gameState.dino.obstacleType == 0) { // Ground obstacle (cactus)
gameState.dino.obstacleH = static_cast<uint8_t>(random(1, 4)); // 1, 2, or 3 high
gameState.dino.obstacleY = 7; // Sits on the ground
} else { // Flying obstacle
gameState.dino.obstacleH = static_cast<uint8_t>(random(1, 3)); // 1 or 2 high
gameState.dino.obstacleY = static_cast<int8_t>(random(4, 6)); // y-pos 4 or 5
}
gameState.dino.passedObstacle = false;
}
void dinoInit() {
gameState.dino.playerY8 = 7 * 8; // Start on the ground (y=7)
gameState.dino.velocityY8 = 0;
gameState.dino.isCrouching = false;
gameState.dino.moveInterval = 150; // Initial speed
gameState.dino.lastMove = millis();
dinoNewObstacle();
score = 0;
}
void dinoUpdate(uint32_t now) {
if (takePress(B_PAUSE)) {
gamePaused = !gamePaused;
beepButton();
}
if (gamePaused) return;
const int8_t DINO_X_POS = 6;
int8_t playerY = static_cast<int8_t>(gameState.dino.playerY8 / 8);
// Handle crouching
if (isDown(B_DOWN) && playerY == 7) {
gameState.dino.isCrouching = true;
} else {
gameState.dino.isCrouching = false;
}
// JUMP: only if on the ground
if (takePress(B_UP) && playerY == 7 && !gameState.dino.isCrouching) {
float speedFactor = 150.0f / gameState.dino.moveInterval;
gameState.dino.velocityY8 = static_cast<int16_t>(-28.0f * speedFactor); // Increased jump speed
beepButton();
}
// Fast fall (spamming crouch in air)
if (takePress(B_DOWN) && playerY < 7) {
gameState.dino.velocityY8 += 30;
}
if (now - gameState.dino.lastMove < gameState.dino.moveInterval) return;
gameState.dino.lastMove = now;
float speedFactor = 150.0f / gameState.dino.moveInterval;
// Apply gravity
gameState.dino.velocityY8 += static_cast<int16_t>(6.0f * speedFactor); // Increased fall speed (gravity)
if (gameState.dino.velocityY8 > 24) {
gameState.dino.velocityY8 = 24; // Terminal velocity
}
// Update player position
gameState.dino.playerY8 += gameState.dino.velocityY8;
// Check for ground collision
if (gameState.dino.playerY8 >= 7 * 8) {
gameState.dino.playerY8 = 7 * 8;
gameState.dino.velocityY8 = 0;
}
// Move obstacle
gameState.dino.obstacleX++;
// Score obstacle
if (!gameState.dino.passedObstacle && gameState.dino.obstacleX > DINO_X_POS) {
gameState.dino.passedObstacle = true;
score++;
beepScore();
// Increase speed
if (gameState.dino.moveInterval > 60) {
gameState.dino.moveInterval -= 4;
}
}
// Spawn new obstacle when old one is off-screen
if (gameState.dino.obstacleX > 8) {
dinoNewObstacle();
}
// Collision detection
playerY = gameState.dino.isCrouching ? 7 : static_cast<int8_t>(gameState.dino.playerY8 / 8);
bool x_collision = (gameState.dino.obstacleX <= DINO_X_POS) && (DINO_X_POS < gameState.dino.obstacleX + gameState.dino.obstacleW);
if (x_collision) {
int8_t obstacleTopY, obstacleBottomY;
if (gameState.dino.obstacleType == 0) { // Ground
obstacleTopY = 8 - gameState.dino.obstacleH;
obstacleBottomY = 7;
} else { // Flying
obstacleTopY = gameState.dino.obstacleY;
obstacleBottomY = gameState.dino.obstacleY + gameState.dino.obstacleH - 1;
}
if (playerY >= obstacleTopY && playerY <= obstacleBottomY) {
beepHit();
finishGame(false, "Ouch!");
return;
}
}
}
void dinoDraw() {
clearFrame();
// Draw scrolling ground line
// The expression `(i - (millis()/100))` makes it scroll right-to-left
for (uint8_t i = 0; i < 8; i++) {
uint32_t groundColor = (( (i - (millis()/100)) % 4) < 2) ? Color(20,20,20) : Color(10,10,10);
setPixel(i, 7, groundColor);
}
// Draw player (dino)
const int8_t DINO_X_POS = 6;
int8_t playerY = static_cast<int8_t>(gameState.dino.playerY8 / 8);
if (gameState.dino.isCrouching) {
setPixel(DINO_X_POS, 7, Color(0, 40, 10)); // Dark green crouching dino
} else {
setPixel(DINO_X_POS, playerY, Color(0, 80, 20)); // Green dino
}
// Draw obstacle (cactus)
for (uint8_t w = 0; w < gameState.dino.obstacleW; ++w) {
for (uint8_t h = 0; h < gameState.dino.obstacleH; ++h) {
int8_t px = gameState.dino.obstacleX + w;
if (px >=0 && px < 8) {
int8_t py = (gameState.dino.obstacleType == 0) ? (7 - h) : (gameState.dino.obstacleY + h);
setPixel(px, py, Color(80, 40, 0));
}
}
}
}
// ========================================
// Game Router Functions
// ========================================
static void initGame(GameId id) {
switch (id) {
case GAME_SNAKE_WALL: snakeInit(false); break;
case GAME_SNAKE_WRAP: snakeInit(true); break;
case GAME_TETRIS: tetrisInit(); break;
case GAME_FLAPPY_EASY: flappyEasyInit(); break;
case GAME_FLAPPY_HARD: flappyHardInit(); break;
case GAME_ASTEROIDS_HARD: asteroidsHardInit(); break;
case GAME_PACMAN_EASY: pacEasyInit(); break;
case GAME_PACMAN_HARD: pacmanInit(); break;
case GAME_SPACE_SHOOTER: spaceShooterInit(); break;
case GAME_BREAKOUT: breakoutInit(); break;
case GAME_TTT_AI: tttInit(true); break;
case GAME_TTT_2P: tttInit(false); break;
case GAME_PONG: pongInit(); break;
case GAME_TUG: tugInit(); break;
case GAME_CHECKERS_AI: checkersInit(true); break;
case GAME_CHECKERS_2P: checkersInit(false); break;
case GAME_MINESWEEPER: minesweeperInit(); break;
case GAME_DINO: dinoInit(); break;
default: break;
}
}
static void updateGame(GameId id, uint32_t now) {
switch (id) {
case GAME_SNAKE_WALL:
case GAME_SNAKE_WRAP: snakeUpdate(now); break;
case GAME_TETRIS: tetrisUpdate(now); break;
case GAME_FLAPPY_EASY: flappyEasyUpdate(now); break;
case GAME_FLAPPY_HARD: flappyHardUpdate(now); break;
case GAME_ASTEROIDS_HARD: asteroidsHardUpdate(now); break;
case GAME_PACMAN_EASY: pacEasyUpdate(now); break;
case GAME_PACMAN_HARD: pacmanUpdate(now); break;
case GAME_SPACE_SHOOTER: spaceShooterUpdate(now); break;
case GAME_BREAKOUT: breakoutUpdate(now); break;
case GAME_TTT_AI:
case GAME_TTT_2P: tttUpdate(now); break;
case GAME_PONG: pongUpdate(now); break;
case GAME_TUG: tugUpdate(now); break;
case GAME_CHECKERS_AI:
case GAME_CHECKERS_2P: checkersUpdate(now); break;
case GAME_MINESWEEPER: minesweeperUpdate(now); break;
case GAME_DINO: dinoUpdate(now); break;
default: break;
}
}
static void drawGame(GameId id) {
switch (id) {
case GAME_SNAKE_WALL:
case GAME_SNAKE_WRAP: snakeDraw(); break;
case GAME_TETRIS: tetrisDraw(); break;
case GAME_FLAPPY_EASY: flappyEasyDraw(); break;
case GAME_FLAPPY_HARD: flappyHardDraw(); break;
case GAME_ASTEROIDS_HARD: asteroidsHardDraw(); break;
case GAME_PACMAN_EASY: pacEasyDraw(); break;
case GAME_PACMAN_HARD: pacmanDraw(); break;
case GAME_SPACE_SHOOTER: spaceShooterDraw(); break;
case GAME_BREAKOUT: breakoutDraw(); break;
case GAME_TTT_AI:
case GAME_TTT_2P: tttDraw(); break;
case GAME_PONG: pongDraw(); break;
case GAME_TUG: tugDraw(); break;
case GAME_CHECKERS_AI:
case GAME_CHECKERS_2P: checkersDraw(); break;
case GAME_MINESWEEPER: minesweeperDraw(); break;
case GAME_DINO: dinoDraw(); break;
default: clearFrame(); break;
}
}
// ========================================
// Display Functions
// ========================================
void drawMenuMatrix(uint32_t now) {
clearFrame();
uint8_t sweep = static_cast<uint8_t>((now / 140) % MATRIX_W);
for (uint8_t y = 0; y < MATRIX_H; ++y) {
for (uint8_t x = 0; x < MATRIX_W; ++x) {
uint8_t r = static_cast<uint8_t>((x * 20 + now / 9) & 0x3F);
uint8_t g = static_cast<uint8_t>((y * 18 + now / 13) & 0x3F);
uint8_t b = static_cast<uint8_t>((x * 9 + y * 11 + now / 17) & 0x3F);
if (x == sweep) r = 70;
setPixel(x, y, Color(r / 2, g / 2, b / 2));
}
}
}
void drawGameOverMatrix(uint32_t now) {
clearFrame();
bool pattern = ((now / 240) % 2) == 0;
for (uint8_t y = 0; y < MATRIX_H; ++y) {
for (uint8_t x = 0; x < MATRIX_W; ++x) {
if (((x + y) % 2 == 0) != pattern) continue;
setPixel(x, y, lastWin ? Color(0, 45, 0) : Color(55, 0, 0));
}
}
}
void drawMenuOLED() {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.print("NB6_Boy");
// WiFi status icon
display.setCursor(68, 0);
if (wifiConnected) {
display.print("WiFi"); // Connected
} else {
display.print("----"); // Not connected
}
// Sound status
display.setCursor(106, 0);
display.print(soundMuted ? "M" : "S");
int8_t start = menuIndex - 2;
if (start < 0) start = 0;
int8_t maxStart = static_cast<int8_t>(GAME_COUNT - 5);
if (maxStart < 0) maxStart = 0;
if (start > maxStart) start = maxStart;
for (uint8_t i = 0; i < 5; ++i) {
int8_t gameIdx = static_cast<int8_t>(start + i);
if (gameIdx >= GAME_COUNT) break;
display.setCursor(0, 14 + i * 10);
display.print(gameIdx == menuIndex ? "> " : " ");
display.print(GAME_NAMES[gameIdx]);
}
display.display();
}
void drawPlayingOLED() {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.print(GAME_NAMES[currentGame]);
// WiFi status (compact)
if (wifiConnected) {
display.setCursor(96, 0);
display.print("W");
}
// Sound status
if (soundMuted) {
display.setCursor(108, 0);
display.print("M");
}
if (gamePaused) {
display.setCursor(0, 16);
display.print("*** PAUSED ***");
display.setCursor(0, 28);
display.print("PAUSE to resume");
} else {
display.setCursor(0, 16);
display.print("Score: ");
display.print(score);
display.setCursor(0, 28);
display.print("Best: ");
display.print(highScore[currentGame]);
}
display.setCursor(0, 46);
display.print("SEL -> Menu");
display.display();
}
void drawGameOverOLED() {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.print(lastWin ? "Round Complete" : "Game Over");
display.setCursor(0, 12);
display.print(GAME_NAMES[currentGame]);
display.setCursor(0, 24);
display.print(lastMsg);
display.setCursor(0, 36);
display.print("Score: ");
display.print(score);
display.setCursor(0, 48);
display.print("Best: ");
display.print(highScore[currentGame]);
display.display();
}
// ========================================
// Setup and Main Loop
// ========================================
void nb_setup() {
// Initialize buzzer pin and ensure it's off
pinMode(BUZZER, OUTPUT);
noTone(BUZZER);
// Configure I2C for OLED display
// Wire.setSDA(D4);
// Wire.setSCL(D5);
// Pass the pins directly to begin(SDA, SCL)
// Wire already begun in master setup()
// Wire.begin(D4, D5);
// Initialize NeoPixel LED nb_matrix
nb_matrix.begin();
nb_matrix.clear();
nb_matrix.show();
// Play startup animation and sound
playBootSequence();
// Initialize OLED display with error checking
// display already initialized in master setup()
/*
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
// OLED initialization failed - continue anyway
// Game is still playable with just LED nb_matrix
// Could add visual error indicator on LED nb_matrix if needed
} */
// Setup button debouncing system
initButtons();
// Load high scores from persistent storage
loadHighScores();
// Seed random number generator with analog noise + microsecond timer
// This ensures different random sequences each time the device powers on
randomSeed(static_cast<uint32_t>(analogRead(A0)) ^ micros());
// Initialize frame timing variables
lastMatrixMs = millis();
lastOledMs = millis();
// Show WiFi connecting message on OLED
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.print("NB6BOY Starting...");
display.setCursor(0, 20);
display.print("Connecting WiFi...");
display.display();
// Connect to WiFi (with visual feedback)
connectWiFi();
// Show connection result
display.setCursor(0, 30);
if (wifiConnected) {
display.print("WiFi: Connected!");
} else {
display.print("WiFi: Offline");
}
display.display();
delay(1500); // Show status for 1.5 seconds
}
void nb_loop() {
// Get current time once per loop for consistency across all timing checks
uint32_t now = millis();
// Update button states with debouncing
// This must be called every loop to properly detect presses/releases
updateButtons(now);
// ============================================================
// GLOBAL INPUT HANDLING (works in any state)
// ============================================================
// SELECT button always returns to menu (except when already in menu)
if (takePress(B_SELECT) && systemState != STATE_MENU) {
beepButton();
returnToMenu();
}
// ============================================================
// STATE-SPECIFIC INPUT HANDLING
// ============================================================
if (systemState == STATE_MENU) {
bool muteComboDown = isDown(B_LEFT) && isDown(B_RIGHT);
if (muteComboDown && !menuMuteComboLatched) {
setSoundMuted(!soundMuted);
if (!soundMuted) {
beepButton();
}
menuMuteComboLatched = true;
clearButtonEdges();
}
if (!muteComboDown) {
menuMuteComboLatched = false;
}
// Menu Navigation
if (takePress(B_UP)) {
// Wrap around to bottom when going up from top
menuIndex = static_cast<int8_t>((menuIndex + GAME_COUNT - 1) % GAME_COUNT);
beepButton();
}
if (takePress(B_DOWN)) {
// Wrap around to top when going down from bottom
menuIndex = static_cast<int8_t>((menuIndex + 1) % GAME_COUNT);
beepButton();
}
if (takePress(B_PAUSE)) {
// PAUSE button starts the selected game
beepButton();
startGame(static_cast<GameId>(menuIndex));
}
} else if (systemState == STATE_PLAYING) {
// Active Gameplay - delegate to current game's update function
// Each game handles its own input and logic
updateGame(currentGame, now);
} else if (systemState == STATE_GAME_OVER) {
// Game Over Screen - auto-return to menu after 2 seconds OR on any button press
if ((now - gameOverStartTime >= Timing::GAME_OVER_DELAY_MS) ||
takePress(B_PAUSE) ||
takePress(B_SELECT)) {
beepButton();
returnToMenu();
}
}
// ============================================================
// SOUND SYSTEM UPDATE
// ============================================================
// Process sound queue - plays queued tones without blocking
updateSound(now);
// ============================================================
// DISPLAY RENDERING (Frame Rate Limited)
// ============================================================
// Render LED Matrix at ~30 FPS (33ms per frame)
// This rate provides smooth animations without overwhelming the LEDs
if (now - lastMatrixMs >= Timing::MATRIX_FRAME_MS) {
lastMatrixMs = now;
// Draw appropriate visual based on current state
if (systemState == STATE_MENU) {
drawMenuMatrix(now); // Animated color pattern
} else if (systemState == STATE_PLAYING) {
drawGame(currentGame); // Active game graphics
drawFlashingPauseBorder(now);
} else {
drawGameOverMatrix(now); // Win/lose pattern
}
// Push frame buffer to physical LEDs
showFrame();
}
// Render OLED Screen at ~10 FPS (100ms per frame)
// Slower rate reduces flicker on OLED displays
if (now - lastOledMs >= Timing::OLED_FRAME_MS) {
lastOledMs = now;
// Draw appropriate text/UI based on current state
if (systemState == STATE_MENU) {
drawMenuOLED(); // Game list with selection indicator
} else if (systemState == STATE_PLAYING) {
drawPlayingOLED(); // Score, high score, pause status
} else {
drawGameOverOLED(); // Final score and results
}
}
}
// ═════════════════════════════════════════════════════════
// ██████╗ ██████╗ █████╗ ██████╗
// ██╔═══██╗██╔══██╗██╔══██╗██╔══██╗
// ██║ ██║██████╔╝███████║██║ ██║
// ██║▄▄ ██║██╔═══╝ ██╔══██║██║ ██║
// ╚██████╔╝██║ ██║ ██║██████╔╝
// ╚══▀▀═╝ ╚═╝ ╚═╝ ╚═╝╚═════╝
//
// QPAD OS — 10 OLED games
// Entry points: qpad_setup() / qpad_loop()
// ═════════════════════════════════════════════════════════
/*
* QPAD-XIAO GAME CARTRIDGE v3.1 — ESP32C6 PORT
* Target: Seeed XIAO ESP32C6 + 0.96" SSD1306 OLED 128x64
*
* XIAO ESP32C6 PIN MAPPING:
* D0 = GPIO2 → UP
* D1 = GPIO3 → LEFT
* D2 = GPIO4 → (unused / was PIN_LEFT on RP2040, remapped)
* D3 = GPIO5 → (unused / was PIN_UP on RP2040, remapped)
* D6 = GPIO20 → ACTION (shoot / rotate / jump / launch / flap / place)
* D7 = GPIO19 → RIGHT
* D8 = GPIO21 → (unused)
* D9 = GPIO8 → MODE (single=pause/resume double=back to menu)
* D10 = GPIO9 → DOWN
* I2C : SDA=D4(GPIO22), SCL=D5(GPIO23)
*
* CONTROLS:
* UP = D0 (GPIO2)
* LEFT = D1 (GPIO3)
* RIGHT = D7 (GPIO19)
* DOWN = D10 (GPIO9)
* MODE = D9 (GPIO8) single=pause/resume double=back to menu
* ACTION= D6 (GPIO20) shoot / rotate / jump / launch / flap / place
*
* QGAMES:
* 1. Breakout
* 2. Bounce Classic
* 3. Snake (Wall)
* 4. Snake (Wrap)
* 5. Tetris
* 6. Flappy Bird
* 7. Asteroids
* 8. Space Shooter
* 9. TicTacToe vs AI
* 10. Pong / Air Hockey
*
* Libraries: Adafruit SSD1306, Adafruit GFX
*
* Board package: esp32 by Espressif (>=3.x)
* Board: "XIAO_ESP32C6" or "Seeed Studio XIAO ESP32C6"
*/
// ─────────────────────────────────────────────────────────
// XIAO ESP32C6 GPIO ASSIGNMENTS
// XIAO pin label → ESP32C6 GPIO number
// ─────────────────────────────────────────────────────────
#define PIN_UP D3
#define PIN_LEFT D2
#define PIN_RIGHT D7 // D7
#define PIN_DOWN D9
#define PIN_MODE D1
#define PIN_ACT D6 // D6
// I2C: SDA=GPIO22 (D4), SCL=GPIO23 (D5) — ESP32C6 defaults
#define I2C_SDA D4
#define I2C_SCL D5
// Built-in LED on XIAO ESP32C6
#ifndef LED_BUILTIN
#define LED_BUILTIN 15
#endif
// ─────────────────────────────────────────────────────────
// OLED
// ─────────────────────────────────────────────────────────
// [DUPLICATE - defined in header] #define SW 128
// [DUPLICATE - defined in header] #define SH 64
// [DUPLICATE - defined in header] #define OLED_ADDR 0x3C
// [DUPLICATE - defined in header] Adafruit_SSD1306 display(SW, SH, &Wire, -1);
// ─────────────────────────────────────────────────────────
// BUTTONS
// ─────────────────────────────────────────────────────────
struct QBtn { uint8_t pin; bool held, pressed; };
QBtn bU={PIN_UP,0,0}, bL={PIN_LEFT,0,0}, bR={PIN_RIGHT,0,0};
QBtn bD={PIN_DOWN,0,0}, bM={PIN_MODE,0,0}, bA={PIN_ACT,0,0};
QBtn* BTNS[]={&bU,&bL,&bR,&bD,&bM,&bA};
void qReadBtns(){
for(auto b:BTNS){
bool c=(digitalRead(b->pin)==LOW);
b->pressed=c&&!b->held; b->held=c;
}
}
// ─────────────────────────────────────────────────────────
// MODE BUTTON: single / double press detection
// ─────────────────────────────────────────────────────────
static unsigned long qModeFirst = 0;
static int qModeCnt = 0;
bool qModeSingle = false;
bool qModeDouble = false;
void qDetectMode(){
qModeSingle = false; qModeDouble = false;
unsigned long now = millis();
if(bM.pressed){
qModeCnt++;
if(qModeCnt == 1) qModeFirst = now;
}
if(qModeCnt > 0 && (now - qModeFirst) > 350){
if(qModeCnt >= 2) qModeDouble = true;
else qModeSingle = true;
qModeCnt = 0;
}
}
// ─────────────────────────────────────────────────────────
// HELPERS
// ─────────────────────────────────────────────────────────
void qcx(const char* t, int y, int s=1){
display.setTextSize(s); display.setTextColor(SSD1306_WHITE);
int16_t x1,y1; uint16_t w,h;
display.getTextBounds(t,0,0,&x1,&y1,&w,&h);
display.setCursor((SW-(int)w)/2,y); display.print(t);
}
void line(int x0,int y0,int x1,int y1){ display.drawLine(x0,y0,x1,y1,SSD1306_WHITE); }
// ═════════════════════════════════════════════════════════
// APP STATE / MENU
// ═════════════════════════════════════════════════════════
enum QApp {
QA_MENU,
QA_BREAKOUT, QA_BOUNCE,
QA_SNAKE_W, QA_SNAKE_WRAP,
QA_TETRIS, QA_FLAPPY,
QA_ASTEROIDS,
QA_SHOOTER, QA_TICTACTOE, QA_PONG
};
QApp appState=QA_MENU;
const char* QGAMES[]={"Breakout","Bounce Classic","Snake Wall","Snake Wrap",
"Tetris","Flappy Bird","Asteroids",
"Space Shooter","TicTacToe AI","Pong"};
const int QNGAMES=10;
int qMenuCur=0;
#define QMENU_VIS 4
int qMenuScroll=0;
bool qAnyBtn(){ return bU.pressed||bD.pressed||bL.pressed||bR.pressed||bA.pressed||bM.pressed; }
void qMenuDraw(){
display.clearDisplay();
qcx("QPAD GAMES",1,1);
display.drawLine(0,10,SW,10,SSD1306_WHITE);
for(int i=0;i<QMENU_VIS&&(i+qMenuScroll)<QNGAMES;i++){
int idx=i+qMenuScroll, y=13+i*13;
bool sel=(idx==qMenuCur);
if(sel){ display.fillRect(0,y-1,SW,11,SSD1306_WHITE); display.setTextColor(SSD1306_BLACK); }
else display.setTextColor(SSD1306_WHITE);
display.setTextSize(1); display.setCursor(4,y+1);
display.print(idx+1); display.print(F(". ")); display.print(QGAMES[idx]);
}
display.setTextColor(SSD1306_WHITE);
if(qMenuScroll>0) { display.setCursor(120,13); display.print('^'); }
if(qMenuScroll+QMENU_VIS<QNGAMES){ display.setCursor(120,52); display.print('v'); }
display.display();
}
void qMenuUpdate(){
if(bU.pressed){ qMenuCur=(qMenuCur-1+QNGAMES)%QNGAMES; if(qMenuCur<qMenuScroll) qMenuScroll=qMenuCur; }
if(bD.pressed){ qMenuCur=(qMenuCur+1)%QNGAMES; if(qMenuCur>=qMenuScroll+QMENU_VIS) qMenuScroll=qMenuCur-QMENU_VIS+1; }
if(bA.pressed){
QApp map[]={QA_BREAKOUT,QA_BOUNCE,QA_SNAKE_W,QA_SNAKE_WRAP,
QA_TETRIS,QA_FLAPPY,QA_ASTEROIDS,
QA_SHOOTER,QA_TICTACTOE,QA_PONG};
appState=map[qMenuCur];
}
}
void qDrawPauseOverlay(){
display.fillRect(24,22,80,22,SSD1306_BLACK);
display.drawRect(24,22,80,22,SSD1306_WHITE);
qcx("PAUSED",25);
qcx("ACT=resume D9x2=menu",34);
}
// ═════════════════════════════════════════════════════════
// 1. BREAKOUT
// ═════════════════════════════════════════════════════════
#define BR_PW 28
#define BR_PH 3
#define BR_PY 59
#define BR_PS 3
#define BR_BR 2
#define BR_BC 8
#define BR_BRW 4
#define BR_BW 14
#define BR_BH 4
#define BR_BP 2
#define BR_X0 4
#define BR_Y0 9
enum {BR_TTL,BR_PLY,BR_PAU,BR_DED,BR_WIN} brSt=BR_TTL;
float brBx,brBy,brDx,brDy; int brPx,brSc,brLv,brLi; bool brOp;
bool brK[BR_BRW][BR_BC];
void brReset(){ for(int r=0;r<BR_BRW;r++) for(int c=0;c<BR_BC;c++) brK[r][c]=true; }
void brBall(){ brBx=brPx+BR_PW/2.0f; brBy=BR_PY-BR_BR-1;
float s=1.8f+brLv*0.2f; brDx=(random(0,2)?s:-s); brDy=-s; brOp=true; }
void brInit(){ brPx=(SW-BR_PW)/2; brSc=0; brLi=3; brLv=0; brReset(); brBall(); brSt=BR_PLY; }
bool brClear(){ for(int r=0;r<BR_BRW;r++) for(int c=0;c<BR_BC;c++) if(brK[r][c]) return false; return true; }
void brScene(){
display.setTextSize(1); display.setTextColor(SSD1306_WHITE);
display.setCursor(0,0); display.print(F("L")); display.print(brLi);
display.print(F(" S:")); display.print(brSc);
char b[8]; sprintf(b,"Lv%d",brLv+1); display.setCursor(SW-24,0); display.print(b);
for(int r=0;r<BR_BRW;r++) for(int c=0;c<BR_BC;c++){
if(!brK[r][c]) continue;
int bx=BR_X0+c*(BR_BW+BR_BP), by=BR_Y0+r*(BR_BH+BR_BP);
if(r<2) display.drawRect(bx,by,BR_BW,BR_BH,SSD1306_WHITE);
else display.fillRect(bx,by,BR_BW,BR_BH,SSD1306_WHITE);
}
display.fillRoundRect(brPx,BR_PY,BR_PW,BR_PH,1,SSD1306_WHITE);
display.fillCircle((int)brBx,(int)brBy,BR_BR,SSD1306_WHITE);
}
void updateBreakout(){
if(qModeDouble){appState=QA_MENU;brSt=BR_TTL;return;}
if(brSt==BR_TTL){qcx("BREAKOUT",10,2);qcx("ACT=start",44);if(bA.pressed)brInit();return;}
if(brSt==BR_DED){qcx("GAME OVER",8,2);char b[20];sprintf(b,"Score:%d",brSc);qcx(b,38);qcx("ACT=retry",54);if(bA.pressed)brInit();return;}
if(brSt==BR_WIN){qcx("YOU WIN!",8,2);char b[24];sprintf(b,"Sc:%d Lv%d",brSc,brLv+1);qcx(b,34);qcx("ACT=next",54);if(bA.pressed){brLv++;brReset();brBall();brSt=BR_PLY;}return;}
if(brSt==BR_PAU){brScene();qDrawPauseOverlay();if(bA.pressed)brSt=BR_PLY;return;}
if(bL.held)brPx-=BR_PS; if(bR.held)brPx+=BR_PS; brPx=constrain(brPx,0,SW-BR_PW);
if(brOp){brBx=brPx+BR_PW/2.0f;if(bA.pressed)brOp=false;brScene();qcx("ACT=launch",48);return;}
if(qModeSingle){brSt=BR_PAU;return;}
brBx+=brDx; brBy+=brDy;
if(brBx-BR_BR<=0){brBx=BR_BR;brDx=fabs(brDx);}
if(brBx+BR_BR>=SW){brBx=SW-BR_BR;brDx=-fabs(brDx);}
if(brBy-BR_BR<=0){brBy=BR_BR;brDy=fabs(brDy);}
if(brDy>0&&brBy+BR_BR>=BR_PY&&brBy-BR_BR<=BR_PY+BR_PH&&brBx>=brPx-BR_BR&&brBx<=brPx+BR_PW+BR_BR){
brBy=BR_PY-BR_BR;brDy=-fabs(brDy);
float r=(brBx-(brPx+BR_PW/2.0f))/(BR_PW/2.0f); brDx=r*3.0f;
if(fabs(brDx)<0.7f) brDx=(brDx>=0?0.7f:-0.7f);
}
if(brBy>SH+4){brLi--;if(brLi<=0){brSt=BR_DED;return;}brBall();return;}
for(int r=0;r<BR_BRW;r++) for(int c=0;c<BR_BC;c++){
if(!brK[r][c]) continue;
int bx=BR_X0+c*(BR_BW+BR_BP),by=BR_Y0+r*(BR_BH+BR_BP);
if(brBx+BR_BR>bx&&brBx-BR_BR<bx+BR_BW&&brBy+BR_BR>by&&brBy-BR_BR<by+BR_BH){
brK[r][c]=false; brSc+=10*(BR_BRW-r);
float oL=(brBx+BR_BR)-bx,oR=(bx+BR_BW)-(brBx-BR_BR),oT=(brBy+BR_BR)-by,oB=(by+BR_BH)-(brBy-BR_BR);
if(min(oL,oR)<min(oT,oB)) brDx=-brDx; else brDy=-brDy;
float sp=sqrt(brDx*brDx+brDy*brDy); if(sp<4.5f){brDx*=1.015f;brDy*=1.015f;}
if(brClear()){brSt=BR_WIN;return;} goto brDone;
}
}
brDone: brScene();
}
// ═════════════════════════════════════════════════════════
// 2. BOUNCE CLASSIC
// ═════════════════════════════════════════════════════════
#define BC_G 0.35f
#define BC_J -4.5f
#define BC_MS 2.0f
#define BC_BR 4
#define NP 10
#define NR 8
#define NS 4
struct Plat{int x,y,w;}; struct Ring{int x,y;bool g;}; struct Spk{int x,y;};
Plat bcP[NP]; Ring bcR[NR]; Spk bcS[NS];
float bcBx,bcBy,bcVx,bcVy,bcCam; int bcSc,bcLi,bcLv,bcRL; bool bcGnd,bcPau;
enum{BC_TTL,BC_PLY,BC_DED,BC_WIN} bcSt=BC_TTL;
void bcGen(){
int lw=600+bcLv*100; bcP[0]={0,58,lw};
randomSeed(bcLv*1234+42);
for(int i=1;i<NP;i++){bcP[i]={60+i*55+(int)random(0,20),30+(int)random(0,18),20+(int)random(0,20)};}
bcRL=NR;
for(int i=0;i<NR;i++){int pi=1+(i%(NP-1));bcR[i]={bcP[pi].x+bcP[pi].w/2+(int)random(-8,8),bcP[pi].y-12,false};}
for(int i=0;i<NS;i++) bcS[i]={80+i*120+(int)random(0,40),52};
}
void bcSpawn(){bcBx=12+bcCam;bcBy=40;bcVx=0;bcVy=0;bcGnd=false;}
void bcInit(){bcLi=3;bcSc=0;bcLv=0;bcCam=0;bcGen();bcSpawn();bcPau=false;bcSt=BC_PLY;}
void bcNext(){bcLv++;bcCam=0;bcGen();bcSpawn();bcSt=BC_PLY;}
void updateBounce(){
if(qModeDouble){appState=QA_MENU;bcSt=BC_TTL;return;}
if(bcSt==BC_TTL){qcx("BOUNCE",4,2);qcx("CLASSIC",22,2);qcx("D10=jump D1/D7=move",46);qcx("ACT=start",56);if(bA.pressed)bcInit();return;}
if(bcSt==BC_DED){qcx("OUCH!",10,2);char b[24];sprintf(b,"Lives:%d Sc:%d",bcLi,bcSc);qcx(b,36);qcx("ACT=continue",52);
if(bA.pressed){if(bcLi<=0){bcSt=BC_TTL;bcSc=0;}else{bcSpawn();bcSt=BC_PLY;}} return;}
if(bcSt==BC_WIN){qcx("LEVEL CLEAR!",6,2);char b[20];sprintf(b,"Score:%d",bcSc);qcx(b,38);qcx("ACT=next",54);if(bA.pressed)bcNext();return;}
if(qModeSingle)bcPau=!bcPau;
if(!bcPau){
bcVx=0; if(bL.held)bcVx=-BC_MS; if(bR.held)bcVx=BC_MS;
if(bA.pressed&&bcGnd){bcVy=BC_J;bcGnd=false;}
bcVy+=BC_G; if(bcVy>6)bcVy=6;
bcBx+=bcVx; bcBy+=bcVy; bcGnd=false;
for(int i=0;i<NP;i++){
auto&p=bcP[i];
if(bcVy>=0&&bcBx+BC_BR>p.x&&bcBx-BC_BR<p.x+p.w&&bcBy+BC_BR>=p.y&&bcBy+BC_BR<=p.y+8){
bcBy=p.y-BC_BR;bcVy=0;bcGnd=true;
}
}
if(bcBx-BC_BR<0)bcBx=BC_BR;
float tc=bcBx-SW/3.0f; if(tc>bcCam)bcCam=tc; if(bcCam<0)bcCam=0;
if(bcBy>SH+20){bcLi--;bcSt=BC_DED;return;}
for(auto&s:bcS){float dx=bcBx-s.x,dy=bcBy-s.y;if(sqrt(dx*dx+dy*dy)<BC_BR+3){bcLi--;bcSt=BC_DED;return;}}
for(auto&r:bcR){if(r.g)continue;float dx=bcBx-r.x,dy=bcBy-r.y;if(sqrt(dx*dx+dy*dy)<BC_BR+5){r.g=true;bcSc+=10;bcRL--;}}
if(bcBx>590+bcLv*100||bcRL<=0){bcSc+=bcLi*50;bcSt=BC_WIN;}
}
int cam=(int)bcCam;
for(int i=0;i<NP;i++){auto&p=bcP[i];int sx=p.x-cam;if(sx+p.w<0||sx>SW)continue;
if(i==0)display.fillRect(sx,p.y,p.w,SH-p.y,SSD1306_WHITE);
else display.fillRect(sx,p.y,p.w,4,SSD1306_WHITE);}
for(auto&s:bcS){int sx=s.x-cam;if(sx<-4||sx>SW+4)continue;display.drawTriangle(sx,s.y+6,sx-4,s.y+6,sx,s.y,SSD1306_WHITE);}
for(auto&r:bcR){if(r.g)continue;int sx=r.x-cam;if(sx<-8||sx>SW+8)continue;display.drawCircle(sx,r.y,4,SSD1306_WHITE);display.drawCircle(sx,r.y,2,SSD1306_WHITE);}
int bsx=(int)bcBx-cam;display.fillCircle(bsx,(int)bcBy,BC_BR,SSD1306_WHITE);display.drawPixel(bsx-1,(int)bcBy-2,SSD1306_BLACK);
display.setTextSize(1);display.setTextColor(SSD1306_WHITE);display.setCursor(0,0);
display.print(F("L"));display.print(bcLi);display.print(F(" R:"));display.print(bcRL);
display.setCursor(SW-36,0);display.print(F("S:"));display.print(bcSc);
if(bcPau){display.fillRect(24,22,80,22,SSD1306_BLACK);display.drawRect(24,22,80,22,SSD1306_WHITE);qcx("PAUSED",25);qcx("ACT=resume",33);}
}
// ═════════════════════════════════════════════════════════
// 3 & 4. SNAKE
// ═════════════════════════════════════════════════════════
#define SN_CELL 4
#define SN_COLS (SW/SN_CELL)
#define SN_ROWS ((SH-8)/SN_CELL)
#define SN_MAX 200
int snX[SN_MAX],snY[SN_MAX],snLen,snDx,snDy,snFx,snFy,snSc,snSpd;
bool snWrap,snDead,snPau;
unsigned long snLast;
void snFood(){
do{snFx=random(0,SN_COLS);snFy=random(0,SN_ROWS);}
while([&](){for(int i=0;i<snLen;i++)if(snX[i]==snFx&&snY[i]==snFy)return true;return false;}());
}
void snInit(bool wrap){
snWrap=wrap;snLen=4;snDx=1;snDy=0;snSc=0;snPau=false;snDead=false;
for(int i=0;i<snLen;i++){snX[i]=snLen-1-i;snY[i]=SN_ROWS/2;}
snFood();snSpd=150;snLast=millis();
}
void updateSnake(bool wrap){
if(qModeDouble){appState=QA_MENU;snDead=false;return;}
if(snDead){qcx("GAME OVER",8,2);char b[20];sprintf(b,"Score:%d",snSc);qcx(b,38);qcx("ACT=retry",54);if(bA.pressed)snInit(wrap);return;}
if(qModeSingle)snPau=!snPau;
if(!snPau){
if(bU.pressed&&snDy==0){snDx=0;snDy=-1;}
if(bD.pressed&&snDy==0){snDx=0;snDy=1;}
if(bL.pressed&&snDx==0){snDx=-1;snDy=0;}
if(bR.pressed&&snDx==0){snDx=1;snDy=0;}
unsigned long now=millis();
if(now-snLast>=(unsigned long)snSpd){
snLast=now;
int nx=snX[0]+snDx, ny=snY[0]+snDy;
if(wrap){nx=(nx+SN_COLS)%SN_COLS;ny=(ny+SN_ROWS)%SN_ROWS;}
else if(nx<0||nx>=SN_COLS||ny<0||ny>=SN_ROWS){snDead=true;return;}
for(int i=1;i<snLen;i++) if(snX[i]==nx&&snY[i]==ny){snDead=true;return;}
bool ate=(nx==snFx&&ny==snFy);
for(int i=snLen-1;i>0;i--){snX[i]=snX[i-1];snY[i]=snY[i-1];}
snX[0]=nx;snY[0]=ny;
if(ate){if(snLen<SN_MAX)snLen++;snSc+=10;if(snSpd>60)snSpd-=3;snFood();}
}
}
display.setTextSize(1);display.setTextColor(SSD1306_WHITE);
display.setCursor(0,0);display.print(snWrap?F("Snake Wrap"):F("Snake Wall"));
display.setCursor(SW-36,0);display.print(F("S:"));display.print(snSc);
display.drawLine(0,8,SW,8,SSD1306_WHITE);
int oy=9;
for(int i=0;i<snLen;i++) display.fillRect(snX[i]*SN_CELL,oy+snY[i]*SN_CELL,SN_CELL-1,SN_CELL-1,SSD1306_WHITE);
display.fillRect(snFx*SN_CELL+1,oy+snFy*SN_CELL+1,SN_CELL-2,SN_CELL-2,SSD1306_WHITE);
if(snPau){qcx("PAUSED",28);qcx("D9x2=menu",38);}
}
// ═════════════════════════════════════════════════════════
// 5. TETRIS
// ═════════════════════════════════════════════════════════
#define TC 10
#define TR 16
#define TS 4
uint8_t tetBoard[TR][TC];
int tetSc,tetLv,tetLines;
bool tetOver,tetPau;
int tetPx,tetPy,tetPt,tetPr;
unsigned long tetLast;
int tetNextT;
const int8_t PIECES[7][4][4][2]={
{{{0,0},{0,1},{0,2},{0,3}},{{0,0},{1,0},{2,0},{3,0}},{{0,0},{0,1},{0,2},{0,3}},{{0,0},{1,0},{2,0},{3,0}}},
{{{0,0},{0,1},{1,0},{1,1}},{{0,0},{0,1},{1,0},{1,1}},{{0,0},{0,1},{1,0},{1,1}},{{0,0},{0,1},{1,0},{1,1}}},
{{{0,1},{1,0},{1,1},{1,2}},{{0,0},{1,0},{1,1},{2,0}},{{1,0},{1,1},{1,2},{2,1}},{{0,1},{1,0},{1,1},{2,1}}},
{{{0,0},{1,0},{1,1},{1,2}},{{0,0},{0,1},{1,0},{2,0}},{{1,0},{1,1},{1,2},{2,2}},{{0,1},{1,1},{2,0},{2,1}}},
{{{0,2},{1,0},{1,1},{1,2}},{{0,0},{1,0},{2,0},{2,1}},{{1,0},{1,1},{1,2},{2,0}},{{0,0},{0,1},{1,1},{2,1}}},
{{{0,1},{0,2},{1,0},{1,1}},{{0,0},{1,0},{1,1},{2,1}},{{0,1},{0,2},{1,0},{1,1}},{{0,0},{1,0},{1,1},{2,1}}},
{{{0,0},{0,1},{1,1},{1,2}},{{0,1},{1,0},{1,1},{2,0}},{{0,0},{0,1},{1,1},{1,2}},{{0,1},{1,0},{1,1},{2,0}}}
};
bool tetFits(int t,int r,int px,int py){
for(int i=0;i<4;i++){
int rr=py+PIECES[t][r][i][0], cc=px+PIECES[t][r][i][1];
if(rr<0||rr>=TR||cc<0||cc>=TC) return false;
if(tetBoard[rr][cc]) return false;
}
return true;
}
void tetPlace(int t,int r,int px,int py){
for(int i=0;i<4;i++) tetBoard[py+PIECES[t][r][i][0]][px+PIECES[t][r][i][1]]=t+1;
}
int tetClear(){
int n=0;
for(int r=TR-1;r>=0;r--){
bool full=true; for(int c=0;c<TC;c++) if(!tetBoard[r][c]){full=false;break;}
if(full){n++;for(int rr=r;rr>0;rr--) for(int c=0;c<TC;c++) tetBoard[rr][c]=tetBoard[rr-1][c];
for(int c=0;c<TC;c++) tetBoard[0][c]=0; r++;}
}
return n;
}
void tetSpawn(){
tetPt=tetNextT; tetNextT=random(0,7); tetPr=0; tetPx=TC/2-2; tetPy=0;
if(!tetFits(tetPt,tetPr,tetPx,tetPy)) tetOver=true;
}
void tetInit(){
memset(tetBoard,0,sizeof(tetBoard));
tetSc=0;tetLv=0;tetLines=0;tetOver=false;tetPau=false;
tetNextT=random(0,7); tetLast=millis();
tetSpawn();
}
int tetSpeed(){ return max(80,500-tetLv*40); }
void updateTetris(){
if(qModeDouble){appState=QA_MENU;tetOver=false;return;}
static bool tetStarted=false;
if(!tetStarted||tetOver){
if(tetOver){qcx("GAME OVER",8,2);char b[20];sprintf(b,"Sc:%d",tetSc);qcx(b,38);qcx("ACT=retry",54);if(bA.pressed){tetInit();tetStarted=true;}}
else{qcx("TETRIS",10,2);qcx("ACT=start",44);if(bA.pressed){tetInit();tetStarted=true;}}
if(!bA.pressed&&!tetOver) return;
return;
}
if(qModeSingle)tetPau=!tetPau;
if(!tetPau){
if(bR.pressed&&tetFits(tetPt,tetPr,tetPx+1,tetPy)) tetPx++;
if(bL.pressed&&tetFits(tetPt,tetPr,tetPx-1,tetPy)) tetPx--;
if(bA.pressed){ int nr=(tetPr+1)%4; if(tetFits(tetPt,nr,tetPx,tetPy)) tetPr=nr; }
if(bD.held&&tetFits(tetPt,tetPr,tetPx,tetPy+1)) tetPy++;
unsigned long now=millis();
if(now-tetLast>=(unsigned long)tetSpeed()){
tetLast=now;
if(tetFits(tetPt,tetPr,tetPx,tetPy+1)) tetPy++;
else{
tetPlace(tetPt,tetPr,tetPx,tetPy);
int cl=tetClear(); tetLines+=cl;
int pts[]={0,40,100,300,1200}; tetSc+=pts[min(cl,4)]*(tetLv+1);
tetLv=tetLines/10; tetSpawn();
}
}
}
int bx=44;
display.drawRect(bx,0,TC*TS+2,SH,SSD1306_WHITE);
for(int r=0;r<TR;r++) for(int c=0;c<TC;c++){
if(tetBoard[r][c]) display.fillRect(bx+1+c*TS,r*TS,TS-1,TS-1,SSD1306_WHITE);
}
for(int i=0;i<4;i++){
int rr=tetPy+PIECES[tetPt][tetPr][i][0], cc=tetPx+PIECES[tetPt][tetPr][i][1];
display.fillRect(bx+1+cc*TS,rr*TS,TS-1,TS-1,SSD1306_WHITE);
}
display.setTextSize(1);display.setTextColor(SSD1306_WHITE);
display.setCursor(0,0);display.print(F("Sc"));
display.setCursor(0,10);display.print(tetSc);
display.setCursor(0,24);display.print(F("Lv"));
display.setCursor(0,34);display.print(tetLv);
display.setCursor(0,46);display.print(F("Nx"));
for(int i=0;i<4;i++){
int rr=PIECES[tetNextT][0][i][0], cc=PIECES[tetNextT][0][i][1];
display.fillRect(2+cc*TS,56+rr*TS,TS-1,TS-1,SSD1306_WHITE);
}
if(tetPau){display.fillRect(20,20,88,24,SSD1306_BLACK);display.drawRect(20,20,88,24,SSD1306_WHITE);qcx("PAUSED",23);qcx("D9x2=menu",32);}
}
// ═════════════════════════════════════════════════════════
// 6. FLAPPY BIRD
// ═════════════════════════════════════════════════════════
#define FB_PIPES 3
#define FB_GAP 18
#define FB_PW 8
#define FB_SPD 2
float fbBy,fbVy; int fbSc; bool fbDead,fbStarted;
struct FbPipe{int x,gap;};
FbPipe fbP[FB_PIPES];
void fbInitPipe(int i,int prevX){
fbP[i].x=prevX+50+random(0,20);
fbP[i].gap=8+random(0,SH-FB_GAP-16);
}
void fbInit(){
fbBy=SH/2; fbVy=0; fbSc=0; fbDead=false; fbStarted=false;
for(int i=0;i<FB_PIPES;i++) fbInitPipe(i,SW+i*60);
}
void updateFlappy(){
if(qModeDouble){appState=QA_MENU;fbDead=false;return;}
if(!fbStarted){qcx("FLAPPY BIRD",4,2);qcx("ACT=flap",36);qcx("D9=pause D9x2=menu",50);if(bA.pressed){fbInit();fbStarted=true;} return;}
if(fbDead){qcx("DEAD!",10,2);char b[20];sprintf(b,"Score:%d",fbSc);qcx(b,36);qcx("ACT=retry",52);if(bA.pressed)fbInit();return;}
if(bA.pressed) fbVy=-3.2f;
fbVy+=0.4f; if(fbVy>5)fbVy=5;
fbBy+=fbVy;
if(fbBy<2){fbBy=2;fbVy=0;}
if(fbBy>SH-4){fbDead=true;return;}
for(int i=0;i<FB_PIPES;i++){
fbP[i].x-=FB_SPD;
if(fbP[i].x+FB_PW<0){fbInitPipe(i,fbP[(i+FB_PIPES-1)%FB_PIPES].x);fbSc++;}
if(14>fbP[i].x&&6<fbP[i].x+FB_PW){
if((int)fbBy<fbP[i].gap||(int)fbBy>fbP[i].gap+FB_GAP) fbDead=true;
}
}
display.fillCircle(14,(int)fbBy,4,SSD1306_WHITE);
for(int i=0;i<FB_PIPES;i++){
display.fillRect(fbP[i].x,0,FB_PW,fbP[i].gap,SSD1306_WHITE);
display.fillRect(fbP[i].x,fbP[i].gap+FB_GAP,FB_PW,SH-(fbP[i].gap+FB_GAP),SSD1306_WHITE);
}
display.setTextSize(1);display.setTextColor(SSD1306_WHITE);
display.setCursor(0,0);display.print(fbSc);
}
// ═════════════════════════════════════════════════════════
// 7. ASTEROIDS
// ═════════════════════════════════════════════════════════
#define AS_MAXAST 8
#define AS_MAXBUL 4
struct AstShip{float x,y,ang,vx,vy;};
struct Asteroid{float x,y,vx,vy,r;bool alive;};
struct QBullet{float x,y,vx,vy;bool alive;int life;};
AstShip asShip;
Asteroid asAst[AS_MAXAST];
QBullet asBul[AS_MAXBUL];
int asSc,asLi,asWave; bool asOver,asPau;
void asSpawnWave(){
for(int i=0;i<min(asWave+2,AS_MAXAST);i++){
float ang=random(0,628)/100.0f;
float spd=0.5f+random(0,10)/10.0f;
float r=8+random(0,6);
float sx,sy;
do{sx=random(0,SW);sy=random(0,SH);}
while(sqrt((sx-asShip.x)*(sx-asShip.x)+(sy-asShip.y)*(sy-asShip.y))<30);
asAst[i]={sx,sy,cos(ang)*spd,sin(ang)*spd,r,true};
}
for(int i=min(asWave+2,AS_MAXAST);i<AS_MAXAST;i++) asAst[i].alive=false;
}
void asInit(){
asShip={SW/2.0f,SH/2.0f,0,0,0};
for(auto&b:asBul) b.alive=false;
asSc=0;asLi=3;asWave=0;asOver=false;asPau=false;
asSpawnWave();
}
bool asAnyAlive(){ for(auto&a:asAst) if(a.alive) return true; return false; }
void updateAsteroids(){
if(qModeDouble){appState=QA_MENU;asOver=false;return;}
if(asOver){qcx("ASTEROIDS",4,2);qcx("GAME OVER",24,2);char b[16];sprintf(b,"Sc:%d",asSc);qcx(b,46);qcx("ACT=start",56);if(bA.pressed)asInit();return;}
if(!asAst[0].alive&&!asAnyAlive()){asWave++;asSpawnWave();}
if(qModeSingle)asPau=!asPau;
if(asPau){qcx("PAUSED",24);qcx("D9x2=menu",36);return;}
if(bL.held) asShip.ang-=0.12f;
if(bR.held) asShip.ang+=0.12f;
if(bU.held){ asShip.vx+=cos(asShip.ang)*0.3f; asShip.vy+=sin(asShip.ang)*0.3f; }
float spd=sqrt(asShip.vx*asShip.vx+asShip.vy*asShip.vy);
if(spd>4){ asShip.vx=asShip.vx/spd*4; asShip.vy=asShip.vy/spd*4; }
asShip.vx*=0.98f; asShip.vy*=0.98f;
asShip.x+=asShip.vx; asShip.y+=asShip.vy;
asShip.x=fmod(asShip.x+SW,SW); asShip.y=fmod(asShip.y+SH,SH);
if(bA.pressed){
for(auto&b:asBul) if(!b.alive){
b={asShip.x,asShip.y,cos(asShip.ang)*5,sin(asShip.ang)*5,true,30}; break;
}
}
for(auto&b:asBul){
if(!b.alive) continue;
b.x+=b.vx; b.y+=b.vy; b.life--;
if(b.life<=0||b.x<0||b.x>SW||b.y<0||b.y>SH) b.alive=false;
}
for(auto&a:asAst){
if(!a.alive) continue;
a.x+=a.vx; a.y+=a.vy;
a.x=fmod(a.x+SW,SW); a.y=fmod(a.y+SH,SH);
float dx=asShip.x-a.x,dy=asShip.y-a.y;
if(sqrt(dx*dx+dy*dy)<a.r+3){ asLi--; if(asLi<=0){asOver=true;return;} asShip={SW/2.0f,SH/2.0f,0,0,0}; }
for(auto&b:asBul){
if(!b.alive) continue;
float bx=b.x-a.x,by=b.y-a.y;
if(sqrt(bx*bx+by*by)<a.r){ b.alive=false; a.alive=false; asSc+=10; break; }
}
}
float tx=asShip.x+cos(asShip.ang)*8, ty=asShip.y+sin(asShip.ang)*8;
float lx=asShip.x+cos(asShip.ang+2.5f)*5, ly=asShip.y+sin(asShip.ang+2.5f)*5;
float rx=asShip.x+cos(asShip.ang-2.5f)*5, ry=asShip.y+sin(asShip.ang-2.5f)*5;
display.drawLine((int)tx,(int)ty,(int)lx,(int)ly,SSD1306_WHITE);
display.drawLine((int)tx,(int)ty,(int)rx,(int)ry,SSD1306_WHITE);
display.drawLine((int)lx,(int)ly,(int)rx,(int)ry,SSD1306_WHITE);
for(auto&a:asAst) if(a.alive) display.drawCircle((int)a.x,(int)a.y,(int)a.r,SSD1306_WHITE);
for(auto&b:asBul) if(b.alive) display.fillCircle((int)b.x,(int)b.y,1,SSD1306_WHITE);
display.setTextSize(1);display.setTextColor(SSD1306_WHITE);
display.setCursor(0,0);display.print(F("L"));display.print(asLi);
display.setCursor(SW-30,0);display.print(F("S:"));display.print(asSc);
display.setCursor(0,56);display.print(F("L/R=rot U=thru ACT=fire"));
}
// ═════════════════════════════════════════════════════════
// 8. SPACE SHOOTER
// ═════════════════════════════════════════════════════════
#define SH_MAXE 8
#define SH_MAXB 6
#define SH_MAXEB 4
struct ShPlayer{int x,y;};
struct ShEnemy{float x,y;bool alive;int type;};
struct ShBullet{float x,y;bool alive;bool enemy;};
ShPlayer shP;
ShEnemy shE[SH_MAXE];
ShBullet shB[SH_MAXB+SH_MAXEB];
int shSc,shLi,shWave; bool shOver,shPau;
unsigned long shShootCD,shESpawn,shEShoot;
void shSpawnEnemy(){
for(auto&e:shE) if(!e.alive){
e={((float)random(10,SW-10)),(0.0f),true,random(0,3)};break;
}
}
void shInit(){
shP={SW/2,SH-8}; shSc=0;shLi=3;shWave=0;shOver=false;shPau=false;
for(auto&e:shE) e.alive=false;
for(auto&b:shB) b.alive=false;
shShootCD=0;shESpawn=millis();shEShoot=millis();
}
void updateShooter(){
if(qModeDouble){appState=QA_MENU;shOver=false;return;}
static bool shStarted=false;
if(!shStarted){qcx("SPACE",4,2);qcx("SHOOTER",20,2);qcx("ACT=start",46);if(bA.pressed){shInit();shStarted=true;}return;}
if(shOver){qcx("GAME OVER",8,2);char b[20];sprintf(b,"Score:%d",shSc);qcx(b,38);qcx("ACT=retry",54);if(bA.pressed)shInit();return;}
if(qModeSingle)shPau=!shPau;
if(shPau){
for(int i=0;i<8;i++) display.drawPixel((i*17)%SW,(i*13)%SH,SSD1306_WHITE);
for(auto&e:shE) if(e.alive){
int ex=(int)e.x,ey=(int)e.y;
display.drawLine(ex-4,ey,ex+4,ey,SSD1306_WHITE);
display.fillCircle(ex,ey,2,SSD1306_WHITE);
}
display.fillTriangle(shP.x,shP.y-5,shP.x-4,shP.y+3,shP.x+4,shP.y+3,SSD1306_WHITE);
qDrawPauseOverlay();
return;
}
if(bL.held)shP.x-=2; if(bR.held)shP.x+=2;
shP.x=constrain(shP.x,4,SW-4);
unsigned long now=millis();
if(bA.held&&now>shShootCD){
shShootCD=now+200;
for(auto&b:shB) if(!b.alive&&!b.enemy){b={((float)shP.x),((float)shP.y),true,false};break;}
}
if(now>shESpawn){shESpawn=now+max(400,1500-shWave*100);shSpawnEnemy();}
if(now>shEShoot){
shEShoot=now+max(600,2000-shWave*150);
for(auto&e:shE) if(e.alive){
for(auto&b:shB) if(!b.alive&&b.enemy){b={e.x,e.y,true,true};break;}
break;
}
}
for(auto&b:shB){
if(!b.alive) continue;
b.y+=b.enemy?1.5f:-3.0f;
if(b.y<0||b.y>SH)b.alive=false;
}
for(int i=0;i<SH_MAXE;i++){
if(!shE[i].alive) continue;
shE[i].y+=0.5f+(shWave*0.05f);
shE[i].x+=sin(millis()/400.0f+i)*0.8f;
if(shE[i].y>SH){shE[i].alive=false;shLi--;if(shLi<=0){shOver=true;return;}}
if(abs(shE[i].x-shP.x)<6&&abs(shE[i].y-shP.y)<6){shLi--;shE[i].alive=false;if(shLi<=0){shOver=true;return;}}
for(auto&b:shB){
if(!b.alive||b.enemy) continue;
if(abs(b.x-shE[i].x)<5&&abs(b.y-shE[i].y)<5){b.alive=false;shE[i].alive=false;shSc+=10*(i%3+1);shWave=shSc/100;break;}
}
}
for(auto&b:shB){
if(!b.alive||!b.enemy) continue;
if(abs(b.x-shP.x)<4&&abs(b.y-shP.y)<4){b.alive=false;shLi--;if(shLi<=0){shOver=true;return;}}
}
for(int i=0;i<8;i++) display.drawPixel((i*17+now/50)%SW,(i*13+now/80)%SH,SSD1306_WHITE);
for(auto&e:shE) if(e.alive){
int ex=(int)e.x,ey=(int)e.y;
display.drawLine(ex-4,ey,ex+4,ey,SSD1306_WHITE);
display.drawLine(ex,ey-3,ex,ey+3,SSD1306_WHITE);
display.fillCircle(ex,ey,2,SSD1306_WHITE);
}
for(auto&b:shB) if(b.alive) display.fillRect((int)b.x-1,(int)b.y-2,2,4,SSD1306_WHITE);
display.fillTriangle(shP.x,shP.y-5,shP.x-4,shP.y+3,shP.x+4,shP.y+3,SSD1306_WHITE);
display.fillRect(0,0,SW,7,SSD1306_BLACK);
display.setTextSize(1);display.setTextColor(SSD1306_WHITE);
display.setCursor(0,0);display.print(F("L"));display.print(shLi);
display.setCursor(SW-36,0);display.print(F("S:"));display.print(shSc);
}
// ═════════════════════════════════════════════════════════
// 9. TICTACTOE vs AI (Minimax)
// ═════════════════════════════════════════════════════════
int ttB[9];
int ttCur; bool ttOver; char ttMsg[24];
int ttCheck(int b[9]){
int w[8][3]={{0,1,2},{3,4,5},{6,7,8},{0,3,6},{1,4,7},{2,5,8},{0,4,8},{2,4,6}};
for(auto&l:w){ if(b[l[0]]&&b[l[0]]==b[l[1]]&&b[l[1]]==b[l[2]]) return b[l[0]]; }
return 0;
}
bool ttFull(int b[9]){ for(int i=0;i<9;i++) if(!b[i]) return false; return true; }
int minimax(int b[9],bool isAI){
int w=ttCheck(b);
if(w==2) return 10;
if(w==1) return -10;
if(ttFull(b)) return 0;
int best=isAI?-100:100;
for(int i=0;i<9;i++){
if(b[i]) continue;
b[i]=isAI?2:1;
int sc=minimax(b,!isAI);
b[i]=0;
if(isAI&&sc>best) best=sc;
if(!isAI&&sc<best) best=sc;
}
return best;
}
void ttAIMove(){
int best=-100,bm=-1;
for(int i=0;i<9;i++){
if(ttB[i]) continue;
ttB[i]=2; int sc=minimax(ttB,false); ttB[i]=0;
if(sc>best){best=sc;bm=i;}
}
if(bm>=0) ttB[bm]=2;
}
void ttInit(){ memset(ttB,0,sizeof(ttB)); ttCur=0; ttOver=false; strcpy(ttMsg,"Your turn (X)"); }
void updateTicTacToe(){
if(qModeDouble){appState=QA_MENU;ttOver=false;return;}
static bool ttStarted=false;
if(!ttStarted){qcx("TICTACTOE",4,2);qcx("vs AI",24,2);qcx("ACT=start",48);if(bA.pressed){ttInit();ttStarted=true;}return;}
int ox=32,oy=2,cs=19;
for(int i=1;i<3;i++){
display.drawLine(ox+i*cs,oy,ox+i*cs,oy+3*cs,SSD1306_WHITE);
display.drawLine(ox,oy+i*cs,ox+3*cs,oy+i*cs,SSD1306_WHITE);
}
for(int i=0;i<9;i++){
int r=i/3,c=i%3,cx2=ox+c*cs+cs/2,cy2=oy+r*cs+cs/2;
if(ttB[i]==1){display.drawLine(cx2-5,cy2-5,cx2+5,cy2+5,SSD1306_WHITE);display.drawLine(cx2+5,cy2-5,cx2-5,cy2+5,SSD1306_WHITE);}
else if(ttB[i]==2) display.drawCircle(cx2,cy2,6,SSD1306_WHITE);
if(i==ttCur&&!ttOver){display.drawRect(ox+c*cs+1,oy+r*cs+1,cs-2,cs-2,SSD1306_WHITE);}
}
display.setTextSize(1);display.setTextColor(SSD1306_WHITE);
display.setCursor(0,24);
char lns[24]; strncpy(lns,ttMsg,23);
for(int i=0;i<16&&lns[i];i++) display.print(lns[i]);
if(ttOver){display.setCursor(0,48);display.print(F("ACT=new game"));}
if(ttOver){if(bA.pressed)ttInit();return;}
if(bL.pressed) ttCur=(ttCur+8)%9;
if(bR.pressed) ttCur=(ttCur+1)%9;
if(bU.pressed) ttCur=(ttCur+6)%9;
if(bD.pressed) ttCur=(ttCur+3)%9;
if(bA.pressed&&!ttB[ttCur]){
ttB[ttCur]=1;
int w=ttCheck(ttB);
if(w){strcpy(ttMsg,"You win!");ttOver=true;return;}
if(ttFull(ttB)){strcpy(ttMsg,"Draw!");ttOver=true;return;}
ttAIMove();
w=ttCheck(ttB);
if(w){strcpy(ttMsg,"AI wins!");ttOver=true;}
else if(ttFull(ttB)){strcpy(ttMsg,"Draw!");ttOver=true;}
else strcpy(ttMsg,"Your turn");
}
}
// ═════════════════════════════════════════════════════════
// 10. PONG / AIR HOCKEY
// ═════════════════════════════════════════════════════════
#define PN_PH 16
#define PN_PW 4
#define PN_BR 3
#define PN_SPD 3
float pnBx,pnBy,pnDx,pnDy;
int pnP1y,pnP2y,pnSc1,pnSc2; bool pnOver,pnPau;
void pnInit(){
pnBx=SW/2;pnBy=SH/2;
float ang=(random(0,4)*1.5707f)+0.3f;
pnDx=cos(ang)*2.5f;pnDy=sin(ang)*2.5f;
pnP1y=SH/2-PN_PH/2;pnP2y=SH/2-PN_PH/2;
pnSc1=0;pnSc2=0;pnOver=false;pnPau=false;
}
void pnResetBall(){
pnBx=SW/2;pnBy=SH/2;
float ang=(random(0,4)*1.5707f)+0.3f;
pnDx=cos(ang)*2.5f;pnDy=sin(ang)*2.5f;
}
void updatePong(){
if(qModeDouble){appState=QA_MENU;pnOver=false;return;}
static bool pnStarted=false;
if(!pnStarted){qcx("PONG",4,2);qcx("AIR HOCKEY",22,2);qcx("U/D=you AI=right",46);qcx("ACT=start",56);if(bA.pressed){pnInit();pnStarted=true;}return;}
if(pnOver){qcx("GAME OVER",8,2);char b[24];sprintf(b,"You %d - %d AI",pnSc1,pnSc2);qcx(b,36);qcx("ACT=retry",54);if(bA.pressed)pnInit();return;}
if(qModeSingle)pnPau=!pnPau;
if(pnPau){
display.drawLine(SW/2,0,SW/2,SH,SSD1306_WHITE);
display.fillRect(0,pnP1y,PN_PW,PN_PH,SSD1306_WHITE);
display.fillRect(SW-PN_PW,pnP2y,PN_PW,PN_PH,SSD1306_WHITE);
display.fillCircle((int)pnBx,(int)pnBy,PN_BR,SSD1306_WHITE);
qDrawPauseOverlay();
return;
}
if(bU.held)pnP1y-=PN_SPD; if(bD.held)pnP1y+=PN_SPD;
pnP1y=constrain(pnP1y,0,SH-PN_PH);
int aiTarget=pnBy-PN_PH/2;
if(pnP2y<aiTarget)pnP2y+=2; else pnP2y-=2;
pnP2y=constrain(pnP2y,0,SH-PN_PH);
pnBx+=pnDx;pnBy+=pnDy;
if(pnBy-PN_BR<=0){pnBy=PN_BR;pnDy=fabs(pnDy);}
if(pnBy+PN_BR>=SH){pnBy=SH-PN_BR;pnDy=-fabs(pnDy);}
if(pnBx-PN_BR<=PN_PW&&pnBy>pnP1y&&pnBy<pnP1y+PN_PH){
pnBx=PN_PW+PN_BR;pnDx=fabs(pnDx)*1.05f;
float rel=(pnBy-(pnP1y+PN_PH/2.0f))/(PN_PH/2.0f);pnDy=rel*3.0f;
if(fabs(pnDx)>5)pnDx=5;
}
if(pnBx+PN_BR>=SW-PN_PW&&pnBy>pnP2y&&pnBy<pnP2y+PN_PH){
pnBx=SW-PN_PW-PN_BR;pnDx=-fabs(pnDx)*1.05f;
float rel=(pnBy-(pnP2y+PN_PH/2.0f))/(PN_PH/2.0f);pnDy=rel*3.0f;
if(fabs(pnDx)>5)pnDx=-5;
}
if(pnBx<0){pnSc2++;if(pnSc2>=7)pnOver=true;else pnResetBall();}
if(pnBx>SW){pnSc1++;if(pnSc1>=7)pnOver=true;else pnResetBall();}
display.drawLine(SW/2,0,SW/2,SH,SSD1306_WHITE);
for(int y=0;y<SH;y+=6) display.drawPixel(SW/2,y,SSD1306_BLACK);
display.fillRect(0,pnP1y,PN_PW,PN_PH,SSD1306_WHITE);
display.fillRect(SW-PN_PW,pnP2y,PN_PW,PN_PH,SSD1306_WHITE);
display.fillCircle((int)pnBx,(int)pnBy,PN_BR,SSD1306_WHITE);
display.setTextSize(1);display.setTextColor(SSD1306_WHITE);
display.setCursor(SW/2-16,0);display.print(pnSc1);display.print(F("-"));display.print(pnSc2);
}
// ═════════════════════════════════════════════════════════
// SETUP / LOOP
// ═════════════════════════════════════════════════════════
void qpad_setup(){
for(auto b:BTNS) pinMode(b->pin, INPUT_PULLUP);
// ESP32C6: use hardware RNG for better randomness
randomSeed((uint32_t)esp_random());
// ESP32C6: explicit SDA/SCL pins required
// Wire already begun in master setup()
// Wire.begin(I2C_SDA, I2C_SCL);
// OLED already initialized in master setup() - display.begin skipped
appState = QA_MENU;
}
void qpad_loop(){
qReadBtns();
qDetectMode();
if(qModeDouble && appState != QA_MENU){ appState = QA_MENU; return; }
if(appState == QA_MENU){ qMenuUpdate(); qMenuDraw(); return; }
display.clearDisplay();
switch(appState){
case QA_BREAKOUT: updateBreakout(); break;
case QA_BOUNCE: updateBounce(); break;
case QA_SNAKE_W: updateSnake(false); break;
case QA_SNAKE_WRAP: updateSnake(true); break;
case QA_TETRIS: updateTetris(); break;
case QA_FLAPPY: updateFlappy(); break;
case QA_ASTEROIDS: updateAsteroids(); break;
case QA_SHOOTER: updateShooter(); break;
case QA_TICTACTOE: updateTicTacToe(); break;
case QA_PONG: updatePong(); break;
default: break;
}
display.display();
delay(16);
}🏆 Hero Shot#
The PCB was successfully milled, assembled, and programmed — every button input, LED output, and serial communication working correctly.
Reflection#
This week made me understand how sensitive the gap between digital design and physical fabrication actually is. Even something as small as a drill diameter matching the tool diameter exactly caused a real failure — and fixing it meant actually understanding why it happened, not just randomly tweaking values until it worked.
The other big one was placing the microcontroller in the wrong orientation. That’s the kind of mistake you make exactly once — the desoldering process is painful enough to make sure you always double-check the pin 1 marker before you touch the iron.
What I really appreciate now is how interconnected the whole workflow is. The decisions you make in KiCad (trace width, hole size, component placement) have direct physical consequences on the milling machine and at the soldering station. Getting something wrong anywhere in the chain means going back to a much earlier step.
Also — building programs that I actually wrote during Week 4 and running them on a board I physically made myself this week is genuinely satisfying in a way that’s hard to describe.
Helpful Websites#
- Gerber2PNG (FabLab Kerala) — https://gerber2png.fablabkerala.in/
- Gerber2PNG KiCad Plugin — https://github.com/gsuberland/gerber2png
- Mods Project — https://modsproject.org/
- FabStash Lab Inventory — https://inventory.fablabkerala.in/
- Seeed Studio XIAO ESP32-C6 Setup Guide — https://wiki.seeedstudio.com/xiao_esp32c6_getting_started/