LightChase3

/*
  Processing Sketch — Light Chase Game Dashboard

  Screen layout left to right:  Node 1 | Teacher | Node 2
  Node IDs used in serial:       1      |    0    |    2

  screenPos[] maps left-to-right screen position to node ID:
    screenPos[0] = 1  (left   = Node 1)
    screenPos[1] = 0  (center = Teacher)
    screenPos[2] = 2  (right  = Node 2)

  Arrow keys move by screen position, then convert to node ID.

  Keyboard:
    LEFT / RIGHT  → move between panels left/right
    UP / DOWN     → move between top / bottom LED
    R             → restart after shutdown

  Mouse:
    Click any LED circle to move the light there.
    Click RESTART button when game is shut down.

  Serial FROM Arduino:
    LIGHT:node,pos          light moved here
    NODE:id,HALL:val        hall sensor state
    NODE:id,STATUS:1        node came online
    EVENT:TIMEOUT_MOVE:n    timeout move n of 2
    EVENT:SHUTDOWN          game shut down
    STATUS:MASTER_READY     master booted
    STATUS:RESTARTED        game restarted

  Serial TO Arduino:
    MOVE:node,pos\n
    RESTART\n
*/

import processing.serial.*;

Serial port;

// ── Screen position → node ID mapping ────────────────────────
// Left=0, Center=1, Right=2  (screen positions)
// Maps to node IDs: Node1=1, Teacher=0, Node2=2
int[] screenPos = {1, 0, 2};   // screenPos[screenCol] = nodeID

// Reverse: nodeID → screenCol
int nodeToCol(int nodeID) {
  for (int i = 0; i < 3; i++) if (screenPos[i] == nodeID) return i;
  return 1;
}

// ── Game state ────────────────────────────────────────────────
int     lightNode  = -1;
int     lightPos   = -1;
int     moveCount  = 0;
boolean gameActive = false;
boolean shutdown   = false;

boolean[] hallActive = {false, false, false};  // indexed by nodeID
boolean[] nodeOnline = {true,  false, false};  // indexed by nodeID

// ── Event log ─────────────────────────────────────────────────
ArrayList<String> eventLog = new ArrayList<String>();
int maxLog = 8;

// ── Layout ────────────────────────────────────────────────────
int panelW  = 190;
int panelH  = 300;
int panelY  = 90;
// Panel x positions by screen column (0=left, 1=center, 2=right)
int[] panelX = {30, 295, 560};

int ledCenterX = 95;
int topLedY    = 130;
int botLedY    = 230;
int ledR       = 32;

int restartBtnX = 300, restartBtnY = 420, restartBtnW = 200, restartBtnH = 38;

// ── Colors ────────────────────────────────────────────────────
color bgColor       = color(15, 15, 22);
color panelBg       = color(26, 26, 38);
color ledOff        = color(40, 40, 58);
color ledOn         = color(255, 210, 50);
color ledGlowCol    = color(255, 230, 100, 70);
color hallGreen     = color(60, 200, 110);
color textMain      = color(215, 215, 235);
color textDim       = color(110, 110, 145);
color teacherAccent = color(80,  130, 220);
color node1Accent   = color(60,  200, 160);
color node2Accent   = color(200, 90,  165);
color warnColor     = color(230, 140,  40);
color shutdownColor = color(220,  60,  60);

// ── Setup ─────────────────────────────────────────────────────
void setup() {
  size(800, 530);
  smooth();

  // Search for Teacher board — avoids hardcoded index
  String targetPort = null;
  String[] ports = Serial.list();
  println("=== Available serial ports ===");
  for (int i = 0; i < ports.length; i++) {
    println("[" + i + "] " + ports[i]);
    if ((ports[i].contains("usbmodem") || ports[i].contains("usbserial") || ports[i].contains("COM"))
        && !ports[i].contains("Bluetooth") && !ports[i].contains("Cricut")) {
      targetPort = ports[i];
    }
  }
  println("==============================");

  if (targetPort != null) {
    println("Connecting to: " + targetPort);
    try {
      port = new Serial(this, targetPort, 115200);
      port.bufferUntil('\n');
      println("Connected OK");
      // Ask Teacher to resend its current state after a short delay
      delay(500);
      port.write("STATUS\n");
    } catch (Exception e) {
      println("ERROR: " + e.getMessage());
      println("Close Arduino Serial Monitor and restart Processing.");
    }
  } else {
    println("ERROR: Teacher board not found. Check USB and close Serial Monitor.");
  }
}

// ── Draw ──────────────────────────────────────────────────────
void draw() {
  background(bgColor);
  drawTitle();

  // Draw panels in screen order: col 0, 1, 2
  drawNode(screenPos[0], "NODE 1",  node1Accent,   panelX[0]);
  drawNode(screenPos[1], "TEACHER", teacherAccent, panelX[1]);
  drawNode(screenPos[2], "NODE 2",  node2Accent,   panelX[2]);

  drawMoveCounter();
  drawEventLog();
  if (shutdown) drawShutdownOverlay();
}

void drawTitle() {
  fill(textMain);
  textAlign(CENTER);
  textSize(20);
  text("Light Chase  —  ESP-NOW Dashboard", width / 2, 44);
  textSize(12);
  fill(textDim);
  text("Click LED · Arrow keys to move · Hall sensor resets 3s timer · ignored twice = shutdown", width / 2, 65);
}

void drawNode(int nodeID, String label, color accent, int px) {
  boolean online = nodeOnline[nodeID];
  boolean warn   = !shutdown && gameActive && moveCount > 0;

  fill(panelBg);
  stroke(shutdown ? shutdownColor : (warn ? warnColor : accent));
  strokeWeight(online ? (shutdown ? 1.5 : 2) : 0.8);
  rect(px, panelY, panelW, panelH, 12);

  noStroke();
  fill(shutdown ? shutdownColor : accent);
  textAlign(CENTER);
  textSize(14);
  text(label, px + ledCenterX, panelY + 28);

  fill(hallActive[nodeID] ? hallGreen : color(45, 55, 50));
  ellipse(px + ledCenterX, panelY + 52, 12, 12);
  fill(hallActive[nodeID] ? hallGreen : textDim);
  textSize(10);
  text("HALL", px + ledCenterX, panelY + 68);

  // TOP LED — Teacher uses GPIO9, nodes use GPIO9 top / GPIO2 bottom
  String topPin = (nodeID == 0) ? "GPIO 9" : "GPIO 9";
  String botPin = (nodeID == 0) ? "GPIO 8" : "GPIO 2";

  drawLed(nodeID, 0, px + ledCenterX, panelY + topLedY, accent, topPin, "TOP");
  drawLed(nodeID, 1, px + ledCenterX, panelY + botLedY, accent, botPin, "BOT");

  if (!online) {
    fill(0, 0, 0, 90);
    noStroke();
    rect(px, panelY, panelW, panelH, 12);
    fill(textDim);
    textAlign(CENTER);
    textSize(12);
    text("waiting...", px + ledCenterX, panelY + panelH / 2);
  }
}

void drawLed(int node, int pos, int cx, int cy, color accent, String pinLabel, String posLabel) {
  boolean active  = (lightNode == node && lightPos == pos && !shutdown);
  boolean hovered = !shutdown && isLedHovered(node, pos);

  if (active) {
    noStroke();
    fill(ledGlowCol);
    ellipse(cx, cy, ledR * 2 + 18, ledR * 2 + 18);
  }

  stroke(active ? ledOn : (hovered ? accent : color(55, 55, 75)));
  strokeWeight(active ? 2.5 : 1.5);
  fill(active ? ledOn : (hovered ? color(50, 52, 72) : ledOff));
  ellipse(cx, cy, ledR * 2, ledR * 2);

  noStroke();
  fill(active ? color(25, 22, 5) : textDim);
  textAlign(CENTER);
  textSize(10);
  text(posLabel, cx, cy - 4);
  text(pinLabel, cx, cy + 9);
}

void drawMoveCounter() {
  if (shutdown) return;
  int cx = width / 2;
  int y  = panelY + panelH + 22;

  textAlign(CENTER);
  textSize(12);
  fill(textDim);
  text("timeout moves used:", cx, y);

  for (int i = 0; i < 2; i++) {
    color c = i < moveCount ? (moveCount >= 2 ? shutdownColor : warnColor) : color(50, 50, 68);
    fill(c);
    noStroke();
    ellipse(cx - 16 + i * 32, y + 18, 18, 18);
  }

  textSize(11);
  fill(moveCount == 0 ? textDim : (moveCount == 1 ? warnColor : shutdownColor));
  String msg = moveCount == 0 ? "active" : (moveCount == 1 ? "1 warning — 1 left" : "shutting down...");
  text(msg, cx, y + 36);
}

void drawEventLog() {
  int lx = 20;
  int ly = panelY + panelH + 80;
  fill(textDim);
  textAlign(LEFT);
  textSize(11);
  text("Event log:", lx, ly);
  ly += 16;
  int start = max(0, eventLog.size() - maxLog);
  for (int i = eventLog.size() - 1; i >= start; i--) {
    String e = eventLog.get(i);
    if      (e.contains("SHUTDOWN"))     fill(shutdownColor);
    else if (e.contains("TIMEOUT_MOVE")) fill(warnColor);
    else if (e.contains("HALL:1"))       fill(hallGreen);
    else                                 fill(textDim);
    text(e, lx, ly);
    ly += 15;
  }
}

void drawShutdownOverlay() {
  fill(0, 0, 0, 140);
  noStroke();
  rect(0, panelY - 10, width, panelH + 20);

  fill(shutdownColor);
  textAlign(CENTER);
  textSize(26);
  text("GAME OFF", width / 2, panelY + panelH / 2 - 10);
  textSize(13);
  fill(textDim);
  text("No hall sensor detected for 9 seconds", width / 2, panelY + panelH / 2 + 18);

  boolean hov = mouseX > restartBtnX && mouseX < restartBtnX + restartBtnW &&
                mouseY > restartBtnY && mouseY < restartBtnY + restartBtnH;
  fill(hov ? color(60, 180, 100) : color(40, 140, 75));
  noStroke();
  rect(restartBtnX, restartBtnY, restartBtnW, restartBtnH, 8);
  fill(255);
  textAlign(CENTER);
  textSize(14);
  text("RESTART  (or press R)", restartBtnX + restartBtnW / 2, restartBtnY + 24);
}

// ── Hit testing — uses nodeToCol to find the panel x ─────────
int ledGlobalX(int node) { return panelX[nodeToCol(node)] + ledCenterX; }
int ledGlobalY(int pos)  { return panelY + (pos == 0 ? topLedY : botLedY); }

boolean isLedHovered(int node, int pos) {
  return dist(mouseX, mouseY, ledGlobalX(node), ledGlobalY(pos)) <= ledR;
}

// ── Mouse ─────────────────────────────────────────────────────
void mouseMoved() {
  if (shutdown) { cursor(ARROW); return; }
  for (int n = 0; n <= 2; n++)
    for (int p = 0; p <= 1; p++)
      if (isLedHovered(n, p)) { cursor(HAND); return; }
  cursor(ARROW);
}

void mousePressed() {
  if (shutdown) {
    boolean overBtn = mouseX > restartBtnX && mouseX < restartBtnX + restartBtnW &&
                      mouseY > restartBtnY && mouseY < restartBtnY + restartBtnH;
    if (overBtn) sendRestart();
    return;
  }
  for (int n = 0; n <= 2; n++)
    for (int p = 0; p <= 1; p++)
      if (isLedHovered(n, p)) { sendMove(n, p); return; }
}

// ── Keyboard ──────────────────────────────────────────────────
void keyPressed() {
  if (shutdown) {
    if (key == 'r' || key == 'R') sendRestart();
    return;
  }
  if (lightNode < 0) return;

  int currentCol = nodeToCol(lightNode);  // current screen column (0,1,2)

  if (keyCode == LEFT) {
    int newCol = max(0, currentCol - 1);
    sendMove(screenPos[newCol], lightPos);
  } else if (keyCode == RIGHT) {
    int newCol = min(2, currentCol + 1);
    sendMove(screenPos[newCol], lightPos);
  } else if (keyCode == UP) {
    sendMove(lightNode, 0);
  } else if (keyCode == DOWN) {
    sendMove(lightNode, 1);
  }
}

// ── Send helpers ──────────────────────────────────────────────
void sendMove(int node, int pos) {
  if (port != null) port.write("MOVE:" + node + "," + pos + "\n");
  lightNode = node;
  lightPos  = pos;
  moveCount = 0;
  logEvent("→ MOVE node=" + node + " pos=" + pos);
}

void sendRestart() {
  if (port != null) port.write("RESTART\n");
  shutdown   = false;
  gameActive = true;
  moveCount  = 0;
  logEvent("→ RESTART sent");
}

// ── Serial receive ────────────────────────────────────────────
void serialEvent(Serial p) {
  String line = trim(p.readStringUntil('\n'));
  if (line == null || line.length() == 0) return;

  logEvent(line);

  if (line.startsWith("LIGHT:")) {
    String[] parts = split(line.substring(6), ',');
    if (parts.length == 2) { lightNode = int(parts[0]); lightPos = int(parts[1]); }
  }
  else if (line.startsWith("NODE:")) {
    String[] parts = split(line.substring(5), ',');
    if (parts.length == 2) {
      int id = int(parts[0]);
      if (parts[1].startsWith("HALL:") && id >= 0 && id <= 2)
        hallActive[id] = (int(parts[1].substring(5)) == 1);
      if (parts[1].startsWith("STATUS:") && id >= 0 && id <= 2)
        nodeOnline[id] = true;
    }
  }
  else if (line.startsWith("EVENT:TIMEOUT_MOVE:")) {
    moveCount = int(line.substring(19));
  }
  else if (line.equals("EVENT:SHUTDOWN")) {
    shutdown = true; gameActive = false; moveCount = 2;
    lightNode = -1; lightPos = -1;
  }
  else if (line.equals("STATUS:MASTER_READY") || line.equals("STATUS:RESTARTED")) {
    gameActive = true; shutdown = false; moveCount = 0;
    nodeOnline[0] = true;
  }
}

void logEvent(String msg) {
  String ts = nf(hour(),2)+":"+nf(minute(),2)+":"+nf(second(),2);
  eventLog.add("["+ts+"] "+msg);
  if (eventLog.size() > 50) eventLog.remove(0);
}