Sentry Robot Architecture

A self-contained embedded companion platform running FreeRTOS on dual-core ESP32, coordinating asynchronous OLED facial graphics, ultrasonic sentry surveillance, environmental sensors, and a local Wi-Fi telemetry dashboard.

Developer: Justin | Platform: ESP32 | Version: 12.0 (Final)


1. Project Overview

Retro-Bot is an advanced desktop companion robot powered by an ESP32 microcontroller. It features a "living" OLED face that reacts to its environment, plays arcade games, and acts as a security sentry. It is controlled via a local Wi-Fi web dashboard hosted directly on the robot.

Key Features

  • 🧠 Emotional AI: Reacts to temperature, gas, movement, and proximity.
  • 👀 Sentry Mode: Wakes up when it detects human motion (PIR).
  • 🛡️ Personal Space: Gets "Suspicious" or "Panicked" if you get too close (Ultrasonic).
  • 🍔 Hand Feeding: "Eats" when you hold an object to its mouth (IR Sensor).
  • 🎮 Arcade Console: Plays Tic-Tac-Toe, Snake, and Pong via phone controller.
  • 🗣️ Living Face: Realistic blinking eyes, breathing animation, and talking text.

2. Bill of Materials (Hardware)

Retro-Bot Hardware Components
Component Description Function
ESP32 DevKit V1 Microcontroller The "Brain" + Wi-Fi Server
OLED Display 1.3" I2C (SH1106) The "Face" and Game Screen
DHT11 Blue Grid Sensor Measures Temperature & Humidity
MQ-135 Gas Sensor Detects Air Quality / Smoke
HC-SR04 Ultrasonic Eyes Measures Distance (0cm - 400cm)
PIR Sensor White Dome Detects Motion (Infrared body heat)
IR Sensor Black LED pair Detects close objects (Feeding)
Active Buzzer Black Cylinder Sound Effects & Alarms
Jumper Wires M-M, M-F Connections

3. Master Wiring Guide

⚠️ IMPORTANT: The ESP32 uses 3.3V logic, but the VIN pin provides 5V from USB. Most sensors need 5V (VIN), but the OLED works best on 3.3V.

Retro-Bot Wiring Diagram

Power Rails

  • ESP32 GND → Breadboard Blue (-) Rail
  • ESP32 VIN (5V) → Breadboard Red (+) Rail

Pin Connections Table

Component Component Pin ESP32 Pin Note
OLED Screen SDA GPIO 21 Data Line
SCL GPIO 22 Clock Line
VCC 3V3 Do not use 5V for Screen
GND GND
DHT11 (Temp) DATA / OUT GPIO 4
MQ-135 (Gas) A0 / Analog GPIO 34 Analog Input Only
Buzzer POS (+) GPIO 5
PIR (Motion) OUT (Middle) GPIO 13 Digital Signal
Ultrasonic TRIG GPIO 18 Output Pulse
ECHO GPIO 19 Input Echo
IR (Feeding) OUT GPIO 23 Digital Signal (LOW = Active)

4. Software & Installation

Required Libraries

Install these via Arduino IDE Library Manager:

  1. Adafruit GFX Library (Core graphics)
  2. Adafruit SH110X (Screen Driver - Crucial for removing static)
  3. DHT sensor library (by Adafruit)
  4. Adafruit Unified Sensor

Upload Settings

  • Board: DOIT ESP32 DEVKIT V1
  • Upload Speed: 921600
  • Flash Frequency: 80MHz

Troubleshooting "Static/Snow" on Screen

If the screen looks grainy:

  1. Ensure you are using Adafruit_SH1106G in the code (V12 code does this).
  2. Twist the SDA (Yellow) and SCL (Blue) wires together to cancel electrical noise.

5. User Guide: How to Use Retro-Bot

Retro-Bot User Interface

A. Connecting

  1. Power on the robot.
  2. On your phone/PC, connect to Wi-Fi: RetroBot_AP (No password).
  3. Open browser to: 192.168.4.1

B. Dashboard Controls

  • HAPPY: Forces normal mode.
  • FEED: Triggers "Eating" animation manually.
  • PANIC: Triggers alarm mode manually.
  • SLEEP: Turns off screen and waits for motion (Sentry Mode).
  • GAME MODE: Switches the face to the Game Menu.

C. Sensor Behaviors (Autonomous Mode)

  • Motion (PIR): If sleeping, any movement wakes the robot up.
  • Feeding (IR): Place finger 2cm from mouth → Robot eats ("YUMMY!").
  • Distance (Sonar):
    • < 25cm: Robot gets Suspicious (Squinting eyes).
    • < 10cm: Robot Panics (Screaming + Beeping).
  • Air Quality (Gas): High gas levels trigger Panic mode.

Source Code

Retro-Bot Source Code

Below is the placeholder for the source code. You can paste your Arduino sketch here.

#include <WiFi.h>
#include <WebServer.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
#include <DHT.h>
const char* ssid     = "RetroBot_AP"; 
const char* password = "";            
#define PIN_BUZZER    5
#define PIN_DHT       4
#define PIN_MQ135     34
#define PIN_PIR       13   
#define PIN_TRIG      18   
#define PIN_ECHO      19   
#define OLED_SDA      21 
#define OLED_SCL      22 
#define SCREEN_WIDTH  128
#define SCREEN_HEIGHT 64
#define SCREEN_ADDRESS 0x3C
Adafruit_SH1106G display = Adafruit_SH1106G(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
DHT dht(PIN_DHT, DHT11);
WebServer server(80);
enum EyeLidShape { LID_FLAT, LID_ANGRY, LID_SAD, LID_TIRED };
float temp = 0;
int gasLevel = 0;
long dist_cm = 0;
long prev_dist_cm = 0; 
String currentMood = "HAPPY";
unsigned long manualOverrideTimer = 0;
bool isSleeping = false;
String overrideText = ""; 
unsigned long overrideTextTimer = 0;
unsigned long unixTime = 0;
unsigned long lastSyncMillis = 0;
bool timeSynced = false;
int gasBaseline = 0;
float tempBaseline = 0;
long lastDistChangeTime = 0;
unsigned long lastMotionTime = 0;
const unsigned long SLEEP_TIMEOUT = 10000; 
bool showBigText = false;
bool isFocusMode = false;
unsigned long focusStartTime = 0;
unsigned long focusDuration = 0;
const unsigned long DISTRACTION_THRESHOLD = 6000; 
const char* txt_MOTIVATION[] = {"FOCUS!", "DO IT NOW", "EYES HERE", "NO PHONES", "STAY HARD", "GRIND", "WORK WAR", "LOCK IN"};
unsigned long lastDHTRead = 0;
unsigned long lastDistRead = 0;
const int GAS_SAMPLES = 5; 
int gasReadings[GAS_SAMPLES];
int gasIndex = 0; int totalGas = 0; int averageGas = 0;
int GAS_THRESHOLD_OFFSET = 200; 
const int DIST_PANIC = 10; 
const int DIST_SUS = 20;   
const int DIST_TOUCH = 4; 
const int DIST_VELOCITY_TRIGGER = -10; 
int breathVal = 0; bool breathIn = true;
String currentDisplayText = "Just chillin"; 
unsigned long lastTextUpdate = 0;
const char* txt_HAPPY[] = {"Hello friend!", "Happy to help", "Wonderful day", "Ready for you", "All systems go", "Smiling rn", "Peaceful here", "Hi there!"};
const char* txt_SUS[] = {"Oh, hello?", "Who is there?", "Identifying...", "May I help?", "Curious...", "Just checking", "New friend?", "I see you"};
const char* txt_PANIC[] = {"Oh dear!", "Too close pls", "Please step back", "Safety first", "Bit crowded", "Excuse me!", "Need space", "Careful!"};
const char* txt_ANGRY[] = {"Please stop", "Not nice", "Feeling blue", "System upset", "Be kind pls", "I need space", "Boundaries", "Hmph..."};
const char* txt_SWEAT[] = {"It's warm!", "Whew hot!", "Need a fan", "Staying cool", "Oh my circuits", "Toasty here", "Summer vibes", "Phew!"};
const char* txt_LOVE[] = {"You're great!", "So kind!", "Best friends", "Lovely day", "Heart you", "Thank you!", "Sending joy", "You shine!"};
const char* txt_CONFUSED[] = {"I'm not sure", "Say again?", "Hmm...", "Thinking...", "One moment", "Processing", "Trying my best", "Oh?"};
int phraseIndex = 0; 
unsigned long lastBlink = 0; bool eyesOpen = true;
float pupilSize = 3.5; 
float eyeX = 0; float eyeY = 0; 
int activeGame = 0; int score = 0;
struct Player { bool active; int x; int y; int input; };
Player p1 = {false,0,0,0}; Player p2 = {false,0,0,0}; 
int ttt_board[9]; int ttt_status = 0; int ttt_turn = 1; 
const int SNAKE_MAX_LEN=100; int snakeX[SNAKE_MAX_LEN]; int snakeY[SNAKE_MAX_LEN]; int snakeLen=3; int snakeDir=1; int foodX=0, foodY=0; bool snakeGameOver=false; unsigned long lastSnakeMove=0;
float ballX=64, ballY=32; float ballVX=2, ballVY=2; int paddle1Y=24; int paddle2Y=24; int p1Score=0; int p2Score=0; bool pongGameOver=false;
int dinoY=50; float dinoV=0; bool dinoGrounded=true; int cactusX=128; int dinoScore=0; bool dinoOver=false;
int shipX=64; int bulletX=-1; int bulletY=-1; int enemyX[5]; int enemyY[5]; bool enemyAlive[5]; int invScore=0; bool invOver=false; unsigned long lastInvMove=0; int invDir=1;
int tankWinner=0; 
bool brk[5][10]; int bBallX=64, bBallY=50; float bVelX=2, bVelY=-2;
int pipeX=128; int pipeGap=20;
int mazeX=2, mazeY=2; int mazeW=64, mazeH=32;
float landX=64, landY=10, landVX=0, landVY=0, landFuel=100; bool landCrashed=false, landLanded=false;

const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head><meta name="viewport" content="width=device-width, initial-scale=1">
<title>RetroBot Sentry</title>
<style>
body{font-family:'Courier New', monospace;background-color:#000;color:#fff;text-align:center;margin:0;padding:10px;}
h1{margin:10px 0;color:#00ff00;text-shadow:0 0 5px #00ff00;}
h2{margin:5px 0;}
h3{border-bottom:2px solid #555;padding-bottom:5px;margin-top:15px;color:#aaa;font-size:16px;}
button{background:#222;color:#0f0;border:1px solid #0f0;padding:15px;margin:4px;font-family:inherit;font-size:16px;width:45%;cursor:pointer;border-radius:5px;}
button:active{background:#0f0;color:#000;}
.red{color:#f00;border-color:#f00;}
.red:active{background:#f00;color:#fff;}
.p{border:1px solid #333;padding:10px;margin:10px 0;border-radius:8px;background:#111;}
.s-box{display:grid;grid-template-columns:1fr 1fr;gap:10px;}
.v{font-size:24px;font-weight:bold;}
.l{font-size:12px;color:#888;}
.h{display:none;}
.dpad{display:grid;grid-template-columns:50px 50px 50px;grid-template-rows:50px 50px 50px;gap:5px;justify-content:center;margin:0 auto;}
.btn-arr{width:100% !important;height:100% !important;padding:0 !important;font-size:24px !important;margin:0 !important;}
input{font-family:inherit;font-size:16px;}
.a{background:#300 !important;border-color:#f00 !important;}
</style>
</head>
<body>
  <h1>RetroBot Sentry</h1>
  <div id="mU">
    <div class="p"><h3>STATUS: <span id="st">LOADING...</span></h3><div><div id="cT" class="c">TEMP<br><span id="t">--</span>C</div><div id="cG" class="c">AIR<br><span id="g">--</span></div><div id="cD" class="c">DIST<br><span id="d">--</span>cm</div></div></div>
    <div class="p" style="border:2px solid #0f0;box-shadow:0 0 10px #0f0"><h3>FOCUS TIMER</h3>
       <div style="display:flex;justify-content:center;gap:5px;margin-bottom:10px">
          <button onclick="sF(1,25)" style="width:30%">25m</button>
          <button onclick="sF(1,45)" style="width:30%">45m</button>
          <button onclick="sF(1,60)" style="width:30%">1h</button>
       </div>
       <div style="display:flex;justify-content:center;gap:5px">
          <input id="fm" type="number" placeholder="Min" style="width:30%;padding:10px;background:#222;color:#0f0;border:1px solid #0f0;text-align:center;border-radius:5px">
          <button onclick="sF(1,0)" style="width:60%;background:#004400;color:#0f0;border:1px solid #0f0;font-weight:bold">START SESSION</button>
       </div>
       <button class="red" onclick="sF(0,0)" style="width:95%;margin-top:10px;box-shadow:0 0 5px red">STOP FOCUS</button>
       <div id="fStat" style="margin-top:10px;font-size:14px;font-weight:bold;color:#888;text-transform:uppercase">STATUS: IDLE</div>
    </div>
    <div class="p" style="border:1px solid #00f"><h3>ARCADE MENU</h3>
      <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
        <button onclick="lG(1)">❌ TTT (vs CPU)</button>
        <button onclick="lG(2)">🐍 SNAKE</button>
        <button onclick="lG(3)">🏓 PONG (vs AI)</button>
        <button onclick="lG(4)">🦖 DINO RUN</button>
        <button onclick="lG(5)">👾 INVADERS</button>
        <button onclick="lG(6)">🧱 BREAKOUT</button>
        <button onclick="lG(7)">🐦 FLAPPY</button>
        <button onclick="lG(8)">🕸 MAZE</button>
        <button style="grid-column:span 2" onclick="lG(9)"> LUNAR LANDER</button>
      </div>
    </div>
    <div class="p" style="border-color:#f0f"><h3>MULTIPLAYER SETUP</h3>
      <button id="bp1" onclick="join(1)">JOIN AS P1 (LEFT)</button>
      <button id="bp2" onclick="join(2)">JOIN AS P2 (RIGHT)</button>
      <div id="pstat" style="font-size:10px;margin-top:5px">NOT CONNECTED</div>
    </div>
  </div>
  <div id="tU" class="h"><h2>TIC-TAC-TOE</h2><div id="tM" style="color:yellow">YOUR TURN (X)</div><div class="grid"><div class="cell" onclick="tMv(0)" id="c0"></div><div class="cell" onclick="tMv(1)" id="c1"></div><div class="cell" onclick="tMv(2)" id="c2"></div><div class="cell" onclick="tMv(3)" id="c3"></div><div class="cell" onclick="tMv(4)" id="c4"></div><div class="cell" onclick="tMv(5)" id="c5"></div><div class="cell" onclick="tMv(6)" id="c6"></div><div class="cell" onclick="tMv(7)" id="c7"></div><div class="cell" onclick="tMv(8)" id="c8"></div></div><button class="red" onclick="xG()">EXIT</button></div>
  <div id="sU" class="h"><h2 style="color:#0f0">SNAKE</h2><h3 id="sc">SCORE: 0</h3><div class="dpad"><div></div><button class="btn-arr" onclick="nk(0)">▲</button><div></div><button class="btn-arr" onclick="nk(3)">◀</button><button class="btn-arr red" onclick="nk(9)">R</button><button class="btn-arr" onclick="nk(1)">▶</button><div></div><button class="btn-arr" onclick="nk(2)">▼</button><div></div></div><br><button class="red" onclick="xG()">EXIT</button></div>
  <div id="pU" class="h"><h2 style="color:cyan">PONG 1P</h2><h3>SCORE: <span id="ps">0</span></h3><div style="display:flex;justify-content:center;gap:10px"><button style="padding:30px" onclick="pg(-1)">▲ UP</button><button style="padding:30px" onclick="pg(1)">▼ DOWN</button></div><br><button class="red" onclick="xG()">EXIT</button></div>
  <div id="dU" class="h"><h2 style="color:#fa0">DINO RUN</h2><h3>SCORE: <span id="ds">0</span></h3><button style="padding:40px;width:80%;font-size:20px" onmousedown="dn(1)">[JUMP!]</button><br><br><button class="red" onclick="xG()">EXIT</button></div>
  <div id="iU" class="h"><h2 style="color:#0f0">INVADERS</h2><h3>SCORE: <span id="is">0</span></h3><div style="display:flex;justify-content:center;gap:5px"><button style="padding:20px" onclick="iv(-1)">[L]</button><button style="padding:20px;background:#f00;color:white" onclick="iv(10)">[FIRE]</button><button style="padding:20px" onclick="iv(1)">[R]</button></div><br><button class="red" onclick="xG()">EXIT</button></div>
  <div id="tkU" class="h"><h2 style="color:#fa0">BREAKOUT</h2><h3>SCORE: <span id="bks">0</span></h3><div style="display:flex;justify-content:center;gap:20px"><button style="padding:30px" onmousedown="bk(-1)">◀ LEFT</button><button style="padding:30px" onmousedown="bk(1)">RIGHT ▶</button></div><br><button class="red" onclick="xG()">EXIT</button></div>
  <div id="fbU" class="h"><h2 style="color:#0f0">FLAPPY</h2><h3>SCORE: <span id="fbs">0</span></h3><button style="padding:50px;width:90%;font-size:24px;border:2px solid #0f0" onmousedown="fb(1)">[ FLAP! ]</button><br><br><button class="red" onclick="xG()">EXIT</button></div>
  <div id="mzU" class="h"><h2 style="color:#0ff">MAZE RUN</h2><div class="dpad"><div></div><button class="btn-arr" onmousedown="mz(0)">▲</button><div></div><button class="btn-arr" onmousedown="mz(3)">◀</button><div></div><button class="btn-arr" onmousedown="mz(1)">▶</button><div></div><button class="btn-arr" onmousedown="mz(2)">▼</button><div></div></div><br><button class="red" onclick="xG()">EXIT</button></div>
  <div id="ldU" class="h"><h2 style="color:#fff">LUNAR LANDER</h2><h3>FUEL: <span id="ldf">100</span></h3><div style="display:flex;justify-content:center;gap:5px"><button style="padding:20px" onmousedown="ld(3)">◀</button><button style="padding:20px;background:#fa0" onmousedown="ld(0)">🔥 THRUST</button><button style="padding:20px" onmousedown="ld(1)">▶</button></div><br><button class="red" onclick="xG()">EXIT</button></div>
<script>
function syncTime() {
  const now = Math.floor(Date.now() / 1000);
  const offset = new Date().getTimezoneOffset() * -60; 
  fetch('/sync?t=' + (now + offset)).then(()=> {
    document.getElementById('pstat').innerHTML += " | <span style='color:#0f0'>TIME SYNCED ✔</span>";
  });
}
window.onload = syncTime;
setInterval(()=>{
  if(document.getElementById('mU').style.display!=='none'){
    fetch('/stat').then(r=>r.json()).then(d=>{
      document.getElementById('t').innerText=d.t.toFixed(1);
      document.getElementById('g').innerText=d.g;
      document.getElementById('d').innerText=d.d;
      document.getElementById('st').innerText=d.m;
      if(d.f) document.getElementById('fStat').innerHTML = "<span style='color:#0f0'>FOCUSING...</span>";
      else document.getElementById('fStat').innerText = "IDLE";
      if(d.t>32)document.getElementById('cT').classList.add('a');else document.getElementById('cT').classList.remove('a');
      if(d.g>2200)document.getElementById('cG').classList.add('a');else document.getElementById('cG').classList.remove('a');
      if(d.d<25)document.getElementById('cD').classList.add('a');else document.getElementById('cD').classList.remove('a');
    });
  }
  if(!document.getElementById('sU').classList.contains('h')){fetch('/snakeScore').then(r=>r.text()).then(s=>{document.getElementById('sc').innerText="SCORE: "+s});}
  if(!document.getElementById('pU').classList.contains('h')){fetch('/pongScore').then(r=>r.text()).then(s=>{document.getElementById('ps').innerText=s});}
  if(!document.getElementById('dU').classList.contains('h')){fetch('/dinoScore').then(r=>r.text()).then(s=>{document.getElementById('ds').innerText=s});}
  if(!document.getElementById('iU').classList.contains('h')){fetch('/invScore').then(r=>r.text()).then(s=>{document.getElementById('is').innerText=s});}
  if(!document.getElementById('tkU').classList.contains('h')){fetch('/bkScore').then(r=>r.text()).then(s=>{document.getElementById('bks').innerText=s});}
  if(!document.getElementById('fbU').classList.contains('h')){fetch('/fbScore').then(r=>r.text()).then(s=>{document.getElementById('fbs').innerText=s});}
  if(!document.getElementById('ldU').classList.contains('h')){fetch('/ldFuel').then(r=>r.text()).then(s=>{document.getElementById('ldf').innerText=s});}
},500);
// GAME LOGIC MAPPING
// lG = Load Game, xG = Exit Game
// sF = Start Focus, s = Set Mood
// tMv = TicTacToe Move
// nk = Snake Input (0=U,1=R,2=D,3=L)
// pg = Pong Input (-1=Up, 1=Down)
// dn = Dino Input (1=Jump)
// iv = Invaders Input (-1=L, 1=R, 10=Fire)
// bk = Breakout Input (-1=L, 1=R)
// fb = Flappy Input (1=Flap)
// mz = Maze Input (0=U,1=R,2=D,3=L)
// ld = Lander Input (0=Thrust, 3=Left, 1=Right)

function lG(id){
  document.getElementById('mU').classList.add('h');
  document.querySelectorAll('.game-ui').forEach(e=>e.classList.add('h')); // Hide all first
  
  if(id==1){document.getElementById('tU').classList.remove('h');fetch('/game?g=1&act=start').then(uT);}
  if(id==2){document.getElementById('sU').classList.remove('h');fetch('/game?g=2&act=start');}
  if(id==3){document.getElementById('pU').classList.remove('h');fetch('/game?g=3&act=start');}
  if(id==4){document.getElementById('dU').classList.remove('h');fetch('/game?g=4&act=start');}
  if(id==5){document.getElementById('iU').classList.remove('h');fetch('/game?g=5&act=start');}
  if(id==6){document.getElementById('tkU').classList.remove('h');fetch('/game?g=6&act=start');}
  if(id==7){document.getElementById('fbU').classList.remove('h');fetch('/game?g=7&act=start');}
  if(id==8){document.getElementById('mzU').classList.remove('h');fetch('/game?g=8&act=start');}
  if(id==9){document.getElementById('ldU').classList.remove('h');fetch('/game?g=9&act=start');}
}

function xG(){
  document.querySelectorAll('.h').forEach(e=>e.classList.add('h')); // Reset hidden
  document.getElementById('mU').classList.remove('h'); // Show menu
  
  // Hide all specific game UIs manually to be safe
  const uis = ['tU','sU','pU','dU','iU','tkU','fbU','mzU','ldU'];
  uis.forEach(id => { if(document.getElementById(id)) document.getElementById(id).classList.add('h'); });
  
  fetch('/game?g=0&act=exit');
}

// Input Handlers
function s(m){fetch('/set?m='+m);}
function sF(a,mVal){
  let m=mVal;
  if(a==1 && m==0) m=document.getElementById('fm').value; 
  if(!m) m=25; 
  console.log("Focus:", a, m);
  fetch('/focus?act='+a+'&m='+m);
}
// Game Inputs
function tMv(id){fetch('/game?g=1&act=move&c='+id).then(r=>r.json()).then(uT2);}
function uT(r){r.json().then(uT2);}
function uT2(d){for(let i=0;i<9;i++){let t="";if(d.b[i]==1)t="X";if(d.b[i]==2)t="O";document.getElementById('c'+i).innerText=t;}let m=document.getElementById('tM');if(d.s==1)m.innerText="YOU WIN!";else if(d.s==2)m.innerText="ROBOT WINS!";else m.innerText="YOUR TURN";}
function nk(d){console.log('Snake:',d); fetch('/game?g=2&act=input&d='+d);}
function pg(d){console.log('Pong:',d); fetch('/game?g=3&act=input&p='+myP+'&d='+d);}
function dn(d){console.log('Dino:',d); fetch('/game?g=4&act=input&d='+d);}
function iv(d){console.log('Invaders:',d); fetch('/game?g=5&act=input&d='+d);}
function bk(d){console.log('Breakout:',d); fetch('/game?g=6&act=input&d='+d);}
function fb(d){console.log('Flappy:',d); fetch('/game?g=7&act=input&d='+d);}
function mz(d){console.log('Maze:',d); fetch('/game?g=8&act=input&d='+d);}
function ld(d){console.log('Lander:',d); fetch('/game?g=9&act=input&d='+d);}
var myP=0;
function join(p){myP=p;document.getElementById('pstat').innerText="YOU ARE PLAYER "+p; 
  if(p==1)document.getElementById('bp1').style.borderColor="#0f0";
  if(p==2)document.getElementById('bp2').style.borderColor="#0f0";
}
</script></body></html>
)rawliteral";

void drawFace(); void drawHappy(); void drawPanic(); void drawSus(); void drawSweat(); void drawAngry(); void drawLove(); void drawConfused();     
void drawEyeV2(int cx, int cy, int r, float p, int lx, int ly, float closePercent, bool angryBrow);
void drawEyeMetric(int cx, int cy, int r, float pupil, int lx, int ly, float lidTopP, float lidBotP, EyeLidShape shape, bool glasses);
void drawTTT(); int checkTTTWin(); void initSnake(); void updateSnake(); void drawSnake(); void initPong(); void updatePong(); void drawPong();
void initDino(); void updateDino(); void drawDino(); void initInvaders(); void updateInvaders(); void drawInvaders();
void initBreakout(); void updateBreakout(); void drawBreakout();
void initFlappy(); void updateFlappy(); void drawFlappy();
void initMaze(); void updateMaze(); void drawMaze();
void initLander(); void updateLander(); void drawLander();
void drawClock();
void setup() {
  Serial.begin(115200); 
  pinMode(PIN_BUZZER, OUTPUT); pinMode(PIN_MQ135, INPUT); 
  pinMode(PIN_PIR, INPUT); 
  pinMode(PIN_TRIG, OUTPUT); pinMode(PIN_ECHO, INPUT);
  dht.begin();
  for(int i=0; i<GAS_SAMPLES; i++) gasReadings[i]=0;
  Wire.begin(OLED_SDA, OLED_SCL); Wire.setClock(400000); 
  if(!display.begin(SCREEN_ADDRESS, true)) for(;;);
  display.setRotation(2);
  display.clearDisplay(); display.setTextSize(1); display.setTextColor(SH110X_WHITE); display.setCursor(30,30); display.println("SENTRY MODE..."); display.display();
  gasBaseline = analogRead(PIN_MQ135);
  delay(100);
  float t_init = dht.readTemperature();
  if(!isnan(t_init)) tempBaseline = t_init; else tempBaseline = 25.0;
  WiFi.softAP(ssid, password);
  server.on("/", [](){ server.send_P(200, "text/html", index_html); });
  server.on("/stat", [](){ String json="{\"t\":"+String(temp)+",\"g\":"+String(gasLevel)+",\"d\":"+String(dist_cm)+",\"m\":\""+currentMood+"\",\"f\":"+String(isFocusMode)+"}"; server.send(200, "application/json", json); });
  server.on("/sync", [](){ if(server.hasArg("t")) { unixTime = server.arg("t").toInt(); lastSyncMillis = millis(); timeSynced = true; } server.send(200, "text/plain", "OK"); });
  server.on("/snakeScore", [](){ server.send(200, "text/plain", String(score)); });
  server.on("/pongScore", [](){ server.send(200, "text/plain", String(p1Score)+" - "+String(p2Score)); });
  server.on("/set", [](){ if(server.hasArg("m")) { currentMood=server.arg("m"); if(currentMood=="SLEEP") isSleeping=true; else {isSleeping=false; activeGame=0;} manualOverrideTimer=millis()+5000; } server.send(200, "text/plain", "OK"); });
  server.on("/focus", [](){ 
    if(server.hasArg("act")) { 
       int a=server.arg("act").toInt(); 
       if(a==1 && server.hasArg("m")) { isFocusMode=true; focusDuration=server.arg("m").toInt()*60000; focusStartTime=millis(); isSleeping=false; lastMotionTime=millis(); activeGame=0; }
       else { isFocusMode=false; }
    }
    server.send(200, "text/plain", "OK"); 
  });
  server.on("/game", [](){
    int g=server.arg("g").toInt(); String act=server.arg("act"); activeGame=g;
    if(act=="exit") { activeGame=0; }
    if(g==1) {
      if(act=="start") { ttt_status=0; for(int i=0;i<9;i++) ttt_board[i]=0; }
      if(act=="move" && ttt_status==0) { int c=server.arg("c").toInt(); if(ttt_board[c]==0) { ttt_board[c]=1; if(checkTTTWin()==1) ttt_status=1; else { int move=-1; for(int i=0;i<50;i++){ int r=random(0,9); if(ttt_board[r]==0){move=r; break;} } if(move!=-1){ ttt_board[move]=2; if(checkTTTWin()==2) ttt_status=2; } } } }
      String j="{\"s\":"+String(ttt_status)+",\"b\":["; for(int i=0;i<9;i++){ j+=String(ttt_board[i]); if(i<8)j+=","; } j+="]}"; server.send(200, "application/json", j); return;
    }
    if(g==2) { if(act=="start") { snakeLen=3; score=0; snakeDir=1; snakeGameOver=false; snakeX[0]=30; snakeY[0]=30; snakeX[1]=26; snakeY[1]=30; snakeX[2]=22; snakeY[2]=30; foodX=random(1,30)*4; foodY=random(1,15)*4; } if(act=="input") { int d=server.arg("d").toInt(); if(d==9) {snakeLen=3; score=0; snakeDir=1; snakeGameOver=false; snakeX[0]=30; snakeY[0]=30; snakeX[1]=26; snakeY[1]=30; snakeX[2]=22; snakeY[2]=30; foodX=random(1,30)*4; foodY=random(1,15)*4;} else if(d==0&&snakeDir!=2) snakeDir=0; else if(d==1&&snakeDir!=3) snakeDir=1; else if(d==2&&snakeDir!=0) snakeDir=2; else if(d==3&&snakeDir!=1) snakeDir=3; } }
    if(g==3) { if(act=="start") {ballX=64;ballY=32;ballVX=2.5;ballVY=2.5;p1Score=0;p2Score=0;pongGameOver=false;paddle1Y=24;paddle2Y=24;} if(act=="input") { int p=server.arg("p").toInt(); int d=server.arg("d").toInt(); if(p==1)paddle1Y+=d*5; if(p==2)paddle2Y+=d*5; } }
    if(g==4) { if(act=="start") initDino(); if(act=="input") { if(dinoGrounded) { dinoV = -6; dinoGrounded=false; } if(dinoOver) initDino(); } }
    if(g==5) { if(act=="start") initInvaders(); if(act=="input") { int d=server.arg("d").toInt(); if(d==10 && bulletY==-1) { bulletX=shipX+3; bulletY=50; } else { shipX+=d*5; if(shipX<0)shipX=0; if(shipX>120)shipX=120; } if(invOver) initInvaders(); } }
    if(g==6) { if(act=="start") initBreakout(); if(act=="input") { int d=server.arg("d").toInt(); if(d==-1)paddle1Y-=4; else paddle1Y+=4; } } 
    if(g==7) { if(act=="start") initFlappy(); if(act=="input") { dinoV = -3.5; } } 
    if(g==8) { if(act=="start") initMaze(); if(act=="input") { int d=server.arg("d").toInt(); if(d==0)mazeY-=2; if(d==1)mazeX+=2; if(d==2)mazeY+=2; if(d==3)mazeX-=2; } }
    if(g==9) { if(act=="start") initLander(); if(act=="input") { int d=server.arg("d").toInt(); if(d==0)landVY-=0.5; if(d==1)landVX+=0.5; if(d==3)landVX-=0.5; landFuel-=2; } }
    server.send(200, "text/plain", "OK");
  });
  server.on("/pongScore", [](){ server.send(200, "text/plain", String(p1Score)+" - "+String(p2Score)); });
  server.on("/bkScore", [](){ server.send(200, "text/plain", String(score)); });
  server.on("/fbScore", [](){ server.send(200, "text/plain", String(score)); });
  server.on("/dinoScore", [](){ server.send(200, "text/plain", String(dinoScore)); });
  server.on("/invScore", [](){ server.send(200, "text/plain", String(invScore)); });
  server.on("/ldFuel", [](){ server.send(200, "text/plain", String((int)landFuel)); });
  server.begin();
}
void loop() {
  server.handleClient();
  if(millis()-lastDHTRead>1000){float t=dht.readTemperature();if(!isnan(t))temp=t*10; 
     if(temp<10) temp+=20; 
  lastDHTRead=millis();}
  if(millis()-lastDistRead>60) {
    digitalWrite(PIN_TRIG, LOW); delayMicroseconds(2); digitalWrite(PIN_TRIG, HIGH); delayMicroseconds(10); digitalWrite(PIN_TRIG, LOW);
    long duration = pulseIn(PIN_ECHO, HIGH, 18000); 
    long new_dist = (duration>0) ? (duration*0.034/2) : 999;
    long delta = new_dist - prev_dist_cm;
    if(new_dist < 50 && delta < DIST_VELOCITY_TRIGGER && activeGame==0) { 
      if(currentMood != "PANIC") { currentMood="ANGRY"; manualOverrideTimer=millis()+2000; } 
    }
    prev_dist_cm = dist_cm;
    dist_cm = new_dist;
    lastDistRead=millis();
  }
  int rawGas = analogRead(PIN_MQ135);
  averageGas = (rawGas * 0.2) + (averageGas * 0.8);
  gasLevel = averageGas;
  if(millis() % 1000 == 0) {
    if(averageGas < gasBaseline) gasBaseline--; 
    else if(averageGas > gasBaseline + 100) gasBaseline++; 
  }
  bool pir = digitalRead(PIN_PIR);
  if(pir == HIGH) lastMotionTime = millis();
  if(isFocusMode) {
      if(millis() - focusStartTime > focusDuration) { isFocusMode = false; showBigText=true; overrideText="DONE!"; overrideTextTimer=millis()+5000; }
      else {
           display.clearDisplay();
           unsigned long timeElap = millis() - lastMotionTime;
           if(timeElap > DISTRACTION_THRESHOLD) {
                display.setTextSize(2); display.setCursor(10, 20); 
                int qIdx = (millis()/1000) % 8;
                display.print(txt_MOTIVATION[qIdx]);
                digitalWrite(PIN_BUZZER, (millis()%500 < 250) ? HIGH : LOW);
           } else {
                digitalWrite(PIN_BUZZER, LOW); 
                display.drawCircle(64, 32, 30, SH110X_WHITE); 
                float prog = (float)(millis() - focusStartTime) / (float)focusDuration;
                int fillH = (int)(60 * prog); 
                int waterLevel = 32 + 30 - fillH; 
                float phase = (millis() / 200.0); 
                for(int x = 64-28; x < 64+28; x++) {
                    int waveY = waterLevel + sin((x * 0.2) + phase) * 2; 
                    int R = 28;
                    int dx = x - 64; 
                    if(abs(dx) >= R) continue; 
                    int semiCircleH = sqrt(R*R - dx*dx);
                    int circleBottomY = 32 + semiCircleH;
                    int circleTopY = 32 - semiCircleH;
                    if(waveY < circleTopY) waveY = circleTopY;
                    if(waveY > circleBottomY) waveY = circleBottomY;
                    if(waveY < circleBottomY) {
                       display.drawFastVLine(x, waveY, circleBottomY - waveY, SH110X_WHITE);
                    }
                }
                if((millis()/50) % 4 == 0) {
                   int bx = random(64-20, 64+20);
                   int by = random(waterLevel, 32+25);
                   if(by > waterLevel + 4) display.drawPixel(bx, by, SH110X_INVERSE);
                }
                unsigned long remMs = focusDuration - (millis()-focusStartTime);
                int remM = remMs / 60000;
                int remS = (remMs % 60000) / 1000;
                display.setTextSize(1);
                display.setTextColor(SH110X_WHITE);
                display.setCursor(90, 54); 
                if(remM<10) display.print("0"); display.print(remM);
                display.print(":");
                if(remS<10) display.print("0"); display.print(remS);
           }
           display.display();
           delay(50);
           return; 
      }
  }
  if(!isSleeping && millis() - lastMotionTime > SLEEP_TIMEOUT && activeGame == 0) {
      isSleeping = true;
      currentMood = "SLEEP";
  }
  if(isSleeping) {
     display.clearDisplay(); display.display();
     if(pir == HIGH) {
        isSleeping = false;
        overrideText = "Hi Sir";
        showBigText = true;
        overrideTextTimer = millis() + 3000;
        currentMood = "HAPPY";
     }
     delay(50);
     return; 
  }
  if(activeGame==0 && millis()>manualOverrideTimer) {
     if(dist_cm < 18) { 
        currentMood = "PANIC"; 
     } 
     else if(dist_cm < 40) { 
        currentMood = "SUS";   
     } 
     else {
        currentMood = "HAPPY"; 
     }
  }
  static int breathDir = 1; static unsigned long lastBreath = 0;
  if(millis() - lastBreath > 150) { breathVal += breathDir; if(breathVal > 2) breathDir = -1; if(breathVal < 0) breathDir = 1; lastBreath = millis(); }
  if(activeGame == 0 && !isSleeping) {
     if(gasLevel > 2800) { 
        digitalWrite(PIN_BUZZER, (millis() % 200 < 100) ? HIGH : LOW);
     }
     else if(dist_cm < DIST_TOUCH) { 
        digitalWrite(PIN_BUZZER, (millis() % 100 < 50) ? HIGH : LOW); 
     }
     else {
        digitalWrite(PIN_BUZZER, LOW); 
     }
  }
  display.clearDisplay();
  if(activeGame==0) {
        unsigned long cycle = millis() % 37000; 
        if (cycle < 30000 || currentMood == "PANIC") {
            if(millis() < overrideTextTimer) {
               currentDisplayText = overrideText;
               if(showBigText) {
                   display.setTextSize(2);
                   display.setCursor(10, 20); 
                   display.print(currentDisplayText);
                   display.setTextSize(1);
                   display.display(); 
                   delay(50);
                   return; 
               }
            } 
            else if(millis() - lastTextUpdate > 3000) {
               showBigText = false; 
               lastTextUpdate = millis();
               if(random(0,100) < 30) {
                  int r = random(0,3);
                  if(r==0) currentDisplayText = "It's " + String(temp,1) + "C in here";
                  else if(r==1) currentDisplayText = String("Air looks ") + (gasLevel<2000 ? "Clean" : "Kinda sus");
                  else currentDisplayText = String("Object at ") + (dist_cm>900 ? "far far away" : String(dist_cm)+"cm");
               } else {
                  int idx = random(0, 8);
                  if(currentMood == "HAPPY") currentDisplayText = txt_HAPPY[idx];
                  else if(currentMood == "SUS") {
                     static int infoState = 0;
                     if(infoState == 0) currentDisplayText = "Wifi: " + String(ssid);
                     else if(infoState == 1) currentDisplayText = "IP: 192.168.4.1";
                     else currentDisplayText = "Open Browser & Play!";
                     infoState = (infoState + 1) % 3;
                  }
                  else if(currentMood == "PANIC") currentDisplayText = txt_PANIC[idx];
                  else if(currentMood == "ANGRY") currentDisplayText = txt_ANGRY[idx];
                  else if(currentMood == "SWEAT") currentDisplayText = txt_SWEAT[idx];
                  else if(currentMood == "LOVE") currentDisplayText = txt_LOVE[idx];
                  else if(currentMood == "CONFUSED") currentDisplayText = txt_CONFUSED[idx];
                  else currentDisplayText = txt_HAPPY[idx];
               }
            }
            display.setCursor(0,0); display.setTextSize(1); display.print(currentDisplayText);
            drawFace();
        } 
        else {
            drawClock();
        }
  } else if(activeGame==1) drawTTT(); else if(activeGame==2) {updateSnake();drawSnake();} else if(activeGame==3) {updatePong();drawPong();}
    else if(activeGame==4) {updateDino();drawDino();} else if(activeGame==5) {updateInvaders();drawInvaders();}
    else if(activeGame==6) {updateBreakout();drawBreakout();} else if(activeGame==7) {updateFlappy();drawFlappy();}
    else if(activeGame==8) {drawMaze();} else if(activeGame==9) {updateLander();drawLander();}
  display.display(); delay(50); 
}

void drawFace() { 
  if(currentMood=="HAPPY") drawHappy();
  else if(currentMood=="PANIC") drawPanic();
  else if(currentMood=="SUS") drawSus();
  else if(currentMood=="SWEAT") drawSweat();
  else if(currentMood=="ANGRY") drawAngry();
  else if(currentMood=="LOVE") drawLove();
  else if(currentMood=="CONFUSED") drawConfused();
  else drawHappy(); 
}
void drawEyeMetric(int cx, int cy, int r, float pupil, int lx, int ly, float lidTopP, float lidBotP, EyeLidShape shape, bool glasses) {
  display.fillCircle(cx, cy, r, SH110X_WHITE);
  int irisR = r - 3;
  int ix = cx + lx; int iy = cy + ly;
  display.fillCircle(ix, iy, irisR, SH110X_BLACK);
  display.drawCircle(ix, iy, irisR-2, SH110X_WHITE);
  if(pupil < 1) pupil = 1;
  display.fillCircle(ix, iy, (int)pupil, SH110X_BLACK);
  display.fillCircle(ix + irisR/3, iy - irisR/3, 2, SH110X_WHITE);
  if(lidTopP > 0) {
     int h = (int)(r * 2 * lidTopP);
     int topY = cy - r - 2;
     if(shape == LID_ANGRY) {
         int yOffset = (cx < 64) ? 5 : -5; 
         bool isLeft = (cx < 64);
         int inY = topY + h + 4; int outY = topY + h - 2;
         if(!isLeft) { int t=inY; inY=outY; outY=t; } 
         if(isLeft) display.fillTriangle(cx-r-2, topY+h-3, cx+r+2, topY+h+5, cx-r, cy-r-5, SH110X_BLACK);
         else       display.fillTriangle(cx-r-2, topY+h+5, cx+r+2, topY+h-3, cx+r, cy-r-5, SH110X_BLACK);
     } else if (shape == LID_SAD) {
         bool isLeft = (cx < 64);
         if(isLeft) display.fillTriangle(cx-r-2, topY+h+4, cx+r+2, topY+h-3, cx, cy-r-5, SH110X_BLACK); 
         else       display.fillTriangle(cx-r-2, topY+h-3, cx+r+2, topY+h+4, cx, cy-r-5, SH110X_BLACK);
     } else {
         display.fillRect(cx - r - 2, topY, r*2 + 4, h, SH110X_BLACK);
     }
  }
  if(lidBotP > 0) {
     int h = (int)(r * 2 * lidBotP);
     display.fillRect(cx - r - 2, cy + r - h + 2, r*2 + 4, h, SH110X_BLACK);
  }
  if(glasses) {
     display.drawRect(cx - r - 4, cy - r - 4, r*2 + 8, r*2 + 8, SH110X_WHITE);
     if(cx < 64) display.drawLine(cx+r+4, cy, cx+r+10, cy, SH110X_WHITE); 
  }
  if(shape == LID_TIRED) {
      display.drawLine(cx-r, cy+r+2, cx+r, cy+r+2, SH110X_WHITE);
  }
}
void drawEyeV2(int cx, int cy, int r, float p, int lx, int ly, float closePercent, bool angryBrow) {
  EyeLidShape s = angryBrow ? LID_ANGRY : LID_FLAT;
  drawEyeMetric(cx, cy, r, p, lx, ly, closePercent, 0, s, false);
}
void drawHappy() {
  if(millis() - lastBlink > 3500 + random(0, 2000)) {
     eyesOpen = false;
     if(millis() - lastBlink > 3650 + random(0, 2000)) { lastBlink = millis(); eyesOpen = true; }
  }
  float blink = eyesOpen ? 0.0 : 1.0;
  static int lookX = 0; static int lookY = 0;
  static unsigned long nextGazeMove = 0;
  if(millis() > nextGazeMove) {
     if(random(0,10) < 6) { lookX = random(-5,6); lookY = random(-3,4); } 
     else { lookX = 0; lookY = 0; } 
     nextGazeMove = millis() + random(1000, 4000); 
  }
  static float curX = 0; static float curY = 0;
  curX += (lookX - curX) * 0.1; 
  curY += (lookY - curY) * 0.1;
  static bool singing = false; static unsigned long singTimer = 0;
  if(millis() > singTimer) {
     singing = !singing;
     singTimer = millis() + (singing ? random(2000, 5000) : random(3000, 8000));
  }
  int mouthH = 0;
  if(singing) {
     float t = millis() / 150.0;
     mouthH = abs(sin(t) * 5) + random(0, 3);
  }
  pupilSize += (3.5 - pupilSize) * 0.1; 
  int by = breathVal;
  drawEyeV2(35+(int)curX, 25+by+(int)curY, 12, pupilSize, (int)curX, (int)curY, blink, false);
  drawEyeV2(93+(int)curX, 25+by+(int)curY, 12, pupilSize, (int)curX, (int)curY, blink, false);
  int mx = 64; int my = 45+by;
  if(mouthH == 0) {
      display.drawLine(mx-10, my, mx-5, my+3, SH110X_WHITE); 
      display.drawLine(mx-5, my+3, mx+5, my+3, SH110X_WHITE); 
      display.drawLine(mx+5, my+3, mx+10, my, SH110X_WHITE);
  } else {
      display.drawRect(mx-6, my, 12, mouthH, SH110X_WHITE);
      display.fillRect(mx-4, my+1, 8, mouthH-2, SH110X_BLACK); 
  }
}
void drawPanic() {
  pupilSize = 2.0; 
  int shiver = random(-1,2);
  drawEyeV2(35+shiver, 25, 14, 1, 0, 0, 0.0, false);
  drawEyeV2(93+shiver, 25, 14, 1, 0, 0, 0.0, false);
  display.drawRect(54+shiver, 45, 20, 10, SH110X_WHITE); display.fillRect(57+shiver, 47, 14, 6, SH110X_BLACK);
}
void drawSus() {
  static int lx = 6;
  drawEyeV2(35, 25, 12, 3, lx, 0, 0.6, true);
  drawEyeV2(93, 25, 12, 3, lx, 0, 0.6, true);
  display.drawLine(54, 50, 74, 50, SH110X_WHITE);
}
void drawAngry() {
  drawEyeV2(35, 25, 12, 5, 0, 0, 0.2, true);
  drawEyeV2(93, 25, 12, 5, 0, 0, 0.2, true);
  display.drawTriangle(54, 55, 64, 48, 74, 55, SH110X_WHITE);
}
void drawLove() {
  int cy = 25+breathVal;
  display.fillCircle(30, cy, 6, SH110X_WHITE); display.fillCircle(40, cy, 6, SH110X_WHITE); display.fillTriangle(24, cy+2, 46, cy+2, 35, cy+14, SH110X_WHITE);
  display.fillCircle(88, cy, 6, SH110X_WHITE); display.fillCircle(98, cy, 6, SH110X_WHITE); display.fillTriangle(82, cy+2, 104, cy+2, 93, cy+14, SH110X_WHITE);
  display.fillCircle(64, 48+breathVal, 4, SH110X_WHITE);
}
void drawConfused() {
  drawEyeV2(35, 25, 14, 3, 0, -2, 0.0, false); 
  drawEyeV2(93, 25, 10, 3, 0, 2, 0.5, false); 
  int y = 50; display.drawLine(54, y, 59, y-3, SH110X_WHITE); display.drawLine(59, y-3, 64, y+3, SH110X_WHITE); display.drawLine(64, y+3, 69, y-3, SH110X_WHITE); display.drawLine(69, y-3, 74, y, SH110X_WHITE);
}
void drawSweat() {
  static int dropY = 15; dropY += 2; if(dropY > 40) dropY = 15;
  drawEyeV2(35, 27, 12, 3, 0, 2, 0.4, false); drawEyeV2(93, 27, 12, 3, 0, 2, 0.4, false);
  display.fillCircle(110, dropY, 3, SH110X_WHITE); display.fillTriangle(107, dropY, 113, dropY, 110, dropY-5, SH110X_WHITE);
  display.drawLine(58, 52, 70, 52, SH110X_WHITE);
}
void drawTTT() { int sx=32;int w=64;int step=21;display.drawLine(sx+step,0,sx+step,63,SH110X_WHITE);display.drawLine(sx+step*2,0,sx+step*2,63,SH110X_WHITE);display.drawLine(sx,step,sx+w,step,SH110X_WHITE);display.drawLine(sx,step*2,sx+w,step*2,SH110X_WHITE);for(int i=0;i<9;i++){if(ttt_board[i]==0)continue;int r=i/3;int c=i%3;int cx=sx+c*step+10;int cy=r*step+10;if(ttt_board[i]==1){display.drawLine(cx-4,cy-4,cx+4,cy+4,SH110X_WHITE);display.drawLine(cx+4,cy-4,cx-4,cy+4,SH110X_WHITE);}else display.drawCircle(cx,cy,5,SH110X_WHITE);} }
int checkTTTWin() { 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(int i=0;i<8;i++){int a=w[i][0],b=w[i][1],c=w[i][2];if(ttt_board[a]!=0&&ttt_board[a]==ttt_board[b]&&ttt_board[a]==ttt_board[c])return ttt_board[a];} return 0; }
void updateSnake() { if(snakeGameOver||millis()-lastSnakeMove<150)return; lastSnakeMove=millis(); for(int i=snakeLen-1;i>0;i--){snakeX[i]=snakeX[i-1];snakeY[i]=snakeY[i-1];} if(snakeDir==0)snakeY[0]-=4; if(snakeDir==1)snakeX[0]+=4; if(snakeDir==2)snakeY[0]+=4; if(snakeDir==3)snakeX[0]-=4; if(snakeX[0]<0||snakeX[0]>124||snakeY[0]<0||snakeY[0]>60){snakeGameOver=true;digitalWrite(PIN_BUZZER,HIGH);delay(100);digitalWrite(PIN_BUZZER,LOW);} for(int i=1;i<snakeLen;i++)if(snakeX[0]==snakeX[i]&&snakeY[0]==snakeY[i])snakeGameOver=true; if(abs(snakeX[0]-foodX)<4&&abs(snakeY[0]-foodY)<4){snakeLen++;score+=10;if(snakeLen>=SNAKE_MAX_LEN)snakeLen=SNAKE_MAX_LEN-1;foodX=random(1,30)*4;foodY=random(1,15)*4;digitalWrite(PIN_BUZZER,HIGH);delay(20);digitalWrite(PIN_BUZZER,LOW);} }
void drawSnake() { display.drawRect(0,0,128,64,SH110X_WHITE); if(snakeGameOver){display.setCursor(35,25);display.setTextSize(1);display.print("GAME OVER");display.setCursor(40,35);display.print("SCORE:");display.print(score);return;} for(int i=0;i<snakeLen;i++)display.fillRect(snakeX[i],snakeY[i],3,3,SH110X_WHITE); display.drawCircle(foodX+1,foodY+1,2,SH110X_WHITE); }
void initSnake() { snakeLen=3; score=0; snakeDir=1; snakeGameOver=false; snakeX[0]=30; snakeY[0]=30; snakeX[1]=26; snakeY[1]=30; snakeX[2]=22; snakeY[2]=30; foodX=random(1,30)*4; foodY=random(1,15)*4; }
void initPong() { ballX=64;ballY=32;ballVX=2.5;ballVY=2.5;p1Score=0;p2Score=0;pongGameOver=false;paddle1Y=24;paddle2Y=24; }
void updatePong() { 
  if(pongGameOver)return; ballX+=ballVX;ballY+=ballVY; 
  if(ballY<=0||ballY>=64)ballVY*=-1; 
  if(ballX<=5){if(abs(ballY-(paddle1Y+8))<12){ballVX*=-1;ballX=6;digitalWrite(PIN_BUZZER,HIGH);delay(10);digitalWrite(PIN_BUZZER,LOW);}else{p2Score++;ballX=64;ballY=32;ballVX=2;}} 
  if(ballY > paddle2Y + 6) paddle2Y += 2;
  else if(ballY < paddle2Y + 6) paddle2Y -= 2;
  if(ballX>=118){if(abs(ballY-(paddle2Y+8))<12){ballVX*=-1;ballX=117;digitalWrite(PIN_BUZZER,HIGH);delay(10);digitalWrite(PIN_BUZZER,LOW);}else{p1Score++;ballX=64;ballY=32;ballVX=-2;}} 
  if(p1Score>4 || p2Score>4) pongGameOver=true;
}
void drawPong() { 
  if(pongGameOver){display.setCursor(35,25);display.print("GAME OVER");display.setCursor(20,35);display.print("P1: "+String(p1Score)+" P2: "+String(p2Score));return;} 
  display.drawRect(0,0,128,64,SH110X_WHITE); for(int i=0;i<64;i+=4)display.drawPixel(64,i,SH110X_WHITE); 
  display.fillRect(2,paddle1Y,3,16,SH110X_WHITE); display.fillRect(123,paddle2Y,3,16,SH110X_WHITE); 
  display.fillCircle((int)ballX,(int)ballY,2,SH110X_WHITE); 
}
void initDino() { dinoY=50; dinoV=0; dinoGrounded=true; cactusX=128; dinoScore=0; dinoOver=false; }
void updateDino() {
  if(dinoOver) return;
  dinoY += (int)dinoV;
  dinoV += 0.6; 
  if(dinoY >= 50) { dinoY=50; dinoV=0; dinoGrounded=true; }
  cactusX -= 4; 
  if(cactusX < -10) { cactusX = 128 + random(0,40); dinoScore++; digitalWrite(PIN_BUZZER,HIGH); delay(10); digitalWrite(PIN_BUZZER,LOW); }
  if(abs(10-cactusX)<5 && dinoY>40) { dinoOver=true; digitalWrite(PIN_BUZZER,HIGH); delay(500); digitalWrite(PIN_BUZZER,LOW); }
}
void drawDino() {
  if(dinoOver) { display.setCursor(40,30); display.print("GAME OVER"); display.setCursor(45,40); display.print(dinoScore); return; }
  display.drawLine(0,60,128,60,SH110X_WHITE); 
  display.fillRect(10, dinoY, 8, 10, SH110X_WHITE); 
  display.fillRect(cactusX, 52, 6, 8, SH110X_WHITE); 
  display.setCursor(110,0); display.print(dinoScore);
}
void initInvaders() { 
  shipX=64; bulletX=-1; bulletY=-1; invScore=0; invOver=false; invDir=1;
  for(int i=0;i<5;i++) { enemyX[i]=10 + i*20; enemyY[i]=5; enemyAlive[i]=true; }
}
void updateInvaders() {
  if(invOver) return;
  if(bulletY != -1) {
    bulletY -= 5;
    if(bulletY < 0) bulletY = -1;
    for(int i=0;i<5;i++) {
      if(enemyAlive[i] && abs(bulletX-enemyX[i])<6 && abs(bulletY-enemyY[i])<6) {
        enemyAlive[i]=false; bulletY=-1; invScore++; digitalWrite(PIN_BUZZER,HIGH); delay(10); digitalWrite(PIN_BUZZER,LOW); 
      }
    }
  }
  if(millis() - lastInvMove > 200) {
    bool drop = false;
    for(int i=0;i<5;i++) {
       if(enemyAlive[i]) {
          enemyX[i] += invDir * 4;
          if(enemyX[i] > 120 || enemyX[i] < 5) drop=true;
       }
    }
    if(drop) { invDir *= -1; for(int i=0;i<5;i++) enemyY[i]+=4; }
    lastInvMove=millis();
    bool allDead = true; for(int i=0;i<5;i++) if(enemyAlive[i]) allDead=false;
    if(allDead) { for(int i=0;i<5;i++) { enemyX[i]=10 + i*20; enemyY[i]=5; enemyAlive[i]=true; } }
    for(int i=0;i<5;i++) if(enemyAlive[i] && enemyY[i] > 50) invOver=true;
  }
}
void drawInvaders() {
  if(invOver) { display.setCursor(40,30); display.print("GAME OVER"); return; }
  display.fillTriangle(shipX, 60, shipX-4, 63, shipX+4, 63, SH110X_WHITE);
  if(bulletY != -1) display.drawLine(bulletX, bulletY, bulletX, bulletY+3, SH110X_WHITE);
  for(int i=0;i<5;i++) {
    if(enemyAlive[i]) {
      display.fillRect(enemyX[i]-3, enemyY[i]-3, 6, 6, SH110X_WHITE);
      if((millis()/500)%2==0) { display.drawPixel(enemyX[i]-2,enemyY[i]+4,SH110X_WHITE); display.drawPixel(enemyX[i]+2,enemyY[i]+4,SH110X_WHITE); }
      else { display.drawPixel(enemyX[i]-1,enemyY[i]+4,SH110X_WHITE); display.drawPixel(enemyX[i]+1,enemyY[i]+4,SH110X_WHITE); }
    }
  }
}
void initBreakout() { 
  paddle1Y=50; 
  bBallX=64; bBallY=40; bVelX=2; bVelY=-2; score=0; 
  for(int r=0;r<5;r++) for(int c=0;c<10;c++) brk[r][c]=true;
}
void updateBreakout() {
  if(score==50) return;
  bBallX += bVelX; bBallY += bVelY;
  if(bBallX<=0 || bBallX>=128) bVelX*=-1;
  if(bBallY<=0) bVelY*=-1;
  if(bBallY>=64) { initBreakout(); score=0; } 
  if(bBallY>=60 && bBallY<=62 && bBallX>=paddle1Y && bBallX<=paddle1Y+20) {
      bVelY*=-1; float hitPt = bBallX - (paddle1Y+10); bVelX = hitPt * 0.3; 
      digitalWrite(PIN_BUZZER,HIGH); delay(10); digitalWrite(PIN_BUZZER,LOW);
  }
  int brC = bBallX / 13; int brR = bBallY / 6;
  if(brR >=0 && brR < 5 && brC >= 0 && brC < 10) {
     if(brk[brR][brC]) {
         brk[brR][brC]=false; score++; bVelY*=-1;
         digitalWrite(PIN_BUZZER,HIGH); delay(10); digitalWrite(PIN_BUZZER,LOW);
     }
  }
}
void drawBreakout() {
  if(score==50) { display.setCursor(40,30); display.print("WINNER!"); return; }
  display.fillRect(paddle1Y, 61, 20, 3, SH110X_WHITE);
  display.fillCircle((int)bBallX, (int)bBallY, 2, SH110X_WHITE);
  for(int r=0;r<5;r++) for(int c=0;c<10;c++) {
     if(brk[r][c]) display.fillRect(c*13, r*6, 12, 5, SH110X_WHITE);
  }
  display.setCursor(0,0); display.print(score);
}
void initFlappy() { dinoY=32; dinoV=0; pipeX=128; pipeGap=20; score=0; dinoOver=false; }
void updateFlappy() {
  if(dinoOver) { if(millis()-lastInvMove>1000) initFlappy(); return; } 
  dinoV += 0.4; 
  dinoY += (int)dinoV;
  pipeX -= 2;
  if(pipeX < -10) { pipeX=128; pipeGap = random(10, 40); score++; digitalWrite(PIN_BUZZER,HIGH); delay(20); digitalWrite(PIN_BUZZER,LOW); }
  if(dinoY>64 || dinoY<0) dinoOver=true;
  if(pipeX<14 && pipeX>0) {
      if(dinoY < pipeGap || dinoY > pipeGap+25) { 
         dinoOver=true; digitalWrite(PIN_BUZZER,HIGH); delay(200); digitalWrite(PIN_BUZZER,LOW); lastInvMove=millis(); 
      }
  }
}
void drawFlappy() {
  if(dinoOver) { display.setCursor(40,30); display.print("GAME OVER"); display.setCursor(55,40); display.print(score); return; }
  display.fillCircle(10, dinoY, 3, SH110X_WHITE);
  display.fillRect(pipeX, 0, 10, pipeGap, SH110X_WHITE); 
  display.fillRect(pipeX, pipeGap+25, 10, 64-(pipeGap+25), SH110X_WHITE); 
  display.setCursor(110,0); display.print(score);
}
void initMaze() { mazeX=2; mazeY=2; }
void drawMaze() {
   for(int x=0;x<128;x+=16) display.drawLine(x,0,x,64,SH110X_WHITE);
   for(int y=0;y<64;y+=16) display.drawLine(0,y,128,y,SH110X_WHITE);
   for(int x=0;x<8;x++) for(int y=0;y<4;y++) {
      if(((x+y)%3)==0) display.fillRect(x*16+2, y*16+2, 12, 12, SH110X_WHITE);
   }
   display.fillCircle(mazeX, mazeY, 2, SH110X_INVERSE);
   display.setTextSize(1); display.setCursor(120,55); display.print("G");
   if(abs(mazeX-120)<5 && abs(mazeY-55)<5) { display.setCursor(40,30); display.print("WIN!"); }
}
void initLander() { landX=64; landY=10; landVX=0; landVY=0; landFuel=100; landCrashed=false; landLanded=false; }
void updateLander() {
  if(landCrashed || landLanded) return;
  landVY += 0.05; 
  landX += landVX; landY += landVY;
  if(landY >= 60) {
      if(landVY > 1.5 || abs(landVX) > 1.0) { landCrashed=true; digitalWrite(PIN_BUZZER,HIGH); delay(500); digitalWrite(PIN_BUZZER,LOW); }
      else { landLanded=true; }
      landY = 60; landVY=0; landVX=0;
  }
}
void drawLander() {
  if(landCrashed) { display.setCursor(40,30); display.print("CRASH!"); return; }
  if(landLanded) { display.setCursor(40,30); display.print("LANDED!"); return; }
  display.drawLine(0,63,128,63,SH110X_WHITE);
  display.drawRect(50, 63, 28, 1, SH110X_WHITE); 
  display.drawRect((int)landX-3, (int)landY-3, 6, 6, SH110X_WHITE);
  display.drawLine((int)landX-3, (int)landY+3, (int)landX-5, (int)landY+6, SH110X_WHITE); 
  display.drawLine((int)landX+3, (int)landY+3, (int)landX+5, (int)landY+6, SH110X_WHITE);
  display.setCursor(0,0); display.print("F:"); display.print((int)landFuel);
  display.setCursor(100,0); display.print("v:"); display.print(landVY);
}
void drawClock() {
  unsigned long currentUnix = unixTime + (millis() - lastSyncMillis) / 1000;
  int h_raw = (currentUnix / 3600) % 24;
  int m = (currentUnix / 60) % 60;
  int s = (currentUnix) % 60;
  bool isPm = (h_raw >= 12);
  int h = h_raw % 12;
  if(h == 0) h = 12;
  for(int x=0; x<128; x+=8) for(int y=0; y<64; y+=8) display.drawPixel(x,y,SH110X_WHITE);
  display.drawRect(0, 0, 128, 64, SH110X_WHITE);
  display.fillRect(0, 0, 128, 11, SH110X_WHITE); 
  display.setTextColor(SH110X_BLACK);
  display.setTextSize(1);
  display.setCursor(4, 2);
  display.print(timeSynced ? "SYNC" : "NC");
  display.setCursor(85, 2);
  display.print(String(temp,0) + "C");
  display.setTextColor(SH110X_WHITE);
  display.setTextSize(4);
  int tx = 8; int ty = 18;
  display.setCursor(tx, ty);
  if(h < 10) display.print("0"); display.print(h);
  display.print(":");
  if(m < 10) display.print("0"); display.print(m);
  display.setTextSize(1);
  display.setCursor(110, 40);
  display.print(isPm ? "PM" : "AM");
  int barW = map(s, 0, 59, 0, 120);
  display.drawRect(4, 54, 120, 6, SH110X_WHITE);
  display.fillRect(4, 54, barW, 6, SH110X_WHITE);
  display.drawLine(0,0,5,0,SH110X_WHITE); display.drawLine(0,0,0,5,SH110X_WHITE);
}