๐ŸŽฎArcadeLab

Color or Die

by BoldCobra33
1292 lines55.1 KB
โ–ถ Play
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Color or Die</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Creepster&display=swap');
*{margin:0;padding:0;box-sizing:border-box;}
body{background:#000;display:flex;flex-direction:column;align-items:center;height:100vh;font-family:'Share Tech Mono',monospace;color:#ddd;overflow:hidden;}
#hud{width:100%;display:flex;align-items:center;justify-content:space-between;padding:5px 14px;background:rgba(0,0,0,0.9);border-bottom:1px solid #111;flex-wrap:wrap;gap:5px;z-index:10;flex-shrink:0;}
.hg{display:flex;align-items:center;gap:10px;}
.hl{font-size:10px;color:#444;text-transform:uppercase;letter-spacing:1px;}
.hv{font-size:12px;color:#bbb;}
#cswatch{width:20px;height:20px;border-radius:3px;border:1px solid #333;}
#htag{font-size:10px;color:#0f0;background:rgba(0,255,0,0.08);border:1px solid #0f0;padding:1px 6px;border-radius:3px;display:none;}
#ctag{font-size:10px;color:#f40;background:rgba(255,60,0,0.08);border:1px solid #f40;padding:1px 6px;border-radius:3px;display:none;}
#livdisp{font-size:14px;letter-spacing:2px;}
#pbar{font-size:10px;color:#fc0;display:none;}
canvas{display:block;flex-shrink:0;}
#ovl{position:fixed;inset:0;background:rgba(0,0,0,0.93);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;z-index:50;}
#ovl h1{font-family:'Creepster',cursive;font-size:3.2rem;letter-spacing:5px;text-shadow:0 0 30px currentColor;}
#ovl p{font-size:12px;color:#777;max-width:420px;text-align:center;line-height:2;}
.ob{background:transparent;color:#ccc;border:1px solid #444;padding:9px 28px;font-family:'Share Tech Mono',monospace;font-size:12px;cursor:pointer;letter-spacing:3px;border-radius:3px;transition:all .2s;}
.ob:hover{border-color:#ccc;color:#fff;background:rgba(255,255,255,0.06);}
#povl{position:fixed;inset:0;background:rgba(0,0,0,0.9);display:none;flex-direction:column;align-items:center;justify-content:center;gap:14px;z-index:40;}
#povl h2{font-family:'Creepster',cursive;font-size:1.9rem;letter-spacing:4px;color:#fc0;}
#povl p{font-size:11px;color:#777;max-width:360px;text-align:center;line-height:1.9;}
#pc{display:flex;flex-direction:column;align-items:center;gap:10px;}
.pb{background:transparent;color:#ccc;border:1px solid #444;padding:7px 20px;font-family:'Share Tech Mono',monospace;font-size:11px;cursor:pointer;letter-spacing:2px;border-radius:3px;transition:all .15s;min-width:170px;}
.pb:hover{border-color:#fc0;color:#fc0;}
.pb.ok{border-color:#4f4;color:#4f4;}
.pb.no{border-color:#f44;color:#f44;}
#hint{position:fixed;bottom:5px;left:50%;transform:translateX(-50%);font-size:9px;color:#222;z-index:10;white-space:nowrap;}
</style>
</head>
<body>
<div id="hud">
  <div class="hg">
    <span class="hl">LVL</span><span id="lvv" class="hv">1</span>
    <span class="hl">LIVES</span><span id="livdisp">โ™ฅโ™ฅโ™ฅ</span>
  </div>
  <div class="hg">
    <span class="hl">PAINT</span>
    <div id="cswatch"></div>
    <span id="cname" class="hv">none</span>
    <span id="htag">โ— HIDDEN</span>
    <span id="ctag">โš  CHASED</span>
  </div>
  <div class="hg">
    <span id="pbar">๐Ÿ”’ PUZZLE โ€” press E</span>
    <span class="hl">PUZZLES</span><span id="pcnt" class="hv">0/0</span>
    <span class="hl" style="margin-left:8px">BRUSHES</span><span id="brcnt" class="hv">0/0</span>
  </div>
</div>
<canvas id="c"></canvas>
<div id="hint">WASD / ARROWS โ€” move &nbsp;|&nbsp; walk into bucket to grab color &nbsp;|&nbsp; E โ€” solve puzzle &nbsp;|&nbsp; collect all ๐Ÿ–Œ brushes + solve puzzles to unlock EXIT &nbsp;|&nbsp; match wall to HIDE</div>

<div id="ovl">
  <h1 id="otitle" style="color:#e84040">COLOR OR DIE</h1>
  <p id="omsg">Every wall is painted a color โ€” collect matching buckets to hide beside them.<br>Color-locked <b>doors</b> block your path: find the matching paint bucket to open them.<br>Collect all <b>paintbrushes</b> ๐Ÿ–Œ and solve all puzzles to unlock the EXIT.<br>Some brushes are locked inside rooms โ€” you'll need the right color to get them.</p>
  <button class="ob" id="obtn">โ–ถ ENTER THE MAZE</button>
</div>

<div id="povl">
  <h2 id="ptitle">PUZZLE</h2>
  <p id="pdesc"></p>
  <div id="pc"></div>
  <button class="ob" id="pclose" style="font-size:10px;padding:5px 16px;margin-top:4px;">โœ• CANCEL</button>
</div>

<script>
const CV=document.getElementById('c'),ctx=CV.getContext('2d');
const TILE=26,COLS=42,ROWS=30;
CV.width=COLS*TILE; CV.height=ROWS*TILE;

function resize(){
  const hh=document.getElementById('hud').offsetHeight;
  const avail=window.innerHeight-hh-18;
  const scale=Math.min(1,avail/CV.height,(window.innerWidth-8)/CV.width);
  CV.style.width=CV.width*scale+'px';CV.style.height=CV.height*scale+'px';
}
window.addEventListener('resize',resize);setTimeout(resize,80);

// All 8 colors available
const COLORS={
  red:'#e84040',blue:'#3a8fe8',green:'#3cd060',yellow:'#f0c020',
  purple:'#a040e0',orange:'#e87020',cyan:'#20d0d0',pink:'#e040a0'
};
const CKEYS=Object.keys(COLORS);

// Darker shades for wall rendering (more saturated, not neon)
const DARK={
  red:'#7a1010',blue:'#103a70',green:'#0e5020',yellow:'#6a5000',
  purple:'#3e0870',orange:'#6a2800',cyan:'#006868',pink:'#6a0848'
};
const MID={
  red:'#c02828',blue:'#2468c0',green:'#20a040',yellow:'#c09000',
  purple:'#7020c0',orange:'#c05010',cyan:'#10a0a0',pink:'#c02080'
};

// Cell types
const T={E:0,W:1,BK:2,EX:3,PZ:4,DOOR:5,BRUSH:6};

let S,keys={},raf,lastTs=null;

// โ”€โ”€ Maze generation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// Uses recursive backtracker then aggressively punches extra passages to break
// up long wall runs and ensure the maze is richly connected with no dead zones.
function genMaze(cols,rows){
  const MC=Math.floor(cols/2), MR=Math.floor(rows/2);
  const vis=Array.from({length:MR},()=>Array(MC).fill(false));
  const g=Array.from({length:rows},()=>Array(cols).fill(T.W));

  // Carve base maze
  function carve(mr,mc){
    vis[mr][mc]=true;
    g[mr*2+1][mc*2+1]=T.E;
    const d=[[0,1],[0,-1],[1,0],[-1,0]].sort(()=>Math.random()-.5);
    for(const[dr,dc]of d){
      const nr=mr+dr,nc=mc+dc;
      if(nr>=0&&nr<MR&&nc>=0&&nc<MC&&!vis[nr][nc]){
        g[mr*2+1+dr][mc*2+1+dc]=T.E;
        carve(nr,nc);
      }
    }
  }
  carve(0,0);

  // โ”€โ”€ Aggressively break up long walls โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // Pass 1: find wall cells that are part of a run of 3+ consecutive walls
  // in any direction, and punch them open if they have floor on both sides.
  function punchLongRuns(){
    for(let r=1;r<rows-1;r++){
      for(let c=1;c<cols-1;c++){
        if(g[r][c]!==T.W)continue;
        // Check horizontal run length
        let hrun=0;
        for(let dc=-2;dc<=2;dc++) if(c+dc>=0&&c+dc<cols&&g[r][c+dc]===T.W)hrun++;
        // Check vertical run length
        let vrun=0;
        for(let dr=-2;dr<=2;dr++) if(r+dr>=0&&r+dr<rows&&g[r+dr][c]===T.W)vrun++;
        // Punch if this is part of a long run AND has floor neighbors on opposite sides
        const floorN=r>0&&g[r-1][c]===T.E, floorS=r<rows-1&&g[r+1][c]===T.E;
        const floorW=c>0&&g[r][c-1]===T.E, floorE=c<cols-1&&g[r][c+1]===T.E;
        if(vrun>=3&&floorN&&floorS) g[r][c]=T.E;
        else if(hrun>=3&&floorW&&floorE) g[r][c]=T.E;
      }
    }
  }
  punchLongRuns();

  // Pass 2: many random extra loops โ€” punch wall cells that have 2+ floor neighbors
  const extraLoops=Math.floor((cols*rows)/12);
  for(let i=0;i<extraLoops;i++){
    const r=1+Math.floor(Math.random()*(rows-2));
    const c=1+Math.floor(Math.random()*(cols-2));
    if(g[r][c]!==T.W)continue;
    let adj=0;
    for(const[dr,dc]of[[0,1],[0,-1],[1,0],[-1,0]]){
      const nr=r+dr,nc=c+dc;
      if(nr>=0&&nr<rows&&nc>=0&&nc<cols&&g[nr][nc]===T.E)adj++;
    }
    if(adj>=2) g[r][c]=T.E;
  }

  // Pass 3: detect and break any remaining wall stretch >= 4 in a row
  // by forcing open the middle cell regardless
  for(let r=2;r<rows-2;r++){
    for(let c=2;c<cols-2;c++){
      if(g[r][c]!==T.W)continue;
      // Horizontal 4-run
      if(g[r][c-1]===T.W&&g[r][c+1]===T.W&&g[r][c-2]===T.W){
        if(r>1&&r<rows-1&&(g[r-1][c]===T.E||g[r+1][c]===T.E)) g[r][c]=T.E;
      }
      // Vertical 4-run
      if(g[r-1][c]===T.W&&g[r+1][c]===T.W&&g[r-2]&&g[r-2][c]===T.W){
        if(c>1&&c<cols-1&&(g[r][c-1]===T.E||g[r][c+1]===T.E)) g[r][c]=T.E;
      }
    }
  }

  // Enforce solid border
  for(let r=0;r<rows;r++){g[r][0]=T.W;g[r][cols-1]=T.W;}
  for(let c=0;c<cols;c++){g[0][c]=T.W;g[rows-1][c]=T.W;}
  return g;
}

// Assign wall colors in chunky noise blobs
function paintWalls(g,wallColor,colors,rows,cols){
  const nc=colors.length;
  const raw=Array.from({length:rows},()=>Array(cols).fill(0));
  for(let r=0;r<rows;r++)for(let c=0;c<cols;c++)
    raw[r][c]=(Math.floor(r/3)*7+Math.floor(c/4)*13+Math.floor(r/6)*5)%nc;
  for(let r=0;r<rows;r++)for(let c=0;c<cols;c++){
    if(g[r][c]===T.W){
      const v=Array(nc).fill(0);
      for(let dr=-1;dr<=1;dr++)for(let dc=-1;dc<=1;dc++){
        const nr=r+dr,nc2=c+dc;
        if(nr>=0&&nr<rows&&nc2>=0&&nc2<cols)v[raw[nr][nc2]]++;
      }
      let b=0;for(let i=1;i<nc;i++)if(v[i]>v[b])b=i;
      wallColor[r][c]=colors[b];
    }
  }
}

function reachable(g,rows,cols){
  const seen=Array.from({length:rows},()=>Array(cols).fill(false));
  const q=[[1,1]];seen[1][1]=true;
  while(q.length){
    const[r,c]=q.pop();
    for(const[dr,dc]of[[0,1],[0,-1],[1,0],[-1,0]]){
      const nr=r+dr,nc=c+dc;
      if(nr>=0&&nr<rows&&nc>=0&&nc<cols&&!seen[nr][nc]&&g[nr][nc]!==T.W&&g[nr][nc]!==T.DOOR){seen[nr][nc]=true;q.push([nr,nc]);}
    }
  }
  const out=[];for(let r=0;r<rows;r++)for(let c=0;c<cols;c++)if(seen[r][c])out.push({r,c});
  return out;
}

function bfsDist(g,rows,cols,sr,sc){
  const dist=Array.from({length:rows},()=>Array(cols).fill(-1));
  dist[sr][sc]=0;const q=[[sr,sc]];
  while(q.length){
    const[r,c]=q.shift();
    for(const[dr,dc]of[[0,1],[0,-1],[1,0],[-1,0]]){
      const nr=r+dr,nc=c+dc;
      if(nr>=0&&nr<rows&&nc>=0&&nc<cols&&dist[nr][nc]===-1&&g[nr][nc]!==T.W&&g[nr][nc]!==T.DOOR){dist[nr][nc]=dist[r][c]+1;q.push([nr,nc]);}
    }
  }
  return dist;
}

function buildLevel(lvl){
  const numC=Math.min(3+Math.floor((lvl-1)/2),8);
  const colors=CKEYS.slice(0,numC);
  const numDoors=Math.min(2+Math.floor(lvl/2),Math.min(colors.length-1,4));
  const doorColors=colors.slice(1,1+numDoors);

  // โ”€โ”€ 1. Generate open maze โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  const g=genMaze(COLS,ROWS);
  const wallColor=Array.from({length:ROWS},()=>Array(COLS).fill(null));
  const buckets=[],puzzles=[],doors=[];
  const used=new Set(['1,1']);

  // โ”€โ”€ 2. Get all open floor cells from start โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  function openCells(){
    const seen=Array.from({length:ROWS},()=>Array(COLS).fill(false));
    const q=[[1,1]];seen[1][1]=true;
    while(q.length){
      const[r,c]=q.pop();
      for(const[dr,dc]of[[0,1],[0,-1],[1,0],[-1,0]]){
        const nr=r+dr,nc=c+dc;
        if(nr>=0&&nr<ROWS&&nc>=0&&nc<COLS&&!seen[nr][nc]&&g[nr][nc]===T.E){seen[nr][nc]=true;q.push([nr,nc]);}
      }
    }
    const out=[];for(let r=0;r<ROWS;r++)for(let c=0;c<COLS;c++)if(seen[r][c])out.push({r,c});
    return out;
  }

  // โ”€โ”€ 3. Carve sealed rooms with guaranteed accessible door โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // Approach: pick an open maze cell as the "doorstep". The door is placed ON that
  // maze cell (replacing the floor). The room is built on the OTHER side โ€” we pick
  // a direction (N/S/E/W) and carve a room starting one step in that direction.
  // The door cell borders the maze on one side and the room interior on the other.
  // This guarantees: maze โ†’ door โ†’ room interior, with no stray wall in between.

  function tryCarveRoom(minDist){
    const open=openCells();
    const candidates=open.filter(p=>
      Math.hypot(p.r-1,p.c-1)>minDist && !used.has(p.r+','+p.c)
    ).sort(()=>Math.random()-.5);

    for(const doorstep of candidates.slice(0,60)){
      const dr=doorstep.r, dc=doorstep.c;
      // Try building a room in each of the 4 directions from the doorstep
      const dirs=[[0,1],[0,-1],[1,0],[-1,0]].sort(()=>Math.random()-.5);
      for(const[growR,growC]of dirs){
        // Room sizes to try
        for(const[rh,rw]of[[3,5],[5,3],[4,4],[3,4],[4,3],[3,3]].sort(()=>Math.random()-.5)){
          // Place room so its wall face (the side facing the door) lines up with
          // the cell one step from the doorstep in the grow direction.
          // The face cell in the room wall that will connect to the door:
          const faceR=dr+growR, faceC=dc+growC;

          // Align room so faceR/faceC is somewhere on that face
          // Room bounding box: choose offset so face is on the correct edge
          let r0,c0,r1,c1;
          if(growR===1){
            // Room is below the door; top face at row faceR
            r0=faceR; c0=faceC-Math.floor(rw/2); r1=r0+rh-1; c1=c0+rw-1;
          } else if(growR===-1){
            // Room is above the door; bottom face at row faceR
            r1=faceR; c1=faceC+Math.floor(rw/2); r0=r1-rh+1; c0=c1-rw+1;
          } else if(growC===1){
            // Room is to the right; left face at col faceC
            c0=faceC; r0=faceR-Math.floor(rh/2); c1=c0+rw-1; r1=r0+rh-1;
          } else {
            // Room is to the left; right face at col faceC
            c1=faceC; r1=faceR+Math.floor(rh/2); c0=c1-rw+1; r0=r1-rh+1;
          }

          // Must fit inside map with 1-cell margin
          if(r0<2||c0<2||r1>ROWS-3||c1>COLS-3)continue;

          // faceR/faceC must be on the room bounding box border
          const onFace=(growR===1&&faceR===r0)||(growR===-1&&faceR===r1)||
                       (growC===1&&faceC===c0)||(growC===-1&&faceC===c1);
          if(!onFace)continue;

          // Check all cells in the room bbox + the doorstep are clear
          let clear=!used.has(dr+','+dc);
          for(let r=r0;r<=r1&&clear;r++)for(let c=c0;c<=c1&&clear;c++) if(used.has(r+','+c))clear=false;
          if(!clear)continue;

          // โ”€โ”€ Stamp room โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
          for(let r=r0;r<=r1;r++)for(let c=c0;c<=c1;c++) g[r][c]=T.W;
          const interior=[];
          for(let r=r0+1;r<r1;r++)for(let c=c0+1;c<c1;c++){g[r][c]=T.E;interior.push({r,c});}
          // Ensure face-adjacent cell is open so doorโ†’interior path is clear
          if(faceR>=r0&&faceR<=r1&&faceC>=c0&&faceC<=c1){
            g[faceR][faceC]=T.E;
            if(!interior.find(p=>p.r===faceR&&p.c===faceC))interior.push({r:faceR,c:faceC});
          }

          if(!interior.length)continue;

          // Mark ALL bbox cells used (prevents room overlap),
          // but track interior separately so content can be placed there
          used.add(dr+','+dc);
          for(let r=r0;r<=r1;r++)for(let c=c0;c<=c1;c++) used.add(r+','+c);
          // UNmark interior cells from used so puzzles/exit can be placed inside
          for(const p of interior) used.delete(p.r+','+p.c);

          return{r0,c0,r1,c1,doorR:dr,doorC:dc,interior};
        }
      }
    }
    return null;
  }

  // Carve one room per door-color + one extra room for the exit
  const totalRooms=numDoors+1;
  const rooms=[];
  for(let i=0;i<totalRooms;i++){
    const minD=4+i*3;
    for(let attempt=0;attempt<120;attempt++){
      const room=tryCarveRoom(minD);
      if(room){rooms.push(room);break;}
    }
  }

  // โ”€โ”€ 4. Assign door keys and stamp door cells โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  for(let i=0;i<rooms.length;i++){
    const room=rooms[i];
    // Puzzle rooms get doorColors[i]; exit room gets last door color
    const ck=doorColors[Math.min(i,doorColors.length-1)];
    g[room.doorR][room.doorC]=T.DOOR;
    doors.push({r:room.doorR,c:room.doorC,key:ck,color:COLORS[ck]});
    room.key=ck;
  }

  // โ”€โ”€ 4b. Connectivity repair โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // After stamping rooms, some maze floor cells may be cut off by room walls.
  // BFS from (1,1) ignoring doors; any floor cell not reached gets a passage punched.
  {
    const seen=Array.from({length:ROWS},()=>Array(COLS).fill(false));
    const q=[[1,1]];seen[1][1]=true;
    while(q.length){
      const[r,c]=q.shift();
      for(const[dr,dc]of[[0,1],[0,-1],[1,0],[-1,0]]){
        const nr=r+dr,nc=c+dc;
        if(nr<1||nr>=ROWS-1||nc<1||nc>=COLS-1||seen[nr][nc])continue;
        if(g[nr][nc]===T.E||g[nr][nc]===T.PZ||g[nr][nc]===T.EX){seen[nr][nc]=true;q.push([nr,nc]);}
        // Cross doors freely for connectivity check
        if(g[nr][nc]===T.DOOR){seen[nr][nc]=true;q.push([nr,nc]);}
      }
    }
    // Find unreachable floor cells and punch a wall between them and a reachable neighbor
    for(let r=1;r<ROWS-1;r++){
      for(let c=1;c<COLS-1;c++){
        if(seen[r][c])continue;
        if(g[r][c]!==T.E)continue;
        // Find an adjacent wall whose other side is reachable
        for(const[dr,dc]of[[0,1],[0,-1],[1,0],[-1,0]]){
          const wr=r+dr,wc=c+dc;
          const br=r+dr*2,bc=c+dc*2;
          if(wr<1||wr>=ROWS-1||wc<1||wc>=COLS-1)continue;
          if(br<1||br>=ROWS-1||bc<1||bc>=COLS-1)continue;
          if(g[wr][wc]===T.W&&seen[br]&&seen[br][bc]){
            g[wr][wc]=T.E; // punch the wall
            seen[r][c]=true;
            break;
          }
        }
      }
    }
  }

  // โ”€โ”€ 5. Paint walls โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  paintWalls(g,wallColor,colors,ROWS,COLS);
  // Tint room walls with their door color so players get a visual hint
  for(const room of rooms){
    for(let r=room.r0;r<=room.r1;r++)for(let c=room.c0;c<=room.c1;c++){
      if(g[r][c]===T.W) wallColor[r][c]=room.key;
    }
  }

  // โ”€โ”€ 6. Place buckets โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  const mazeOpen=openCells(); // only main maze floor, not inside rooms
  function rFrom(pool){return pool[Math.floor(Math.random()*pool.length)];}

  // Free starter bucket: close to spawn
  {
    const nearby=mazeOpen.filter(p=>!used.has(p.r+','+p.c)&&Math.hypot(p.r-1,p.c-1)>=1&&Math.hypot(p.r-1,p.c-1)<=5);
    nearby.sort((a,b)=>Math.hypot(a.r-1,a.c-1)-Math.hypot(b.r-1,b.c-1));
    const pos=nearby[0]||mazeOpen.find(p=>!used.has(p.r+','+p.c));
    if(pos){used.add(pos.r+','+pos.c);buckets.push({r:pos.r,c:pos.c,color:COLORS[colors[0]],key:colors[0]});}
  }

  // Each door-key bucket: placed freely in the open maze (not inside any room)
  for(const ck of doorColors){
    const pool=mazeOpen.filter(p=>!used.has(p.r+','+p.c)&&Math.hypot(p.r-1,p.c-1)>3);
    if(!pool.length)continue;
    const pos=rFrom(pool);
    used.add(pos.r+','+pos.c);
    buckets.push({r:pos.r,c:pos.c,color:COLORS[ck],key:ck});
  }

  // Extra buckets for remaining colors
  for(const ck of colors){
    if(buckets.find(b=>b.key===ck))continue;
    const pool=mazeOpen.filter(p=>!used.has(p.r+','+p.c));
    if(!pool.length)continue;
    const pos=rFrom(pool);
    used.add(pos.r+','+pos.c);
    buckets.push({r:pos.r,c:pos.c,color:COLORS[ck],key:ck});
  }

  // โ”€โ”€ 6b. Place paintbrushes โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // Brushes are one-time collectibles โ€” must all be collected to win the level.
  // Most go in the open maze; a couple go inside locked rooms.
  const brushes=[];
  const totalMazeBrushes=5+lvl; // scales with level
  const mazeOpen2=openCells();

  // Place brushes in open maze
  for(let i=0;i<totalMazeBrushes;i++){
    const pool=mazeOpen2.filter(p=>!used.has(p.r+','+p.c)&&Math.hypot(p.r-1,p.c-1)>3);
    if(!pool.length)break;
    const pos=pool[Math.floor(Math.random()*pool.length)];
    used.add(pos.r+','+pos.c);
    g[pos.r][pos.c]=T.BRUSH;
    brushes.push({r:pos.r,c:pos.c,collected:false});
  }

  // Place 1 brush inside each room (in a free interior cell)
  for(const room of rooms){
    const interior=room.interior.filter(p=>!used.has(p.r+','+p.c));
    if(!interior.length)continue;
    const pos=interior[Math.floor(Math.random()*interior.length)];
    used.add(pos.r+','+pos.c);
    g[pos.r][pos.c]=T.BRUSH;
    brushes.push({r:pos.r,c:pos.c,collected:false});
  }

  // โ”€โ”€ 7. Place puzzles inside rooms 0..numDoors-1, exit in last room โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  const pTypes=['sequence','wires','tiles','lock'];
  const numPz=Math.max(0,rooms.length-1); // all rooms except the last get a puzzle
  for(let i=0;i<rooms.length;i++){
    const room=rooms[i];
    const interior=room.interior.filter(p=>!used.has(p.r+','+p.c));
    if(!interior.length)continue;
    const pos=interior[Math.floor(Math.random()*interior.length)];
    used.add(pos.r+','+pos.c);
    const isLast=(i===rooms.length-1);
    if(isLast){
      // Exit goes in the last room
      g[pos.r][pos.c]=T.EX;
    } else {
      // Puzzle goes in every other room
      g[pos.r][pos.c]=T.PZ;
      puzzles.push({r:pos.r,c:pos.c,type:pTypes[i%pTypes.length],solved:false,id:i});
    }
  }

  // Fallback exit if room carving failed to place one
  const hasExit=rooms.length>0&&rooms[rooms.length-1];
  if(!hasExit){
    const mo=openCells();
    const dist2=bfsDist(g,ROWS,COLS,1,1);
    let best={r:ROWS-2,c:COLS-2,d:0};
    for(const p of mo){if(!used.has(p.r+','+p.c)&&dist2[p.r]&&dist2[p.r][p.c]>best.d)best={...p,d:dist2[p.r][p.c]};}
    g[best.r][best.c]=T.EX;
  }

  return{g,wallColor,buckets,brushes,puzzles,doors,numPz:puzzles.length,colors:colors.map(k=>({k,hex:COLORS[k]}))};
}

function initState(lvl,prevLives){
  const ld=buildLevel(lvl);
  // Find open cell nearest to map center for monster spawn
  const open=reachable(ld.g,ROWS,COLS);
  const cr=ROWS/2,cc=COLS/2;
  let mSpawn=open.reduce((best,p)=>{
    const d=Math.hypot(p.r-cr,p.c-cc);
    return d<best.d?{...p,d}:best;
  },{r:Math.floor(cr),c:Math.floor(cc),d:999});
  return{
    lvl,lives:prevLives!==undefined?prevLives:3,
    player:{x:1.5,y:1.5,color:null,key:null,speed:4.2},
    monster:{x:mSpawn.c+.5,y:mSpawn.r+.5,speed:3.2+(lvl-1)*0.45,chase:false,angle:Math.PI,pT:0,stunT:0},
    g:ld.g,wallColor:ld.wallColor,
    buckets:ld.buckets,brushes:ld.brushes,puzzles:ld.puzzles,doors:ld.doors,
    totalPz:ld.numPz,solvedPz:0,
    totalBrushes:ld.brushes.length,collectedBrushes:0,
    exitUnlocked:ld.numPz===0&&ld.brushes.length===0,
    levelColors:ld.colors,
    phase:'play',flash:0,hidden:false,nearPz:null
  };
}

// โ”€โ”€ Collision โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function blocked(s,nx,ny,rad=0.32){
  for(const[dx,dy]of[[-rad,-rad],[rad,-rad],[-rad,rad],[rad,rad]]){
    const gc=Math.floor(nx+dx),gr=Math.floor(ny+dy);
    if(gr<0||gr>=ROWS||gc<0||gc>=COLS)return true;
    const cell=s.g[gr][gc];
    if(cell===T.W)return true;
    if(cell===T.DOOR){
      const d=s.doors.find(d=>d.r===gr&&d.c===gc);
      if(d&&d.key!==s.player.key)return true; // blocked unless holding right color
    }
  }
  return false;
}

function mBlock(s,nx,ny,rad=0.28){
  for(const[dx,dy]of[[-rad,-rad],[rad,-rad],[-rad,rad],[rad,rad]]){
    const gc=Math.floor(nx+dx),gr=Math.floor(ny+dy);
    if(gr<0||gr>=ROWS||gc<0||gc>=COLS)return true;
    const cell=s.g[gr][gc];
    if(cell===T.W||cell===T.DOOR)return true;
  }
  return false;
}

// Hidden: player is adjacent to a wall whose color matches their paint
function chkHidden(s){
  if(!s.player.key)return false;
  const{x,y}=s.player;
  for(const[dx,dy]of[[0,0],[0.45,0],[-0.45,0],[0,0.45],[0,-0.45]]){
    const gc=Math.floor(x+dx),gr=Math.floor(y+dy);
    if(gr>=0&&gr<ROWS&&gc>=0&&gc<COLS&&s.g[gr][gc]===T.W){
      if(s.wallColor[gr][gc]===s.player.key)return true;
    }
  }
  return false;
}

// โ”€โ”€ BFS pathfinding for monster โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// Returns array of {r,c} grid cells from monster pos toward target pos
function bfsPath(g,fromX,fromY,toX,toY){
  const sr=Math.floor(fromY),sc=Math.floor(fromX);
  const er=Math.floor(toY),ec=Math.floor(toX);
  if(sr===er&&sc===ec)return[];
  const prev=Array.from({length:ROWS},()=>Array(COLS).fill(null));
  const visited=Array.from({length:ROWS},()=>Array(COLS).fill(false));
  const q=[[sr,sc]];visited[sr][sc]=true;
  let found=false;
  outer:while(q.length){
    const[r,c]=q.shift();
    for(const[dr,dc]of[[0,1],[0,-1],[1,0],[-1,0]]){
      const nr=r+dr,nc=c+dc;
      if(nr<0||nr>=ROWS||nc<0||nc>=COLS)continue;
      if(visited[nr][nc])continue;
      const cell=g[nr][nc];
      if(cell===T.W||cell===T.DOOR)continue; // monster blocked by doors too
      visited[nr][nc]=true;
      prev[nr][nc]=[r,c];
      if(nr===er&&nc===ec){found=true;break outer;}
      q.push([nr,nc]);
    }
  }
  if(!found)return[];
  // Reconstruct path
  const path=[];let cur=[er,ec];
  while(cur[0]!==sr||cur[1]!==sc){
    path.unshift({r:cur[0],c:cur[1]});
    cur=prev[cur[0]][cur[1]];
    if(!cur)return[];
  }
  return path;
}

// โ”€โ”€ Update โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function update(dt){
  const s=S;if(s.phase!=='play')return;
  const p=s.player;
  let dx=0,dy=0;
  if(keys['ArrowLeft']||keys['a']||keys['A'])dx-=1;
  if(keys['ArrowRight']||keys['d']||keys['D'])dx+=1;
  if(keys['ArrowUp']||keys['w']||keys['W'])dy-=1;
  if(keys['ArrowDown']||keys['s']||keys['S'])dy+=1;
  if(dx&&dy){dx*=.707;dy*=.707;}
  const nx=p.x+dx*p.speed*dt,ny=p.y+dy*p.speed*dt;
  if(dx&&!blocked(s,nx,p.y))p.x=nx;
  if(dy&&!blocked(s,p.x,ny))p.y=ny;
  p.x=Math.max(.5,Math.min(COLS-.5,p.x));
  p.y=Math.max(.5,Math.min(ROWS-.5,p.y));

  // Pick up bucket (walk into it โ€” persistent, stays on map)
  for(const b of s.buckets){
    if(Math.hypot(p.x-b.c-.5,p.y-b.r-.5)<.72){p.color=b.color;p.key=b.key;}
  }

  // Collect paintbrushes (one-time, removed on pickup)
  for(const br of s.brushes){
    if(!br.collected&&Math.hypot(p.x-br.c-.5,p.y-br.r-.5)<.72){
      br.collected=true;
      s.g[br.r][br.c]=T.E;
      s.collectedBrushes++;
      if(s.solvedPz>=s.totalPz&&s.collectedBrushes>=s.totalBrushes) s.exitUnlocked=true;
    }
  }

  // Near puzzle?
  s.nearPz=null;
  for(const pz of s.puzzles){
    if(!pz.solved&&Math.hypot(p.x-pz.c-.5,p.y-pz.r-.5)<1.8){s.nearPz=pz;break;}
  }

  s.hidden=chkHidden(s);

  // Monster โ€” BFS pathfinding + last-known pos + hearing
  const m=s.monster;
  if(m.stunT>0){
    m.stunT-=dt;
  } else {
    const d=Math.hypot(p.x-m.x,p.y-m.y);
    const sightRange=6+s.lvl*0.8;
    const hearRange=3.5; // player makes noise when moving

    const playerMoving=(dx!==0||dy!==0);
    const canSee=d<sightRange&&!s.hidden;
    const canHear=d<hearRange&&playerMoving&&!s.hidden;

    // Update last-known position
    if(canSee||canHear){
      m.lastKnownX=p.x; m.lastKnownY=p.y;
      m.lostTimer=0;
    } else if(m.lastKnownX!==undefined){
      m.lostTimer=(m.lostTimer||0)+dt;
      if(m.lostTimer>1.2){
        // Give up โ€” immediately pick a patrol target far from where we lost the player
        const lx=m.lastKnownX, ly=m.lastKnownY;
        m.lastKnownX=undefined;
        m.path=null;
        const open2=reachable(s.g,ROWS,COLS);
        // Pick from cells that are far from the last-known position
        const farCells=open2.filter(c=>Math.hypot(c.c+.5-lx,c.r+.5-ly)>10);
        const pool=farCells.length>0?farCells:open2;
        m.patrolTarget=pool[Math.floor(Math.random()*pool.length)];
      }
    }

    m.chase=canSee||canHear;

    // Repath every 0.25s (BFS to target)
    m.pathTimer=(m.pathTimer||0)+dt;
    if(m.pathTimer>0.25||(m.chase&&!m.path?.length)){
      m.pathTimer=0;
      const targetX=m.lastKnownX!==undefined?m.lastKnownX:(m.patrolTarget?m.patrolTarget.c+.5:m.x);
      const targetY=m.lastKnownY!==undefined?m.lastKnownY:(m.patrolTarget?m.patrolTarget.r+.5:m.y);
      m.path=bfsPath(s.g,m.x,m.y,targetX,targetY);
    }

    // Refresh patrol target when idle โ€” pick somewhere far from the player
    if(!m.chase&&m.lastKnownX===undefined){
      if(!m.patrolTarget||Math.hypot(m.x-m.patrolTarget.c-.5,m.y-m.patrolTarget.r-.5)<1.2){
        const open2=reachable(s.g,ROWS,COLS);
        const farCells=open2.filter(c=>Math.hypot(c.c+.5-p.x,c.r+.5-p.y)>12);
        const pool=farCells.length>0?farCells:open2;
        m.patrolTarget=pool[Math.floor(Math.random()*pool.length)];
        m.path=null;
      }
    }

    let mdx=0,mdy=0;
    if(m.path&&m.path.length>0){
      const next=m.path[0];
      const tx=next.c+.5, ty=next.r+.5;
      const dd=Math.hypot(tx-m.x,ty-m.y);
      if(dd<0.18) m.path.shift();
      else { mdx=(tx-m.x)/dd; mdy=(ty-m.y)/dd; }
    } else {
      // Fallback: jitter wander
      m.pT=(m.pT||0)-dt;
      if(m.pT<=0){m.angle=(m.angle||0)+(Math.random()-.5)*2.5;m.pT=.5+Math.random();}
      mdx=Math.cos(m.angle);mdy=Math.sin(m.angle);
    }

    const spd=m.chase?m.speed*2.1:m.speed*.9;
    const mnx=m.x+mdx*spd*dt, mny=m.y+mdy*spd*dt;
    if(!mBlock(s,mnx,m.y))m.x=mnx; else m.path=null;
    if(!mBlock(s,m.x,mny))m.y=mny; else m.path=null;

    if(Math.hypot(p.x-m.x,p.y-m.y)<.6&&!s.hidden){
      s.lives--;s.flash=.7;
      p.x=1.5;p.y=1.5;p.color=null;p.key=null;
      m.stunT=2; m.lastKnownX=undefined; m.path=null;
      const open2=reachable(s.g,ROWS,COLS);
      const cr2=ROWS/2,cc2=COLS/2;
      const ms=open2.reduce((b,p2)=>{const dd=Math.hypot(p2.r-cr2,p2.c-cc2);return dd<b.d?{...p2,d:dd}:b;},{r:Math.floor(cr2),c:Math.floor(cc2),d:999});
      m.x=ms.c+.5;m.y=ms.r+.5;
      if(s.lives<=0){
        s.phase='dead';
        showOvl('YOU DIED','๐Ÿ’€ The monster consumed you.','#e84040','RETRY',()=>{S=initState(1);updateHUD();});
      }
    }
  }

  // Exit
  const ec=Math.floor(p.x),er=Math.floor(p.y);
  if(s.g[er]&&s.g[er][ec]===T.EX&&s.exitUnlocked){
    s.phase='win';
    showOvl(`LEVEL ${s.lvl} CLEAR`,'โœ“ You escaped the maze!','#3cd060','NEXT LEVEL',()=>{S=initState(s.lvl+1,s.lives);updateHUD();});
  }
  if(s.flash>0)s.flash-=dt;
  updateHUD();
}

function updateHUD(){
  const s=S;
  document.getElementById('cswatch').style.background=s.player.color||'#111';
  document.getElementById('cname').textContent=s.player.key||'none';
  document.getElementById('htag').style.display=s.hidden?'inline':'none';
  document.getElementById('ctag').style.display=s.monster.chase?'inline':'none';
  document.getElementById('lvv').textContent=s.lvl;
  document.getElementById('livdisp').textContent='โ™ฅ'.repeat(s.lives)+'โ™ก'.repeat(Math.max(0,3-s.lives));
  document.getElementById('pcnt').textContent=s.solvedPz+'/'+s.totalPz;
  document.getElementById('brcnt').textContent=s.collectedBrushes+'/'+s.totalBrushes;
  document.getElementById('pbar').style.display=s.nearPz&&!s.nearPz.solved?'inline':'none';
}

// โ”€โ”€ Draw โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// Pre-build a lookup: color key โ†’ darkened hex for wall fill, mid hex for edge
function hexDarken(hex,f){
  const r=parseInt(hex.slice(1,3),16),g=parseInt(hex.slice(3,5),16),b=parseInt(hex.slice(5,7),16);
  return '#'+[r,g,b].map(v=>Math.round(v*f).toString(16).padStart(2,'0')).join('');
}

// Precomputed wall tones per color key
const WALL_BASE={},WALL_EDGE={},WALL_BRIGHT={};
for(const k of CKEYS){
  WALL_BASE[k]=hexDarken(COLORS[k],.28);
  WALL_EDGE[k]=hexDarken(COLORS[k],.55);
  WALL_BRIGHT[k]=hexDarken(COLORS[k],.85);
}

function draw(){
  const s=S;
  const now=Date.now();
  ctx.fillStyle='#000';ctx.fillRect(0,0,CV.width,CV.height);
  if(s.flash>0){ctx.fillStyle=`rgba(220,30,30,${s.flash*.5})`;ctx.fillRect(0,0,CV.width,CV.height);}

  for(let r=0;r<ROWS;r++){
    for(let c=0;c<COLS;c++){
      const x=c*TILE,y=r*TILE,cell=s.g[r][c];
      if(cell===T.W){
        const k=s.wallColor[r][c];
        // Base fill โ€” dark shade of the color
        ctx.fillStyle=WALL_BASE[k]||'#111';
        ctx.fillRect(x,y,TILE,TILE);
        // Inner bevel highlight (top-left lighter edge)
        ctx.fillStyle=WALL_EDGE[k]||'#222';
        ctx.fillRect(x+1,y+1,TILE-2,TILE-2);
        // Core face โ€” medium shade
        ctx.fillStyle=WALL_BASE[k]||'#111';
        ctx.fillRect(x+3,y+3,TILE-6,TILE-6);
        // Subtle color tint on face
        ctx.fillStyle=(COLORS[k]||'#888')+'18';
        ctx.fillRect(x+3,y+3,TILE-6,TILE-6);
        // Top-left bevel shine
        ctx.fillStyle=WALL_BRIGHT[k]+'55';
        ctx.fillRect(x+1,y+1,TILE-2,2);
        ctx.fillRect(x+1,y+1,2,TILE-2);
      } else {
        // Floor โ€” very dark, slight grid line
        ctx.fillStyle='#0a0a10';
        ctx.fillRect(x,y,TILE,TILE);
        ctx.fillStyle='#0d0d16';
        ctx.fillRect(x+1,y+1,TILE-1,TILE-1);
      }
    }
  }

  // Doors
  for(const d of s.doors){
    const dx=d.c*TILE,dy=d.r*TILE;
    const held=s.player.key===d.key;
    ctx.save();
    if(held){ctx.shadowColor=d.color;ctx.shadowBlur=14;}
    // Door frame
    ctx.fillStyle=d.color+'33';ctx.fillRect(dx,dy,TILE,TILE);
    ctx.strokeStyle=d.color+(held?'ff':'99');ctx.lineWidth=held?2.5:1.5;
    ctx.strokeRect(dx+1,dy+1,TILE-2,TILE-2);
    // Bars
    ctx.strokeStyle=d.color+(held?'cc':'66');ctx.lineWidth=2;
    for(let bar=1;bar<=3;bar++){
      const bx=dx+bar*(TILE/4);
      ctx.beginPath();ctx.moveTo(bx,dy+2);ctx.lineTo(bx,dy+TILE-2);ctx.stroke();
    }
    // Lock icon when you DON'T have the key
    if(!held){
      ctx.fillStyle=d.color+'cc';
      ctx.font=`bold ${TILE*.32}px Share Tech Mono`;ctx.textAlign='center';ctx.textBaseline='middle';
      ctx.fillText('๐Ÿ”’',dx+TILE/2,dy+TILE/2);
    } else {
      ctx.fillStyle='#fff';
      ctx.font=`bold ${TILE*.28}px Share Tech Mono`;ctx.textAlign='center';ctx.textBaseline='middle';
      ctx.fillText('OPEN',dx+TILE/2,dy+TILE/2);
    }
    ctx.restore();
  }

  // Buckets
  for(const b of s.buckets){
    const bx=b.c*TILE+TILE/2,by=b.r*TILE+TILE/2;
    const near=Math.hypot(s.player.x-b.c-.5,s.player.y-b.r-.5)<.72;
    ctx.save();
    if(near){ctx.shadowColor=b.color;ctx.shadowBlur=16;}
    const bw=TILE*.44,bh=TILE*.4;
    ctx.fillStyle=b.color+'dd';ctx.fillRect(bx-bw/2,by-bh/2+1,bw,bh);
    ctx.fillStyle='rgba(255,255,255,.22)';ctx.fillRect(bx-bw/2,by-bh/2+1,bw,bh*.32);
    ctx.strokeStyle=b.color;ctx.lineWidth=1.5;ctx.strokeRect(bx-bw/2,by-bh/2+1,bw,bh);
    ctx.beginPath();ctx.arc(bx,by-bh/2+1,bw*.32,Math.PI,0);ctx.strokeStyle='#999';ctx.lineWidth=1.2;ctx.stroke();
    ctx.fillStyle='#fff';ctx.font=`bold ${TILE*.22}px Share Tech Mono`;ctx.textAlign='center';ctx.textBaseline='middle';
    ctx.fillText(b.key[0].toUpperCase(),bx,by+2);
    ctx.restore();
  }

  // Paintbrushes (collectibles)
  for(const br of s.brushes){
    if(br.collected)continue;
    const bx=br.c*TILE+TILE/2, by=br.r*TILE+TILE/2;
    const near=Math.hypot(s.player.x-br.c-.5,s.player.y-br.r-.5)<.72;
    const pulse=near?.18+.1*Math.sin(Date.now()/120):0;
    ctx.save();
    ctx.shadowColor='#fff';ctx.shadowBlur=near?16:6;
    // Handle (stick)
    ctx.strokeStyle='#c8a060';ctx.lineWidth=3;ctx.lineCap='round';
    ctx.beginPath();ctx.moveTo(bx+TILE*.18,by+TILE*.28);ctx.lineTo(bx-TILE*.06,by-TILE*.1);ctx.stroke();
    // Metal ferrule
    ctx.strokeStyle='#aaa';ctx.lineWidth=4;
    ctx.beginPath();ctx.moveTo(bx-TILE*.06,by-TILE*.1);ctx.lineTo(bx-TILE*.12,by-TILE*.18);ctx.stroke();
    // Bristles (colorful fan shape)
    const bristleColors=['#e84040','#3a8fe8','#3cd060','#f0c020','#a040e0','#20d0d0'];
    const bc2=bristleColors[Math.floor((br.r*31+br.c*17)%bristleColors.length)];
    ctx.fillStyle=bc2+'ee';
    ctx.beginPath();
    ctx.ellipse(bx-TILE*.18,by-TILE*.28,TILE*.14,TILE*.07,-0.6,0,Math.PI*2);
    ctx.fill();
    ctx.strokeStyle='#fff4';ctx.lineWidth=1;ctx.stroke();
    // Sparkle when nearby
    if(near){
      ctx.fillStyle='#ffffffcc';
      ctx.beginPath();ctx.arc(bx-TILE*.1,by-TILE*.38,2,0,Math.PI*2);ctx.fill();
      ctx.beginPath();ctx.arc(bx+TILE*.08,by-TILE*.18,1.5,0,Math.PI*2);ctx.fill();
    }
    ctx.restore();
  }

  // Puzzle panels
  for(const pz of s.puzzles){
    const px=pz.c*TILE,py=pz.r*TILE;
    const near=s.nearPz===pz;
    ctx.save();
    if(pz.solved){
      ctx.fillStyle='#0a1a0a';ctx.fillRect(px,py,TILE,TILE);
      ctx.fillStyle='#3cd060';ctx.font=`${TILE*.62}px monospace`;ctx.textAlign='center';ctx.textBaseline='middle';
      ctx.fillText('โœ“',px+TILE/2,py+TILE/2+1);
    }else{
      if(near){ctx.shadowColor='#fc0';ctx.shadowBlur=20;}
      ctx.fillStyle=near?'rgba(255,192,0,.18)':'rgba(255,192,0,.07)';ctx.fillRect(px,py,TILE,TILE);
      ctx.strokeStyle=near?'#fc0':'#664400';ctx.lineWidth=near?2:1;ctx.strokeRect(px+1,py+1,TILE-2,TILE-2);
      ctx.fillStyle=near?'#fc0':'#664400';ctx.font=`bold ${TILE*.54}px Share Tech Mono`;ctx.textAlign='center';ctx.textBaseline='middle';
      ctx.fillText('?',px+TILE/2,py+TILE/2+1);
    }
    ctx.restore();
  }

  // Exit tile
  for(let r=0;r<ROWS;r++)for(let c=0;c<COLS;c++){
    if(s.g[r][c]===T.EX){
      const ex=c*TILE,ey=r*TILE;
      ctx.save();
      if(s.exitUnlocked){ctx.shadowColor='#ffe87a';ctx.shadowBlur=22+6*Math.sin(now/400);}
      ctx.fillStyle=s.exitUnlocked?'rgba(255,232,100,.2)':'rgba(50,50,50,.15)';ctx.fillRect(ex,ey,TILE,TILE);
      ctx.strokeStyle=s.exitUnlocked?'#ffe87a':'#444';ctx.lineWidth=2;ctx.strokeRect(ex+1,ey+1,TILE-2,TILE-2);
      ctx.fillStyle=s.exitUnlocked?'#ffe87a':'#555';ctx.font=`${TILE*.3}px Share Tech Mono`;ctx.textAlign='center';ctx.textBaseline='middle';
      ctx.fillText(s.exitUnlocked?'EXIT':'LOCK',ex+TILE/2,ey+TILE/2);
      ctx.restore();
    }
  }

  drawMonster();
  drawPlayer();
  drawMinimap();
}

function drawMonster(){
  const m=S.monster,mx=m.x*TILE,my=m.y*TILE,sz=TILE*.88;
  ctx.save();
  const stun=m.stunT>0;
  const cc=stun?'#444':(m.chase?'#ff1a00':'#c8c8c8');
  if(m.chase&&!stun){ctx.shadowColor='#ff1a00';ctx.shadowBlur=20;}
  ctx.strokeStyle=cc;ctx.fillStyle=cc;ctx.lineWidth=m.chase&&!stun?2.5:1.8;ctx.lineCap='round';
  ctx.beginPath();ctx.arc(mx,my-sz*.37,sz*.16,0,Math.PI*2);ctx.stroke();
  ctx.beginPath();ctx.moveTo(mx,my-sz*.21);ctx.lineTo(mx,my+sz*.12);ctx.stroke();
  ctx.beginPath();ctx.moveTo(mx-sz*.22,my-sz*.05);ctx.lineTo(mx+sz*.22,my-sz*.05);ctx.stroke();
  ctx.beginPath();ctx.moveTo(mx,my+sz*.12);ctx.lineTo(mx-sz*.16,my+sz*.38);ctx.stroke();
  ctx.beginPath();ctx.moveTo(mx,my+sz*.12);ctx.lineTo(mx+sz*.16,my+sz*.38);ctx.stroke();
  if(m.chase&&!stun){
    ctx.fillStyle='#ff2200';
    ctx.beginPath();ctx.arc(mx-sz*.065,my-sz*.4,sz*.044,0,Math.PI*2);ctx.fill();
    ctx.beginPath();ctx.arc(mx+sz*.065,my-sz*.4,sz*.044,0,Math.PI*2);ctx.fill();
  }
  ctx.restore();
}

function drawPlayer(){
  const p=S.player,px=p.x*TILE,py=p.y*TILE;
  ctx.save();
  if(S.hidden)ctx.globalAlpha=.25;
  ctx.shadowColor=p.color||'#fff';ctx.shadowBlur=S.hidden?3:14;
  ctx.fillStyle=p.color||'#eee';
  ctx.beginPath();ctx.arc(px,py,TILE*.27,0,Math.PI*2);ctx.fill();
  ctx.strokeStyle='rgba(255,255,255,.5)';ctx.lineWidth=1.5;ctx.stroke();
  ctx.restore();
}

function drawMinimap(){
  const s=S,sc=3;
  const mw=COLS*sc,mh=ROWS*sc;
  const mx0=CV.width-mw-7,my0=7;
  ctx.fillStyle='rgba(0,0,0,.8)';ctx.fillRect(mx0-1,my0-1,mw+2,mh+2);
  for(let r=0;r<ROWS;r++)for(let c=0;c<COLS;c++){
    const cell=s.g[r][c];
    if(cell===T.W){
      const k=s.wallColor[r][c];
      ctx.fillStyle=k?COLORS[k]+'99':'#333';
    }else if(cell===T.EX)ctx.fillStyle=s.exitUnlocked?'#ffe87a':'#555';
    else if(cell===T.PZ){const pz=s.puzzles.find(p=>p.r===r&&p.c===c);ctx.fillStyle=pz&&pz.solved?'#3cd060':'#fc0';}
    else ctx.fillStyle='#08080f';
    ctx.fillRect(mx0+c*sc,my0+r*sc,sc,sc);
  }
  for(const b of s.buckets){ctx.fillStyle=b.color;ctx.fillRect(mx0+b.c*sc,my0+b.r*sc,sc,sc);}
  for(const d of s.doors){ctx.fillStyle=d.color;ctx.fillRect(mx0+d.c*sc,my0+d.r*sc,sc,sc);}
  for(const br of s.brushes){if(!br.collected){ctx.fillStyle='#fff';ctx.fillRect(mx0+br.c*sc,my0+br.r*sc,sc,sc);}}
  ctx.fillStyle='#f44';ctx.fillRect(mx0+Math.floor(s.monster.x)*sc,my0+Math.floor(s.monster.y)*sc,sc,sc);
  ctx.fillStyle='#fff';ctx.fillRect(mx0+Math.floor(s.player.x)*sc,my0+Math.floor(s.player.y)*sc,sc,sc);
}

// โ”€โ”€ Game loop โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function loop(ts){
  if(!lastTs)lastTs=ts;
  const dt=Math.min((ts-lastTs)/1000,.05);lastTs=ts;
  update(dt);draw();raf=requestAnimationFrame(loop);
}

function showOvl(title,msg,color,btn,cb){
  const o=document.getElementById('ovl');
  document.getElementById('otitle').textContent=title;
  document.getElementById('otitle').style.color=color;
  document.getElementById('omsg').textContent=msg;
  document.getElementById('obtn').textContent=btn;
  document.getElementById('obtn').onclick=()=>{o.style.display='none';cb();};
  o.style.display='flex';
}

// โ”€โ”€ Puzzle system โ€” 4 physical puzzle types โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// Colors used across puzzles
const PC=['#e84040','#3a8fe8','#3cd060','#f0c020','#a040e0','#e87020'];

function openPuzzle(pz){
  if(pz.solved)return;
  S.phase='puzzle';
  const pc=document.getElementById('pc');pc.innerHTML='';
  const lvl=S.lvl;
  const titles={sequence:'SIMON SAYS',wires:'WIRE MATCH',tiles:'SLIDE TILES',lock:'PATTERN LOCK'};
  document.getElementById('ptitle').textContent=titles[pz.type]||'PUZZLE';

  if(pz.type==='sequence') buildSequencePuzzle(pc,pz,lvl);
  else if(pz.type==='wires') buildWiresPuzzle(pc,pz,lvl);
  else if(pz.type==='tiles') buildTilesPuzzle(pc,pz,lvl);
  else if(pz.type==='lock') buildLockPuzzle(pc,pz,lvl);

  document.getElementById('povl').style.display='flex';
}

// โ”€โ”€ 1. SIMON SAYS: watch flashing sequence, repeat it exactly โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function buildSequencePuzzle(pc,pz,lvl){
  const n=Math.min(4+lvl,6); // 4โ€“6 buttons
  const len=4+Math.min(lvl,5); // 5โ€“9 steps
  const cols=PC.slice(0,n);
  const seq=Array.from({length:len},()=>Math.floor(Math.random()*n));
  let inp=[],locked=true;

  document.getElementById('pdesc').textContent=`Watch the ${len}-step sequence, then repeat it. Wrong input resets.`;

  const progress=document.createElement('div');
  progress.style.cssText='font-size:11px;color:#666;margin:4px 0;letter-spacing:1px;';
  progress.textContent='โ€” watch โ€”';
  pc.appendChild(progress);

  const grid=document.createElement('div');
  grid.style.cssText=`display:grid;grid-template-columns:repeat(${Math.min(n,3)},1fr);gap:8px;margin-top:8px;`;
  const btns=[];
  for(let i=0;i<n;i++){
    const btn=document.createElement('button');
    btn.style.cssText=`width:58px;height:58px;border-radius:8px;border:2px solid ${cols[i]}44;background:${cols[i]}22;cursor:pointer;transition:all .1s;`;
    btn.onclick=()=>{
      if(locked)return;
      inp.push(i);
      btn.style.background=cols[i]+'cc';
      setTimeout(()=>btn.style.background=cols[i]+'22',150);
      const idx=inp.length-1;
      if(inp[idx]!==seq[idx]){
        inp=[];progress.textContent='โœ— WRONG โ€” watch again';progress.style.color='#f44';
        setTimeout(()=>{locked=true;progress.style.color='#666';playSeq();},800);
      } else if(inp.length===len){
        progress.textContent='โœ“ CORRECT!';progress.style.color='#4f4';
        setTimeout(()=>solvePz(pz),500);
      } else {
        progress.textContent=`${inp.length} / ${len}`;progress.style.color='#fc0';
      }
    };
    btns.push(btn);grid.appendChild(btn);
  }
  pc.appendChild(grid);

  function playSeq(){
    locked=true;inp=[];
    let step=0;
    function flash(){
      if(step>=len){locked=false;progress.textContent=`0 / ${len}`;progress.style.color='#fc0';return;}
      const i=seq[step];
      setTimeout(()=>{
        btns[i].style.background=cols[i]+'ff';btns[i].style.transform='scale(1.15)';
        setTimeout(()=>{btns[i].style.background=cols[i]+'22';btns[i].style.transform='scale(1)';step++;flash();},380);
      },520);
    }
    flash();
  }
  setTimeout(playSeq,400);
}

// โ”€โ”€ 2. WIRE MATCH: drag colored wires from left nodes to matching right nodes โ”€
function buildWiresPuzzle(pc,pz,lvl){
  const n=Math.min(3+Math.floor(lvl/2),6);
  const cols=PC.slice(0,n).sort(()=>Math.random()-.5);
  const rightOrder=[...cols].sort(()=>Math.random()-.5);
  const connections={};// leftIdx โ†’ rightIdx

  document.getElementById('pdesc').textContent='Click a left node then a right node to connect matching colors. All pairs must match.';

  const wrap=document.createElement('div');
  wrap.style.cssText='position:relative;display:flex;justify-content:space-between;align-items:center;width:300px;height:'+(n*52+10)+'px;';

  const svg=document.createElementNS('http://www.w3.org/2000/svg','svg');
  svg.style.cssText='position:absolute;inset:0;width:100%;height:100%;pointer-events:none;';
  wrap.appendChild(svg);

  let selectedLeft=null;
  const leftBtns=[],rightBtns=[];

  function redrawLines(){
    svg.innerHTML='';
    for(const[li,ri] of Object.entries(connections)){
      const lb=leftBtns[li],rb=rightBtns[ri];
      const lx=50,rx=250;
      const ly=parseInt(lb.style.top)+14,ry=parseInt(rb.style.top)+14;
      const line=document.createElementNS('http://www.w3.org/2000/svg','line');
      line.setAttribute('x1',lx);line.setAttribute('y1',ly);
      line.setAttribute('x2',rx);line.setAttribute('y2',ry);
      line.setAttribute('stroke',cols[li]);line.setAttribute('stroke-width','3');
      line.setAttribute('opacity','0.85');
      svg.appendChild(line);
    }
  }

  function checkDone(){
    if(Object.keys(connections).length<n)return;
    for(const[li,ri] of Object.entries(connections)){
      if(cols[parseInt(li)]!==rightOrder[parseInt(ri)])return;
    }
    setTimeout(()=>solvePz(pz),400);
  }

  const leftCol=document.createElement('div');leftCol.style.cssText='display:flex;flex-direction:column;gap:6px;';
  const rightCol=document.createElement('div');rightCol.style.cssText='display:flex;flex-direction:column;gap:6px;';

  for(let i=0;i<n;i++){
    const lb=document.createElement('div');
    lb.style.cssText=`width:28px;height:28px;border-radius:50%;background:${cols[i]};border:2px solid #fff4;cursor:pointer;position:relative;top:0;`;
    lb.dataset.i=i;
    lb.onclick=()=>{
      leftBtns.forEach(b=>b.style.outline='none');
      selectedLeft=i;lb.style.outline='2px solid #fff';
    };
    leftBtns.push(lb);leftCol.appendChild(lb);

    const rb=document.createElement('div');
    rb.style.cssText=`width:28px;height:28px;border-radius:50%;background:${rightOrder[i]};border:2px solid #fff4;cursor:pointer;`;
    rb.dataset.i=i;
    rb.onclick=()=>{
      if(selectedLeft===null)return;
      // Remove existing connection from this left or to this right
      delete connections[selectedLeft];
      for(const k of Object.keys(connections))if(connections[k]===i)delete connections[k];
      connections[selectedLeft]=i;
      selectedLeft=null;leftBtns.forEach(b=>b.style.outline='none');
      redrawLines();checkDone();
    };
    rightBtns.push(rb);rightCol.appendChild(rb);
  }

  // Position nodes by setting top on each
  leftBtns.forEach((b,i)=>{b.style.top=(i*52)+'px';b.style.position='absolute';b.style.left='22px';});
  rightBtns.forEach((b,i)=>{b.style.top=(i*52)+'px';b.style.position='absolute';b.style.right='22px';});

  // Use absolute positioning in wrap
  leftBtns.forEach(b=>wrap.appendChild(b));
  rightBtns.forEach(b=>wrap.appendChild(b));

  pc.appendChild(wrap);
}

// โ”€โ”€ 3. SLIDE TILES: 3ร—3 sliding puzzle โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function buildTilesPuzzle(pc,pz,lvl){
  const sz=3;
  const total=sz*sz; // 9 cells
  // Goal: [1,2,3,4,5,6,7,8,0] โ€” numbers 1-8 in order, blank (0) at end
  let tiles=Array.from({length:total},(_,i)=>(i+1)%total); // [1,2,3,4,5,6,7,8,0]

  function isSolved(){return tiles.every((v,i)=>v===(i+1)%total);}

  // Count inversions for solvability (standard 8-puzzle check)
  function solvable(arr){
    const t=arr.filter(x=>x!==0);
    let inv=0;
    for(let i=0;i<t.length-1;i++)for(let j=i+1;j<t.length;j++)if(t[i]>t[j])inv++;
    return inv%2===0; // 3x3: solvable iff even inversions
  }

  function shuffle(){
    do{
      for(let i=total-1;i>0;i--){
        const j=Math.floor(Math.random()*(i+1));
        [tiles[i],tiles[j]]=[tiles[j],tiles[i]];
      }
    }while(!solvable(tiles)||isSolved());
  }
  shuffle();

  document.getElementById('pdesc').textContent='Slide tiles into order 1โ€“8, blank tile at bottom-right.';

  const tileColors=['#e84040','#3a8fe8','#3cd060','#f0c020','#a040e0','#e87020','#20d0d0','#e040a0'];
  const tsize=80;
  const board=document.createElement('div');
  board.style.cssText=`display:grid;grid-template-columns:repeat(3,${tsize}px);gap:3px;background:#111;border:2px solid #333;padding:3px;border-radius:4px;`;

  function render(){
    board.innerHTML='';
    for(let i=0;i<total;i++){
      const v=tiles[i];
      const cell=document.createElement('div');
      cell.style.cssText=`width:${tsize}px;height:${tsize}px;display:flex;align-items:center;justify-content:center;font-size:${tsize*.35}px;font-weight:bold;border-radius:3px;font-family:Share Tech Mono,monospace;user-select:none;transition:background .08s;`;
      if(v===0){
        cell.style.cssText+=`cursor:default;background:#0a0a0a;`;
      } else {
        cell.style.cssText+=`cursor:pointer;background:${tileColors[(v-1)%tileColors.length]}cc;color:#fff;text-shadow:0 1px 3px #0008;`;
        cell.textContent=v;
        cell.onclick=()=>{
          const blank=tiles.indexOf(0);
          const br=Math.floor(blank/sz), bc=blank%sz;
          const tr2=Math.floor(i/sz), tc2=i%sz;
          const adj=(Math.abs(br-tr2)===1&&bc===tc2)||(Math.abs(bc-tc2)===1&&br===tr2);
          if(!adj)return;
          [tiles[blank],tiles[i]]=[tiles[i],tiles[blank]];
          render();
          if(isSolved()){
            board.style.border='2px solid #4f4';
            setTimeout(()=>solvePz(pz),400);
          }
        };
      }
      board.appendChild(cell);
    }
  }
  render();
  pc.appendChild(board);
}

// โ”€โ”€ 4. PATTERN LOCK: recreate a shown grid pattern by clicking cells โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function buildLockPuzzle(pc,pz,lvl){
  const sz=Math.min(3+Math.floor(lvl/3),5); // 3ร—5 grid size
  const total=sz*sz;
  const numOn=Math.min(4+lvl,Math.floor(total*.65));
  // Generate target pattern
  const target=Array(total).fill(false);
  const idxs=[...Array(total).keys()].sort(()=>Math.random()-.5).slice(0,numOn);
  idxs.forEach(i=>target[i]=true);

  document.getElementById('pdesc').textContent=`Memorize the pattern, then re-draw it exactly. ${sz}ร—${sz} grid, ${numOn} cells.`;

  const user=Array(total).fill(false);
  let phase='show'; // 'show' โ†’ 'draw'
  let countdown=3;

  const status=document.createElement('div');
  status.style.cssText='font-size:12px;color:#fc0;letter-spacing:1px;margin-bottom:6px;';
  status.textContent='Memorize this pattern...';
  pc.appendChild(status);

  const csize=52;
  const grid=document.createElement('div');
  grid.style.cssText=`display:grid;grid-template-columns:repeat(${sz},${csize}px);gap:4px;`;

  const cells=[];
  for(let i=0;i<total;i++){
    const c=document.createElement('div');
    c.style.cssText=`width:${csize}px;height:${csize}px;border-radius:5px;border:1px solid #333;cursor:default;transition:background .12s;`;
    c.onclick=()=>{
      if(phase!=='draw')return;
      user[i]=!user[i];
      c.style.background=user[i]?'#f0c020cc':'#111';
      // Check
      if(user.every((v,idx)=>v===target[idx]))setTimeout(()=>solvePz(pz),350);
    };
    cells.push(c);grid.appendChild(c);
  }
  pc.appendChild(grid);

  function showPattern(){
    cells.forEach((c,i)=>{c.style.background=target[i]?'#f0c020cc':'#111';c.style.cursor='default';});
  }
  function hidePattern(){
    cells.forEach(c=>{c.style.background='#111';c.style.cursor='pointer';});
    phase='draw';status.textContent='Now re-draw it!';status.style.color='#4f4';
  }

  // Show for 2+lvl seconds then hide
  showPattern();
  const showTime=Math.max(1500,3000-lvl*200);
  let t=Math.ceil(showTime/1000);
  status.textContent=`Memorize โ€” ${t}s`;
  const tick=setInterval(()=>{
    t--;
    if(t>0)status.textContent=`Memorize โ€” ${t}s`;
    else{clearInterval(tick);hidePattern();}
  },1000);
  setTimeout(hidePattern,showTime);
}

function solvePz(pz){
  pz.solved=true;S.solvedPz++;
  if(S.solvedPz>=S.totalPz&&S.collectedBrushes>=S.totalBrushes) S.exitUnlocked=true;
  document.getElementById('povl').style.display='none';
  S.phase='play';updateHUD();
}
document.getElementById('pclose').onclick=()=>{document.getElementById('povl').style.display='none';S.phase='play';};

// โ”€โ”€ Input โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
document.addEventListener('keydown',e=>{
  keys[e.key]=true;
  if(['ArrowUp','ArrowDown','ArrowLeft','ArrowRight',' '].includes(e.key))e.preventDefault();
  if((e.key==='e'||e.key==='E')&&S&&S.nearPz&&S.phase==='play')openPuzzle(S.nearPz);
});
document.addEventListener('keyup',e=>{keys[e.key]=false;});

// โ”€โ”€ Boot โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
document.getElementById('obtn').onclick=()=>{
  document.getElementById('ovl').style.display='none';
  S=initState(1);updateHUD();lastTs=null;raf=requestAnimationFrame(loop);
};
</script>
</body>
</html>

Game Source: Color or Die

Creator: BoldCobra33

Libraries: none

Complexity: complex (1292 lines, 55.1 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: color-or-die-boldcobra33" to link back to the original. Then publish at arcadelab.ai/publish.