Color or Die
by BoldCobra331292 lines55.1 KB
<!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 | walk into bucket to grab color | E โ solve puzzle | collect all ๐ brushes + solve puzzles to unlock EXIT | 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.