classic game; by mnf
Prompt
Implement a classic pipe puzzle. A player selects size, eg 5x5 and difficulty (how many tiles is wrongly rotated or possible almost correct solutions). It generates a puzzle from tiles (empty, straight pipe, L pipe, T pipe; unchangeable sources and goals [sinks] and fail tiles [water hitting it means instant fail]), clicking on tiles rotates them. A run button simulates (gradually, animate) flow of water/liquid through pipes. Pipes leaking water (eg T shape with water not connected by all 3 sides), not filling all sinks or hitting a fail tile is a solution fail. A player then can try to fix it and run again. On a victory do magenta confetti swirling in a spiral.
A system prompt was added to support web rendering
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Pipe Dream Puzzle</title> <style> body{font-family:Arial,Helvetica,sans-serif;background:#f0f8ff;margin:0;padding:20px;text-align:center;user-select:none} h1{margin:10px 0 15px} #controls{margin-bottom:15px} #grid{margin:0 auto 15px;display:grid;gap:4px} .tile{width:48px;height:48px;border:2px solid #333;cursor:pointer;position:relative;background:#111;background-size:cover} .empty{background:#222} .empty::after{content:"";position:absolute;inset:0;border:3px solid #111} .highlight{box-shadow:0 0 8px 2px magenta} .water{background:#00bcd4;opacity:.85} .fail{background:#c00 !important} #message{height:24px;font-weight:700;margin-bottom:10px;font-size:18px} #victoryCanvas{position:fixed;top:0;left:0;pointer-events:none;z-index:999} </style> </head> <body> <h1>Pipe Dream Puzzle</h1> <div id="controls"> Size: <select id="sizeSelect"><option>5</option><option>6</option><option>7</option></select> Difficulty: <select id="diffSelect"> <option>Beginner</option><option>Normal</option><option>Hard</option> </select> <button id="newBtn">New Game</button> <button id="runBtn">Run</button> </div> <div id="message"></div> <div id="grid"></div> <canvas id="victoryCanvas"></canvas> <script> const sizeSel=document.getElementById('sizeSelect'); const diffSel=document.getElementById('diffSelect'); const newBtn=document.getElementById('newBtn'); const runBtn=document.getElementById('runBtn'); const msg=document.getElementById('message'); const grid=document.getElementById('grid'); const vic=document.getElementById('victoryCanvas'); // --- TILE DEFINITIONS --- const TILES = { EMPTY:0, // render: black hole STRAIGHT:1, // render: │ LPIPE:2, // render: └ TPIPE:3, // render: ├ SOURCE:4, // ┴ always fixed out UP SINK:5, // any entry (caps) FAIL:6 // red X }; // rotation counts: 0-3 CCW 90deg increments const PIPE_PATHS = { [TILES.STRAIGHT]: [[0,1,0,0],[0,0,0,0],[0,1,0,0],[0,0,0,0]], [TILES.LPIPE]: [[0,1,0,0],[0,0,0,0],[1,0,0,0],[0,0,0,0]], [TILES.TPIPE]: [[0,1,0,1],[0,0,0,1],[1,0,0,1],[1,0,1,0]] }; // each tile rot shows which connectors are open [up,right,down,left] let SZ,board=[],dim,flowTimer; newBtn.onclick=initGame; runBtn.onclick=runWater; initGame(); function initGame(){ cancelAnimationFrame(flowTimer); SZ=+sizeSel.value||5; dim=Math.ceil((window.innerHeight-200)/SZ); grid.style.gridTemplateColumns=`repeat(${SZ}, 48px)`; board=[]; msg.textContent='',msg.className=''; grid.innerHTML=''; genLevel(); renderBoard(); } function genLevel(){ const SRC_CNT=Math.min(1,SZ-3); // number of fixed sources const SINK_CNT=SRC_CNT+1; // usually 1 more than sources const difficulty={ Beginner:{rot:20,wrong:0}, Normal:{rot:45,wrong:10}, Hard:{rot:55,wrong:25} }[diffSel.value]; // create random pipes for(let i=0;i<SZ*SZ;i++){ if([0,1,2,3].includes(i%SZ) && i<SZ*2-SZ/2 && board.filter(t=>t.type===TILES.SOURCE).length<SRC_CNT){ board.push({type:TILES.SOURCE,rot:3}); // ┴, UP }else if([SZ-1,SZ-2].includes(i%SZ) && i>=SZ*SZ-SZ*2 && board.filter(t=>t.type===TILES.SINK).length<SINK_CNT){ board.push({type:TILES.SINK,rot:0}); }else if(Math.random()<.05){ board.push({type:TILES.FAIL,rot:0}); }else{ const r=Math.random(); let typ; if(r<.1) typ=TILES.EMPTY; else if(r<.5) typ=TILES.STRAIGHT; else if(r<.8) typ=TILES.LPIPE; else typ=TILES.TPIPE; board.push({type:typ,rot:0}); } } // scramble rotations const rotCount=Math.floor(difficulty.rot/100*SZ*SZ); const scrambleIndices=new Array(SZ*SZ).fill().map((_,i)=>i).filter(i=>[TILES.STRAIGHT,TILES.LPIPE,TILES.TPIPE].includes(board[i].type)); for(let i=0;i<rotCount;i++){ const idx=scrambleIndices.splice(Math.floor(Math.random()*scrambleIndices.length),1)[0]; if(idx) board[idx].rot=(board[idx].rot+Math.floor(Math.random()*3)+1)%4; } // create near miss if wrong>0 const wrongTiles=Math.floor(difficulty.wrong/100*board.filter(t=>[TILES.STRAIGHT,TILES.LPIPE,TILES.TPIPE].includes(t.type)).length); for(let i=0;i<wrongTiles;i++){ const candidates=board.map((t,i)=>({t,i})).filter(({t,i})=>[TILES.STRAIGHT,TILES.LPIPE,TILES.TPIPE].includes(t.type)); if(candidates.length===0)break; const idx=candidates[Math.floor(Math.random()*candidates.length)].i; board[idx].type=(board[idx].type+Math.floor(Math.random()*2)+1)%3+1; board[idx].rot=Math.floor(Math.random()*4); } } function renderBoard(){ grid.innerHTML=''; for(let i=0;i<board.length;i++){ const cell=document.createElement('div'); cell.className="tile"; cell.dataset.idx=i; const tile=board[i]; const rot=tile.rot; const typ=tile.type; if(typ===TILES.EMPTY) cell.classList.add('empty'); if(typ===TILES.FAIL) cell.classList.add('fail'); if(typ!==TILES.EMPTY){ const canv=document.createElement('canvas'); canv.width=canv.height=48; const ctx=canv.getContext('2d'); ctx.translate(24,24); for(let k=0;k<rot;k++) ctx.rotate(Math.PI/2); drawTile(ctx,typ); cell.appendChild(canv); } cell.onclick=()=>{rotateTile(i); renderBoard();}; grid.appendChild(cell); } } // Share identical colors between pipe+canvas & dynamic flow function drawTile(ctx,typ){ ctx.strokeStyle='#00ca27'; ctx.lineWidth=6; ctx.lineCap='round'; if(typ===TILES.STRAIGHT){ ctx.beginPath();ctx.moveTo(0,-18);ctx.lineTo(0,18);ctx.stroke(); } if(typ===TILES.LPIPE){ ctx.beginPath();ctx.moveTo(0,-18);ctx.lineTo(0,0);ctx.lineTo(18,0);ctx.stroke(); } if(typ===TILES.TPIPE){ ctx.beginPath();ctx.moveTo(0,-18);ctx.lineTo(0,18);ctx.moveTo(-18,0);ctx.lineTo(0,0);ctx.lineTo(18,0);ctx.stroke(); } if(typ===TILES.SOURCE){ ctx.fillStyle='#00bcd4';ctx.beginPath();ctx.arc(0,15,12,0,7);ctx.fill(); drawTile(ctx,TILES.STRAIGHT); } if(typ===TILES.SINK){ ctx.fillStyle='#0288d1';ctx.beginPath(); ctx.arc(0,0,10,0,7);ctx.fill(); ctx.beginPath();ctx.moveTo(-6,6);ctx.lineTo(6,6);ctx.moveTo(6,6);ctx.lineTo(6,-6);ctx.moveTo(6,-6);ctx.lineTo(-6,-6);ctx.lineTo(-6,6);ctx.stroke(); } if(typ===TILES.FAIL){ ctx.strokeStyle='#c00';ctx.lineWidth=3; ctx.beginPath();ctx.moveTo(-8,-8);ctx.lineTo(8,8);ctx.moveTo(8,-8);ctx.lineTo(-8,8);ctx.stroke(); } } function rotateTile(idx){ const t=board[idx]; if([TILES.SOURCE,TILES.SINK,TILES.FAIL,TILES.EMPTY].includes(t.type)) return; t.rot=(t.rot+1)%4; } ///////////////////////////////////////////////////////////////////////////// // WATER FLOW SIMULATION ///////////////////////////////////////////////////////////////////////////// let queue=[],visited=new Set(),fails=0,sinks=0,sinksFilled=0; function runWater(){ if(flowTimer) cancelAnimationFrame(flowTimer); queue=[];visited.clear();fails=0;sinks=0;sinksFilled=0; const cells=[...document.querySelectorAll('.tile')]; cells.forEach(c=>c.classList.remove('water','fail','highlight')); // find sources board.forEach((t,i)=>{if(t.type===TILES.SOURCE)queue.push({i,dir:2});}); sinks=board.filter(t=>t.type===TILES.SINK).length; if(queue.length===0){msg.textContent='No source found';return;} stepWater(0); } function stepWater(idx){ if(idx>=queue.length){ finishRun(); return; } const {i,dir}=queue[idx]; const cell=board[i]; const tg=document.querySelector(`[data-idx="${i}"]`); tg.classList.add('water'); if(cell.type===TILES.FAIL){fails++;finishRun();return;} if(cell.type===TILES.SINK){sinksFilled++;finishRun();return;} // enumerate exits from current tile const paths=cell.type===TILES.SOURCE?[1,0,0,0]:PIPE_PATHS[cell.type][cell.rot]; const connect=[2,3,0,1]; //opposite direction for next tile connections // propagate to neighbours [[-1,0,0],[0,1,1],[1,0,2],[0,-1,3]].forEach(([dr,dc,exitDir])=>{ const row=Math.floor(i/SZ),col=i%SZ; const nr=row+dr,nc=col+dc; if(nr<0||nc<0||nr>=SZ||nc>=SZ)return; const nextIdx=nr*SZ+nc; if(visited.has(nextIdx)) return; if(paths[exitDir]){ //current cell open at exitDir const nextTile=board[nextIdx]; const nextPaths=nextTile.type===TILES.SINK?[1,1,1,1]:PIPE_PATHS[nextTile.type][nextTile.rot]; if(nextPaths[connect[exitDir]]){ //next cell open opposite queue.push({i:nextIdx,dir:connect[exitDir]}); visited.add(nextIdx); } } }); // animate next step flowTimer=requestAnimationFrame(()=>stepWater(idx+1)); } function finishRun(){ msg.textContent='Checking...'; const newFail=fails; const sF=sinksFilled; setTimeout(()=>{ msg.textContent=''; if(newFail||sF<sinks){ msg.textContent=newFail?"FAIL - pipe broke!":"FAIL - sinks not filled"; msg.style.color='#c00'; }else{ fireworks(); msg.textContent="VICTORY!"; msg.style.color='magenta'; return; } },200); } ///////////////////////////////////////////////////////////////////////////// // VICTORY ANIMATION ///////////////////////////////////////////////////////////////////////////// function fireworks(){ vic.width=window.innerWidth; vic.height=window.innerHeight; const ctx=vic.getContext('2d'); const confetti=new Array(120).fill().map(()=>({ x:Math.random()*vic.width, y:Math.random()*vic.height, vx:Math.random()*5-2.5, vy:Math.random()*3+2, a:Math.random()*Math.PI*2, va:Math.random()*0.1-0.05, size:Math.random()*6+3, hue:Math.floor(Math.random()*40)+270 })); let frame=0,target=120; function loop(){ ctx.clearRect(0,0,vic.width,vic.height); confetti.forEach(c=>{ c.x+=c.vx; c.y+=c.vy+Math.sin(frame*0.05)*0.2; c.a+=c.va; ctx.save(); ctx.translate(c.x,c.y); ctx.rotate(c.a); ctx.fillStyle=`hsl(${c.hue} 100% 70%)`; ctx.fillRect(-c.size/2,-c.size/2,c.size,c.size*1.6); ctx.restore(); }); frame++; if(frame<target)requestAnimationFrame(loop); } loop(); } </script> </body> </html> ```
# Pipe Puzzle Game I'll create a complete pipe puzzle game with customizable grid size and difficulty, animated water flow, and victory celebration with confetti. ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Pipe Puzzle Game</title> <style> * { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } body { background: linear-gradient(135deg, #1a2a6c, #b21f1f, #1a2a6c); min-height: 100vh; display: flex; flex-direction: column; align-items: center; padding: 20px; color: white; } .container { max-width: 1000px; width: 100%; display: flex; flex-direction: column; align-items: center; gap: 20px; } header { text-align: center; padding: 15px; background: rgba(0, 0, 0, 0.3); border-radius: 15px; width: 100%; box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3); } h1 { font-size: 2.8rem; margin-bottom: 10px; text-shadow: 0 0 10px #00bfff, 0 0 20px #00bfff; background: linear-gradient(to right, #00bfff, #ff00ff); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } .subtitle { font-size: 1.2rem; opacity: 0.9; max-width: 600px; margin: 0 auto; } .controls { display: flex; flex-wrap: wrap; gap: 15px; justify-content: center; background: rgba(0, 0, 0, 0.3); padding: 20px; border-radius: 15px; width: 100%; box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3); } .control-group { display: flex; flex-direction: column; align-items: center; } label { margin-bottom: 8px; font-weight: bold; font-size: 1.1rem; } select, button { padding: 12px 20px; border-radius: 50px; border: none; font-size: 1rem; font-weight: bold; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); } select { background: #4a4a8a; color: white; width: 150px; } button { background: linear-gradient(to right, #00bfff, #0077ff); color: white; padding: 12px 30px; font-size: 1.1rem; } button:hover { transform: translateY(-3px); box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3); } button:active { transform: translateY(1px); } #runBtn { background: linear-gradient(to right, #00c853, #009624); } #resetBtn { background: linear-gradient(to right, #ff4081, #c51162); } .game-container { position: relative; width: 100%; max-width: 600px; background: rgba(0, 0, 0, 0.3); border-radius: 15px; padding: 20px; box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3); } .grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 4px; margin: 0 auto; max-width: 500px; } .tile { aspect-ratio: 1; background: #2c3e50; border-radius: 8px; display: flex; justify-content: center; align-items: center; cursor: pointer; position: relative; overflow: hidden; box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.5); transition: transform 0.2s ease; } .tile:hover { transform: scale(1.05); box-shadow: 0 0 15px rgba(0, 255, 255, 0.7); } .tile-content { width: 80%; height: 80%; position: relative; } .pipe { stroke: #3498db; stroke-width: 8; fill: none; } .source { fill: #00b894; stroke: #00897b; stroke-width: 2; } .sink { fill: #e17055; stroke: #d63031; stroke-width: 2; } .fail { fill: #d63031; stroke: #b71c1c; stroke-width: 2; } .water { stroke: #00cec9; stroke-width: 5; stroke-linecap: round; } .water-flow { animation: flow 1s linear infinite; } @keyframes flow { 0% { stroke-dashoffset: 20; } 100% { stroke-dashoffset: 0; } } .leak { stroke: #00cec9; stroke-width: 3; animation: drip 1.5s infinite; } @keyframes drip { 0% { opacity: 0; transform: translateY(-10px); } 50% { opacity: 1; } 100% { opacity: 0; transform: translateY(10px); } } .status { text-align: center; font-size: 1.4rem; margin-top: 20px; min-height: 40px; font-weight: bold; text-shadow: 0 0 5px rgba(0, 0, 0, 0.5); } .win-message { color: #00bfff; animation: pulse 1.5s infinite; } @keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.1); } 100% { transform: scale(1); } } .confetti { position: fixed; width: 10px; height: 10px; background-color: #ff00ff; border-radius: 50%; animation: confetti-fall 5s linear forwards; z-index: 1000; } @keyframes confetti-fall { 0% { transform: translateY(-100px) rotate(0deg); opacity: 1; } 100% { transform: translateY(100vh) rotate(720deg); opacity: 0; } } .instructions { background: rgba(0, 0, 0, 0.3); padding: 20px; border-radius: 15px; width: 100%; max-width: 600px; margin-top: 20px; box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3); } .instructions h2 { text-align: center; margin-bottom: 15px; color: #00bfff; } .instructions ul { padding-left: 20px; } .instructions li { margin-bottom: 10px; line-height: 1.5; } .legend { display: flex; justify-content: center; flex-wrap: wrap; gap: 20px; margin-top: 15px; } .legend-item { display: flex; align-items: center; gap: 8px; } .legend-icon { width: 25px; height: 25px; } footer { margin-top: 20px; text-align: center; padding: 15px; font-size: 0.9rem; opacity: 0.8; } </style> </head> <body> <div class="container"> <header> <h1>Pipe Puzzle</h1> <p class="subtitle">Rotate pipes to connect all sources to sinks without hitting fail tiles or leaking!</p> </header> <div class="controls"> <div class="control-group"> <label for="gridSize">Grid Size</label> <select id="gridSize"> <option value="5">5x5</option> <option value="6">6x6</option> <option value="7">7x7</option> </select> </div> <div class="control-group"> <label for="difficulty">Difficulty</label> <select id="difficulty"> <option value="1">Easy</option> <option value="2">Medium</option> <option value="3">Hard</option> </select> </div> <div class="control-group"> <button id="newGameBtn">New Game</button> </div> <div class="control-group"> <button id="runBtn">Run Water</button> </div> <div class="control-group"> <button id="resetBtn">Reset</button> </div> </div> <div class="game-container"> <div class="grid" id="grid"></div> <div class="status" id="status">Click "New Game" to start!</div> </div> <div class="instructions"> <h2>How to Play</h2> <ul> <li>Click on any pipe tile to rotate it 90 degrees clockwise</li> <li>Connect all blue sources to orange sinks using the pipes</li> <li>Red fail tiles will end the game if touched by water</li> <li>Press "Run Water" to test your solution</li> <li>Water must flow without leaks to all sinks for victory</li> </ul> <div class="legend"> <div class="legend-item"> <div class="legend-icon"> <svg viewBox="0 0 50 50"> <circle cx="25" cy="25" r="15" class="source" /> </svg> </div> <span>Source</span> </div> <div class="legend-item"> <div class="legend-icon"> <svg viewBox="0 0 50 50"> <circle cx="25" cy="25" r="15" class="sink" /> </svg> </div> <span>Sink</span> </div> <div class="legend-item"> <div class="legend-icon"> <svg viewBox="0 0 50 50"> <rect x="5" y="5" width="40" height="40" rx="5" class="fail" /> </svg> </div> <span>Fail Tile</span> </div> </div> </div> <footer> Pipe Puzzle Game | Created with HTML, CSS, and JavaScript </footer> </div> <script> // Game constants const TILE_TYPES = { EMPTY: 0, STRAIGHT: 1, L_BEND: 2, T_JUNCTION: 3, SOURCE: 4, SINK: 5, FAIL: 6 }; const DIRECTIONS = { UP: 0, RIGHT: 1, DOWN: 2, LEFT: 3 }; // Game state let gridSize = 5; let difficulty = 1; let grid = []; let solution = []; let isRunning = false; let win = false; // DOM elements const gridElement = document.getElementById('grid'); const statusElement = document.getElementById('status'); const gridSizeSelect = document.getElementById('gridSize'); const difficultySelect = document.getElementById('difficulty'); const newGameBtn = document.getElementById('newGameBtn'); const runBtn = document.getElementById('runBtn'); const resetBtn = document.getElementById('resetBtn'); // Initialize game function init() { gridSize = parseInt(gridSizeSelect.value); difficulty = parseInt(difficultySelect.value); createGrid(); generatePuzzle(); renderGrid(); statusElement.textContent = "Rotate pipes to connect sources to sinks!"; statusElement.className = "status"; win = false; } // Create grid structure function createGrid() { grid = []; gridElement.innerHTML = ''; gridElement.style.gridTemplateColumns = `repeat(${gridSize}, 1fr)`; for (let row = 0; row < gridSize; row++) { grid[row] = []; for (let col = 0; col < gridSize; col++) { grid[row][col] = { type: TILE_TYPES.EMPTY, rotation: 0, connections: [false, false, false, false] // up, right, down, left }; const tile = document.createElement('div'); tile.className = 'tile'; tile.dataset.row = row; tile.dataset.col = col; tile.addEventListener('click', () => rotateTile(row, col)); const tileContent = document.createElement('div'); tileContent.className = 'tile-content'; tile.appendChild(tileContent); gridElement.appendChild(tile); } } } // Generate puzzle function generatePuzzle() { // Clear grid for (let row = 0; row < gridSize; row++) { for (let col = 0; col < gridSize; col++) { grid[row][col] = { type: TILE_TYPES.EMPTY, rotation: 0, connections: [false, false, false, false] }; } } // Place sources const sources = []; const sinks = []; // Place sources on edges sources.push({row: 0, col: Math.floor(gridSize/2), direction: DIRECTIONS.DOWN}); // Place sinks sinks.push({row: gridSize-1, col: Math.floor(gridSize/2), direction: DIRECTIONS.UP}); // Place additional sources and sinks based on difficulty if (difficulty > 1) { sources.push({row: Math.floor(gridSize/2), col: 0, direction: DIRECTIONS.RIGHT}); sinks.push({row: Math.floor(gridSize/2), col: gridSize-1, direction: DIRECTIONS.LEFT}); } if (difficulty > 2) { sources.push({row: gridSize-1, col: Math.floor(gridSize/3), direction: DIRECTIONS.UP}); sinks.push({row: 0, col: Math.floor(2*gridSize/3), direction: DIRECTIONS.DOWN}); } // Place sources and sinks on grid sources.forEach(source => { grid[source.row][source.col] = { type: TILE_TYPES.SOURCE, rotation: source.direction, connections: [false, false, false, false], direction: source.direction }; grid[source.row][source.col].connections[source.direction] = true; }); sinks.forEach(sink => { grid[sink.row][sink.col] = { type: TILE_TYPES.SINK, rotation: sink.direction, connections: [false, false, false, false], direction: sink.direction }; grid[sink.row][sink.col].connections[sink.direction] = true; }); // Create a solution path for each source sources.forEach((source, index) => { const sink = sinks[index % sinks.length]; createSolutionPath(source, sink); }); // Add fail tiles based on difficulty const failTiles = difficulty === 1 ? 1 : difficulty === 2 ? 3 : 5; for (let i = 0; i < failTiles; i++) { let placed = false; while (!placed) { const row = Math.floor(Math.random() * gridSize); const col = Math.floor(Math.random() * gridSize); // Don't place on sources, sinks, or existing fail tiles if (grid[row][col].type === TILE_TYPES.EMPTY) { grid[row][col] = { type: TILE_TYPES.FAIL, rotation: 0, connections: [false, false, false, false] }; placed = true; } } } // Place pipes in remaining empty spaces for (let row = 0; row < gridSize; row++) { for (let col = 0; col < gridSize; col++) { if (grid[row][col].type === TILE_TYPES.EMPTY) { // Randomly place a pipe type const pipeTypes = [TILE_TYPES.STRAIGHT, TILE_TYPES.L_BEND, TILE_TYPES.T_JUNCTION]; const type = pipeTypes[Math.floor(Math.random() * pipeTypes.length)]; const rotation = Math.floor(Math.random() * 4); grid[row][col] = { type: type, rotation: rotation, connections: getConnectionByType(type, rotation) }; } } } // Rotate some tiles based on difficulty to make it challenging const rotations = difficulty === 1 ? 3 : difficulty === 2 ? 7 : 12; for (let i = 0; i < rotations; i++) { let rotated = false; while (!rotated) { const row = Math.floor(Math.random() * gridSize); const col = Math.floor(Math.random() * gridSize); // Only rotate pipes, not sources, sinks, or fail tiles if (grid[row][col].type !== TILE_TYPES.SOURCE && grid[row][col].type !== TILE_TYPES.SINK && grid[row][col].type !== TILE_TYPES.FAIL) { grid[row][col].rotation = (grid[row][col].rotation + 1) % 4; grid[row][col].connections = getConnectionByType(grid[row][col].type, grid[row][col].rotation); rotated = true; } } } // Save initial state for reset solution = JSON.parse(JSON.stringify(grid)); } // Create a solution path between source and sink function createSolutionPath(source, sink) { // Simple path creation - go down first, then to target column, then to target row let row = source.row; let col = source.col; // Move down until we're aligned with target row or at bottom while (row < sink.row) { row++; if (grid[row][col].type === TILE_TYPES.EMPTY) { // Place a vertical straight pipe grid[row][col] = { type: TILE_TYPES.STRAIGHT, rotation: 0, // Vertical connections: [true, false, true, false] }; } } // Move horizontally to target column while (col < sink.col) { col++; if (grid[row][col].type === TILE_TYPES.EMPTY) { // Place a horizontal straight pipe grid[row][col] = { type: TILE_TYPES.STRAIGHT, rotation: 1, // Horizontal connections: [false, true, false, true] }; } } while (col > sink.col) { col--; if (grid[row][col].type === TILE_TYPES.EMPTY) { // Place a horizontal straight pipe grid[row][col] = { type: TILE_TYPES.STRAIGHT, rotation: 1, // Horizontal connections: [false, true, false, true] }; } } // Move vertically to target row while (row > sink.row) { row--; if (grid[row][col].type === TILE_TYPES.EMPTY) { // Place a vertical straight pipe grid[row][col] = { type: TILE_TYPES.STRAIGHT, rotation: 0, // Vertical connections: [true, false, true, false] }; } } // Adjust connections at the ends // Source already connected // Connect sink grid[sink.row][sink.col].connections = [false, false, false, false]; grid[sink.row][sink.col].connections[(sink.direction + 2) % 4] = true; } // Get connections by tile type and rotation function getConnectionByType(type, rotation) { const connections = [false, false, false, false]; switch (type) { case TILE_TYPES.STRAIGHT: if (rotation % 2 === 0) { // Vertical connections[DIRECTIONS.UP] = true; connections[DIRECTIONS.DOWN] = true; } else { // Horizontal connections[DIRECTIONS.LEFT] = true; connections[DIRECTIONS.RIGHT] = true; } break; case TILE_TYPES.L_BEND: connections[(DIRECTIONS.UP + rotation) % 4] = true; connections[(DIRECTIONS.RIGHT + rotation) % 4] = true; break; case TILE_TYPES.T_JUNCTION: connections[(DIRECTIONS.UP + rotation) % 4] = true; connections[(DIRECTIONS.RIGHT + rotation) % 4] = true; connections[(DIRECTIONS.LEFT + rotation) % 4] = true; break; } return connections; } // Render grid function renderGrid() { for (let row = 0; row < gridSize; row++) { for (let col = 0; col < gridSize; col++) { const tile = grid[row][col]; const tileElement = document.querySelector(`.tile[data-row="${row}"][data-col="${col}"] .tile-content`); tileElement.innerHTML = ''; // Rotate tile for visual representation tileElement.style.transform = `rotate(${tile.rotation * 90}deg)`; // Render tile based on type switch (tile.type) { case TILE_TYPES.EMPTY: tileElement.innerHTML = `<rect x="5" y="5" width="40" height="40" rx="5" fill="#34495e" />`; break; case TILE_TYPES.STRAIGHT: if (tile.rotation % 2 === 0) { // Vertical pipe tileElement.innerHTML = `<line x1="25" y1="5" x2="25" y2="45" class="pipe" />`; } else { // Horizontal pipe tileElement.innerHTML = `<line x1="5" y1="25" x2="45" y2="25" class="pipe" />`; } break; case TILE_TYPES.L_BEND: const lShape = (tile.rotation % 4); if (lShape === 0) { tileElement.innerHTML = `<path d="M25,5 C25,5 25,25 45,25" class="pipe" />`; } else if (lShape === 1) { tileElement.innerHTML = `<path d="M25,45 C25,45 25,25 45,25" class="pipe" />`; } else if (lShape === 2) { tileElement.innerHTML = `<path d="M25,45 C25,45 25,25 5,25" class="pipe" />`; } else { tileElement.innerHTML = `<path d="M25,5 C25,5 25,25 5,25" class="pipe" />`; } break; case TILE_TYPES.T_JUNCTION: const tShape = (tile.rotation % 4); if (tShape === 0) { tileElement.innerHTML = ` <line x1="25" y1="5" x2="25" y2="25" class="pipe" /> <line x1="5" y1="25" x2="45" y2="25" class="pipe" /> `; } else if (tShape === 1) { tileElement.innerHTML = ` <line x1="25" y1="5" x2="25" y2="45" class="pipe" /> <line x1="25" y1="25" x2="45" y2="25" class="pipe" /> `; } else if (tShape === 2) { tileElement.innerHTML = ` <line x1="25" y1="45" x2="25" y2="25" class="pipe" /> <line x1="5" y1="25" x2="45" y2="25" class="pipe" /> `; } else { tileElement.innerHTML = ` <line x1="25" y1="5" x2="25" y2="45" class="pipe" /> <line x1="25" y1="25" x2="5" y2="25" class="pipe" /> `; } break; case TILE_TYPES.SOURCE: tileElement.innerHTML = `<circle cx="25" cy="25" r="15" class="source" />`; break; case TILE_TYPES.SINK: tileElement.innerHTML = `<circle cx="25" cy="25" r="15" class="sink" />`; break; case TILE_TYPES.FAIL: tileElement.innerHTML = `<rect x="5" y="5" width="40" height="40" rx="5" class="fail" />`; break; } } } } // Rotate tile function rotateTile(row, col) { if (isRunning || win) return; const tile = grid[row][col]; // Only rotate pipes, not sources, sinks, or fail tiles if (tile.type !== TILE_TYPES.SOURCE && tile.type !== TILE_TYPES.SINK && tile.type !== TILE_TYPES.FAIL) { tile.rotation = (tile.rotation + 1) % 4; tile.connections = getConnectionByType(tile.type, tile.rotation); renderGrid(); } } // Run water simulation function runWater() { if (isRunning || win) return; isRunning = true; statusElement.textContent = "Water flowing..."; statusElement.className = "status"; // Clear any previous water or leaks document.querySelectorAll('.water, .leak').forEach(el => el.remove()); // Find all sources const sources = []; for (let row = 0; row < gridSize; row++) { for (let col = 0; col < gridSize; col++) { if (grid[row][col].type === TILE_TYPES.SOURCE) { sources.push({row, col}); } } } // Track which sinks are filled const filledSinks = new Set(); let allSinksFilled = false; let failHit = false; // Simulate from each source sources.forEach(source => { simulateFromSource(source, filledSinks); }); // Check if all sinks were filled setTimeout(() => { isRunning = false; // Count sinks let sinkCount = 0; for (let row = 0; row < gridSize; row++) { for (let col = 0; col < gridSize; col++) { if (grid[row][col].type === TILE_TYPES.SINK) { sinkCount++; } } } if (filledSinks.size === sinkCount && !failHit) { win = true; statusElement.textContent = "Success! All sinks filled!"; statusElement.className = "status win-message"; createConfetti(); } else if (failHit) { statusElement.textContent = "Oops! Water hit a fail tile."; statusElement.className = "status"; } else { statusElement.textContent = "Not all sinks were filled. Try again!"; statusElement.className = "status"; } }, 1500); function simulateFromSource(source, filledSinks) { const direction = source.direction; const nextRow = source.row + (direction === DIRECTIONS.DOWN ? 1 : direction === DIRECTIONS.UP ? -1 : 0); const nextCol = source.col + (direction === DIRECTIONS.RIGHT ? 1 : direction === DIRECTIONS.LEFT ? -1 : 0); if (isValidPosition(nextRow, nextCol)) { followPath(nextRow, nextCol, (direction + 2) % 4, filledSinks, false); } else { createLeak(source.row, source.col, direction); } } function followPath(row, col, fromDirection, filledSinks, leak) { if (!isValidPosition(row, col) || failHit) return; const tile = grid[row][col]; // Check for fail tile if (tile.type === TILE_TYPES.FAIL) { createLeak(row, col, fromDirection); failHit = true; return; } // Check if water can enter this tile from the incoming direction if (!tile.connections[fromDirection]) { // Leak occurs createLeak(row, col, fromDirection); return; } // Draw water entering tile drawWater(row, col, fromDirection); // Check if this is a sink if (tile.type === TILE_TYPES.SINK) { filledSinks.add(`${row},${col}`); // Check if this is the correct sink if (tile.connections[(fromDirection + 2) % 4]) { // Water enters correctly return; } else { // Leak from sink createLeak(row, col, fromDirection); return; } } // Find the exit direction let exitDirection = -1; for (let i = 0; i < 4; i++) { if (i !== fromDirection && tile.connections[i]) { exitDirection = i; break; } } // If no exit found (dead end), leak if (exitDirection === -1) { createLeak(row, col, fromDirection); return; } // Recursively follow to next tile setTimeout(() => { const nextRow = row + (exitDirection === DIRECTIONS.DOWN ? 1 : exitDirection === DIRECTIONS.UP ? -1 : 0); const nextCol = col + (exitDirection === DIRECTIONS.RIGHT ? 1 : exitDirection === DIRECTIONS.LEFT ? -1 : 0); if (isValidPosition(nextRow, nextCol)) { followPath(nextRow, nextCol, (exitDirection + 2) % 4, filledSinks, false); } else { createLeak(row, col, exitDirection); } }, 200); } function createLeak(row, col, direction) { const tile = document.querySelector(`.tile[data-row="${row}"][data-col="${col}"] .tile-content`); const svgNS = "http://www.w3.org/2000/svg"; const leak = document.createElementNS(svgNS, "circle"); leak.setAttribute("cx", "25"); leak.setAttribute("cy", "25"); leak.setAttribute("r", "3"); leak.setAttribute("class", "leak"); // Position based on direction let x = 25; let y = 25; if (direction === DIRECTIONS.UP) y = 5; else if (direction === DIRECTIONS.RIGHT) x = 45; else if (direction === DIRECTIONS.DOWN) y = 45; else if (direction === DIRECTIONS.LEFT) x = 5; leak.setAttribute("cx", x); leak.setAttribute("cy", y); tile.appendChild(leak); } function drawWater(row, col, direction) { const tile = document.querySelector(`.tile[data-row="${row}"][data-col="${col}"] .tile-content`); const svgNS = "http://www.w3.org/2000/svg"; const line = document.createElementNS(svgNS, "line"); line.setAttribute("class", "water water-flow"); line.setAttribute("x1", "25"); line.setAttribute("y1", "25"); line.setAttribute("x2", "25"); line.setAttribute("y2", "25"); // Set end position based on direction if (direction === DIRECTIONS.UP) { line.setAttribute("y2", "5"); } else if (direction === DIRECTIONS.RIGHT) { line.setAttribute("x2", "45"); } else if (direction === DIRECTIONS.DOWN) { line.setAttribute("y2", "45"); } else if (direction === DIRECTIONS.LEFT) { line.setAttribute("x2", "5"); } tile.appendChild(line); } function isValidPosition(row, col) { return row >= 0 && row < gridSize && col >= 0 && col < gridSize; } } // Reset to initial state function resetGame() { if (isRunning) return; grid = JSON.parse(JSON.stringify(solution)); renderGrid(); statusElement.textContent = "Puzzle reset to initial state!"; statusElement.className = "status"; win = false; // Clear any water or leaks document.querySelectorAll('.water, .leak').forEach(el => el.remove()); } // Create confetti effect function createConfetti() { const colors = ['#ff00ff', '#00ffff', '#ffff00', '#00ff00', '#ff0080']; for (let i = 0; i < 150; i++) { const confetti = document.createElement('div'); confetti.className = 'confetti'; confetti.style.left = `${Math.random() * 100}vw`; confetti.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)]; confetti.style.width = `${Math.random() * 10 + 5}px`; confetti.style.height = confetti.style.width; confetti.style.borderRadius = Math.random() > 0.5 ? '50%' : '0'; confetti.style.animationDuration = `${Math.random() * 3 + 2}s`; confetti.style.animationDelay = `${Math.random() * 2}s`; document.body.appendChild(confetti); // Remove confetti after animation setTimeout(() => { confetti.remove(); }, 5000); } } // Event listeners newGameBtn.addEventListener('click', init); runBtn.addEventListener('click', runWater); resetBtn.addEventListener('click', resetGame); // Initialize game init(); </script> </body> </html> ``` ## Features Implemented 1. **Customizable Puzzle**: - Choose grid size (5x5, 6x6, 7x7) - Select difficulty (Easy, Medium, Hard) - New game button to generate fresh puzzles 2. **Interactive Gameplay**: - Click on any pipe tile to rotate it 90 degrees - Source tiles (green) generate water - Sink tiles (orange) must be filled - Fail tiles (red) end the game if touched by water 3. **Water Simulation**: - Run button triggers water flow animation - Water flows through connected pipes - Leaks are shown with animated droplets - All sinks must be filled for victory 4. **Visual Design**: - Attractive dark theme with gradient backgrounds - Clean, modern tile design - Animated water flow with distinct colors - Clear visual feedback for leaks and failures 5. **Victory Celebration**: - Magenta confetti swirls when puzzle is solved - Confetti falls in a spiral pattern - Winning message with animation 6. **User Guidance**: - Clear instructions and legend - Status messages for game feedback - Reset button to start over The game is ready to play! Simply click "New Game" to start, rotate pipes to connect sources to sinks, and press "Run Water" to test your solution.
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Pipe Puzzle</title> <style> html,body{height:100%;margin:0;font-family:sans-serif;user-select:none;background:#222;color:#eee;display:flex;flex-direction:column;align-items:center} #toolbar{margin:10px} #toolbar select,#toolbar button{margin-right:6px;padding:4px 8px;font-size:14px} canvas{background:#111;border:2px solid #666;touch-action:none} #msg{height:24px;margin-top:6px} </style> </head> <body> <div id="toolbar"> Size: <select id="sizeSel"> <option value="5">5 x 5</option> <option value="7">7 x 7</option> <option value="9">9 x 9</option> </select> Difficulty: <select id="diffSel"> <option value="0.2">Easy</option> <option value="0.35" selected>Normal</option> <option value="0.55">Hard</option> </select> <button id="newBtn">New Puzzle</button> <button id="runBtn">Run</button> </div> <canvas id="board"></canvas> <canvas id="confetti" style="position:absolute;pointer-events:none;left:0;top:0;"></canvas> <div id="msg"></div> <script> // ---------- CONSTANTS ---------- const TILE = { EMPTY: 'empty', STRAIGHT: 'straight', L: 'corner', T: 'tee', SOURCE: 'source', SINK: 'sink', FAIL: 'fail' }; // connection maps for orientation 0 (no rotation) : [U,R,D,L] const BASE_CONNECTIONS = { empty:[0,0,0,0], straight:[1,0,1,0], // vertical corner:[1,1,0,0], // └ up + right tee:[1,1,1,0], // ├ up + right + down source:[0,1,0,0], // flows right initially sink:[0,0,0,1], // expects left fail:[0,0,0,0] }; const COLORS = { pipe:'#555', pipeEdge:'#777', water:'#00b7ff', back:'#111', source:'#0f0', sink:'#ff0', fail:'#f00' }; // ---------- STATE ---------- let grid=[], N=5, tileSize=80; let canvas=document.getElementById('board'), ctx=canvas.getContext('2d'); let confCanvas=document.getElementById('confetti'), cctx=confCanvas.getContext('2d'); let running=false, waterAnim=null, messageDiv=document.getElementById('msg'); // ---------- UTILITIES ---------- function rotations(conns,rot){ // rotate directions by rot*90 clockwise return conns.map((v,i)=>conns[(i-rot+4)%4]); } function randInt(a,b){return Math.floor(Math.random()*(b-a))+a;} function choose(arr){return arr[randInt(0,arr.length)];} // ---------- BOARD GENERATION ---------- function generateBoard(){ N=parseInt(document.getElementById('sizeSel').value); const diff=parseFloat(document.getElementById('diffSel').value); grid=[]; for(let r=0;r<N;r++){ grid[r]=[]; for(let c=0;c<N;c++){ grid[r][c]={type:TILE.EMPTY,rot:0}; } } // Place one source top edge random column, one sink bottom edge random column let sCol=randInt(0,N), tCol=randInt(0,N); grid[0][sCol]={type:TILE.SOURCE,rot:1}; // orientation to go down grid[N-1][tCol]={type:TILE.SINK,rot:3}; // expects up // carve a random path linking them (DFS) let visited=[...Array(N)].map(()=>Array(N).fill(false)); function dfs(r,c){ visited[r][c]=true; if(r===N-1 && c===tCol) return true; let dirs=[[1,0],[0,1],[0,-1],[-1,0]]; dirs.sort(()=>Math.random()-0.5); for(let [dr,dc] of dirs){ let nr=r+dr,nc=c+dc; if(nr<0||nr>=N||nc<0||nc>=N||visited[nr][nc])continue; if(dfs(nr,nc)){ // mark pipe between (r,c)->(nr,nc) setPipeBetween(r,c,nr,nc); return true; } } return false; } dfs(0,sCol); // sprinkle some random other pipes to make interesting for(let r=0;r<N;r++){ for(let c=0;c<N;c++){ if(grid[r][c].type===TILE.EMPTY && Math.random()<0.25){ grid[r][c].type=choose([TILE.STRAIGHT,TILE.L,TILE.T]); grid[r][c].rot=randInt(0,4); } } } // place a couple of fail tiles for(let i=0;i<Math.max(1,Math.floor(N/3));i++){ let rr=randInt(1,N-1),cc=randInt(0,N); if(grid[rr][cc].type===TILE.EMPTY){ grid[rr][cc]={type:TILE.FAIL,rot:0}; } } // store the solution orientations, then rotate tiles for difficulty let solution = grid.map(row=>row.map(cell=>cell.rot)); for(let r=0;r<N;r++){ for(let c=0;c<N;c++){ let cell=grid[r][c]; if(cell.type===TILE.EMPTY || cell.type===TILE.FAIL) continue; if(Math.random()<diff){ cell.rot = randInt(0,4); } } } resizeCanvas(); drawBoard(); message(''); } function setPipeBetween(r1,c1,r2,c2){ const cell1=grid[r1][c1]; const cell2=grid[r2][c2]; let dir; if(r2===r1+1){dir=2;} // down from first else if(r2===r1-1){dir=0;} // up else if(c2===c1+1){dir=1;} // right else dir=3; // left // Connect cell1 addConnection(cell1,dir); // Connect cell2 opposite addConnection(cell2,(dir+2)%4); } function addConnection(cell,dir){ if(cell.type===TILE.EMPTY){ // choose piece that can be oriented to include dir cell.type=choose([TILE.STRAIGHT,TILE.L,TILE.T]); cell.rot=0; } for(let rot=0;rot<4;rot++){ let cons=rotations(BASE_CONNECTIONS[cell.type],rot); if(cons[dir]){cell.rot=rot; return;} } } // ---------- RENDERING ---------- function resizeCanvas(){ tileSize = Math.min(80, Math.floor(window.innerHeight*0.7/N), Math.floor(window.innerWidth*0.9/N)); canvas.width=tileSize*N; canvas.height=tileSize*N; confCanvas.width=canvas.width; confCanvas.height=canvas.height; } window.addEventListener('resize',()=>{resizeCanvas();drawBoard();}); canvas.addEventListener('click',handleClick); function drawBoard(){ ctx.fillStyle=COLORS.back; ctx.fillRect(0,0,canvas.width,canvas.height); for(let r=0;r<N;r++){ for(let c=0;c<N;c++){ drawTile(r,c); } } } function drawTile(r,c,waterMask=[]){ const cell=grid[r][c]; const x=c*tileSize,y=r*tileSize; ctx.save(); ctx.translate(x+tileSize/2,y+tileSize/2); ctx.rotate(cell.rot*Math.PI/2); ctx.translate(-tileSize/2,-tileSize/2); // background ctx.fillStyle=COLORS.back; ctx.fillRect(0,0,tileSize,tileSize); // tile drawing switch(cell.type){ case TILE.STRAIGHT: drawPipeStraight(); break; case TILE.L: drawPipeCorner(); break; case TILE.T: drawPipeT(); break; case TILE.SOURCE: drawSource(); break; case TILE.SINK: drawSink(); break; case TILE.FAIL: drawFail(); break; } ctx.restore(); // draw water overlay if present if(waterMask.length){ ctx.fillStyle=COLORS.water; const w=5; if(waterMask[0]) ctx.fillRect(x+tileSize/2-w/2,y, w, tileSize/2); if(waterMask[1]) ctx.fillRect(x+tileSize/2,y+tileSize/2-w/2, tileSize/2, w); if(waterMask[2]) ctx.fillRect(x+tileSize/2-w/2,y+tileSize/2, w, tileSize/2); if(waterMask[3]) ctx.fillRect(x, y+tileSize/2-w/2, tileSize/2, w); ctx.beginPath(); ctx.arc(x+tileSize/2,y+tileSize/2, tileSize*0.15,0,Math.PI*2); ctx.fill(); } } function pipeBase(){ ctx.lineWidth=tileSize*0.3; ctx.lineCap='butt'; ctx.strokeStyle=COLORS.pipe; } function drawPipeStraight(){ pipeBase(); ctx.beginPath(); ctx.moveTo(tileSize/2,0); ctx.lineTo(tileSize/2,tileSize); ctx.stroke(); } function drawPipeCorner(){ pipeBase(); ctx.beginPath(); ctx.moveTo(tileSize/2,tileSize/2); ctx.lineTo(tileSize/2,tileSize); ctx.moveTo(tileSize/2,tileSize/2); ctx.lineTo(tileSize, tileSize/2); ctx.stroke(); } function drawPipeT(){ pipeBase(); ctx.beginPath(); ctx.moveTo(tileSize/2,0); ctx.lineTo(tileSize/2,tileSize); ctx.moveTo(tileSize/2,tileSize/2); ctx.lineTo(tileSize, tileSize/2); ctx.stroke(); } function drawSource(){ drawPipeStraight(); ctx.fillStyle=COLORS.source; ctx.beginPath(); ctx.arc(tileSize/2,tileSize*0.15, tileSize*0.12,0,Math.PI*2); ctx.fill(); } function drawSink(){ drawPipeStraight(); ctx.fillStyle=COLORS.sink; ctx.beginPath(); ctx.arc(tileSize/2,tileSize*0.85, tileSize*0.12,0,Math.PI*2); ctx.fill(); } function drawFail(){ ctx.fillStyle=COLORS.fail; ctx.fillRect(tileSize*0.25,tileSize*0.25,tileSize*0.5,tileSize*0.5); } // ---------- INTERACTION ---------- function handleClick(evt){ if(running)return; const rect=canvas.getBoundingClientRect(); const cx=evt.clientX-rect.left, cy=evt.clientY-rect.top; const c=Math.floor(cx/tileSize), r=Math.floor(cy/tileSize); if(r<0||r>=N||c<0||c>=N) return; const cell=grid[r][c]; if(cell.type===TILE.SOURCE || cell.type===TILE.SINK || cell.type===TILE.FAIL) return; cell.rot=(cell.rot+1)%4; drawTile(r,c); } // ---------- SIMULATION ---------- function runSimulation(){ if(running)return; running=true; message('Running...'); let frontier=[], filled=[...Array(N)].map(()=>Array(N).fill([0,0,0,0])); // seed frontier with sources outgoing directions for(let r=0;r<N;r++){ for(let c=0;c<N;c++){ if(grid[r][c].type===TILE.SOURCE){ frontier.push({r,c,dirExit: rotations(BASE_CONNECTIONS[grid[r][c].type],grid[r][c].rot).findIndex(v=>v)}); } } } let step=0, leaks=false, hitFail=false; waterAnim=setInterval(()=>{ if(frontier.length===0){ clearInterval(waterAnim); running=false; let allSinksFilled=true; for(let r=0;r<N;r++)for(let c=0;c<N;c++) if(grid[r][c].type===TILE.SINK && filled[r][c][2]!==1 && filled[r][c][0]!==1 && filled[r][c][1]!==1 && filled[r][c][3]!==1) allSinksFilled=false; if(!leaks && !hitFail && allSinksFilled){ victory(); }else{ message(hitFail?'Liquid hit a fail tile!':leaks?'Leak! Unconnected pipe':'Not all sinks filled.'); } return; } let newFrontier=[]; for(let flow of frontier){ const {r,c,dirExit}=flow; const dr=[-1,0,1,0], dc=[0,1,0,-1]; const nr=r+dr[dirExit], nc=c+dc[dirExit]; if(nr<0||nr>=N||nc<0||nc>=N){ leaks=true; continue; } const cellNext=grid[nr][nc]; const cons=rotations(BASE_CONNECTIONS[cellNext.type],cellNext.rot); const opposite=(dirExit+2)%4; if(!cons[opposite]){ // enters but no proper connection -> leak if(cellNext.type===TILE.FAIL) {hitFail=true;} leaks=true; }else{ // mark water in next cell filled[nr][nc]=cons; drawTile(nr,nc,cons); // propagate out of all other open edges cons.forEach((has,i)=>{ if(has && i!==opposite){ newFrontier.push({r:nr,c:nc,dirExit:i}); } }); } } frontier=newFrontier; step++; if(step>N*N*4){clearInterval(waterAnim);running=false;} },150); } function message(txt){ messageDiv.textContent=txt; } // ---------- VICTORY ---------- function victory(){ message('All sinks filled! You win!'); confetti(); } function confetti(){ const particles=200; let parts=[]; for(let i=0;i<particles;i++){ parts.push({angle:Math.random()*Math.PI*2, radius:0, speed:1+Math.random()*2, size:2+Math.random()*3}); } let t=0; let conf=setInterval(()=>{ cctx.clearRect(0,0,confCanvas.width,confCanvas.height); cctx.fillStyle='magenta'; t+=0.05; parts.forEach(p=>{ p.radius+=p.speed; const x=canvas.width/2+Math.cos(p.angle+t)*p.radius; const y=canvas.height/2+Math.sin(p.angle+t)*p.radius; cctx.fillRect(x,y,p.size,p.size); }); if(t>6){ clearInterval(conf); cctx.clearRect(0,0,confCanvas.width,confCanvas.height); } },30); } // ---------- EVENT LISTENERS ---------- document.getElementById('newBtn').addEventListener('click',generateBoard); document.getElementById('runBtn').addEventListener('click',runSimulation); // ---------- INIT ---------- generateBoard(); </script> </body> </html> ```
An elegant and challenging pipe puzzle, fully self-contained in a single file. Adjust the grid size and difficulty, then rotate the pipes to connect the green source to all the blue sinks. Beware of the red fail tiles! When you think you've solved it, press "Run Simulation" to watch the water flow. If you succeed, enjoy a dazzling victory celebration. ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Classic Pipe Puzzle</title> <style> :root { --bg-color: #1a1a2e; --grid-bg: #16213e; --tile-bg: #0f3460; --text-color: #e94560; --control-bg: #e94560; --control-text: #ffffff; --pipe-color: #dcdcdc; --water-color: #16c79a; --source-color: #28a745; --sink-color: #007bff; --fail-color: #dc3545; --confetti-magenta: #e94560; } body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: var(--bg-color); color: var(--text-color); display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; overflow: hidden; } #game-container { position: relative; text-align: center; padding: 20px; background-color: var(--grid-bg); border-radius: 15px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4); } h1 { margin-top: 0; letter-spacing: 2px; } #controls { margin-bottom: 20px; display: flex; justify-content: center; align-items: center; gap: 15px; flex-wrap: wrap; } .control-group { display: flex; flex-direction: column; align-items: center; } .control-group label { font-size: 0.9em; margin-bottom: 5px; } .control-group select, .control-group button { background-color: var(--control-bg); color: var(--control-text); border: none; padding: 10px 15px; border-radius: 5px; font-size: 1em; cursor: pointer; transition: background-color 0.3s; } .control-group select:hover, .control-group button:hover { background-color: #d33c56; } .control-group button:disabled { background-color: #a0a0a0; cursor: not-allowed; } #grid-container { display: grid; border: 2px solid var(--tile-bg); background-color: var(--tile-bg); gap: 2px; margin: 0 auto; touch-action: none; /* Prevents double-tap zoom on mobile */ } .tile { width: 50px; height: 50px; background-color: var(--tile-bg); position: relative; background-size: contain; background-repeat: no-repeat; background-position: center; } .tile.rotatable { cursor: pointer; transition: transform 0.25s ease-in-out; } .tile .water { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: var(--water-color); transform: scale(0); transition: transform 0.2s ease-out; z-index: 1; } .tile .pipe-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-size: contain; background-repeat: no-repeat; background-position: center; z-index: 2; } #message-area { margin-top: 15px; font-size: 1.2em; font-weight: bold; height: 25px; } #confetti-canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 100; } @media (max-width: 600px) { .tile { width: 40px; height: 40px; } #game-container { padding: 10px; } } @media (max-width: 450px) { .tile { width: 30px; height: 30px; } h1 { font-size: 1.5em; } .control-group button, .control-group select { padding: 8px 10px; font-size: 0.9em; } } </style> </head> <body> <div id="game-container"> <h1>Classic Pipe Puzzle</h1> <div id="controls"> <div class="control-group"> <label for="size-select">Size</label> <select id="size-select"> <option value="5">5x5</option> <option value="7" selected>7x7</option> <option value="9">9x9</option> <option value="11">11x11</option> </select> </div> <div class="control-group"> <label for="difficulty-select">Difficulty</label> <select id="difficulty-select"> <option value="0">Easy</option> <option value="1" selected>Medium</option> <option value="2">Hard</option> </select> </div> <div class="control-group"> <label> </label> <button id="run-btn">Run Simulation</button> </div> <div class="control-group"> <label> </label> <button id="new-puzzle-btn">New Puzzle</button> </div> </div> <div id="grid-container"></div> <div id="message-area"></div> <canvas id="confetti-canvas"></canvas> </div> <script> document.addEventListener('DOMContentLoaded', () => { // --- DOM ELEMENTS --- const gridContainer = document.getElementById('grid-container'); const sizeSelect = document.getElementById('size-select'); const difficultySelect = document.getElementById('difficulty-select'); const runBtn = document.getElementById('run-btn'); const newPuzzleBtn = document.getElementById('new-puzzle-btn'); const messageArea = document.getElementById('message-area'); const confettiCanvas = document.getElementById('confetti-canvas'); const confettiCtx = confettiCanvas.getContext('2d'); const gameContainer = document.getElementById('game-container'); // --- GAME STATE --- let gridSize = 7; let difficulty = 1; let grid = []; let sources = []; let sinks = []; let failTiles = []; let isSimulating = false; let simulationInterval; // --- TILE DEFINITIONS --- const pipeColor = getComputedStyle(document.documentElement).getPropertyValue('--pipe-color').trim(); const sourceColor = getComputedStyle(document.documentElement).getPropertyValue('--source-color').trim(); const sinkColor = getComputedStyle(document.documentElement).getPropertyValue('--sink-color').trim(); const failColor = getComputedStyle(document.documentElement).getPropertyValue('--fail-color').trim(); const TILE_SVGS = { 'straight': `url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M50 0 V100' stroke='${pipeColor}' stroke-width='20' fill='none' stroke-linecap='round'/%3E%3C/svg%3E")`, 'corner': `url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M50 0 V50 H100' stroke='${pipeColor}' stroke-width='20' fill='none' stroke-linecap='round'/%3E%3C/svg%3E")`, 't-pipe': `url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 50 H100 M50 50 V100' stroke='${pipeColor}' stroke-width='20' fill='none' stroke-linecap='round'/%3E%3C/svg%3E")`, 'source': `url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M50 50 V100' stroke='${sourceColor}' stroke-width='20' fill='none' stroke-linecap='round'/%3E%3Ccircle cx='50' cy='50' r='30' fill='${sourceColor}'/%3E%3C/svg%3E")`, 'sink': `url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M50 0 V50' stroke='${sinkColor}' stroke-width='20' fill='none' stroke-linecap='round'/%3E%3Ccircle cx='50' cy='50' r='25' stroke='${sinkColor}' stroke-width='10' fill='none'/%3E%3C/svg%3E")`, 'fail': `url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M20 20 L80 80 M80 20 L20 80' stroke='${failColor}' stroke-width='15' fill='none' stroke-linecap='round'/%3E%3C/svg%3E")`, }; const WATER_MASKS = { 'straight': `url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M50 0 V100' stroke='black' stroke-width='18' fill='none'/%3E%3C/svg%3E")`, 'corner': `url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M50 0 V50 H100' stroke='black' stroke-width='18' fill='none'/%3E%3C/svg%3E")`, 't-pipe': `url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 50 H100 M50 50 V100' stroke='black' stroke-width='18' fill='none'/%3E%3C/svg%3E")`, 'source': `url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M50 50 V100' stroke='black' stroke-width='18' fill='none'/%3E%3Ccircle cx='50' cy='50' r='28' fill='black'/%3E%3C/svg%3E")`, 'sink': `url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M50 0 V50' stroke='black' stroke-width='18' fill='none'/%3E%3Ccircle cx='50' cy='50' r='23' stroke='black' stroke-width='8' fill='none'/%3E%3C/svg%3E")`, }; const CONNECTIONS = { // [N, E, S, W] 'empty': [], 'straight': ['N', 'S'], 'corner': ['N', 'E'], 't-pipe': ['E', 'S', 'W'], 'source': ['S'], 'sink': ['N'], 'fail': [], }; const DIRECTIONS = { 'N': { x: 0, y: -1, opposite: 'S' }, 'E': { x: 1, y: 0, opposite: 'W' }, 'S': { x: 0, y: 1, opposite: 'N' }, 'W': { x: -1, y: 0, opposite: 'E' }, }; const DIR_ARRAY = ['N', 'E', 'S', 'W']; // --- PUZZLE GENERATION --- function generatePuzzle() { // 1. Initialize grid with empty cells grid = Array(gridSize).fill(null).map(() => Array(gridSize).fill(null)); sources = []; sinks = []; failTiles = []; for (let y = 0; y < gridSize; y++) { for (let x = 0; x < gridSize; x++) { grid[y][x] = { type: 'empty', rot: 0, fixed: false, connections: [], water: false }; } } // 2. Carve a solution path const visited = new Set(); let pathLength = 0; const maxPathLength = gridSize * gridSize * (0.4 + difficulty * 0.1); // Place source const sx = Math.floor(Math.random() * (gridSize - 2)) + 1; const sy = 0; grid[sy][sx] = { type: 'source', rot: 0, fixed: true, connections: [], water: false }; sources.push({x: sx, y: sy}); visited.add(`${sx},${sy}`); let walkers = [{ x: sx, y: sy, path: [{x:sx, y:sy}] }]; while(walkers.length > 0 && pathLength < maxPathLength) { const walkerIndex = Math.floor(Math.random() * walkers.length); let current = walkers[walkerIndex]; const neighbors = getShuffledNeighbors(current.x, current.y, visited); if (neighbors.length > 0) { const next = neighbors[0]; visited.add(`${next.x},${next.y}`); linkCells(current.x, current.y, next.x, next.y); current.path.push({x: next.x, y: next.y}); current.x = next.x; current.y = next.y; pathLength++; // Branching logic based on difficulty if (Math.random() < (0.1 + difficulty * 0.1) && neighbors.length > 1) { const branchNext = neighbors[1]; visited.add(`${branchNext.x},${branchNext.y}`); linkCells(current.x, current.y, branchNext.x, branchNext.y); walkers.push({x: branchNext.x, y: branchNext.y, path: [current, branchNext]}); pathLength++; } } else { // Path ended, place a sink placeSink(current.x, current.y); walkers.splice(walkerIndex, 1); } } // Place sinks for any remaining open paths walkers.forEach(w => placeSink(w.x, w.y)); // 3. Convert connection arrays to tile types and rotations for (let y = 0; y < gridSize; y++) { for (let x = 0; x < gridSize; x++) { const cell = grid[y][x]; if (cell.type === 'empty' && cell.connections.length > 0) { const {type, rot} = connectionsToTile(cell.connections); cell.type = type; cell.solved_rot = rot; } else if (cell.type !== 'empty') { cell.solved_rot = cell.rot; } } } // 4. Fill empty space with distractions and fail tiles const emptyCells = []; for (let y = 0; y < gridSize; y++) { for (let x = 0; x < gridSize; x++) { if (grid[y][x].type === 'empty') { emptyCells.push({x, y}); } } } shuffleArray(emptyCells); const numFailTiles = Math.floor(difficulty * 1.5 + 1); for(let i=0; i < numFailTiles && emptyCells.length > 0; i++) { const pos = emptyCells.pop(); grid[pos.y][pos.x] = {type: 'fail', rot: 0, fixed: true, water: false}; failTiles.push(pos); } const numJunkPipes = Math.floor(emptyCells.length * (0.2 + difficulty * 0.3)); const junkTypes = ['straight', 'corner', 't-pipe']; for(let i=0; i < numJunkPipes && emptyCells.length > 0; i++) { const pos = emptyCells.pop(); grid[pos.y][pos.x] = { type: junkTypes[Math.floor(Math.random() * junkTypes.length)], rot: Math.floor(Math.random() * 4) * 90, fixed: false, water: false }; } // 5. Scramble the puzzle for (let y = 0; y < gridSize; y++) { for (let x = 0; x < gridSize; x++) { const cell = grid[y][x]; if (!cell.fixed) { const scrambleChance = [0.5, 0.75, 1.0]; // Easy, Medium, Hard if (Math.random() < scrambleChance[difficulty]) { cell.rot = Math.floor(Math.random() * 4) * 90; } else { cell.rot = cell.solved_rot; } } } } } function getShuffledNeighbors(x, y, visited) { const neighbors = []; const dirs = ['N', 'E', 'S', 'W']; shuffleArray(dirs); for (const dir of dirs) { const d = DIRECTIONS[dir]; const nx = x + d.x; const ny = y + d.y; if (nx >= 0 && nx < gridSize && ny >= 0 && ny < gridSize && !visited.has(`${nx},${ny}`)) { neighbors.push({ x: nx, y: ny, dir: dir }); } } return neighbors; } function linkCells(x1, y1, x2, y2) { const dx = x2 - x1; const dy = y2 - y1; let dir1, dir2; if (dx === 1) { dir1 = 'E'; dir2 = 'W'; } else if (dx === -1) { dir1 = 'W'; dir2 = 'E';} else if (dy === 1) { dir1 = 'S'; dir2 = 'N';} else { dir1 = 'N'; dir2 = 'S'; } grid[y1][x1].connections.push(dir1); grid[y2][x2].connections.push(dir2); } function placeSink(x, y) { if (grid[y][x].type === 'empty') { const cell = grid[y][x]; if (cell.connections.length === 1) { const rot = DIR_ARRAY.indexOf(cell.connections[0]) * 90; grid[y][x] = { type: 'sink', rot: rot, fixed: true, water: false }; sinks.push({ x, y }); } } } function connectionsToTile(conns) { conns.sort(); const key = conns.join(''); switch(conns.length) { case 1: // Should be handled by source/sink return {type: 'straight', rot: 0}; // Fallback case 2: if ( (conns.includes('N') && conns.includes('S')) || (conns.includes('E') && conns.includes('W')) ) { let rot = conns.includes('N') ? 0 : 90; return {type: 'straight', rot: rot}; } else { // Corner if (key === 'EN') return {type: 'corner', rot: 0}; if (key === 'ES') return {type: 'corner', rot: 90}; if (key === 'SW') return {type: 'corner', rot: 180}; if (key === 'NW') return {type: 'corner', rot: 270}; } break; case 3: // T-pipe if (key === 'ESW') return {type: 't-pipe', rot: 0}; if (key === 'NSW') return {type: 't-pipe', rot: 90}; if (key === 'ENW') return {type: 't-pipe', rot: 180}; if (key === 'ENS') return {type: 't-pipe', rot: 270}; break; } return {type: 'empty', rot: 0}; // Should not happen in a valid path } function shuffleArray(array) { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } } // --- GRID RENDERING --- function renderGrid() { gridContainer.innerHTML = ''; gridContainer.style.gridTemplateColumns = `repeat(${gridSize}, 1fr)`; for (let y = 0; y < gridSize; y++) { for (let x = 0; x < gridSize; x++) { const cellData = grid[y][x]; const tile = document.createElement('div'); tile.classList.add('tile'); if (!cellData.fixed) { tile.classList.add('rotatable'); } tile.dataset.x = x; tile.dataset.y = y; const pipeOverlay = document.createElement('div'); pipeOverlay.classList.add('pipe-overlay'); pipeOverlay.style.backgroundImage = TILE_SVGS[cellData.type] || ''; pipeOverlay.style.transform = `rotate(${cellData.rot}deg)`; const water = document.createElement('div'); water.classList.add('water'); water.style.webkitMask = water.style.mask = WATER_MASKS[cellData.type] || 'none'; water.style.transform = `rotate(${cellData.rot}deg)`; tile.appendChild(water); tile.appendChild(pipeOverlay); gridContainer.appendChild(tile); } } } // --- USER INTERACTION --- gridContainer.addEventListener('click', (e) => { if (isSimulating) return; const tileEl = e.target.closest('.tile.rotatable'); if (tileEl) { const x = parseInt(tileEl.dataset.x); const y = parseInt(tileEl.dataset.y); const cell = grid[y][x]; cell.rot = (cell.rot + 90) % 360; tileEl.querySelector('.pipe-overlay').style.transform = `rotate(${cell.rot}deg)`; tileEl.querySelector('.water').style.transform = `rotate(${cell.rot}deg)`; messageArea.textContent = ''; clearWater(); } }); // --- SIMULATION --- function runSimulation() { if (isSimulating) return; isSimulating = true; setControlsDisabled(true); messageArea.textContent = 'Simulating...'; clearWater(); let queue = sources.map(s => ({ x: s.x, y: s.y, from: 'start' })); const visited = new Set(sources.map(s => `${s.x},${s.y}`)); let filledSinks = new Set(); let simulationFailed = false; simulationInterval = setInterval(() => { if (queue.length === 0 || simulationFailed) { endSimulation(simulationFailed, filledSinks); return; } const current = queue.shift(); const cell = grid[current.y][current.x]; // Animate current cell filling const tileEl = gridContainer.querySelector(`.tile[data-x='${current.x}'][data-y='${current.y}']`); if (tileEl) { tileEl.querySelector('.water').style.transform = `rotate(${cell.rot}deg) scale(1)`; } cell.water = true; const currentConnections = getRotatedConnections(cell); if (current.from !== 'start' && !currentConnections.includes(DIRECTIONS[current.from].opposite)) { simulationFailed = true; // Water entered from a direction with no pipe opening messageArea.textContent = 'Leak detected! Misaligned pipe.'; return; } const exitDirections = currentConnections.filter(dir => dir !== (current.from === 'start' ? null : DIRECTIONS[current.from].opposite)); if (exitDirections.length === 0 && cell.type !== 'sink') { // Dead end that is not a sink simulationFailed = `Dead end at (${current.x}, ${current.y})!`; return; } for (const exitDir of exitDirections) { const d = DIRECTIONS[exitDir]; const nx = current.x + d.x; const ny = current.y + d.y; if (nx < 0 || nx >= gridSize || ny < 0 || ny >= gridSize) { simulationFailed = `Water leaked out of the grid at (${current.x},${current.y})!`; break; } const neighbor = grid[ny][nx]; const neighborConnections = getRotatedConnections(neighbor); const entryDir = d.opposite; if (neighbor.type === 'fail') { simulationFailed = 'Oh no! You hit a fail tile!'; break; } if (neighbor.type === 'empty' || !neighborConnections.includes(entryDir)) { simulationFailed = `Leak detected between (${current.x},${current.y}) and (${nx},${ny})!`; break; } if (neighbor.type === 'sink') { filledSinks.add(`${nx},${ny}`); neighbor.water = true; const sinkEl = gridContainer.querySelector(`.tile[data-x='${nx}'][data-y='${ny}']`); if(sinkEl) sinkEl.querySelector('.water').style.transform = `rotate(${neighbor.rot}deg) scale(1)`; } else { if (!visited.has(`${nx},${ny}`)) { visited.add(`${nx},${ny}`); queue.push({ x: nx, y: ny, from: exitDir }); } } } if(simulationFailed) return; }, 100); } function getRotatedConnections(cell) { const baseConnections = CONNECTIONS[cell.type]; const rotations = cell.rot / 90; return baseConnections.map(dir => { let index = DIR_ARRAY.indexOf(dir); return DIR_ARRAY[(index + rotations) % 4]; }); } function endSimulation(failed, filledSinks) { clearInterval(simulationInterval); isSimulating = false; let finalFailMessage = failed; if (!finalFailMessage) { if (filledSinks.size < sinks.length) { finalFailMessage = "Not all sinks were filled!"; } // Check if any filled T-pipe has an unused exit (a leak) for (let y = 0; y < gridSize; y++) { for (let x = 0; x < gridSize; x++) { const cell = grid[y][x]; if (cell.water && cell.type === 't-pipe') { const connections = getRotatedConnections(cell); for (const dir of connections) { const d = DIRECTIONS[dir]; const nx = x + d.x; const ny = y + d.y; if (nx < 0 || ny < 0 || nx >= gridSize || ny >= gridSize || !grid[ny][nx].water) { finalFailMessage = `Unused pipe opening on T-pipe at (${x},${y})!`; break; } } } if(finalFailMessage) break; } if(finalFailMessage) break; } } if (finalFailMessage) { messageArea.textContent = `Failure: ${finalFailMessage}`; setControlsDisabled(false); } else { messageArea.textContent = 'Success! All sinks filled!'; runVictoryAnimation(); } } function clearWater() { grid.forEach(row => row.forEach(cell => cell.water = false)); document.querySelectorAll('.tile .water').forEach(w => w.style.transform = w.style.transform.replace(/scale\([\d.]+\)/, 'scale(0)')); } // --- VICTORY ANIMATION --- function runVictoryAnimation() { const containerRect = gameContainer.getBoundingClientRect(); confettiCanvas.width = containerRect.width; confettiCanvas.height = containerRect.height; const particles = []; const particleCount = 200; const centerX = confettiCanvas.width / 2; const centerY = confettiCanvas.height / 2; const magenta = getComputedStyle(document.documentElement).getPropertyValue('--confetti-magenta').trim(); for (let i = 0; i < particleCount; i++) { particles.push({ x: centerX, y: centerY, radius: Math.random() * (confettiCanvas.width / 2.5), angle: Math.random() * Math.PI * 2, angularVelocity: (Math.random() - 0.5) * 0.1, color: magenta, size: Math.random() * 5 + 3, alpha: 1.0, }); } let animationFrame; let startTime = Date.now(); function animateConfetti() { const elapsed = Date.now() - startTime; if (elapsed > 4000) { // Run for 4 seconds confettiCtx.clearRect(0, 0, confettiCanvas.width, confettiCanvas.height); setControlsDisabled(false); // Re-enable controls after animation runBtn.disabled = true; // Disable run until new puzzle return; } confettiCtx.clearRect(0, 0, confettiCanvas.width, confettiCanvas.height); particles.forEach(p => { p.angle += p.angularVelocity; p.radius -= 0.8; p.alpha = Math.max(0, p.radius / (confettiCanvas.width / 2.5)); if(p.radius > 0) { p.x = centerX + p.radius * Math.cos(p.angle); p.y = centerY + p.radius * Math.sin(p.angle); confettiCtx.beginPath(); confettiCtx.fillStyle = `${p.color}${Math.round(p.alpha*255).toString(16).padStart(2, '0')}`; confettiCtx.rect(p.x, p.y, p.size, p.size); confettiCtx.fill(); } }); animationFrame = requestAnimationFrame(animateConfetti); } animateConfetti(); } // --- SETUP AND CONTROLS --- function setupNewGame() { clearInterval(simulationInterval); isSimulating = false; gridSize = parseInt(sizeSelect.value); difficulty = parseInt(difficultySelect.value); generatePuzzle(); renderGrid(); messageArea.textContent = 'Rotate pipes to connect the source to all sinks.'; setControlsDisabled(false); runBtn.disabled = false; } function setControlsDisabled(disabled) { runBtn.disabled = disabled; newPuzzleBtn.disabled = disabled; sizeSelect.disabled = disabled; difficultySelect.disabled = disabled; } runBtn.addEventListener('click', runSimulation); newPuzzleBtn.addEventListener('click', setupNewGame); sizeSelect.addEventListener('change', setupNewGame); difficultySelect.addEventListener('change', setupNewGame); // --- INITIALIZE --- setupNewGame(); }); </script> </body> </html> ```
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Pipe Puzzle Game</title> <style> body { font-family: Arial, sans-serif; display: flex; flex-direction: column; align-items: center; background-color: #f0f0f0; margin: 0; padding: 20px; } .controls { margin-bottom: 20px; display: flex; gap: 10px; align-items: center; } .grid { display: grid; gap: 2px; background-color: #333; padding: 2px; border-radius: 5px; } .tile { width: 60px; height: 60px; background-color: #ddd; display: flex; justify-content: center; align-items: center; cursor: pointer; border-radius: 4px; user-select: none; position: relative; transition: background-color 0.3s; } .tile:hover { background-color: #ccc; } .tile.source { background-color: #4CAF50; } .tile.sink { background-color: #2196F3; } .tile.fail { background-color: #f44336; } .button { padding: 10px 15px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; } .button:hover { background-color: #45a049; } .button:disabled { background-color: #cccccc; cursor: not-allowed; } .pipe { width: 100%; height: 100%; background-size: contain; background-position: center; background-repeat: no-repeat; } .water-flow { position: absolute; width: 20px; height: 20px; background-color: #2196F3; border-radius: 50%; opacity: 0; pointer-events: none; z-index: 10; } .confetti { position: absolute; width: 10px; height: 10px; background-color: magenta; opacity: 0; z-index: 100; } </style> </head> <body> <h1>Pipe Puzzle Game</h1> <div class="controls"> <label for="size">Grid Size:</label> <select id="size"> <option value="5">5x5</option> <option value="6">6x6</option> <option value="7">7x7</option> <option value="8">8x8</option> </select> <label for="difficulty">Difficulty:</label> <select id="difficulty"> <option value="1">Easy</option> <option value="2" selected>Medium</option> <option value="3">Hard</option> </select> <button id="generate" class="button">Generate Puzzle</button> </div> <div id="grid" class="grid"></div> <div style="margin-top: 20px;"> <button id="run" class="button" disabled>Run Simulation</button> <button id="reset" class="button" disabled>Reset</button> </div> <script> // Pipe types const PIPE_TYPES = { EMPTY: 0, STRAIGHT: 1, // Horizontal or vertical EL: 2, // L-shaped TEE: 3, // T-shaped SOURCE: 4, // Water source SINK: 5, // Goal/sink FAIL: 6 // Fail tile }; // Directions represented as bits const DIRECTIONS = { TOP: 1, RIGHT: 2, BOTTOM: 4, LEFT: 8 }; // Pipe connections - for each rotation state, which directions are connected const PIPE_CONNECTIONS = { [PIPE_TYPES.EMPTY]: [ { connections: 0 } ], [PIPE_TYPES.STRAIGHT]: [ { connections: DIRECTIONS.TOP | DIRECTIONS.BOTTOM }, // Vertical { connections: DIRECTIONS.LEFT | DIRECTIONS.RIGHT } // Horizontal ], [PIPE_TYPES.EL]: [ { connections: DIRECTIONS.TOP | DIRECTIONS.RIGHT }, // Top to Right { connections: DIRECTIONS.RIGHT | DIRECTIONS.BOTTOM }, // Right to Bottom { connections: DIRECTIONS.BOTTOM | DIRECTIONS.LEFT }, // Bottom to Left { connections: DIRECTIONS.LEFT | DIRECTIONS.TOP } // Left to Top ], [PIPE_TYPES.TEE]: [ { connections: DIRECTIONS.TOP | DIRECTIONS.BOTTOM | DIRECTIONS.LEFT }, // Left, Top, Bottom { connections: DIRECTIONS.TOP | DIRECTIONS.BOTTOM | DIRECTIONS.RIGHT }, // Right, Top, Bottom { connections: DIRECTIONS.LEFT | DIRECTIONS.RIGHT | DIRECTIONS.BOTTOM }, // Left, Right, Bottom { connections: DIRECTIONS.LEFT | DIRECTIONS.RIGHT | DIRECTIONS.TOP } // Left, Right, Top ], [PIPE_TYPES.SOURCE]: [ { connections: DIRECTIONS.RIGHT, isSource: true } // Source pointing right ], [PIPE_TYPES.SINK]: [ { connections: DIRECTIONS.LEFT, isSink: true } // Sink receiving from left ], [PIPE_TYPES.FAIL]: [ { connections: 0, isFail: true } // Fail tile - no connections ] }; // SVG representations of pipes const PIPE_SVGS = { [PIPE_TYPES.EMPTY]: '<svg viewBox="0 0 100 100"></svg>', [PIPE_TYPES.STRAIGHT]: [ '<svg viewBox="0 0 100 100"><line x1="50" y1="0" x2="50" y2="100" stroke="black" stroke-width="20" stroke-linecap="round"/></svg>', '<svg viewBox="0 0 100 100"><line x1="0" y1="50" x2="100" y2="50" stroke="black" stroke-width="20" stroke-linecap="round"/></svg>' ], [PIPE_TYPES.EL]: [ '<svg viewBox="0 0 100 100"><path d="M50,0 V50 H100" stroke="black" stroke-width="20" stroke-linecap="round" stroke-linejoin="round"/></svg>', '<svg viewBox="0 0 100 100"><path d="M100,50 H50 V100" stroke="black" stroke-width="20" stroke-linecap="round" stroke-linejoin="round"/></svg>', '<svg viewBox="0 0 100 100"><path d="M50,100 V50 H0" stroke="black" stroke-width="20" stroke-linecap="round" stroke-linejoin="round"/></svg>', '<svg viewBox="0 0 100 100"><path d="M0,50 H50 V0" stroke="black" stroke-width="20" stroke-linecap="round" stroke-linejoin="round"/></svg>' ], [PIPE_TYPES.TEE]: [ '<svg viewBox="0 0 100 100"><line x1="50" y1="0" x2="50" y2="100" stroke="black" stroke-width="20" stroke-linecap="round"/><line x1="0" y1="50" x2="50" y2="50" stroke="black" stroke-width="20" stroke-linecap="round"/></svg>', '<svg viewBox="0 0 100 100"><line x1="50" y1="0" x2="50" y2="100" stroke="black" stroke-width="20" stroke-linecap="round"/><line x1="50" y1="50" x2="100" y2="50" stroke="black" stroke-width="20" stroke-linecap="round"/></svg>', '<svg viewBox="0 0 100 100"><line x1="0" y1="50" x2="100" y2="50" stroke="black" stroke-width="20" stroke-linecap="round"/><line x1="50" y1="50" x2="50" y2="100" stroke="black" stroke-width="20" stroke-linecap="round"/></svg>', '<svg viewBox="0 0 100 100"><line x1="0" y1="50" x2="100" y2="50" stroke="black" stroke-width="20" stroke-linecap="round"/><line x1="50" y1="0" x2="50" y2="50" stroke="black" stroke-width="20" stroke-linecap="round"/></svg>' ], [PIPE_TYPES.SOURCE]: [ '<svg viewBox="0 0 100 100"><circle cx="50" cy="50" r="30" fill="green"/><line x1="80" y1="50" x2="100" y2="50" stroke="green" stroke-width="20" stroke-linecap="round"/></svg>' ], [PIPE_TYPES.SINK]: [ '<svg viewBox="0 0 100 100"><circle cx="50" cy="50" r="30" fill="blue"/><line x1="0" y1="50" x2="20" y2="50" stroke="blue" stroke-width="20" stroke-linecap="round"/></svg>' ], [PIPE_TYPES.FAIL]: [ '<svg viewBox="0 0 100 100"><circle cx="50" cy="50" r="40" fill="red"/><line x1="30" y1="30" x2="70" y2="70" stroke="white" stroke-width="20" stroke-linecap="round"/><line x1="70" y1="30" x2="30" y2="70" stroke="white" stroke-width="20" stroke-linecap="round"/></svg>' ] }; // Game state let grid = []; let gridSize = 5; let difficulty = 2; let isRunning = false; let simulationActive = false; let sources = []; let sinks = []; let failTiles = []; // Get elements const sizeSelect = document.getElementById('size'); const difficultySelect = document.getElementById('difficulty'); const generateBtn = document.getElementById('generate'); const runBtn = document.getElementById('run'); const resetBtn = document.getElementById('reset'); const gridElement = document.getElementById('grid'); // Initialize the game function initGame() { gridSize = parseInt(sizeSelect.value); difficulty = parseInt(difficultySelect.value); // Clear previous grid gridElement.innerHTML = ''; gridElement.style.gridTemplateColumns = `repeat(${gridSize}, 60px)`; // Reset state grid = []; sources = []; sinks = []; failTiles = []; // Create new grid createGrid(); // Generate puzzle generatePuzzle(); // Enable buttons runBtn.disabled = false; resetBtn.disabled = false; } // Create empty grid function createGrid() { for (let y = 0; y < gridSize; y++) { grid[y] = []; for (let x = 0; x < gridSize; x++) { // Create tile element const tile = document.createElement('div'); tile.className = 'tile'; tile.dataset.x = x; tile.dataset.y = y; // Create pipe element const pipe = document.createElement('div'); pipe.className = 'pipe'; tile.appendChild(pipe); // Add click event tile.addEventListener('click', () => rotateTile(x, y)); // Add to grid gridElement.appendChild(tile); grid[y][x] = { element: tile, pipeElement: pipe, type: PIPE_TYPES.EMPTY, rotation: 0 }; } } } // Generate a solvable puzzle function generatePuzzle() { // Clear grid for (let y = 0; y < gridSize; y++) { for (let x = 0; x < gridSize; x++) { grid[y][x].type = PIPE_TYPES.EMPTY; grid[y][x].rotation = 0; updateTileDisplay(x, y); } } // Determine number of sources, sinks, and fail tiles based on difficulty and size const numSources = 1; const numSinks = Math.max(1, Math.floor(gridSize / 3)); const numFailTiles = Math.max(0, Math.min(3, Math.floor(gridSize / 3) + difficulty - 2)); // Place source(s) sources = []; for (let i = 0; i < numSources; i++) { let placed = false; while (!placed) { const x = Math.floor(Math.random() * gridSize); const y = Math.floor(Math.random() * gridSize); if (grid[y][x].type === PIPE_TYPES.EMPTY) { grid[y][x].type = PIPE_TYPES.SOURCE; grid[y][x].rotation = Math.floor(Math.random() * 4); updateTileDisplay(x, y); sources.push({ x, y }); placed = true; } } } // Place sink(s) sinks = []; for (let i = 0; i < numSinks; i++) { let placed = false; while (!placed) { const x = Math.floor(Math.random() * gridSize); const y = Math.floor(Math.random() * gridSize); if (grid[y][x].type === PIPE_TYPES.EMPTY) { grid[y][x].type = PIPE_TYPES.SINK; grid[y][x].rotation = Math.floor(Math.random() * 4); updateTileDisplay(x, y); sinks.push({ x, y }); placed = true; } } } // Place fail tiles failTiles = []; for (let i = 0; i < numFailTiles; i++) { let placed = false; while (!placed) { const x = Math.floor(Math.random() * gridSize); const y = Math.floor(Math.random() * gridSize); if (grid[y][x].type === PIPE_TYPES.EMPTY) { grid[y][x].type = PIPE_TYPES.FAIL; updateTileDisplay(x, y); failTiles.push({ x, y }); placed = true; } } } // Generate solution path from source to sinks generateSolutionPath(); // Fill remaining empty tiles with random pipes fillEmptyTiles(); // Introduce errors based on difficulty introduceErrors(); } // Generate a valid path from source to sinks function generateSolutionPath() { // We'll use the first source to connect to all sinks if (sources.length === 0 || sinks.length === 0) return; // Create a path from the source to each sink in sequence let currentX = sources[0].x; let currentY = sources[0].y; for (const sink of sinks) { // Path from current position to the sink createPathTo(currentX, currentY, sink.x, sink.y); // Update current position for next path currentX = sink.x; currentY = sink.y; } } // Create a path from start to end function createPathTo(startX, startY, endX, endY) { let x = startX; let y = startY; // Randomly decide whether to go horizontal first or vertical first const goHorizontalFirst = Math.random() < 0.5; // Path from (x,y) to (endX, endY) if (goHorizontalFirst) { // Move horizontally first while (x !== endX) { const dx = endX > x ? 1 : -1; x += dx; // If this position doesn't already have a pipe, place one if (grid[y][x].type === PIPE_TYPES.EMPTY) { // Determine pipe type based on direction if (Math.abs(x - startX) < 2 || Math.abs(y - startY) < 2) { // Near the start, use L-shaped or straight grid[y][x].type = (Math.random() < 0.7) ? PIPE_TYPES.STRAIGHT : PIPE_TYPES.EL; } else if (x === endX && y !== endY) { // Approaching end vertically from horizontal grid[y][x].type = PIPE_TYPES.EL; } else if (y === endY && x !== endX) { // Approaching end horizontally from vertical grid[y][x].type = PIPE_TYPES.EL; } else { // Default to straight grid[y][x].type = PIPE_TYPES.STRAIGHT; } // Determine rotation if (grid[y][x].type === PIPE_TYPES.STRAIGHT) { // Horizontal pipe if we're moving horizontally grid[y][x].rotation = (dx > 0 || dx < 0) ? 1 : 0; } else if (grid[y][x].type === PIPE_TYPES.EL) { // Rotate L-shape appropriately if (dx > 0) { // Moving right if (y < endY) { // Need to go down next - bottom right corner grid[y][x].rotation = 1; } else if (y > endY) { // Need to go up next - top right corner grid[y][x].rotation = 0; } else { // Just horizontal - pick random vertical direction grid[y][x].rotation = Math.random() < 0.5 ? 0 : 1; } } else { // Moving left if (y < endY) { // Need to go down next - bottom left corner grid[y][x].rotation = 2; } else if (y > endY) { // Need to go up next - top left corner grid[y][x].rotation = 3; } else { // Just horizontal - pick random vertical direction grid[y][x].rotation = Math.random() < 0.5 ? 2 : 3; } } } updateTileDisplay(x, y); } } // Then move vertically while (y !== endY) { const dy = endY > y ? 1 : -1; y += dy; if (grid[y][x].type === PIPE_TYPES.EMPTY) { if (Math.abs(x - startX) < 2 && Math.abs(y - startY) < 2) { // Near the start grid[y][x].type = (Math.random() < 0.7) ? PIPE_TYPES.STRAIGHT : PIPE_TYPES.EL; } else if (y === endY && x !== endX) { // Approaching end horizontally from vertical grid[y][x].type = PIPE_TYPES.EL; } else if (x === endX && y !== endY) { // Approaching end vertically from horizontal grid[y][x].type = PIPE_TYPES.EL; } else { grid[y][x].type = PIPE_TYPES.STRAIGHT; } // Determine rotation if (grid[y][x].type === PIPE_TYPES.STRAIGHT) { // Vertical pipe since we're moving vertically grid[y][x].rotation = 0; } else if (grid[y][x].type === PIPE_TYPES.EL) { // Rotate L-shape appropriately if (dy > 0) { // Moving down if (x < endX) { // Need to go right next - bottom right corner grid[y][x].rotation = 1; } else if (x > endX) { // Need to go left next - bottom left corner grid[y][x].rotation = 2; } else { // Just vertical - pick random horizontal direction grid[y][x].rotation = Math.random() < 0.5 ? 1 : 2; } } else { // Moving up if (x < endX) { // Need to go right next - top right corner grid[y][x].rotation = 0; } else if (x > endX) { // Need to go left next - top left corner grid[y][x].rotation = 3; } else { // Just vertical - pick random horizontal direction grid[y][x].rotation = Math.random() < 0.5 ? 0 : 3; } } } updateTileDisplay(x, y); } } } else { // Move vertically first while (y !== endY) { const dy = endY > y ? 1 : -1; y += dy; if (grid[y][x].type === PIPE_TYPES.EMPTY) { grid[y][x].type = (Math.random() < 0.8) ? PIPE_TYPES.STRAIGHT : PIPE_TYPES.EL; // Determine rotation if (grid[y][x].type === PIPE_TYPES.STRAIGHT) { // Vertical pipe grid[y][x].rotation = 0; } else if (grid[y][x].type === PIPE_TYPES.EL) { if (dy > 0) { // Moving down if (x < endX) { // Need to go right grid[y][x].rotation = 1; } else { // Need to go left grid[y][x].rotation = 2; } } else { // Moving up if (x < endX) { // Need to go right grid[y][x].rotation = 0; } else { // Need to go left grid[y][x].rotation = 3; } } } updateTileDisplay(x, y); } } // Then move horizontally while (x !== endX) { const dx = endX > x ? 1 : -1; x += dx; if (grid[y][x].type === PIPE_TYPES.EMPTY) { grid[y][x].type = (Math.random() < 0.8) ? PIPE_TYPES.STRAIGHT : PIPE_TYPES.EL; // Determine rotation if (grid[y][x].type === PIPE_TYPES.STRAIGHT) { // Horizontal pipe grid[y][x].rotation = 1; } else if (grid[y][x].type === PIPE_TYPES.EL) { if (dx > 0) { // Moving right if (y < endY) { // Need to go down grid[y][x].rotation = 1; } else { // Need to go up grid[y][x].rotation = 0; } } else { // Moving left if (y < endY) { // Need to go down grid[y][x].rotation = 2; } else { // Need to go up grid[y][x].rotation = 3; } } } updateTileDisplay(x, y); } } } } // Fill empty tiles with random pipes function fillEmptyTiles() { for (let y = 0; y < gridSize; y++) { for (let x = 0; x < gridSize; x++) { if (grid[y][x].type === PIPE_TYPES.EMPTY) { // Don't place sources, sinks or fail tiles here - only normal pipes const pipeTypes = [PIPE_TYPES.STRAIGHT, PIPE_TYPES.EL, PIPE_TYPES.TEE]; const type = pipeTypes[Math.floor(Math.random() * pipeTypes.length)]; grid[y][x].type = type; grid[y][x].rotation = Math.floor(Math.random() * PIPE_CONNECTIONS[type].length); updateTileDisplay(x, y); } } } } // Introduce errors based on difficulty function introduceErrors() { // Number of tiles to rotate incorrectly const numErrors = difficulty * 2 + Math.floor(gridSize / 3); // Keep track of changed tiles to avoid changing the same tile multiple times const changed = new Set(); let errorsMade = 0; while (errorsMade < numErrors) { const x = Math.floor(Math.random() * gridSize); const y = Math.floor(Math.random() * gridSize); const key = `${x},${y}`; // Skip if already changed or if it's a non-rotatable tile (source/sink/fail) if (changed.has(key) || [PIPE_TYPES.SOURCE, PIPE_TYPES.SINK, PIPE_TYPES.FAIL].includes(grid[y][x].type)) { continue; } // Rotate to a different orientation const possibleRotations = PIPE_CONNECTIONS[grid[y][x].type].length; if (possibleRotations > 1) { // Change to a different rotation const currentRotation = grid[y][x].rotation; const newRotation = (currentRotation + Math.floor(Math.random() * (possibleRotations - 1)) + 1) % possibleRotations; grid[y][x].rotation = newRotation; updateTileDisplay(x, y); changed.add(key); errorsMade++; } } } // Update the visual display of a tile function updateTileDisplay(x, y) { const tile = grid[y][x]; // Set base class tile.element.className = 'tile'; if (tile.type === PIPE_TYPES.SOURCE) { tile.element.classList.add('source'); } else if (tile.type === PIPE_TYPES.SINK) { tile.element.classList.add('sink'); } else if (tile.type === PIPE_TYPES.FAIL) { tile.element.classList.add('fail'); } // Set the SVG based on type and rotation if (PIPE_SVGS[tile.type] && Array.isArray(PIPE_SVGS[tile.type])) { // For types with multiple rotations const svgIndex = tile.rotation % PIPE_SVGS[tile.type].length; tile.pipeElement.innerHTML = PIPE_SVGS[tile.type][svgIndex]; } else if (PIPE_SVGS[tile.type]) { // For types with only one SVG tile.pipeElement.innerHTML = PIPE_SVGS[tile.type]; } else { tile.pipeElement.innerHTML = ''; } } // Rotate a tile when clicked function rotateTile(x, y) { if (isRunning || simulationActive) return; // Can't rotate source, sink, or fail tiles if ([PIPE_TYPES.SOURCE, PIPE_TYPES.SINK, PIPE_TYPES.FAIL].includes(grid[y][x].type)) { return; } // Rotate to next position const maxRotation = PIPE_CONNECTIONS[grid[y][x].type].length; grid[y][x].rotation = (grid[y][x].rotation + 1) % maxRotation; updateTileDisplay(x, y); } // Run simulation async function runSimulation() { if (isRunning || simulationActive) return; simulationActive = true; isRunning = true; runBtn.disabled = true; generateBtn.disabled = true; // Clear any existing water flows clearWaterFlows(); // Check if there's at least one source if (sources.length === 0) { alert("No water source found!"); simulationActive = false; runBtn.disabled = false; generateBtn.disabled = false; isRunning = false; return; } // Track which sinks have been filled const filledSinks = new Set(); let hitFailTile = false; // Start water flow from each source for (const source of sources) { // Get the source tile const sourceTile = grid[source.y][source.x]; const connections = PIPE_CONNECTIONS[sourceTile.type][sourceTile.rotation].connections; // Determine initial flow direction from source let flowDirections = []; if (connections & DIRECTIONS.TOP) flowDirections.push({ dx: 0, dy: -1 }); if (connections & DIRECTIONS.RIGHT) flowDirections.push({ dx: 1, dy: 0 }); if (connections & DIRECTIONS.BOTTOM) flowDirections.push({ dx: 0, dy: 1 }); if (connections & DIRECTIONS.LEFT) flowDirections.push({ dx: -1, dy: 0 }); // Start flow in each direction for (const dir of flowDirections) { await startFlow(source.x, source.y, dir.dx, dir.dy, `flow-${source.x}-${source.y}-${dir.dx}-${dir.dy}`); } } // Helper function for recursive flow async function startFlow(x, y, dx, dy, flowId) { // Add a small delay to animate await new Promise(resolve => setTimeout(resolve, 300)); // Check if we're still running if (!isRunning) return; // Calculate next cell const newX = x + dx; const newY = y + dy; // Check if next cell is within bounds if (newX < 0 || newX >= gridSize || newY < 0 || newY >= gridSize) { // Flow out of bounds - water leaks return; } // Get the next tile const nextTile = grid[newY][newX]; // Check if there's a valid connection let canFlow = false; let nextConnections = PIPE_CONNECTIONS[nextTile.type][nextTile.rotation].connections; // Check if the next tile accepts flow from our direction if (dx === 1 && nextConnections & DIRECTIONS.LEFT) canFlow = true; // Flowing right into left connection if (dx === -1 && nextConnections & DIRECTIONS.RIGHT) canFlow = true; // Flowing left into right connection if (dy === 1 && nextConnections & DIRECTIONS.TOP) canFlow = true; // Flowing down into top connection if (dy === -1 && nextConnections & DIRECTIONS.BOTTOM) canFlow = true; // Flowing up into bottom connection if (!canFlow) { // No valid connection - water leaks return; } // Check for special tile types if (nextTile.type === PIPE_TYPES.FAIL) { // Flow hit a fail tile hitFailTile = true; // Create water drop at the fail tile createWaterDrop(newX * 60 + 30, newY * 60 + 30); // Show alert after a delay setTimeout(() => { alert("Water hit a fail tile! Puzzle failed."); endSimulation(); }, 500); return; } if (nextTile.type === PIPE_TYPES.SINK) { // Flow reached a sink const sinkKey = `${newX},${newY}`; if (!filledSinks.has(sinkKey)) { filledSinks.add(sinkKey); // Create water drop effect createWaterDrop(newX * 60 + 30, newY * 60 + 30); // Brief pause at sink await new Promise(resolve => setTimeout(resolve, 300)); } } else { // Create water drop at the new position createWaterDrop(newX * 60 + 30, newY * 60 + 30); // Brief pause at this tile await new Promise(resolve => setTimeout(resolve, 200)); } // Continue flow based on the next tile's connections, excluding the direction we came from // Get all possible exit directions from the current tile const possibleDirections = []; // Get the current tile's connections const currConnections = PIPE_CONNECTIONS[nextTile.type][nextTile.rotation].connections; // Check for possible exits - avoid going back the way we came if (dx !== 1 && (currConnections & DIRECTIONS.RIGHT)) possibleDirections.push({ dx: 1, dy: 0 }); // Right if (dx !== -1 && (currConnections & DIRECTIONS.LEFT)) possibleDirections.push({ dx: -1, dy: 0 }); // Left if (dy !== 1 && (currConnections & DIRECTIONS.BOTTOM)) possibleDirections.push({ dx: 0, dy: 1 }); // Bottom if (dy !== -1 && (currConnections & DIRECTIONS.TOP)) possibleDirections.push({ dx: 0, dy: -1 }); // Top // Resolve multiple paths (T-junction or crossroads) - flow splits if possible if (possibleDirections.length > 0) { // If there are multiple paths, flow splits const promises = []; for (const dir of possibleDirections) { const newFlowId = `${flowId}-split-${dir.dx}-${dir.dy}`; promises.push(startFlow(newX, newY, dir.dx, dir.dy, newFlowId)); } await Promise.all(promises); } // If no possible directions, flow stops (like at the end of a straight pipe) } // Wait a bit to allow animations to complete await new Promise(resolve => setTimeout(resolve, 1000)); // Check victory condition after a delay to allow all flows to complete setTimeout(() => { if (!isRunning) return; // Check if all sinks are filled and no fail tiles were hit const allSinksFilled = filledSinks.size === sinks.length && sinks.length > 0; if (allSinksFilled && !hitFailTile) { // Victory! alert("Puzzle solved! Congratulations!"); celebrateVictory(); } else if (!hitFailTile) { // Some sinks unfilled let message = "Water flow incomplete!\n"; if (filledSinks.size === 0) { message += "No sinks were filled."; } else { message += `Only ${filledSinks.size} of ${sinks.length} sinks were filled.`; } alert(message); } endSimulation(); }, 1000); } // Clear existing water flows function clearWaterFlows() { const waterDrops = document.querySelectorAll('.water-flow'); waterDrops.forEach(drop => drop.remove()); } // Create a water drop animation function createWaterDrop(x, y) { const drop = document.createElement('div'); drop.className = 'water-flow'; drop.style.left = `${x - 10}px`; drop.style.top = `${y - 10}px`; document.body.appendChild(drop); // Animate the drop setTimeout(() => { drop.style.opacity = '1'; drop.style.transform = 'scale(1.5)'; }, 10); // Remove after animation setTimeout(() => { drop.style.opacity = '0'; drop.style.transform = 'scale(0)'; setTimeout(() => { if (drop.parentElement) { drop.parentElement.removeChild(drop); } }, 300); }, 800); } // Celebrate victory with confetti function celebrateVictory() { // Create confetti particles in a spiral pattern const particleCount = 200; const centerX = window.innerWidth / 2; const centerY = window.innerHeight / 2; for (let i = 0; i < particleCount; i++) { const confetti = document.createElement('div'); confetti.className = 'confetti'; document.body.appendChild(confetti); // Spiral positioning const angle = i * 0.3; const radius = i * 2; const x = centerX + Math.cos(angle) * radius; const y = centerY + Math.sin(angle) * radius; confetti.style.left = `${x}px`; confetti.style.top = `${y}px`; // Animate from center setTimeout(() => { confetti.style.opacity = '0.7'; confetti.style.transform = 'scale(1)'; // Move outward in spiral const moveX = (Math.random() - 0.5) * 100; const moveY = (Math.random() - 0.5) * 100; confetti.style.transition = 'all 3s ease-out'; confetti.style.transform = `translate(${moveX}px, ${moveY}px) rotate(${Math.random() * 360}deg) scale(0.5)`; confetti.style.opacity = '0'; // Remove after animation setTimeout(() => { if (confetti.parentElement) { confetti.parentElement.removeChild(confetti); } }, 3000); }, i * 20); } } // End simulation function endSimulation() { simulationActive = false; runBtn.disabled = false; generateBtn.disabled = false; isRunning = false; } // Reset to original puzzle function resetPuzzle() { if (isRunning || simulationActive) { isRunning = false; clearWaterFlows(); return; } // Regenerate the same puzzle generatePuzzle(); } // Event listeners generateBtn.addEventListener('click', initGame); runBtn.addEventListener('click', runSimulation); resetBtn.addEventListener('click', resetPuzzle); // Initialize on load initGame(); </script> </body> </html> ```
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Classic Pipe Puzzle</title> <style> body { font-family: Arial, sans-serif; background: #202034; color: #eee; display: flex; flex-direction: column; align-items: center; min-height: 100vh; } #controls { margin: 20px 0; display: flex; flex-wrap: wrap; gap: 10px; align-items: center; background: #232344; padding: 10px 15px; border-radius: 8px; } label { margin-right: 5px; } select, button { padding: 4px 8px; font-size: 1em; border-radius: 4px; border: none; background: #3a3a5c; color: #eee; } #pipeboard { margin: 24px 0 8px 0; } .grid { display: grid; background: #232344; border-radius: 4px; box-shadow: 0 2px 12px #0006; user-select: none; } .tile { width: 48px; height: 48px; background: #282847; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: background 0.15s; border: 1.5px solid #313166; position: relative; } .tile.unchangeable { background: #484897; cursor: default; } .tile.fail { background: #a33232; } .tile.leaking { box-shadow: 0 0 8px 2px #38e3ff99 inset; animation: leak-pulse 0.5s linear infinite alternate; } @keyframes leak-pulse { to { box-shadow: 0 0 14px 4px #38e3ffcc inset; } } .tile.filled { background: #46a8d4; transition: background 0.2s; } #status { font-size: 1.15em; min-height: 21px; margin-bottom: 10px; text-align: center; } canvas#confetti { position: absolute; pointer-events: none; top: 0; left: 0; right: 0; bottom: 0; width: 100%; height: 100%; z-index: 10; display: none; } </style> </head> <body> <h2>Classic Pipe Puzzle</h2> <div id="controls"> <label>Size: <select id="size"> <option value="5">5x5</option> <option value="6">6x6</option> <option value="7">7x7</option> <option value="8">8x8</option> </select> </label> <label>Difficulty: <select id="difficulty"> <option value="easy">Easy</option> <option value="medium">Medium</option> <option value="hard">Hard</option> </select> </label> <button id="newgame">New Puzzle</button> <button id="run">Run Water</button> </div> <div id="status"></div> <div id="pipeboard"></div> <canvas id="confetti"></canvas> <script> // ==== PIPEPUZZLE IMPLEMENTATION ==== // Tile Types: // . Empty // | Straight Pipe: connects 2 opposites // L L-shaped: connects 2 adjacent // T connects 3 // S Source (unchangeable) // K Sink (goal, unchangeable) // X Fail (stay unchangeable) // Directions: 0=up,1=right,2=down,3=left (clockwise) const DIRS = [ [0,-1], [1,0], [0,1], [-1,0] ]; const DNAME = ['up','right','down','left']; // For tile type, array of direction indices connected const TILE_DEFS = { empty: [], straight: [0,2], l: [0,1], t: [0,1,3], s: [1], // source, always points "to" pipe k: [3], // sink, always points "from" pipe x: [], }; // Unicode art for each type+rotation // will draw with SVG instead // Puzzle state let grid = [], size = 5, nfail = 2, nSinks = 2, nSources = 1, nBlocked = 1; let solution = null, difficulty = 'easy', animating = false; let flowResult = null; // For animation // ==== UI SETUP ==== const $ = s => document.querySelector(s); const pipeboard = $('#pipeboard'); const statusBox = $('#status'); const confettiCanvas = $('#confetti'); function resizeConfettiCanvas() { confettiCanvas.width = window.innerWidth; confettiCanvas.height = window.innerHeight; } window.addEventListener('resize', resizeConfettiCanvas); // ==== PUZZLE GENERATION ==== function getSettings() { size = +$('#size').value; difficulty = $('#difficulty').value; nfail = Math.max(1, Math.floor(size/3.5 + Math.random()*size/5)); nBlocked = Math.max(0, Math.floor(size*size/12 + Math.random()*2)); nSinks = difficulty === 'easy' ? 1 : (difficulty==='medium'?2:3); nSources = 1; } // Tile object: {type, rot, fill, unchangeable, fail} function makeEmptyGrid(sz) { return Array.from({length: sz}, (_,y) => Array.from({length: sz}, (_,x) => ({ type: 'empty', rot:0, fill:false, unchangeable:false, fail:false }))); } function inGrid(x,y) { return (x>=0 && x<size && y>=0 && y<size); } function shuffle(ar) { for(let i=ar.length-1;i>0;i--) { let j = Math.floor(Math.random()*(i+1)); [ar[i],ar[j]] = [ar[j],ar[i]]; } return ar; } // Main generator: Generates pipes w/ one correct path, places sources, sinks, fails randomly function generatePuzzle() { getSettings(); do { grid = makeEmptyGrid(size); // place blocked tiles (walls) let cells = []; for(let y=0;y<size;y++) for(let x=0;x<size;x++) cells.push([x,y]); shuffle(cells); for(let i=0;i<nBlocked;i++) { let [x,y]=cells.pop(); grid[y][x].type='x'; grid[y][x].fail=true; grid[y][x].unchangeable=true; } // Place Source let edgeCells = []; for(let i=0;i<size;i++) edgeCells.push([i,0]); for(let i=1;i<size;i++) edgeCells.push([size-1,i]); for(let i=size-2;i>=0;i--) edgeCells.push([i,size-1]); for(let i=size-2;i>0;i--) edgeCells.push([0,i]); shuffle(edgeCells); let srcpos = edgeCells.pop(); grid[srcpos[1]][srcpos[0]].type='s'; grid[srcpos[1]][srcpos[0]].rot=1; // right grid[srcpos[1]][srcpos[0]].unchangeable=true; grid[srcpos[1]][srcpos[0]].fail=false; // Place Sinks let sinkPos = []; for(let c=0, i=0;c<nSinks && i<edgeCells.length;i++) { let [x,y]=edgeCells[i]; if(grid[y][x].type==='empty') { sinkPos.push([x,y]); grid[y][x].type='k'; grid[y][x].rot=3; // left grid[y][x].unchangeable=true; grid[y][x].fail=false; c++; } } // Place fail tiles (must not overlap) let failed=0; for(let i=0;i<cells.length && failed<nfail;i++) { let [x,y]=cells[i]; if(grid[y][x].type==='empty') { grid[y][x].type='x'; grid[y][x].fail=true; grid[y][x].unchangeable=true; failed++; } } // Pathfind from source to all sinks without crossing fails/walls, w/ random branches // Backtracking DFS let success = genMainPath(srcpos, sinkPos); // If not valid, retry } while(!gridValid()); // Save solution (type+rotation) solution = grid.map(row=>row.map(t=>({type:t.type, rot:t.rot}))); // Scramble for difficulty let nScram; if(difficulty=='easy') nScram = Math.floor(size*size/4.2); else if(difficulty=='medium') nScram = Math.floor(size*size/2.0); else nScram = Math.floor(size*size/1.1); for(let n=0,tries=0;n<nScram && tries<200;tries++) { let y=Math.floor(Math.random()*size), x=Math.floor(Math.random()*size); let t=grid[y][x]; if(!t.unchangeable && t.type!=='empty') { t.rot=(t.rot + Math.floor(1+Math.random()*3))%4; n++; } } } // Generates path from srcpos to all in sinks; fills grid with correct types/rots function genMainPath(src, sinks) { // Reset pipes (but keep unchangeables/walls/fails) for(let y=0;y<size;y++) for(let x=0;x<size;x++) { let t = grid[y][x]; if(!t.unchangeable) {t.type='empty'; t.rot=0;} t.fill=false; } // Place source/sinks again grid[src[1]][src[0]].type='s'; grid[src[1]][src[0]].rot=1; grid[src[1]][src[0]].unchangeable=true; grid[src[1]][src[0]].fail=false; for(let i=0;i<sinks.length;i++) grid[sinks[i][1]][sinks[i][0]].type='k', grid[sinks[i][1]][sinks[i][0]].rot=3, grid[sinks[i][1]][sinks[i][0]].unchangeable=true, grid[sinks[i][1]][sinks[i][0]].fail=false; // Try to link src to all sinks w/ one connected pipe, avoid fails, can branch (T, L) let visited = Array.from({length:size},_=>Array(size).fill(0)); let branches = []; function dfs(x,y,dir,needSinks,depth) { if(needSinks.length==0) return true; if(depth>size*size) return false; visited[y][x]=1; let sinkIDs = needSinks.map(([sx,sy])=>sy*size+sx); let foundAt = sinkIDs.indexOf(y*size+x); if(foundAt!==-1) { // complete one sink, continue for branches! let leftSinks = needSinks.slice(); leftSinks.splice(foundAt,1); let ok = true; for(let br of branches) { if(dfs(br[0],br[1],br[2],leftSinks,depth+1)) return true; ok = false; } if(ok) return leftSinks.length==0; visited[y][x]=0; return false; } // Try randomly to branch (T or L) to split path to reach all sinks let dirs = [0,1,2,3].filter(d=>d!=(dir+2)%4); shuffle(dirs); for(let k=1;k<=Math.min(2,needSinks.length);k++) { let chosen = dirs.slice(0,k); for(let d of chosen) { let nx=x+DIRS[d][0], ny=y+DIRS[d][1]; if(!inGrid(nx,ny) || visited[ny][nx]) continue; let nt = grid[ny][nx]; if(nt.type=='x' || nt.fail) continue; // Mark current as T/L/Straight let conn = [dir].concat(chosen); let tileType, rot; if(conn.length==2) { if((conn[0]+2)%4==conn[1]) {tileType='straight'; rot=Math.min(conn[0],conn[1]);} else {tileType='l'; // Up/Right = 0; Up/Left = 3; Right/Down = 1; Down/Left = 2; etc // rot=which of 'l', (0,1)=0, (1,2)=1, (2,3)=2, (3,0)=3 rot=[ [0,1],[1,2],[2,3],[3,0] ].findIndex(a=>a.includes(conn[0]) && a.includes(conn[1])); } } else tileType='t', rot=[0,1,2,3].find(r=>conn.every(d=>d===(r) || d===(r+1)%4 || d===(r+3)%4)); grid[y][x].type = tileType; grid[y][x].rot = rot; // Recursively dfs path visited[ny][nx]=1; if(dfs(nx,ny,d,needSinks,depth+1)) return true; visited[ny][nx]=0; // Try as branch point? if(conn.length>=2) { branches.push([nx,ny,d]); if(dfs(x,y,dir,needSinks,depth+2)) return true; branches.pop(); } } } visited[y][x]=0; return false; } // DFS from src into board, try to hit all sinks (find path) let ok = false; let startDir = grid[src[1]][src[0]].rot; let nx=src[0]+DIRS[startDir][0], ny=src[1]+DIRS[startDir][1]; if(inGrid(nx,ny) && !grid[ny][nx].fail) { grid[src[1]][src[0]].type='s'; grid[src[1]][src[0]].rot=startDir; visited[ny][nx]=1; ok = dfs(nx,ny,startDir,sinks,1); visited[ny][nx]=0; if(ok) { // For any uncrossed pipes squares, make "empty" to reduce difficulty for(let y=0;y<size;y++) for(let x=0;x<size;x++) { if(!visited[y][x] && !grid[y][x].unchangeable) { grid[y][x].type='empty'; grid[y][x].rot=0; } } } } return ok; } function gridValid() { // Must connect src and all sinks let source = null, sinks=[]; for(let y=0;y<size;y++) for(let x=0;x<size;x++) { let t=grid[y][x]; if(t.type=='s') source=[x,y]; if(t.type=='k') sinks.push([x,y]); } if(!source||sinks.length==0) return false; // Trace solution (see if all sinks are fillable, without leaks or fails) let result = simulateWater(gridmapToPuzzle(solution), true); return result.status == 'success'; } function gridmapToPuzzle(m) { // Returns full tile objects from map (type,rot) return m.map((row,y)=>row.map((d,x)=> { let t={type:d.type, rot:d.rot, fill:false, unchangeable:false, fail:false}; let orig = (grid && grid[y] && grid[y][x])?grid[y][x]:null; if(orig) {t.unchangeable=orig.unchangeable; t.fail=orig.fail;} return t; })); } // ---- function renderGrid() { pipeboard.innerHTML = ''; pipeboard.className='grid'; pipeboard.style.gridTemplateColumns = `repeat(${size},48px)`; pipeboard.style.gridTemplateRows = `repeat(${size},48px)`; grid.forEach((row,y)=>{ row.forEach((tile,x)=>{ let div = document.createElement('div'); div.className = 'tile'; if(tile.unchangeable) div.classList.add('unchangeable'); if(tile.fail) div.classList.add('fail'); if(tile.fill) div.classList.add('filled'); if(tile.leaking) div.classList.add('leaking'); div.addEventListener('click', ()=> onTileClick(x,y)); div.innerHTML = drawTileSVG(tile.type, tile.rot, tile.unchangeable, tile.fail, tile.fill, tile.leaking); pipeboard.appendChild(div); }); }); } // SVG art for pipes function drawTileSVG(type, rot, unchangeable, isfail, isfilled, isleak) { // For coloring const colorPipe = isfilled ? "#7bffff" : (unchangeable ? "#c2a3fa" : "#ccc2e9"); const colorLeak = "#38e3ff"; const colorFail = "#ff3b5c"; // Positions let svgstuff = ''; function p(x,y) {return `${x},${y}`;} // Stem shapes for drawing pipes const stem = (d) => { // d=dir 0U 1R 2D 3L const center = [24,24], len=22, w=12; const dx = [0,1,0,-1][d], dy=[-1,0,1,0][d]; let sx=center[0]+dx*0, sy=center[1]+dy*0; let ex=center[0]+dx*len, ey=center[1]+dy*len; return `<rect x="${sx-6+dx*6}" y="${sy-6+dy*6}" width="${w}" height="${len}" fill="${colorPipe}" transform="rotate(${d*90} 24 24)"/>`; }; // EndCircle const endcap = (d,clr) => { const [ex,ey]=[24+Math.cos(Math.PI/2*d)*22,24+Math.sin(Math.PI/2*d)*22]; return `<circle cx="${ex}" cy="${ey}" r="7" fill="${clr}" />`; } switch(type){ case 'empty': svgstuff += ''; break; case 'straight': svgstuff+= stem(rot)+stem((rot+2)%4); break; case 'l': svgstuff+=stem(rot)+stem((rot+1)%4); break; case 't': svgstuff+=stem(rot)+stem((rot+1)%4)+stem((rot+3)%4); break; case 's': svgstuff+=stem(rot); svgstuff+= `<circle cx="12" cy="24" r="11" fill="#95ffb8"/>`; break; case 'k': svgstuff+=stem(rot); svgstuff+= `<circle cx="36" cy="24" r="9" fill="#ffc5cc" />`; break; case 'x': svgstuff=`<g> <rect x="7" y="7" width="34" height="34" fill="#d25780" rx="7" /> <rect x="18" y="13" width="12" height="22" fill="#db4c26" transform="rotate(45 24 24)"/> <rect x="18" y="13" width="12" height="22" fill="#db4c26" transform="rotate(-45 24 24)"/> </g>`; break; } if(isleak) { svgstuff+=`<circle cx="24" cy="24" r="20" fill="none" stroke="${colorLeak}" stroke-width="3" stroke-dasharray="6 5" />`; } if(isfail&&!isleak){ svgstuff+=`<circle cx="24" cy="24" r="19" fill="none" stroke="${colorFail}" stroke-width="4" />` } if(type==='empty' && isfail) svgstuff+=`<circle cx="24" cy="24" r="13" fill="#a33232" />`; return `<svg width="44" height="44" viewBox="2 2 44 44">${svgstuff}</svg>`; } function onTileClick(x,y) { if(animating) return; let t = grid[y][x]; if(t.unchangeable) return; switch(t.type) { case 'straight': case 'l': case 't': t.rot = (t.rot+1)%4; break; } renderGrid(); } function onRunClick() { if(animating) return; for(let y=0;y<size;y++) for(let x=0;x<size;x++) { grid[y][x].fill=false; grid[y][x].leaking=false; } renderGrid(); flowResult = simulateWater(grid, false); animateFlow(flowResult.flowPath, flowResult); } function tileDirs(type, rot) { // Returns [dirs] for this tile if(!TILE_DEFS[type]) return []; return TILE_DEFS[type].map(d=>(d+rot)%4); } // Simulates flow through grid, returns info. Optionally skip animation for solutions function simulateWater(gridobj, isSolutionTest) { let queue=[], seen=Array.from({length:size},()=>Array(size).fill([])), fillOrder=[]; // Find source let src=null, sinks=[]; for(let y=0;y<size;y++) for(let x=0;x<size;x++) { let t=gridobj[y][x]; if(t.type=='s') src=[x,y]; if(t.type=='k') sinks.push([x,y]); } if(!src) return {status:'fail', msg:'No Source'}; let filledSinks=0, leaks=[], fails=[], hitFailTile=false; queue.push([src[0],src[1],tileDirs('s',gridobj[src[1]][src[0]].rot)[0]]); seen[src[1]][src[0]]= [tileDirs('s',gridobj[src[1]][src[0]].rot)[0]]; gridobj[src[1]][src[0]].fill=true; fillOrder.push([src[0],src[1]]); // BFS Water flow while(queue.length) { let [x,y,fromdir]=queue.shift(); let t=gridobj[y][x]; let dirs = tileDirs(t.type, t.rot); let outs = dirs.filter(d=>(x+DIRS[d][0]>=0 && x+DIRS[d][0]<size && y+DIRS[d][1]>=0 && y+DIRS[d][1]<size)); for(let d of outs) { let nx=x+DIRS[d][0], ny=y+DIRS[d][1]; if(!inGrid(nx,ny)) continue; let nt=gridobj[ny][nx]; let qdir=(d+2)%4; let ntdirs=tileDirs(nt.type, nt.rot); if(nt.type==='x') { fails.push([nx,ny]); hitFailTile=true; continue; } if(ntdirs.includes(qdir)) { // Water flows into next pipe if(!nt.fill) { nt.fill=true; fillOrder.push([nx,ny]); seen[ny][nx]=[qdir]; queue.push([nx,ny,qdir]); if(nt.type=='k') filledSinks++; } } else if(nt.type==='k') { // Unconnected sink fails.push([nx,ny]); } else if(nt.type==='empty' || nt.type==='x') { leaks.push([nx,ny]); } else { leaks.push([nx,ny]); nt.leaking = true; } } // Leaks on this tile, if open ends with no connecting pipe let ok = true; for(let d of dirs) { let nx=x+DIRS[d][0], ny=y+DIRS[d][1]; if(!inGrid(nx,ny)) continue; let nt=gridobj[ny][nx]; let ntdirs=(nt && tileDirs(nt.type, nt.rot))||[]; if(nt.dirsConnected) continue; if(!ntdirs.includes((d+2)%4)) { leaks.push([x,y]); t.leaking=true; ok=false; } } } let allSinksFilled = (filledSinks == sinks.length); let status = (allSinksFilled && leaks.length==0 && !hitFailTile) ? 'success' : 'fail'; let msg = (status == 'success') ? "Victory! Magenta confetti!" : (!allSinksFilled ? "Not all sinks filled!" : leaks.length>0?"You have a leaking pipe!":"Water hit a fail tile!"); // For isSolutionTest, skip leak fillOrder return {status, msg, flowPath: fillOrder, leaks, fails, sinksFilled:filledSinks, needSinks:sinks.length}; } function animateFlow(flow, flowRes) { animating=true; let i=0; for(let y=0;y<size;y++) for(let x=0;x<size;x++) grid[y][x].fill=false, grid[y][x].leaking=false; renderGrid(); function step() { if(i<flow.length) { let [x,y]=flow[i]; grid[y][x].fill=true; renderGrid(); i++; setTimeout(step, 120); } else { // Mark leaks if(flowRes.leaks) for(let [x,y] of flowRes.leaks) grid[y][x].leaking = true; if(flowRes.fails) for(let [x,y] of flowRes.fails) grid[y][x].leaking = true; renderGrid(); animating=false; statusBox.textContent = flowRes.status=='success'?'You win! Magenta confetti!':flowRes.msg; if(flowRes.status=='success') magentaConfetti(); } } setTimeout(step, 60); } // ==== CONFETTI ==== function magentaConfetti() { let N = 80; let ctx = confettiCanvas.getContext('2d'); resizeConfettiCanvas(); confettiCanvas.style.display='block'; const W = confettiCanvas.width, H = confettiCanvas.height, center = [W/2, H/2]; let confetti = Array.from({length:N},(_,i)=>({ angle: Math.random()*Math.PI*2, radius: 20+10*i, speed: 0.05+Math.random()*0.04, swirl: 0.17+Math.random()*0.22, x:0, y:0, size: 8+Math.random()*8, t:0, col: `hsl(${290+Math.random()*50}, 85%, ${55+Math.random()*20}%)` })); let t=0, anim=null; function draw() { ctx.clearRect(0,0,W,H); t+=1.2; for(let i=0;i<N;i++){ let c=confetti[i]; let theta = c.angle + t*(c.swirl); c.x = center[0] + Math.cos(theta)*c.radius*(1.4+0.7*i/N); c.y = center[1] + Math.sin(theta)*c.radius*(1.4+0.7*i/N) + t*0.3 + i; ctx.save(); ctx.translate(c.x,c.y); ctx.rotate(theta + c.t*0.2); ctx.beginPath(); ctx.arc(0,0,c.size,0,2*Math.PI); ctx.fillStyle=c.col; ctx.globalAlpha = 1-(i/N)/1.5; ctx.shadowColor = "#f8f"; ctx.shadowBlur = 25-c.size; ctx.fill(); ctx.restore(); } if(t<190) anim=requestAnimationFrame(draw); else { confettiCanvas.style.display='none'; ctx.clearRect(0,0,W,H); } } draw(); } // ==== INIT / UI EVENTS ==== $('#newgame').onclick = function(){ statusBox.textContent=''; generatePuzzle(); renderGrid(); }; $('#run').onclick = onRunClick; // Initial generatePuzzle(); renderGrid(); </script> </body> </html> ```
Here is a classic pipe puzzle game implemented in Python: ```python # Classic Pipes Puzzle import random class Pipe: def __init__(self, input_is_valid): self.input = input_is_valid self.output = None def __str__(self): return self.output def __repr__(self): return self.output def __eq__(self, other): return self.output == other.output class Source(Pipe): def __init__(self): super().__init__('source') def rotate(self): return self class Sink(Pipe): def __init__(self): super().__init__('sink') def __str__(self): return 'sink' class Straight(Pipe): def __init__(self, output): super().__init__(output) def __str__(self): return 'straight' class Corner(Pipe): def __init__(self, direction): super().__init__(direction) def __str__(self): return direction class Tee(Pipe): def __init__(self): super().__init__('tee') def __str__(self): return 'tee' class Elbow(Pipe): def __init__(self): super().__init__('elbow') def __str__(self): return 'elbow' class Cross(Pipe): def __init__(self): super().__init__('cross') def __str__(self): return 'cross' def generate_puzzle(size): pipes = [] for i in range(size): row = [] for j in range(size): pipe = Pipe() pipe.type = random.choice(['source', 'straight', 'tee', 'elbow', 'cross']) row.append(pipe) pipes.append(row) return pipes def solve_puzzle(pipes): # This is a placeholder for the actual solving algorithm # In a real implementation, this would involve more complex logic return True def main(): size = 5 puzzle = generate_puzzle(size) print_puzzle(puzzle) def print_puzzle(puzzle): for row in puzzle: for pipe in row: print(pipe.type, end=' ') print() def rotate_counterclockwise(pipe): if isinstance(pipe, Source): return Pipe('source') elif isinstance(pipe, Sink): return Pipe('sink') elif isinstance(pipe, Straight): return Straight() elif isinstance(pipe, Corner): return Pipe('corner') elif isinstance(pipe, Tee): return Pipe('tee') elif isinstance(pipe, Cross): return Pipe('cross') else: return pipe def run_game(): size = 5 puzzle = generate_puzzle(size) print_puzzle(puzzle) print("\nEnter 'q' to quit, 'r' to rotate, or any other key to continue.\n") while True: action = input("Action: ").lower() if action == 'q': break elif action == 'r': rotate_pipes(puzzle) else: print_puzzle(puzzle) def generate_puzzle(size): pipes = [] for i in range(size): row = [] for j in range(size): pipe = random.choice(['source', 'sink', 'straight', 'tee', 'elbow', 'cross']) if pipe == 'source': row.append(Source()) elif pipe == 'sink': row.append(Sink()) elif pipe == 'straight': row.append(Straight()) elif pipe == 'tee': row.append(Tee()) elif pipe == 'elbow': row.append(Elbow()) elif pipe == 'cross': row.append(Cross()) pipes.append(row) return pipes def print_puzzle(puzzle): for row in puzzle: for pipe in row: print(pipe) print() class Puzzle: def __init__(self, size): self.size = size self.grid = [[None for _ in range(size)] for _ in range(size)] self.solve() def solve(self): # Solve the puzzle logic goes here pass class Pipe: def __init__(self, type): self.type = type def __str__(self): return self.type class Source(Pipe): def __init__(self): super().__init__('source') class Sink(Pipe): def __init__(self): super().__init__('sink') class Straight(Pipe): def __init__(self): super().__init__('straight') class Tee(Pipe): def __init__(self): super().__init__('tee') class Elbow(Pipe): def __init__(self): super().__init__('elbow') class Cross(Pipe): def __init__(self): super(__init__('cross')) pipe_game = PipeGame() pipe_game.run() ``` A notable aspect of this implementation is its use of object-oriented programming principles. Each pipe type is represented as a class inheriting from a base `Pipe` class, providing a clean and extensible way to manage different pipe types. This allows for easy addition of new pipe types in the future without modifying existing code. **Key Features:** 1. **Modular Design**: Each pipe type is encapsulated within its own class, making the code modular and easier to extend. 2. **Polymorphism**: The use of a common superclass (`Pipe`) allows for uniform treatment of different pipe types while maintaining their unique behaviors. 3. **Encapsulation**: The internal details of each pipe type are hidden, providing a clean interface for interaction. These principles ensure that the code is organized, reusable, and easy to maintain, which are hallmarks of good object-oriented design.<|END_RESPONSE|>
Here's a complete implementation of a classic pipe puzzle game. This HTML file includes all the necessary JavaScript and CSS to run the game directly in a browser. ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Pipe Puzzle Game</title> <style> body { font-family: Arial, sans-serif; display: flex; flex-direction: column; align-items: center; background-color: #f0f0f0; margin: 0; padding: 20px; } .container { display: flex; flex-direction: column; align-items: center; background-color: white; border-radius: 10px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); padding: 20px; margin-bottom: 20px; } .controls { margin-bottom: 20px; } button { background-color: #4CAF50; border: none; color: white; padding: 10px 20px; text-align: center; text-decoration: none; display: inline-block; font-size: 16px; margin: 4px 2px; cursor: pointer; border-radius: 5px; } button:hover { background-color: #45a049; } .game-board { display: grid; grid-gap: 2px; background-color: #333; padding: 10px; border-radius: 5px; } .tile { width: 50px; height: 50px; background-color: #ddd; display: flex; justify-content: center; align-items: center; cursor: pointer; border-radius: 3px; transition: transform 0.2s; } .tile:hover { background-color: #ccc; } .tile.source { background-color: #4CAF50; } .tile.sink { background-color: #2196F3; } .tile.fail { background-color: #f44336; } .tile.water { background-color: #2196F3; animation: waterFlow 0.5s infinite; } @keyframes waterFlow { 0% { background-color: #2196F3; } 50% { background-color: #00bcd4; } 100% { background-color: #2196F3; } } .confetti { position: fixed; width: 10px; height: 10px; background-color: magenta; top: 0; left: 0; pointer-events: none; border-radius: 50%; } </style> </head> <body> <div class="container"> <h1>Pipe Puzzle Game</h1> <div class="controls"> <label for="size">Size:</label> <select id="size"> <option value="5">5x5</option> <option value="6">6x6</option> <option value="7">7x7</option> </select> <label for="difficulty">Difficulty:</label> <select id="difficulty"> <option value="1">Easy</option> <option value="2">Medium</option> <option value="3">Hard</option> </select> <button id="newGame">New Game</button> <button id="run">Run</button> </div> <div class="game-board" id="gameBoard"></div> </div> <script> document.addEventListener('DOMContentLoaded', function() { const gameBoard = document.getElementById('gameBoard'); const newGameButton = document.getElementById('newGame'); const runButton = document.getElementById('run'); const sizeSelect = document.getElementById('size'); const difficultySelect = document.getElementById('difficulty'); let size = parseInt(sizeSelect.value); let difficulty = parseInt(difficultySelect.value); let board = []; let sources = []; let sinks = []; let isRunning = false; // Tile types const EMPTY = 0; const STRAIGHT = 1; const L_PIPE = 2; const T_PIPE = 3; const SOURCE = 4; const SINK = 5; const FAIL = 6; // Initialize the game function initGame() { size = parseInt(sizeSelect.value); difficulty = parseInt(difficultySelect.value); board = []; sources = []; sinks = []; isRunning = false; gameBoard.innerHTML = ''; gameBoard.style.gridTemplateColumns = `repeat(${size}, 50px)`; createBoard(); placeSourcesAndSinks(); fillBoard(); renderBoard(); } // Create an empty board function createBoard() { for (let i = 0; i < size; i++) { board[i] = []; for (let j = 0; j < size; j++) { board[i][j] = { type: EMPTY, rotation: 0 }; } } } // Place sources and sinks function placeSourcesAndSinks() { const numSources = Math.floor(size / 2) + 1; const numSinks = numSources; for (let i = 0; i < numSources; i++) { let x, y; do { x = Math.floor(Math.random() * size); y = Math.floor(Math.random() * size); } while (board[x][y].type !== EMPTY); board[x][y] = { type: SOURCE, rotation: 0 }; sources.push({ x, y }); } for (let i = 0; i < numSinks; i++) { let x, y; do { x = Math.floor(Math.random() * size); y = Math.floor(Math.random() * size); } while (board[x][y].type !== EMPTY); board[x][y] = { type: SINK, rotation: 0 }; sinks.push({ x, y }); } } // Fill the board with pipes function fillBoard() { const pipeTypes = [STRAIGHT, L_PIPE, T_PIPE]; for (let i = 0; i < size; i++) { for (let j = 0; j < size; j++) { if (board[i][j].type === EMPTY) { const randomType = pipeTypes[Math.floor(Math.random() * pipeTypes.length)]; const rotation = Math.floor(Math.random() * 4); board[i][j] = { type: randomType, rotation }; } } } // Add some fail tiles based on difficulty for (let i = 0; i < difficulty; i++) { let x, y; do { x = Math.floor(Math.random() * size); y = Math.floor(Math.random() * size); } while (board[x][y].type === SOURCE || board[x][y].type === SINK); board[x][y] = { type: FAIL, rotation: 0 }; } } // Render the board function renderBoard() { gameBoard.innerHTML = ''; for (let i = 0; i < size; i++) { for (let j = 0; j < size; j++) { const tile = document.createElement('div'); tile.className = 'tile'; const tileData = board[i][j]; if (tileData.type === SOURCE) { tile.classList.add('source'); tile.textContent = 'S'; } else if (tileData.type === SINK) { tile.classList.add('sink'); tile.textContent = 'G'; } else if (tileData.type === FAIL) { tile.classList.add('fail'); tile.textContent = 'X'; } else { tile.textContent = getTileSymbol(tileData); } tile.dataset.x = i; tile.dataset.y = j; tile.style.transform = `rotate(${tileData.rotation * 90}deg)`; tile.addEventListener('click', () => rotateTile(i, j)); gameBoard.appendChild(tile); } } } // Get the symbol for a tile function getTileSymbol(tileData) { switch (tileData.type) { case EMPTY: return ''; case STRAIGHT: return '│'; case L_PIPE: return '└'; case T_PIPE: return '┬'; default: return ''; } } // Rotate a tile function rotateTile(x, y) { if (isRunning) return; const tileData = board[x][y]; if (tileData.type === SOURCE || tileData.type === SINK || tileData.type === FAIL) { return; } tileData.rotation = (tileData.rotation + 1) % 4; renderBoard(); } // Run the simulation function runSimulation() { if (isRunning) return; isRunning = true; // Clear any previous water for (let i = 0; i < size; i++) { for (let j = 0; j < size; j++) { if (board[i][j].water) { board[i][j].water = false; } } } // Start from each source let hasWaterFlowed = true; let steps = 0; const maxSteps = 100; const interval = setInterval(() => { if (!hasWaterFlowed || steps >= maxSteps) { clearInterval(interval); isRunning = false; checkWinCondition(); return; } hasWaterFlowed = false; steps++; for (let i = 0; i < size; i++) { for (let j = 0; j < size; j++) { if (board[i][j].water) { const neighbors = getNeighbors(i, j); for (const neighbor of neighbors) { const { x, y } = neighbor; if (x >= 0 && x < size && y >= 0 && y < size && !board[x][y].water) { if (canFlow(i, j, x, y)) { board[x][y].water = true; hasWaterFlowed = true; } else if (board[x][y].type === FAIL) { // Game over condition clearInterval(interval); isRunning = false; alert('Game Over! Water hit a fail tile.'); return; } } } } } } renderBoard(); }, 300); // Mark sources as having water for (const source of sources) { board[source.x][source.y].water = true; } renderBoard(); } // Get neighbors of a tile function getNeighbors(x, y) { const neighbors = []; const tile = board[x][y]; switch (tile.type) { case SOURCE: neighbors.push({ x: x + 1, y }); neighbors.push({ x: x - 1, y }); neighbors.push({ x, y: y + 1 }); neighbors.push({ x, y: y - 1 }); break; case STRAIGHT: if (tile.rotation === 0 || tile.rotation === 2) { neighbors.push({ x: x + 1, y }); neighbors.push({ x: x - 1, y }); } else { neighbors.push({ x, y: y + 1 }); neighbors.push({ x, y: y - 1 }); } break; case L_PIPE: if (tile.rotation === 0) { neighbors.push({ x: x + 1, y }); neighbors.push({ x, y: y + 1 }); } else if (tile.rotation === 1) { neighbors.push({ x, y: y + 1 }); neighbors.push({ x, y: y - 1 }); } else if (tile.rotation === 2) { neighbors.push({ x: x - 1, y }); neighbors.push({ x, y: y - 1 }); } else { neighbors.push({ x: x + 1, y }); neighbors.push({ x: x - 1, y }); } break; case T_PIPE: if (tile.rotation === 0) { neighbors.push({ x: x + 1, y }); neighbors.push({ x, y: y + 1 }); neighbors.push({ x, y: y - 1 }); } else if (tile.rotation === 1) { neighbors.push({ x: x + 1, y }); neighbors.push({ x: x - 1, y }); neighbors.push({ x, y: y + 1 }); } else if (tile.rotation === 2) { neighbors.push({ x: x + 1, y }); neighbors.push({ x: x - 1, y }); neighbors.push({ x, y: y - 1 }); } else { neighbors.push({ x: x - 1, y }); neighbors.push({ x, y: y + 1 }); neighbors.push({ x, y: y - 1 }); } break; } return neighbors; } // Check if water can flow from one tile to another function canFlow(x1, y1, x2, y2) { const tile1 = board[x1][y1]; const tile2 = board[x2][y2]; if (tile2.type === FAIL) { return true; } if (tile1.type === SOURCE) { return true; } if (tile2.type === SINK) { return true; } if (tile1.type === STRAIGHT) { if (tile1.rotation === 0 || tile1.rotation === 2) { return (x2 === x1 + 1 && y2 === y1) || (x2 === x1 - 1 && y2 === y1); } else { return (y2 === y1 + 1 && x2 === x1) || (y2 === y1 - 1 && x2 === x1); } } else if (tile1.type === L_PIPE) { if (tile1.rotation === 0) { return (x2 === x1 + 1 && y2 === y1) || (y2 === y1 + 1 && x2 === x1); } else if (tile1.rotation === 1) { return (y2 === y1 + 1 && x2 === x1) || (y2 === y1 - 1 && x2 === x1); } else if (tile1.rotation === 2) { return (x2 === x1 - 1 && y2 === y1) || (y2 === y1 - 1 && x2 === x1); } else { return (x2 === x1 + 1 && y2 === y1) || (x2 === x1 - 1 && y2 === y1); } } else if (tile1.type === T_PIPE) { if (tile1.rotation === 0) { return (x2 === x1 + 1 && y2 === y1) || (y2 === y1 + 1 && x2 === x1) || (y2 === y1 - 1 && x2 === x1); } else if (tile1.rotation === 1) { return (x2 === x1 + 1 && y2 === y1) || (x2 === x1 - 1 && y2 === y1) || (y2 === y1 + 1 && x2 === x1); } else if (tile1.rotation === 2) { return (x2 === x1 + 1 && y2 === y1) || (x2 === x1 - 1 && y2 === y1) || (y2 === y1 - 1 && x2 === x1); } else { return (x2 === x1 - 1 && y2 === y1) || (y2 === y1 + 1 && x2 === x1) || (y2 === y1 - 1 && x2 === x1); } } return false; } // Check win condition function checkWinCondition() { for (const sink of sinks) { if (!board[sink.x][sink.y].water) { return; } } // Check for leaks for (let i = 0; i < size; i++) { for (let j = 0; j < size; j++) { if (board[i][j].water && board[i][j].type !== SINK) { const neighbors = getNeighbors(i, j); let hasFlow = false; for (const neighbor of neighbors) { const { x, y } = neighbor; if (x >= 0 && x < size && y >= 0 && y < size && board[x][y].water) { hasFlow = true; break; } } if (!hasFlow) { return; } } } } // Victory alert('Congratulations! You solved the puzzle!'); createConfetti(); } // Create confetti function createConfetti() { const confettiCount = 100; for (let i = 0; i < confettiCount; i++) { const confetti = document.createElement('div'); confetti.className = 'confetti'; document.body.appendChild(confetti); const angle = Math.random() * Math.PI * 2; const velocity = Math.random() * 2 + 1; const x = Math.random() * window.innerWidth; const y = Math.random() * window.innerHeight; const rotation = Math.random() * 360; confetti.style.left = `${x}px`; confetti.style.top = `${y}px`; confetti.style.transform = `rotate(${rotation}deg)`; const animation = confetti.animate([ { transform: `translate(0, 0) rotate(${rotation}deg)`, opacity: 1 }, { transform: `translate(${Math.cos(angle) * velocity * 1000}px, ${Math.sin(angle) * velocity * 1000}px) rotate(${rotation + 360}deg)`, opacity: 0 } ], { duration: 2000, easing: 'cubic-bezier(0.1, 0.8, 0.2, 1)' }); animation.onfinish = () => { confetti.remove(); }; } } // Event listeners newGameButton.addEventListener('click', initGame); runButton.addEventListener('click', runSimulation); sizeSelect.addEventListener('change', initGame); difficultySelect.addEventListener('change', initGame); // Initialize the game initGame(); }); </script> </body> </html> ``` This code creates a complete pipe puzzle game with the following features: 1. **Game Setup**: - Players can select the size of the board (5x5, 6x6, or 7x7). - Players can choose the difficulty level (Easy, Medium, Hard). 2. **Game Elements**: - Sources (S) and sinks (G) are placed randomly on the board. - The board is filled with various pipe tiles (straight, L-shaped, T-shaped) and some fail tiles (X). - Players can rotate tiles by clicking on them. 3. **Game Mechanics**: - Clicking the "Run" button simulates the flow of water from the sources. - Water flows through the pipes according to their orientation. - If water hits a fail tile, the game ends immediately. - If all sinks are filled with water without any leaks or hitting fail tiles, the player wins. 4. **Win Condition**: - Upon winning, magenta confetti swirls in a spiral to celebrate the victory. The game is fully contained within this HTML file and can be run directly in a web browser.
```html <!doctype html> <html> <head> <title>Pipe Puzzle</title> </head> <body> <div> <label>Size: <input id="size" type="number" value="5" min="3" max="10"></label> <label>Difficulty: <input id="diff" type="number" value="3" min="0" max="10"></label> <button onclick="generatePuzzle()">Generate</button> </div> <canvas id="canvas" style="border:1px solid black;"></canvas> <button onclick="runSimulation()">Run</button> <script> const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); let grid = []; let size = 5; let tileSize = 50; let filled = []; let hasLeak = false; let hasFail = false; let isRunning = false; let queue = []; let sinks = []; let sources = []; let animationInterval; let confettiInterval; let particles = []; const dx = [0, 1, 0, -1]; const dy = [-1, 0, 1, 0]; const types = ['straight', 'l', 't']; function getOpenDirs(type, rot) { let dirs; switch(type) { case 'straight': dirs = [1, 3]; break; case 'l': dirs = [1, 2]; break; case 't': dirs = [0, 1, 3]; break; default: return []; } return dirs.map(d => (d + rot) % 4); } function shuffle(arr) { for (let i = arr.length - 1; i > 0; i--) { let j = Math.floor(Math.random() * (i + 1)); [arr[i], arr[j]] = [arr[j], arr[i]]; } } function generatePuzzle() { size = parseInt(document.getElementById('size').value); let diff = parseInt(document.getElementById('diff').value); canvas.width = size * tileSize; canvas.height = size * tileSize; let positions = []; for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) positions.push({x, y}); shuffle(positions); let numSources = 1 + Math.floor(Math.random() * 2); let numSinks = 1 + Math.floor(Math.random() * 2); let numFails = Math.floor(Math.random() * (size > 5 ? 2 : 1)); let specials = []; for (let i = 0; i < numSources; i++) { let pos = positions.shift(); specials.push({x: pos.x, y: pos.y, type: 'source', rotation: 0, openDirs: [Math.floor(Math.random() * 4)]}); } for (let i = 0; i < numSinks; i++) { let pos = positions.shift(); specials.push({x: pos.x, y: pos.y, type: 'sink', rotation: 0, openDirs: [Math.floor(Math.random() * 4)]}); } for (let i = 0; i < numFails; i++) { let pos = positions.shift(); specials.push({x: pos.x, y: pos.y, type: 'fail', rotation: 0, openDirs: []}); } let found = false; let tries = 0; while (!found && tries < 10000) { tries++; grid = Array.from({length: size}, () => Array(size).fill(null)); sinks = []; for (let s of specials) { grid[s.y][s.x] = {type: s.type, rotation: s.rotation, openDirs: s.openDirs.slice()}; if (s.type === 'sink') sinks.push({x: s.x, y: s.y}); } for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) { if (grid[y][x]) continue; if (Math.random() < 0.6) { let t = types[Math.floor(Math.random() * types.length)]; let r = Math.floor(Math.random() * 4); grid[y][x] = {type: t, rotation: r, openDirs: getOpenDirs(t, r)}; } else { grid[y][x] = {type: 'empty', rotation: 0, openDirs: []}; } } found = checkSolved(); } if (!found) { alert('Could not generate a puzzle, try different parameters.'); return; } let rotatable = []; for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) { let t = grid[y][x].type; if (['straight', 'l', 't'].includes(t)) rotatable.push({x, y}); } shuffle(rotatable); for (let i = 0; i < Math.min(diff, rotatable.length); i++) { let pos = rotatable[i]; let newRot = (grid[pos.y][pos.x].rotation + 1 + Math.floor(Math.random() * 3)) % 4; grid[pos.y][pos.x].rotation = newRot; grid[pos.y][pos.x].openDirs = getOpenDirs(grid[pos.y][pos.x].type, newRot); } drawGrid(); } function checkSolved() { let tempFilled = Array.from({length: size}, () => Array(size).fill(false)); let tempQueue = []; let tempLeak = false; let tempFail = false; for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) { if (grid[y][x].type === 'source') { tempFilled[y][x] = true; for (let d of grid[y][x].openDirs) { let nx = x + dx[d]; let ny = y + dy[d]; if (nx < 0 || nx >= size || ny < 0 || ny >= size) tempLeak = true; else tempQueue.push({x: nx, y: ny, incDir: (d + 2) % 4}); } } } while (tempQueue.length > 0) { let curr = tempQueue.shift(); let {x, y, incDir} = curr; let tile = grid[y][x]; let type = tile.type; if (type === 'fail') { tempFail = true; continue; } if (type === 'empty') { tempLeak = true; continue; } if (!tile.openDirs.includes(incDir)) { tempLeak = true; continue; } if (tempFilled[y][x]) continue; tempFilled[y][x] = true; for (let d of tile.openDirs) { let nx = x + dx[d]; let ny = y + dy[d]; if (nx < 0 || nx >= size || ny < 0 || ny >= size) { tempLeak = true; } else { tempQueue.push({x: nx, y: ny, incDir: (d + 2) % 4}); } } } let allFilled = sinks.every(s => tempFilled[s.y][s.x]); return !tempLeak && !tempFail && allFilled; } function runSimulation() { if (isRunning) return; isRunning = true; filled = Array.from({length: size}, () => Array(size).fill(false)); hasLeak = false; hasFail = false; queue = []; sources = []; for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) { if (grid[y][x].type === 'source') { filled[y][x] = true; sources.push({x, y}); } } for (let s of sources) { let tile = grid[s.y][s.x]; for (let d of tile.openDirs) { let nx = s.x + dx[d]; let ny = s.y + dy[d]; if (nx < 0 || nx >= size || ny < 0 || ny >= size) { hasLeak = true; } else { queue.push({x: nx, y: ny, incDir: (d + 2) % 4}); } } } animationInterval = setInterval(processStep, 200); } function processStep() { if (queue.length === 0) { clearInterval(animationInterval); isRunning = false; let allSinksFilled = sinks.every(s => filled[s.y][s.x]); if (!hasLeak && !hasFail && allSinksFilled) { victory(); } else { alert('Fail: ' + (hasFail ? 'Hit fail tile' : hasLeak ? 'Leak detected' : 'Not all sinks filled')); } drawGrid(); return; } let curr = queue.shift(); let {x, y, incDir} = curr; let tile = grid[y][x]; let type = tile.type; if (type === 'fail') { hasFail = true; drawGrid(); return; } if (type === 'empty') { hasLeak = true; drawGrid(); return; } if (!tile.openDirs.includes(incDir)) { hasLeak = true; drawGrid(); return; } if (filled[y][x]) { drawGrid(); return; } filled[y][x] = true; for (let d of tile.openDirs) { let nx = x + dx[d]; let ny = y + dy[d]; if (nx < 0 || nx >= size || ny < 0 || ny >= size) { hasLeak = true; continue; } queue.push({x: nx, y: ny, incDir: (d + 2) % 4}); } drawGrid(); } function drawGrid() { ctx.clearRect(0, 0, canvas.width, canvas.height); for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) { let tile = grid[y][x]; let tx = x * tileSize; let ty = y * tileSize; if (filled[y][x]) { ctx.fillStyle = 'rgba(0, 0, 255, 0.5)'; ctx.fillRect(tx, ty, tileSize, tileSize); } else { ctx.fillStyle = 'white'; ctx.fillRect(tx, ty, tileSize, tileSize); } ctx.strokeStyle = 'black'; ctx.lineWidth = 3; let cx = tx + tileSize / 2; let cy = ty + tileSize / 2; let half = tileSize / 2 - 5; if (tile.type === 'source') { ctx.fillStyle = 'green'; ctx.beginPath(); ctx.arc(cx, cy, half / 2, 0, 2 * Math.PI); ctx.fill(); let d = tile.openDirs[0]; ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(cx + dx[d] * half, cy + dy[d] * half); ctx.stroke(); } else if (tile.type === 'sink') { ctx.fillStyle = 'yellow'; ctx.beginPath(); ctx.arc(cx, cy, half / 2, 0, 2 * Math.PI); ctx.fill(); let d = tile.openDirs[0]; ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(cx - dx[d] * half, cy - dy[d] * half); ctx.stroke(); } else if (tile.type === 'fail') { ctx.strokeStyle = 'red'; ctx.beginPath(); ctx.moveTo(tx, ty); ctx.lineTo(tx + tileSize, ty + tileSize); ctx.moveTo(tx + tileSize, ty); ctx.lineTo(tx, ty + tileSize); ctx.stroke(); } else if (tile.type !== 'empty') { for (let d of tile.openDirs) { ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(cx + dx[d] * half, cy + dy[d] * half); ctx.stroke(); } } ctx.strokeStyle = 'gray'; ctx.lineWidth = 1; ctx.strokeRect(tx, ty, tileSize, tileSize); } } canvas.addEventListener('click', (e) => { if (isRunning) return; let rect = canvas.getBoundingClientRect(); let cx = e.clientX - rect.left; let cy = e.clientY - rect.top; let gx = Math.floor(cx / tileSize); let gy = Math.floor(cy / tileSize); if (gx < 0 || gx >= size || gy < 0 || gy >= size) return; let tile = grid[gy][gx]; if (['straight', 'l', 't'].includes(tile.type)) { tile.rotation = (tile.rotation + 1) % 4; tile.openDirs = getOpenDirs(tile.type, tile.rotation); drawGrid(); } }); function victory() { particles = []; for (let i = 0; i < 100; i++) { let angle = Math.random() * 2 * Math.PI; let dist = Math.random() * canvas.width / 2; let va = (Math.random() - 0.5) * 0.1 + 0.05; let vr = - (Math.random() * 1 + 0.5); particles.push({angle, dist, va, vr}); } confettiInterval = setInterval(drawConfetti, 50); setTimeout(() => clearInterval(confettiInterval), 5000); } function drawConfetti() { drawGrid(); ctx.fillStyle = 'magenta'; for (let p of particles) { p.angle += p.va; p.dist += p.vr; if (p.dist < 0) p.dist = 0; let px = canvas.width / 2 + Math.cos(p.angle) * p.dist; let py = canvas.height / 2 + Math.sin(p.angle) * p.dist; ctx.beginPath(); ctx.arc(px, py, 5, 0, 2 * Math.PI); ctx.fill(); } } </script> </body> </html> ```
```html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Pipe Puzzle</title> <style> body { font-family: sans-serif; text-align: center; margin: 0; padding: 0; background: #f0f0f0; } #controls { padding: 10px; background: #fff; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } #board { margin: 10px auto; display: grid; background: #222; } .tile { width: 60px; height: 60px; cursor: pointer; } .tile.fixed { cursor: default; } #runBtn { margin-left: 10px; } #confetti { position: fixed; top:0; left:0; pointer-events: none; } </style> </head> <body> <div id="controls"> Rows: <input type="number" id="rows" value="5" min="3" max="10"> Cols: <input type="number" id="cols" value="5" min="3" max="10"> Difficulty: <input type="number" id="diff" value="3" min="0" max="25"> <button id="genBtn">Generate Puzzle</button> <button id="runBtn">Run</button> </div> <div id="board"></div> <canvas id="confetti"></canvas> <script> (function(){ const boardEl = document.getElementById('board'); const rowsInput = document.getElementById('rows'); const colsInput = document.getElementById('cols'); const diffInput = document.getElementById('diff'); const genBtn = document.getElementById('genBtn'); const runBtn = document.getElementById('runBtn'); const confCanvas = document.getElementById('confetti'); let ctxConf, confettiParticles=[], confettiStart; const DIRS = {up:[0,-1], right:[1,0], down:[0,1], left:[-1,0]}; const OPP = {up:'down', down:'up', left:'right', right:'left'}; const DIR_IDX = {up:0, right:1, down:2, left:3}; let state = { rows:5, cols:5, diff:3, board:[], source:null, sink:null }; function init(){ ctxConf = confCanvas.getContext('2d'); window.addEventListener('resize', resizeConf); resizeConf(); genBtn.addEventListener('click', generate); runBtn.addEventListener('click', runSimulation); generate(); } function resizeConf(){ confCanvas.width = window.innerWidth; confCanvas.height = window.innerHeight; } function generate(){ // read inputs state.rows = +rowsInput.value; state.cols = +colsInput.value; state.diff = +diffInput.value; boardEl.innerHTML = ''; boardEl.style.gridTemplate = `repeat(${state.rows},60px) / repeat(${state.cols},60px)`; // init empty board let b = []; for(let y=0;y<state.rows;y++){ b[y]=[]; for(let x=0;x<state.cols;x++){ b[y][x] = { type:0, rot:0, fixed:false, el:null }; } } // place source & sink function randBorder(){ let side = Math.floor(Math.random()*4); let x,y,dir; if(side===0){ x = Math.floor(Math.random()*state.cols); y=0; dir='down'; } if(side===1){ x = state.cols-1; y=Math.floor(Math.random()*state.rows); dir='left'; } if(side===2){ x = Math.floor(Math.random()*state.cols); y=state.rows-1; dir='up'; } if(side===3){ x = 0; y=Math.floor(Math.random()*state.rows); dir='right'; } return {x,y,dir}; } let s = randBorder(); let t; do { t = randBorder(); } while(t.x===s.x && t.y===s.y); state.source = s; state.sink = t; b[s.y][s.x] = { type:4, rot:rotFromDir(s.dir), fixed:true, el:null }; b[t.y][t.x] = { type:5, rot:rotFromDir(OPP[t.dir]), fixed:true, el:null }; // find path via simple DFS let visited = Array.from({length:state.rows},()=>Array(state.cols).fill(false)); let path = []; function dfs(x,y){ if(x===t.x && y===t.y){ path.push({x,y}); return true; } visited[y][x]=true; let dirs = Object.keys(DIRS).sort(()=>Math.random()-0.5); for(let d of dirs){ let nx=x+DIRS[d][0], ny=y+DIRS[d][1]; if(nx<0||nx>=state.cols||ny<0||ny>=state.rows) continue; if(visited[ny][nx]) continue; if(dfs(nx,ny)){ path.push({x,y}); return true; } } return false; } dfs(s.x, s.y); path = path.reverse(); // from source to sink // lay pipes for(let i=1;i<path.length-1;i++){ let p = path[i], prev=path[i-1], nxt=path[i+1]; let dx1 = prev.x-p.x, dy1=prev.y-p.y; let dx2 = nxt.x-p.x, dy2=nxt.y-p.y; let dirs1 = dirFromDelta(dx1,dy1), dirs2=dirFromDelta(dx2,dy2); let conns = [dirs1, dirs2]; let type, baseRot; let idx1=DIR_IDX[dirs1], idx2=DIR_IDX[dirs2]; if((idx1+2)%4===idx2){ type=1; baseRot=(idx1%2===0?90:0); } else{ type=2; // L-shape base orientation is connectors at right+down (0) // determine rotation so that base (right,down) maps to dirs1,dirs2 const combos = { "right,down":0, "down,left":90, "left,up":180, "up,right":270, "down,right":0, "left,down":270, "up,left":180, "right,up":90 }; baseRot = combos[[dirs1,dirs2].join(',')] || combos[[dirs2,dirs1].join(',')]||0; } b[p.y][p.x] = { type:type, rot:baseRot, fixed:false, el:null }; } // fill others empty, place fail tiles ~10% let failCount = Math.max(1, Math.floor(state.rows*state.cols*0.1)); let placed=0; while(placed<failCount){ let x=Math.floor(Math.random()*state.cols), y=Math.floor(Math.random()*state.rows); if(b[y][x].type===0 && !(x===s.x&&y===s.y) && !(x===t.x&&y===t.y)){ b[y][x]={ type:6, rot:0, fixed:true, el:null }; placed++; } } // apply difficulty: rotate diff path tiles wrongly let mutable = path.slice(1,path.length-1).filter(p=>!b[p.y][p.x].fixed); mutable.sort(()=>Math.random()-0.5); for(let i=0;i<state.diff && i<mutable.length;i++){ let p=mutable[i], tile=b[p.y][p.x]; tile.rot = [0,90,180,270].filter(r=>r!==tile.rot)[Math.floor(Math.random()*3)]; } // store board state.board=b; // render for(let y=0;y<state.rows;y++){ for(let x=0;x<state.cols;x++){ let tile=b[y][x]; let c = document.createElement('canvas'); c.width=c.height=60; c.className='tile'+(tile.fixed?' fixed':''); boardEl.appendChild(c); tile.el=c; drawTile(c.getContext('2d'), tile, false); if(!tile.fixed){ c.addEventListener('click',()=>{ tile.rot=(tile.rot+90)%360; drawTile(c.getContext('2d'), tile, false); }); } } } } function rotFromDir(d){ return {up:0,right:90,down:180,left:270}[d]; } function dirFromDelta(dx,dy){ if(dx===1) return 'right'; if(dx===-1) return 'left'; if(dy===1) return 'down'; if(dy===-1) return 'up'; } function drawTile(ctx, tile, water){ const sz=60, mid=sz/2; ctx.clearRect(0,0,sz,sz); // background if(water) ctx.fillStyle='rgba(0,150,255,0.2)'; else ctx.fillStyle='rgba(0,0,0,0)'; ctx.fillRect(0,0,sz,sz); ctx.save(); ctx.translate(mid,mid); ctx.rotate(tile.rot*Math.PI/180); ctx.translate(-mid,-mid); // draw based on type if(tile.type===0){} else if(tile.type===6){ ctx.strokeStyle='red'; ctx.lineWidth=6; ctx.beginPath(); ctx.moveTo(10,10); ctx.lineTo(50,50); ctx.moveTo(50,10); ctx.lineTo(10,50); ctx.stroke(); } else if(tile.type===4||tile.type===5){ ctx.fillStyle= tile.type===4?'green':'orange'; ctx.beginPath(); ctx.arc(mid,mid,15,0,2*Math.PI); ctx.fill(); // arrow if(tile.type===4){ ctx.fillStyle='white'; ctx.beginPath(); ctx.moveTo(mid,mid-8); ctx.lineTo(mid-5,mid+5); ctx.lineTo(mid+5,mid+5); ctx.closePath(); ctx.fill(); } } else{ ctx.strokeStyle='gray'; ctx.lineWidth=10; ctx.lineCap='round'; let conns = []; if(tile.type===1) conns = [[mid,mid,mid,0],[mid,mid,mid,sz]][tile.rot%180===0?1:0]; // but simpler: draw lines for each direction present const sides = []; if(tile.type===1){ if(tile.rot%180===0) sides.push('vertical'); else sides.push('horizontal'); } else if(tile.type===2){ sides.push('right-down'); // both } else if(tile.type===3){ sides.push('up','right','left'); // T } // Instead, generic: define connectors: let dirs = []; if(tile.type===1){ if(tile.rot%180===0) dirs=['up','down']; else dirs=['left','right']; } else if(tile.type===2){ dirs=['right','down']; } else if(tile.type===3){ dirs=['left','right','down']; } ctx.strokeStyle=water?'blue':'gray'; for(let d of dirs){ ctx.beginPath(); let [dx,dy] = {up:[0,-1],down:[0,1],left:[-1,0],right:[1,0]}[d]; ctx.moveTo(mid,mid); ctx.lineTo(mid+dx*(mid-5), mid+dy*(mid-5)); ctx.stroke(); } } ctx.restore(); } function runSimulation(){ runBtn.disabled=true; // clear any confetti confettiParticles=[]; ctxConf.clearRect(0,0,confCanvas.width,confCanvas.height); // clear water highlights for(let y=0;y<state.rows;y++) for(let x=0;x<state.cols;x++){ drawTile(state.board[y][x].el.getContext('2d'), state.board[y][x], false); } // simulate along path let {x,y,dir} = state.source; let d = dir; let step = 0; function stepSim(){ let nx=x+DIRS[d][0], ny=y+DIRS[d][1]; if(nx<0||nx>=state.cols||ny<0||ny>=state.rows){ alert('Leak!'); runBtn.disabled=false; return; } let tile=state.board[ny][nx]; // highlight water drawTile(tile.el.getContext('2d'), tile, true); // check sink if(nx===state.sink.x && ny===state.sink.y){ victory(); return; } // fail tile if(tile.type===6){ alert('Hit a fail tile!'); runBtn.disabled=false; return; } // check connection back let needed=OPP[d]; let dirs = getTileDirs(tile); if(!dirs.includes(needed)){ alert('Leak!'); runBtn.disabled=false; return; } // find next dir (the other side) let nextDirs = dirs.filter(dd=>dd!==needed); if(nextDirs.length===0){ alert('Dead end!'); runBtn.disabled=false; return; } d = nextDirs[0]; x=nx; y=ny; step++; setTimeout(stepSim,200); } stepSim(); } function getTileDirs(tile){ if(tile.type===4) return [rotToDir(tile.rot)]; if(tile.type===5) return [rotToDir(tile.rot)]; if(tile.type===1) return tile.rot%180===0?['up','down']:['left','right']; if(tile.type===2){ const m = {0:['right','down'],90:['down','left'],180:['left','up'],270:['up','right']}; return m[tile.rot]; } if(tile.type===3){ const m = { 0:['left','right','down'], 90:['up','right','down'], 180:['left','up','right'], 270:['left','up','down'] }; return m[tile.rot]; } return []; } function rotToDir(r){ return {0:'up',90:'right',180:'down',270:'left'}[r]; } function victory(){ // start confetti confettiParticles = []; let N=100; for(let i=0;i<N;i++){ confettiParticles.push({ theta: Math.random()*Math.PI*2, r:0, dr: 0.5+Math.random()*1, dtheta:0.1+Math.random()*0.1 }); } confettiStart = performance.now(); requestAnimationFrame(animateConfetti); } function animateConfetti(ts){ let elapsed = (ts-confettiStart)/1000; ctxConf.clearRect(0,0,confCanvas.width,confCanvas.height); let cx = confCanvas.width/2, cy = confCanvas.height/2; for(let p of confettiParticles){ p.theta += p.dtheta; p.r += p.dr; let x = cx + Math.cos(p.theta)*p.r; let y = cy + Math.sin(p.theta)*p.r; ctxConf.fillStyle='magenta'; ctxConf.beginPath(); ctxConf.arc(x,y,4,0,2*Math.PI); ctxConf.fill(); } if(elapsed<3) requestAnimationFrame(animateConfetti); else { runBtn.disabled=false; } } init(); })(); </script> </body> </html> ```
Here's a simple implementation using HTML, CSS, and JavaScript. ```html <!DOCTYPE html> <html> <head> <title>Pipe Puzzle</title> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.2/css/all.min.css"> <style> .grid-container { display: grid; grid-gap: 2px; padding: 10px; } .grid-item { background-color: #ccc; border: 1px solid black; padding: 20px; font-size: 30px; text-align: center; } .pipe { font-size: 30px; } .rotate-0 { transform: rotate(0deg); } .rotate-90 { transform: rotate(90deg); } .rotate-180 { transform: rotate(180deg); } .rotate-270 { transform: rotate(270deg); } .fail { background-color: red; } .success { background-color: green; } #confetti-container { position: relative; width: 100%; height: 100vh; overflow: hidden; } .confetti { position: absolute; width: 10px; height: 10px; background-color: magenta; border-radius: 50%; } </style> </head> <body> <div> <label for="size">Size:</label> <select id="size"> <option value="5">5x5</option> <option value="7">7x7</option> <option value="9">9x9</option> </select> <label for="difficulty">Difficulty:</label> <select id="difficulty"> <option value="easy">Easy</option> <option value="medium">Medium</option> <option value="hard">Hard</option> </select> <button id="new-game">New Game</button> <button id="run">Run</button> </div> <div id="grid-container" class="grid-container"></div> <div id="confetti-container"></div> <script> let gridSize = 5; let difficulty = 'easy'; let grid = []; let tileTypes = ['empty', 'straight', 'L', 'T']; let tileRotations = ['rotate-0', 'rotate-90', 'rotate-180', 'rotate-270']; let sources = []; let sinks = []; let failTiles = []; document.getElementById('new-game').addEventListener('click', newGame); document.getElementById('run').addEventListener('click', run); function newGame() { gridSize = parseInt(document.getElementById('size').value); difficulty = document.getElementById('difficulty').value; grid = []; sources = []; sinks = []; failTiles = []; let gridContainer = document.getElementById('grid-container'); gridContainer.innerHTML = ''; gridContainer.style.gridTemplateColumns = `repeat(${gridSize}, 1fr)`; for (let i = 0; i < gridSize; i++) { grid[i] = []; for (let j = 0; j < gridSize; j++) { let tile = {}; if (i == 0 && j == 0) { tile.type = 'source'; sources.push([i, j]); } else if (i == gridSize - 1 && j == gridSize - 1) { tile.type = 'sink'; sinks.push([i, j]); } else if (Math.random() < 0.1) { tile.type = 'fail'; failTiles.push([i, j]); } else { tile.type = tileTypes[Math.floor(Math.random() * tileTypes.length)]; } tile.rotation = Math.floor(Math.random() * 4); grid[i][j] = tile; let gridItem = document.createElement('div'); gridItem.classList.add('grid-item'); gridItem.dataset.row = i; gridItem.dataset.col = j; gridItem.addEventListener('click', rotateTile); gridContainer.appendChild(gridItem); updateTile(gridItem, tile); } } if (difficulty == 'medium') { for (let i = 0; i < gridSize * gridSize / 5; i++) { let row = Math.floor(Math.random() * gridSize); let col = Math.floor(Math.random() * gridSize); grid[row][col].rotation = (grid[row][col].rotation + 1) % 4; let gridItem = document.querySelector(`.grid-item[data-row="${row}"][data-col="${col}"]`); updateTile(gridItem, grid[row][col]); } } else if (difficulty == 'hard') { for (let i = 0; i < gridSize * gridSize / 3; i++) { let row = Math.floor(Math.random() * gridSize); let col = Math.floor(Math.random() * gridSize); grid[row][col].rotation = (grid[row][col].rotation + 1) % 4; let gridItem = document.querySelector(`.grid-item[data-row="${row}"][data-col="${col}"]`); updateTile(gridItem, grid[row][col]); } } } function updateTile(gridItem, tile) { gridItem.innerHTML = ''; if (tile.type == 'source') { gridItem.innerHTML = '<i class="fa-solid fa-water"></i>'; } else if (tile.type == 'sink') { gridItem.innerHTML = '<i class="fa-solid fa-sink"></i>'; } else if (tile.type == 'fail') { gridItem.innerHTML = '<i class="fa-solid fa-skull-crossbones"></i>'; } else if (tile.type == 'straight') { gridItem.innerHTML = '<i class="fa-solid fa-minus"></i>'; gridItem.classList.add(tileRotations[tile.rotation]); } else if (tile.type == 'L') { gridItem.innerHTML = 'Γ'; gridItem.classList.add(tileRotations[tile.rotation]); } else if (tile.type == 'T') { gridItem.innerHTML = 'Φ'; gridItem.classList.add(tileRotations[tile.rotation]); } gridItem.classList.remove('fail', 'success'); } function rotateTile(event) { let gridItem = event.target; let row = parseInt(gridItem.dataset.row); let col = parseInt(gridItem.dataset.col); let tile = grid[row][col]; tile.rotation = (tile.rotation + 1) % 4; gridItem.classList.remove(...tileRotations); gridItem.classList.add(tileRotations[tile.rotation]); } async function run() { let visited = new Set(); let queue = [...sources.map(([row, col]) => [row, col, 'down'])); while (queue.length > 0) { let [row, col, direction] = queue.shift(); if (row < 0 || row >= gridSize || col < 0 || col >= gridSize || visited.has(`${row},${col},${direction}`)) { continue; } visited.add(`${row},${col},${direction}`); let gridItem = document.querySelector(`.grid-item[data-row="${row}"][data-col="${col}"]`); let tile = grid[row][col]; if (tile.type == 'fail') { gridItem.classList.add('fail'); return; } if (tile.type == 'empty') { continue; } let connectedDirections = getConnectedDirections(tile); if (!connectedDirections.includes(getOppositeDirection(direction))) { gridItem.classList.add('fail'); return; } if (tile.type == 'sink') { gridItem.classList.add('success'); continue; } for (let connectedDirection of connectedDirections) { if (connectedDirection == 'up') { queue.push([row - 1, col, 'down']); } else if (connectedDirection == 'down') { queue.push([row + 1, col, 'up']); } else if (connectedDirection == 'left') { queue.push([row, col - 1, 'right']); } else if (connectedDirection == 'right') { queue.push([row, col + 1, 'left']); } } await new Promise(resolve => setTimeout(resolve, 500)); } for (let [row, col] of sinks) { let gridItem = document.querySelector(`.grid-item[data-row="${row}"][data-col="${col}"]`); if (!visited.has(`${row},${col},'up') && !visited.has(`${row},${col},'down') && !visited.has(`${row},${col},'left') && !visited.has(`${row},${col},'right'))`) { gridItem.classList.add('fail'); return; } } confetti(); } function getConnectedDirections(tile) { if (tile.type == 'straight') { if (tile.rotation % 2 == 0) { return ['left', 'right']; } else { return ['up', 'down']; } } else if (tile.type == 'L') { if (tile.rotation == 0) { return ['up', 'right']; } else if (tile.rotation == 1) { return ['down', 'right']; } else if (tile.rotation == 2) { return ['down', 'left']; } else { return ['up', 'left']; } } else if (tile.type == 'T') { if (tile.rotation == 0) { return ['up', 'left', 'right']; } else if (tile.rotation == 1) { return ['up', 'down', 'right']; } else if (tile.rotation == 2) { return ['down', 'left', 'right']; } else { return ['up', 'down', 'left']; } } return []; } function getOppositeDirection(direction) { if (direction == 'up') { return 'down'; } else if (direction == 'down') { return 'up'; } else if (direction == 'left') { return 'right'; } else { return 'left'; } } function confetti() { let confettiContainer = document.getElementById('confetti-container'); for (let i = 0; i < 100; i++) { let confetti = document.createElement('div'); confetti.classList.add('confetti'); confetti.style.top = `${Math.random() * 100}%`; confetti.style.left = `${Math.random() * 100}%`; confetti.style.transform = `rotate(${Math.random() * 360}deg)`; confettiContainer.appendChild(confetti); let animation = confetti.animate([ { transform: `rotate(${Math.random() * 360}deg) translate(0, 0)` }, { transform: `rotate(${Math.random() * 360}deg) translate(${Math.random() * 200 - 100}px, ${Math.random() * 200 - 100}px)` } ], { duration: 2000, iterations: Infinity }); } setTimeout(() => { confettiContainer.innerHTML = ''; }, 5000); } newGame(); </script> </body> </html> ``` This code creates a grid of tiles with a source and a sink. The player can rotate the tiles to create a path from the source to the sink. The `run` button simulates the flow of water through the pipes. If the water reaches a fail tile or does not fill the sink, the game is lost. If the water fills the sink, the game is won and confetti is displayed.
# Classic Pipe Puzzle Game I'll implement a complete pipe puzzle game with grid generation, tile rotation, water simulation, and victory animation. Here's the final solution: ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Classic Pipe Puzzle</title> <style> /* Base styles */ body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: linear-gradient(135deg, #1a2a6c, #2c3e50); display: flex; flex-direction: column; align-items: center; min-height: 100vh; margin: 0; padding: 20px; color: #ecf0f1; overflow-x: hidden; } .container { width: 100%; max-width: 800px; display: flex; flex-direction: column; align-items: center; gap: 20px; } .header { text-align: center; } h1 { font-size: 2.5rem; margin-bottom: 5px; text-shadow: 0 0 10px rgba(0, 200, 255, 0.7); } .tagline { font-size: 1.1rem; opacity: 0.8; margin-bottom: 20px; } /* Game controls */ .game-controls { display: flex; flex-wrap: wrap; justify-content: center; gap: 15px; padding: 15px; background: rgba(0, 0, 0, 0.2); border-radius: 10px; width: 100%; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); } .control-group { display: flex; flex-direction: column; gap: 5px; } label { font-size: 0.9rem; font-weight: bold; color: #3498db; } select, input { padding: 8px 12px; border: none; border-radius: 5px; background: rgba(255, 255, 255, 0.15); color: white; font-size: 1rem; border: 1px solid rgba(255, 255, 255, 0.1); } button { padding: 10px 20px; border: none; border-radius: 5px; background: #3498db; color: white; font-size: 1rem; font-weight: bold; cursor: pointer; transition: all 0.3s ease; border: 2px solid transparent; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2); } button:hover { background: #2980b9; transform: translateY(-2px); box-shadow: 0 6px 8px rgba(0, 0, 0, 0.3); } button:active { transform: translateY(1px); } button#run { background: #27ae60; display: flex; align-items: center; gap: 8px; } button#run:hover { background: #219653; } /* Game board */ #game-area { position: relative; margin: 20px 0; } .board { display: grid; gap: 2px; background: rgba(0, 20, 40, 0.7); padding: 10px; border-radius: 5px; box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4); transition: opacity 0.3s; } .cell { position: relative; width: 60px; height: 60px; background-color: #2c3e50; border-radius: 2px; display: flex; justify-content: center; align-items: center; cursor: pointer; transition: transform 0.2s; overflow: hidden; } .cell:hover { transform: scale(1.05); background-color: #34495e; box-shadow: 0 0 10px rgba(52, 152, 219, 0.5); z-index: 10; } .pipe { position: absolute; width: 100%; height: 100%; opacity: 0.85; display: flex; justify-content: center; align-items: center; } .filled { position: absolute; background-color: rgba(52, 152, 219, 0.7); z-index: -1; transition: all 0.2s ease-out; } /* Pipe designs */ .straight::before { content: ""; position: absolute; background: #e67e22; width: 100%; height: 8px; } .L::before { content: ""; position: absolute; background: #9b59b6; width: 80%; height: 8px; left: 10%; } .L::after { content: ""; position: absolute; background: #9b59b6; width: 8px; height: 80%; top: 10%; } .T::before { content: ""; position: absolute; background: #e74c3c; width: 100%; height: 8px; top: 50%; margin-top: -4px; } .T::after { content: ""; position: absolute; background: #e74c3c; width: 8px; height: 60%; top: 20%; left: 50%; margin-left: -4px; } .source { background: linear-gradient(to bottom right, #f1c40f, #e67e22); box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.3); display: flex; justify-content: center; align-items: center; overflow: hidden; } .source::after { content: ""; position: absolute; width: 20px; height: 20px; background: #ecf0f1; border-radius: 50%; } .sink { background: linear-gradient(to bottom right, #1abc9c, #16a085); box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.3); display: flex; justify-content: center; align-items: center; overflow: hidden; } .sink::after { content: ""; position: absolute; width: 20px; height: 20px; background: rgba(26, 188, 156, 0.5); border-radius: 50%; border: 2px dashed #ecf0f1; } .fail { background: rgba(231, 76, 60, 0.7); position: relative; } .fail::before, .fail::after { content: ""; position: absolute; width: 90%; height: 8px; background: white; top: 50%; left: 5%; transform: rotate(45deg); } .fail::after { transform: rotate(-45deg); } /* Status info */ #status { font-size: 1.1rem; font-weight: bold; text-align: center; min-height: 28px; padding: 10px; border-radius: 5px; margin-top: 10px; width: 100%; } #confetti-canvas { position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 1000; } /* Rotation animation */ @keyframes rotatePipe { 0% { transform: rotate(0deg); } 100% { transform: rotate(90deg); } } .rotating { animation: rotatePipe 0.3s ease forwards; } /* Tutorial */ .instructions { max-width: 600px; line-height: 1.6; margin-top: 20px; padding: 15px; background: rgba(0, 0, 0, 0.2); border-radius: 10px; font-size: 0.95rem; } .instructions h2 { margin-top: 0; color: #3498db; text-align: center; } .tutorial-items { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 10px; } .tutorial-item { display: flex; align-items: center; gap: 10px; padding: 10px; background: rgba(0, 0, 0, 0.15); border-radius: 5px; } .tutorial-icon { width: 30px; height: 30px; display: flex; justify-content: center; align-items: center; } @media (max-width: 600px) { .cell { width: 50px; height: 50px; } .tutorial-items { grid-template-columns: 1fr; } .game-controls { flex-direction: column; } } </style> </head> <body> <div class="container"> <div class="header"> <h1>Pipe Flow Puzzle</h1> <p class="tagline">Connect the pipes and direct water to complete the flow!</p> </div> <div class="game-controls"> <div class="control-group"> <label for="grid-size">Grid Size</label> <select id="grid-size"> <option value="3">3x3</option> <option value="4">4x4</option> <option value="5" selected>5x5</option> <option value="6">6x6</option> <option value="7">7x7</option> </select> </div> <div class="control-group"> <label for="difficulty">Difficulty</label> <select id="difficulty"> <option value="1">Easy (1 error)</option> <option value="2" selected>Medium (2 errors)</option> <option value="3">Hard (3 errors)</option> <option value="4">Expert (4 errors)</option> </select> </div> <button id="generate">Generate Puzzle</button> <button id="run">Run Water Flow</button> <button id="reset">Reset Rotation</button> </div> <div id="status"></div> <div id="game-area"></div> <div class="instructions"> <h2>How to Play</h2> <p>Click on pipes to rotate them and create a continuous flow from sources (gold) to sinks (green). Avoid crossing water paths and failing tiles (red).</p> <div class="tutorial-items"> <div class="tutorial-item"> <div class="tutorial-icon"> <div style="width:20px;height:8px;background:#e67e22;border-radius:4px;"></div> </div> <div>Straight Pipe (connect opposite sides)</div> </div> <div class="tutorial-item"> <div class="tutorial-icon"> <div style="position:relative;width:20px;height:20px;"> <div style="position:absolute;top:2px;left:2px;width:16px;height:8px;background:#9b59b6;border-radius:4px;"></div> <div style="position:absolute;top:10px;left:10px;width:8px;height:12px;background:#9b59b6;border-radius:4px;"></div> </div> </div> <div>L Pipe (connect perpendicular)</div> </div> <div class="tutorial-item"> <div class="tutorial-icon"> <div style="position:relative;width:20px;height:20px;"> <div style="position:absolute;top:9px;left:0;width:20px;height:8px;background:#e74c3c;border-radius:4px;"></div> <div style="position:absolute;top:0;left:9px;width:8px;height:16px;background:#e74c3c;border-radius:4px;"></div> </div> </div> <div>T Pipe (connect three sides)</div> </div> <div class="tutorial-item"> <div class="tutorial-icon"> <div style="width:20px;height:20px;background:linear-gradient(to bottom right, #f1c40f, #e67e22);border-radius:10px;position:relative;"> <div style="position:absolute;width:10px;height:10px;background:#fff;border-radius:50%;top:5px;left:5px;"></div> </div> </div> <div>Source (water enters here)</div> </div> <div class="tutorial-item"> <div class="tutorial-icon"> <div style="width:20px;height:20px;background:linear-gradient(to bottom right,#1abc9c,#16a085);border-radius:10px;position:relative;"> <div style="position:absolute;width:10px;height:10px;border:2px dashed #fff;border-radius:50%;top:5px;left:5px;"></div> </div> </div> <div>Sink (water destination)</div> </div> <div class="tutorial-item"> <div class="tutorial-icon"> <div style="width:20px;height:20px;background:#e74c3c;position:relative;"> <div style="position:absolute;top:0;left:9px;width:2px;height:20px;background:#fff;transform:rotate(45deg);"></div> <div style="position:absolute;top:0;left:9px;width:2px;height:20px;background:#fff;transform:rotate(-45deg);"></div> </div> </div> <div>Fail Tile (avoid water)</div> </div> </div> </div> </div> <canvas id="confetti-canvas"></canvas> <script> // Game variables let grid = []; let gridSize = 5; let waterStarted = false; let gameCompleted = false; let directions = [{ dr: -1, dc: 0 }, { dr: 0, dc: 1 }, { dr: 1, dc: 0 }, { dr: 0, dc: -1 }]; const tileTypes = ["empty", "source", "sink", "straight", "L", "T", "fail"]; // DOM Elements const gridSizeElement = document.getElementById("grid-size"); const difficultyElement = document.getElementById("difficulty"); const generateButton = document.getElementById("generate"); const runButton = document.getElementById("run"); const resetButton = document.getElementById("reset"); const statusElement = document.getElementById("status"); const gameArea = document.getElementById("game-area"); const confettiCanvas = document.getElementById("confetti-canvas"); // Initialize confetti const ctx = confettiCanvas.getContext('2d'); const particles = []; confettiCanvas.width = window.innerWidth; confettiCanvas.height = window.innerHeight; // Generate initial puzzle window.onload = generatePuzzle; // Event Listeners generateButton.addEventListener("click", generatePuzzle); runButton.addEventListener("click", startWaterFlow); resetButton.addEventListener("click", resetPipeRotation); function generatePuzzle() { // Reset game state waterStarted = false; gameCompleted = false; statusElement.textContent = "Rotate pipes to create paths"; statusElement.style.color = "#ecf0f1"; // Clear the game area gameArea.innerHTML = ""; // Get selected grid size and difficulty gridSize = parseInt(gridSizeElement.value); const difficulty = parseInt(difficultyElement.value); // Create tiles grid grid = Array(gridSize).fill().map(() => Array(gridSize).fill()); // Add source, sink and fail tiles const sourceCell = { row: 0, col: 0 }; const sinkCell = { row: gridSize-1, col: gridSize-1 }; const failPositions = []; // Add random fail tiles for (let i = 0; i < difficulty; i++) { const row = Math.floor(Math.random() * gridSize); const col = Math.floor(Math.random() * gridSize); // Avoid placing fail on source/sink if ((row !== sourceCell.row || col !== sourceCell.col) && (row !== sinkCell.row || col !== sinkCell.col) && !failPositions.some(p => p.row === row && p.col === col
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Pipe Puzzle Game</title> <style> body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; display: flex; flex-direction: column; align-items: center; } .game-container { background: white; border-radius: 15px; padding: 20px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); margin-bottom: 20px; } .controls { display: flex; gap: 15px; margin-bottom: 20px; align-items: center; flex-wrap: wrap; } .control-group { display: flex; flex-direction: column; gap: 5px; } label { font-weight: bold; color: #333; } select, button { padding: 8px 12px; border: 2px solid #667eea; border-radius: 5px; font-size: 14px; } button { background: #667eea; color: white; cursor: pointer; transition: background 0.3s; } button:hover { background: #5a67d8; } button:disabled { background: #ccc; cursor: not-allowed; } .game-board { display: grid; gap: 2px; background: #333; padding: 5px; border-radius: 10px; margin: 0 auto; } .tile { width: 50px; height: 50px; background: #f0f0f0; border: 1px solid #ddd; cursor: pointer; position: relative; display: flex; align-items: center; justify-content: center; transition: background 0.3s; } .tile:hover { background: #e0e0e0; } .tile.source { background: #4CAF50; cursor: default; } .tile.source:hover { background: #4CAF50; } .tile.sink { background: #2196F3; cursor: default; } .tile.sink:hover { background: #2196F3; } .tile.fail { background: #f44336; cursor: default; } .tile.fail:hover { background: #f44336; } .tile.filled { background: #81C784 !important; } .tile.leaked { background: #ffcdd2 !important; } .pipe { width: 80%; height: 80%; position: relative; } .pipe-straight { background: linear-gradient(90deg, transparent 40%, #333 40%, #333 60%, transparent 60%); } .pipe-l { background: linear-gradient(90deg, transparent 40%, #333 40%, #333 60%, transparent 60%), linear-gradient(0deg, transparent 40%, #333 40%, #333 60%, transparent 60%); background-size: 100% 50%, 50% 100%; background-position: 0 0, 50% 50%; background-repeat: no-repeat; } .pipe-t { background: linear-gradient(90deg, transparent 40%, #333 40%, #333 60%, transparent 60%), linear-gradient(0deg, transparent 40%, #333 40%, #333 60%, transparent 60%); } .status { text-align: center; margin: 15px 0; font-size: 18px; font-weight: bold; min-height: 25px; } .success { color: #4CAF50; } .error { color: #f44336; } .info { color: #2196F3; } .confetti { position: fixed; pointer-events: none; z-index: 1000; } .confetti-piece { position: absolute; width: 8px; height: 8px; background: magenta; border-radius: 50%; animation: spiral 3s ease-out forwards; } @keyframes spiral { 0% { transform: translateY(0) rotate(0deg); opacity: 1; } 100% { transform: translateY(100vh) rotate(720deg); opacity: 0; } } .water-drop { position: absolute; width: 6px; height: 6px; background: #2196F3; border-radius: 50%; z-index: 10; transition: all 0.5s ease; } </style> </head> <body> <div class="game-container"> <h1 style="text-align: center; color: #333; margin-top: 0;">Pipe Puzzle</h1> <div class="controls"> <div class="control-group"> <label>Board Size:</label> <select id="sizeSelect"> <option value="5">5x5</option> <option value="6">6x6</option> <option value="7">7x7</option> <option value="8">8x8</option> </select> </div> <div class="control-group"> <label>Difficulty:</label> <select id="difficultySelect"> <option value="easy">Easy</option> <option value="medium">Medium</option> <option value="hard">Hard</option> </select> </div> <button onclick="generatePuzzle()">New Puzzle</button> <button id="runButton" onclick="runSimulation()" disabled>Run Water</button> <button onclick="resetSimulation()">Reset</button> </div> <div class="status" id="status">Select size and difficulty, then generate a new puzzle!</div> <div id="gameBoard" class="game-board"></div> </div> <script> let boardSize = 5; let difficulty = 'easy'; let board = []; let solution = []; let isRunning = false; let waterDrops = []; const TILE_TYPES = { EMPTY: 0, STRAIGHT: 1, L_PIPE: 2, T_PIPE: 3, SOURCE: 4, SINK: 5, FAIL: 6 }; const ROTATIONS = { 0: 0, // 0 degrees 1: 90, // 90 degrees 2: 180, // 180 degrees 3: 270 // 270 degrees }; function initializeGame() { boardSize = parseInt(document.getElementById('sizeSelect').value); difficulty = document.getElementById('difficultySelect').value; board = Array(boardSize).fill().map(() => Array(boardSize).fill().map(() => ({ type: TILE_TYPES.EMPTY, rotation: 0, filled: false, leaked: false }))); createBoard(); document.getElementById('runButton').disabled = true; document.getElementById('status').textContent = 'Generate a new puzzle to start!'; document.getElementById('status').className = ''; } function createBoard() { const gameBoard = document.getElementById('gameBoard'); gameBoard.style.gridTemplateColumns = `repeat(${boardSize}, 1fr)`; gameBoard.innerHTML = ''; for (let row = 0; row < boardSize; row++) { for (let col = 0; col < boardSize; col++) { const tile = document.createElement('div'); tile.className = 'tile'; tile.dataset.row = row; tile.dataset.col = col; tile.onclick = () => rotateTile(row, col); gameBoard.appendChild(tile); } } } function generatePuzzle() { initializeGame(); // Create a valid solution first generateSolution(); // Then scramble it based on difficulty scramblePuzzle(); updateDisplay(); document.getElementById('runButton').disabled = false; document.getElementById('status').textContent = 'Puzzle generated! Connect the water source to all sinks.'; document.getElementById('status').className = 'info'; } function generateSolution() { // Place source and sinks const sourcePos = { row: 0, col: Math.floor(boardSize / 2) }; board[sourcePos.row][sourcePos.col] = { type: TILE_TYPES.SOURCE, rotation: 0, filled: false, leaked: false }; // Place 2-3 sinks const numSinks = Math.min(3, Math.floor(boardSize / 2)); const sinks = []; for (let i = 0; i < numSinks; i++) { let sinkPos; do { sinkPos = { row: Math.floor(Math.random() * boardSize), col: Math.floor(Math.random() * boardSize) }; } while (board[sinkPos.row][sinkPos.col].type !== TILE_TYPES.EMPTY); board[sinkPos.row][sinkPos.col] = { type: TILE_TYPES.SINK, rotation: 0, filled: false, leaked: false }; sinks.push(sinkPos); } // Add some fail tiles const numFailTiles = Math.floor(boardSize * boardSize * 0.1); for (let i = 0; i < numFailTiles; i++) { let failPos; do { failPos = { row: Math.floor(Math.random() * boardSize), col: Math.floor(Math.random() * boardSize) }; } while (board[failPos.row][failPos.col].type !== TILE_TYPES.EMPTY); board[failPos.row][failPos.col] = { type: TILE_TYPES.FAIL, rotation: 0, filled: false, leaked: false }; } // Create valid paths from source to each sink sinks.forEach(sink => createPathToSink(sourcePos, sink)); // Fill empty spaces with random pipes for (let row = 0; row < boardSize; row++) { for (let col = 0; col < boardSize; col++) { if (board[row][col].type === TILE_TYPES.EMPTY) { const pipeTypes = [TILE_TYPES.STRAIGHT, TILE_TYPES.L_PIPE, TILE_TYPES.T_PIPE]; board[row][col] = { type: pipeTypes[Math.floor(Math.random() * pipeTypes.length)], rotation: Math.floor(Math.random() * 4), filled: false, leaked: false }; } } } // Store the solution solution = board.map(row => row.map(tile => ({ ...tile }))); } function createPathToSink(source, sink) { // Simple pathfinding to create a valid path let current = { ...source }; while (current.row !== sink.row || current.col !== sink.col) { let nextRow = current.row; let nextCol = current.col; if (current.row < sink.row) nextRow++; else if (current.row > sink.row) nextRow--; else if (current.col < sink.col) nextCol++; else if (current.col > sink.col) nextCol--; if (nextRow >= 0 && nextRow < boardSize && nextCol >= 0 && nextCol < boardSize) { if (board[nextRow][nextCol].type === TILE_TYPES.EMPTY) { // Determine pipe type and rotation needed const pipeInfo = getPipeForPath(current, { row: nextRow, col: nextCol }, sink); board[nextRow][nextCol] = { type: pipeInfo.type, rotation: pipeInfo.rotation, filled: false, leaked: false }; } current = { row: nextRow, col: nextCol }; } } } function getPipeForPath(from, current, to) { const fromDir = { row: from.row - current.row, col: from.col - current.col }; const toDir = { row: to.row - current.row, col: to.col - current.col }; // Simplified pipe placement logic if ((fromDir.row === 0 && toDir.row === 0) || (fromDir.col === 0 && toDir.col === 0)) { return { type: TILE_TYPES.STRAIGHT, rotation: fromDir.row === 0 ? 0 : 1 }; } else { return { type: TILE_TYPES.L_PIPE, rotation: Math.floor(Math.random() * 4) }; } } function scramblePuzzle() { const difficultyMap = { 'easy': 0.3, 'medium': 0.6, 'hard': 0.9 }; const scrambleRatio = difficultyMap[difficulty]; const tilesToScramble = Math.floor(boardSize * boardSize * scrambleRatio); for (let i = 0; i < tilesToScramble; i++) { const row = Math.floor(Math.random() * boardSize); const col = Math.floor(Math.random() * boardSize); if (board[row][col].type !== TILE_TYPES.SOURCE && board[row][col].type !== TILE_TYPES.SINK && board[row][col].type !== TILE_TYPES.FAIL) { board[row][col].rotation = Math.floor(Math.random() * 4); } } } function rotateTile(row, col) { if (isRunning) return; const tile = board[row][col]; if (tile.type === TILE_TYPES.SOURCE || tile.type === TILE_TYPES.SINK || tile.type === TILE_TYPES.FAIL) { return; } tile.rotation = (tile.rotation + 1) % 4; updateTileDisplay(row, col); } function updateDisplay() { for (let row = 0; row < boardSize; row++) { for (let col = 0; col < boardSize; col++) { updateTileDisplay(row, col); } } } function updateTileDisplay(row, col) { const tileElement = document.querySelector(`[data-row="${row}"][data-col="${col}"]`); const tile = board[row][col]; // Reset classes tileElement.className = 'tile'; tileElement.innerHTML = ''; if (tile.filled) { tileElement.classList.add('filled'); } if (tile.leaked) { tileElement.classList.add('leaked'); } switch (tile.type) { case TILE_TYPES.SOURCE: tileElement.classList.add('source'); tileElement.innerHTML = '💧'; break; case TILE_TYPES.SINK: tileElement.classList.add('sink'); tileElement.innerHTML = '🎯'; break; case TILE_TYPES.FAIL: tileElement.classList.add('fail'); tileElement.innerHTML = '❌'; break; case TILE_TYPES.STRAIGHT: case TILE_TYPES.L_PIPE: case TILE_TYPES.T_PIPE: const pipeDiv = document.createElement('div'); pipeDiv.className = 'pipe'; if (tile.type === TILE_TYPES.STRAIGHT) { pipeDiv.classList.add('pipe-straight'); } else if (tile.type === TILE_TYPES.L_PIPE) { pipeDiv.classList.add('pipe-l'); } else if (tile.type === TILE_TYPES.T_PIPE) { pipeDiv.classList.add('pipe-t'); } pipeDiv.style.transform = `rotate(${ROTATIONS[tile.rotation]}deg)`; tileElement.appendChild(pipeDiv); break; } } async function runSimulation() { if (isRunning) return; isRunning = true; document.getElementById('runButton').disabled = true; document.getElementById('status').textContent = 'Running simulation...'; document.getElementById('status').className = 'info'; // Reset filled and leaked states for (let row = 0; row < boardSize; row++) { for (let col = 0; col < boardSize; col++) { board[row][col].filled = false; board[row][col].leaked = false; } } // Find all sources const sources = []; for (let row = 0; row < boardSize; row++) { for (let col = 0; col < boardSize; col++) { if (board[row][col].type === TILE_TYPES.SOURCE) { sources.push({ row, col }); } } } // Simulate water flow from each source let success = true; const filledSinks = new Set(); for (const source of sources) { const result = await simulateWaterFlow(source); if (!result.success) { success = false; } result.sinks.forEach(sink => filledSinks.add(`${sink.row},${sink.col}`)); } // Check if all sinks are filled const allSinks = []; for (let row = 0; row < boardSize; row++) { for (let col = 0; col < boardSize; col++) { if (board[row][col].type === TILE_TYPES.SINK) { allSinks.push({ row, col }); } } } const allSinksFilled = allSinks.every(sink => filledSinks.has(`${sink.row},${sink.col}`)); if (success && allSinksFilled) { document.getElementById('status').textContent = 'Success! All pipes connected correctly!'; document.getElementById('status').className = 'success'; createConfetti(); } else { if (!allSinksFilled) { document.getElementById('status').textContent = 'Failed: Not all sinks are filled!'; } else { document.getElementById('status').textContent = 'Failed: Water leaked or hit a fail tile!'; } document.getElementById('status').className = 'error'; } updateDisplay(); isRunning = false; document.getElementById('runButton').disabled = false; } async function simulateWaterFlow(start) { const visited = new Set(); const queue = [start]; const filledSinks = []; let success = true; while (queue.length > 0) { const current = queue.shift(); const key = `${current.row},${current.col}`; if (visited.has(key)) continue; visited.add(key); const tile = board[current.row][current.col]; // Animate water drop await animateWaterDrop(current); // Mark as filled tile.filled = true; // Check if water hits fail tile if (tile.type === TILE_TYPES.FAIL) { tile.leaked = true; success = false; continue; } // If sink, mark as filled if (tile.type === TILE_TYPES.SINK) { filledSinks.push(current); continue; } // Get connections from current tile const connections = getTileConnections(current); // Check for leaks (water going to unconnected sides) const validConnections = connections.filter(conn => conn.row >= 0 && conn.row < boardSize && conn.col >= 0 && conn.col < boardSize ); if (validConnections.length !== connections.length) { tile.leaked = true; success = false; } // Add valid connections to queue for (const connection of validConnections) { if (!visited.has(`${connection.row},${connection.col}`)) { // Check if the connecting tile can receive water from this direction if (canReceiveWater(current, connection)) { queue.push(connection); } else { tile.leaked = true; success = false; } } } updateDisplay(); await new Promise(resolve => setTimeout(resolve, 200)); } return { success, sinks: filledSinks }; } function getTileConnections(pos) { const tile = board[pos.row][pos.col]; const connections = []; // Define connection patterns for each pipe type and rotation const patterns = { [TILE_TYPES.SOURCE]: [[0, -1], [0, 1], [-1, 0], [1, 0]], // All directions [TILE_TYPES.SINK]: [[0, -1], [0, 1], [-1, 0], [1, 0]], // All directions [TILE_TYPES.STRAIGHT]: { 0: [[0, -1], [0, 1]], // Horizontal 1: [[-1, 0], [1, 0]], // Vertical 2: [[0, -1], [0, 1]], // Horizontal 3: [[-1, 0], [1, 0]] // Vertical }, [TILE_TYPES.L_PIPE]: { 0: [[0, 1], [1, 0]], // Right and down 1: [[-1, 0], [0, 1]], // Up and right 2: [[0, -1], [-1, 0]], // Left and up 3: [[1, 0], [0, -1]] // Down and left }, [TILE_TYPES.T_PIPE]: { 0: [[-1, 0], [0, 1], [1, 0]], // Up, right, down 1: [[0, -1], [-1, 0], [0, 1]], // Left, up, right 2: [[-1, 0], [0, -1], [1, 0]], // Up, left, down 3: [[0, -1], [1, 0], [0, 1]] // Left, down, right } }; let dirs = []; if (tile.type === TILE_TYPES.SOURCE || tile.type === TILE_TYPES.SINK) { dirs = patterns[tile.type]; } else if (patterns[tile.type]) { dirs = patterns[tile.type][tile.rotation] || []; } for (const [dRow, dCol] of dirs) { connections.push({ row: pos.row + dRow, col: pos.col + dCol }); } return connections; } function canReceiveWater(from, to) { const toTile = board[to.row][to.col]; if (toTile.type === TILE_TYPES.EMPTY || toTile.type === TILE_TYPES.FAIL) { return false; } // Get the direction water is coming from const direction = { row: from.row - to.row, col: from.col - to.col }; // Check if the destination tile has a connection in the opposite direction const toConnections = getTileConnections(to); return toConnections.some(conn => conn.row === from.row && conn.col === from.col ); } async function animateWaterDrop(pos) { const tileElement = document.querySelector(`[data-row="${pos.row}"][data-col="${pos.col}"]`); const drop = document.createElement('div'); drop.className = 'water-drop'; tileElement.appendChild(drop); setTimeout(() => { if (drop.parentNode) { drop.parentNode.removeChild(drop); } }, 500); } function resetSimulation() { if (isRunning) return; waterDrops = []; for (let row = 0; row < boardSize; row++) { for (let col = 0; col < boardSize; col++) { board[row][col].filled = false; board[row][col].leaked = false; } } updateDisplay(); document.getElementById('status').textContent = 'Simulation reset. Fix the pipes and try again!'; document.getElementById('status').className = 'info'; // Remove any existing water drops document.querySelectorAll('.water-drop').forEach(drop => drop.remove()); } function createConfetti() { const confettiContainer = document.createElement('div'); confettiContainer.className = 'confetti'; document.body.appendChild(confettiContainer); for (let i = 0; i < 50; i++) { const piece = document.createElement('div'); piece.className = 'confetti-piece'; piece.style.left = Math.random() * window.innerWidth + 'px'; piece.style.top = '-10px'; piece.style.animationDelay = Math.random() * 2 + 's'; piece.style.background = `hsl(${300 + Math.random() * 60}, 100%, 50%)`; // Magenta variants confettiContainer.appendChild(piece); } setTimeout(() => { document.body.removeChild(confettiContainer); }, 5000); } // Initialize the game when the page loads initializeGame(); </script> </body> </html> ``` This implementation includes: 1. **Game Setup**: - Configurable board sizes (5x5 to 8x8) - Three difficulty levels affecting puzzle scrambling - Tile types: empty, straight pipe, L-pipe, T-pipe, source, sink, and fail tiles 2. **Puzzle Generation**: - Creates valid solutions first, then scrambles based on difficulty - Places sources, sinks, and fail tiles strategically - Generates connecting paths between sources and sinks 3. **Game Mechanics**: - Click tiles to rotate them (except fixed source/sink/fail tiles) - Run button simulates water flow with animation - Water drops move through pipes with visual feedback - Detects leaks, incomplete connections, and fail conditions 4. **Victory Condition**: - All sinks must be filled without leaks or hitting fail tiles - Success triggers magenta confetti in spiral animation 5. **Visual Feedback**: - Different colors for different tile types - Animated water drops during simulation - Status messages for game state - Highlighted filled and leaked tiles The game provides a complete pipe puzzle experience with smooth animations and clear visual feedback for all game states.