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#

DayWhat I Did
WEDLecture on electronics production by Saheen Neil
THUGroup assignment — characterising mill design rules , Opened PCB design from Week 6, checked trace widths and clearances
FRIExported Gerber files, ran Gerber2PNG, set up Mods toolpaths , Milled traces, drilled holes, cut outline on Roland MDX-20
SATCreated the Arduino sketch that is required for testing the PCB
SUNCreated the bill of materials (BOM) for the components needed to populate the PCB, used FabStash to request components from the lab inventory
MONSoldering the PCB, tested the Arduino sketch
TUERegional 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:

ParameterValue
Minimum Trace Width0.4 mm
Minimum Clearance0.4 mm

Group Assignment — Design Rule Test Board

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:#

LayerWhat It Contains
F.CuFront copper traces
B.CuBack copper traces
Edge.CutsBoard outline
Drill file (.drl)Hole positions and diameters

Steps:

Steps to Export Gerber Files:#

  1. In the PCB Editor, go to File → Fabrication Outputs → Gerbers (.gbr)
  2. 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
  3. Click Plot to generate the Gerber files

KiCad Gerber Export

KiCad Gerber Export

Exporting the Drill File:#

  1. Still in the Plot dialog, click Generate Drill Files
  2. In the Drill File dialog:
    • Set the same output directory as the Gerbers
  3. Click Generate Drill File

Generate Drill Files

Creating Zip Files:#

  1. Navigate to your gerber/ output folder Gerber Files
  2. Select all the generated files (.gbr, .drl, etc.)
  3. 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.

Gerber2PNG KiCad Plugin

Files generated:#

PNG FilePurpose
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 TracesSame as above but for the back copper layer
Back Drill HolesHole positions from the back side
Back OutlineBack boundary cutout

Steps:

  1. Go to gerber2png.fablabkerala.in
  2. Upload the Gerber zip file.
  3. Click “Generate all PNGs”
  4. Download the generated PNGs

Gerber2PNG Tool

Gerber2PNG Output


🗺️ 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:

  1. Front traces → V-bit pass
  2. Front drill holes → 1/32" end mill
  3. Front outline → 1/32" end mill

Flip the board, then:

  1. Back traces → V-bit pass
  2. Back drill holes → 1/32" end mill
  3. Back outline → 1/32" end mill

V-bit settings in Mods:#

ParameterValue
V-bit Tip Diameter0.2 mm
V-bit Point Angle60°
Cut Depth0.9 mm

Mods Project Interface Mods 2


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.

PCB Milling Setup 1 PCB Milling Setup 2 PCB Milling Setup 3 PCB Milling Setup 4 PCB Milling Setup 5 PCB Milling Setup 6

Setting the Origin#

Before sending any file, the machine origin needs to be set:

  1. Jog the machine to the desired XY start position on the blank
  2. Set this as the origin in the Mods interface
  3. Press Change Tool to move the spindle to the tool change position
  4. Insert the V-bit and tighten the grub screw with an Allen key

Setting Machine Origin 1 Setting Machine Origin 2 Setting Machine Origin 3

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

Z Zero Setup 1 Z Zero Setup 2 Z Zero Setup 3 Z Zero Setup 4


🔪 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.

Front Traces Milled

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:

  1. Used a scraper to carefully peel the PCB off the machine bed
  2. Cleaned the back of the board with IPA (isopropyl alcohol) to remove double-sided tape residue
  3. 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:

  1. Log in at inventory.fablabkerala.in using lab credentials
  2. Search for each component by name or category
  3. Click + to add it to your request list
  4. Submit the request
  5. Wait for instructor approval
  6. 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#

ReferencePartQuantity
C1, C2100nf2
R11k1
D1LED_12061
SW1-6Push Button6
M1Module_XIAO-ESP32C61
J2, J3, J9, J10S4B-PH-SM4-TB4
J7, J8Conn_01x07_Socket2
J1PinSocket_01x04_P2.54mm_Vertical_SMD1
J5Screw_Terminal_01x02_P5mm1
P1S3B-PH-SM4-TB1

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.

Components Collected from FabStash


Part 4 – Soldering#

🔧 Soldering Station Setup#

Before starting, a brief orientation on the soldering workstation:

ToolPurpose
Soldering IronHeating pads and melting solder
Hot Air BlowerReflow soldering / component removal
Fume ExtractorPulling solder fumes away from your face
Tip CleanerKeeping the iron tip clean for good heat transfer
Silicone Work MatHeat-resistant surface, keeps small parts from rolling away
TweezersPlacing SMD components accurately
Digital MicroscopeInspecting solder joint quality up close

Soldering Station Setup

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.

Via Soldering


📐 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:

  1. Resistors (1206 SMD — lowest profile)
  2. Capacitors (1206 SMD)
  3. XIAO ESP32-C6 (the microcontroller)
  4. Header pins
  5. JST connectors
  6. Terminal blocks / tall connectors

For each SMD component:

  1. Tin one pad first — add a small blob of solder
  2. Pick up the component with tweezers, hold it in position
  3. Touch the iron to the tinned pad — the component sinks into place
  4. Solder the remaining pads
  5. Go back and reflow the first joint to clean it up

SMD Soldering in Progress

XIAO ESP32-C6 Taped Down

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.

Continuity Testing 1 Continuity Testing 2 Continuity Testing 3


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.

  1. Open Arduino IDE
  2. Go to File → Preferences
  3. Add the Seeed board manager URL to Additional Boards Manager URLs
  4. Go to Tools → Board → Boards Manager, search ESP32, install the Seeed package
  5. Select XIAO ESP32-C6 as the board
  6. Select the correct COM port
  7. Upload

Setup reference: wiki.seeedstudio.com/xiao_esp32c6_getting_started

Arduino IDE XIAO ESP32-C6 Setup

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#


Source Files#

📦 Download KiCad project files (week-6.rar)