mirror of
https://codeberg.org/ashley/poke
synced 2025-05-30 02:59:43 +00:00
1695 lines
70 KiB
Plaintext
1695 lines
70 KiB
Plaintext
<!--
|
||
This Source Code Form is subject to the terms of the GNU General Public License:
|
||
|
||
Copyright (C) 2021-2025 PokeTube (https://codeberg.org/Ashley/poketube)
|
||
|
||
This program is free software: you can redistribute it and/or modify
|
||
it under the terms of the GNU General Public License as published by
|
||
the Free Software Foundation, either version 3 of the License, or
|
||
(at your option) any later version.
|
||
|
||
This program is distributed in the hope that it will be useful,
|
||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
GNU General Public License for more details.
|
||
|
||
You should have received a copy of the GNU General Public License
|
||
along with this program. If not, see https://www.gnu.org/licenses/.
|
||
--><!--//--><![CDATA[//><!--
|
||
/**
|
||
* @licstart The following is the entire license notice for the JavaScript
|
||
* code in this page.
|
||
*
|
||
* Copyright (C) 2021-2024 POKETUBE (https://codeberg.org/Ashley/poketube)
|
||
*
|
||
* The JavaScript code in this page is free software: you can redistribute
|
||
* it and/or modify it under the terms of the GNU General Public License
|
||
* (GNU GPL) as published by the Free Software Foundation, either version 3
|
||
* of the License, or (at your option) any later version. The code is
|
||
* distributed WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU GPL
|
||
* for more details.
|
||
*
|
||
* As additional permission under GNU GPL version 3 section 7, you may
|
||
* distribute non-source (e.g., minimized or compacted) forms of that code
|
||
* without the copy of the GNU GPL normally required by section 4, provided
|
||
* you include this license notice and a URL through which recipients can
|
||
* access the Corresponding Source.
|
||
*
|
||
* @licend The above is the entire license notice for the JavaScript code
|
||
* in this page.
|
||
*/
|
||
|
||
//--><!]]>
|
||
<% if (!game) { %>
|
||
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>Poke! Games Hub</title>
|
||
<style>
|
||
:root {
|
||
--bg-start: #111;
|
||
--bg-end: #000;
|
||
--accent: #00ff99;
|
||
--card-bg: rgba(0,0,0,0.6);
|
||
--card-border: rgba(0,255,153,0.5);
|
||
--card-hover: rgba(0,255,153,0.2);
|
||
--text-light: #fff;
|
||
}
|
||
* { box-sizing: border-box; margin:0; padding:0; }
|
||
html, body {
|
||
width:100%; height:100%;
|
||
background: linear-gradient(135deg, var(--bg-start), var(--bg-end));
|
||
font-family: 'Press Start 2P', monospace;
|
||
color: var(--text-light);
|
||
overflow: hidden;
|
||
}
|
||
.emoji-bg {
|
||
position: fixed; top: 0; left: 0;
|
||
width: 100vw; height: 100vh;
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(3rem,1fr));
|
||
grid-auto-rows:3rem;
|
||
pointer-events:none; z-index:0;
|
||
font-size:2.5rem; opacity:0.1;
|
||
}
|
||
.wrapper {
|
||
position: relative; z-index:1;
|
||
max-width:960px; margin:3rem auto; padding:2rem;
|
||
background: rgba(0,0,0,0.5);
|
||
border:2px solid var(--accent);
|
||
border-radius:16px;
|
||
box-shadow: 0 0 16px var(--accent);
|
||
}
|
||
h1 {
|
||
font-size:3rem; text-align:center;
|
||
background: linear-gradient(90deg, #00ff99, #08d9d6);
|
||
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
||
margin-bottom:1.5rem;
|
||
}
|
||
.game-container {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px,1fr));
|
||
gap:1rem;
|
||
}
|
||
.game {
|
||
position: relative;
|
||
background: var(--card-bg);
|
||
border:2px solid var(--card-border);
|
||
border-radius:12px;
|
||
padding:1rem;
|
||
text-decoration:none;
|
||
color: var(--text-light);
|
||
transition: transform 0.3s, background 0.3s;
|
||
overflow:hidden;
|
||
}
|
||
.game:hover {
|
||
transform: translateY(-5px);
|
||
background: var(--card-hover);
|
||
}
|
||
.icon { font-size:2.5rem; margin-bottom:0.5rem; }
|
||
.title { font-size:1.2rem; margin-bottom:0.3rem; }
|
||
.subtitle { font-size:0.7rem; color: #ccc; margin-bottom:0.5rem; }
|
||
.info-btn {
|
||
position:absolute; top:8px; right:8px;
|
||
background:none; border:none; color:var(--accent);
|
||
font-size:1rem; cursor:pointer;
|
||
}
|
||
.info-modal {
|
||
position:absolute; top:0; left:0; width:100%; height:100%;
|
||
background: rgba(0,0,0,0.8); display:flex;
|
||
align-items:center; justify-content:center;
|
||
text-align:left; padding:1rem;
|
||
font-size:0.8rem;
|
||
}
|
||
.info-modal.hidden { display:none; }
|
||
.info-content {
|
||
background: var(--bg-start); padding:1rem;
|
||
border:2px solid var(--accent); border-radius:8px;
|
||
max-width:90%; max-height:80%; overflow:auto;
|
||
}
|
||
.close-info {
|
||
display:block; margin-top:1rem; background:var(--accent);
|
||
border:none; padding:0.5rem; color:var(--bg-start);
|
||
cursor:pointer;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="emoji-bg"></div>
|
||
<div class="wrapper">
|
||
<h1>Poke! Games Hub</h1>
|
||
<div class="game-container">
|
||
<!-- Snake -->
|
||
<div class="game" data-game="snake">
|
||
<button class="info-btn">ℹ️</button>
|
||
<div class="icon">🐍</div>
|
||
<div class="title">Snake</div>
|
||
<div class="subtitle">Guide your snake to eat apples and grow longer—avoid crashing into yourself</div>
|
||
<div class="info-modal hidden">
|
||
<div class="info-content">
|
||
<p><strong>Snake</strong> is a retro arcade game where you control a growing line. Eat food to grow longer, but avoid running into walls or yourself.</p>
|
||
<button class="close-info">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Tic-Tac-Toe -->
|
||
<div class="game" data-game="tic-tac-toe">
|
||
<button class="info-btn">ℹ️</button>
|
||
<div class="icon">❌⭕</div>
|
||
<div class="title">Tic-Tac-Toe</div>
|
||
<div class="subtitle">Classic 3×3 grid game. Get three in a row before your opponent.</div>
|
||
<div class="info-modal hidden">
|
||
<div class="info-content">
|
||
<p><strong>Tic-Tac-Toe</strong> lets you play X vs O on a 3×3 grid. Win by placing three in a row. Play against AI or a friend!</p>
|
||
<button class="close-info">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Sudoku -->
|
||
<div class="game" data-game="sudoku">
|
||
<button class="info-btn">ℹ️</button>
|
||
<div class="icon">🧮</div>
|
||
<div class="title">Sudoku</div>
|
||
<div class="subtitle">Two-player or vs AI. Bounce the ball past opponent’s paddle to score!</div>
|
||
<div class="info-modal hidden">
|
||
<div class="info-content">
|
||
<p><strong>Sudoku</strong> challenges you to fill a 9×9 grid so that each column, row, and 3×3 block contains all digits 1–9.</p>
|
||
<button class="close-info">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Pong -->
|
||
<div class="game" data-game="pong">
|
||
<button class="info-btn">ℹ️</button>
|
||
<div class="icon">🏓</div>
|
||
<div class="title">Ping-Pong</div>
|
||
<div class="subtitle">Retro table tennis</div>
|
||
<div class="info-modal hidden">
|
||
<div class="info-content">
|
||
<p><strong>Pong</strong> is the classic table tennis arcade game. Move your paddle and bounce the ball past your opponent.</p>
|
||
<button class="close-info">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Minesweeper -->
|
||
<div class="game" data-game="minesweeper">
|
||
<button class="info-btn">ℹ️</button>
|
||
<div class="icon">💣</div>
|
||
<div class="title">Minesweeper</div>
|
||
<div class="subtitle">Find all safe cells</div>
|
||
<div class="info-modal hidden">
|
||
<div class="info-content">
|
||
<p><strong>Minesweeper</strong> tasks you to clear a grid avoiding hidden mines. Use numbers to deduce safe spots.</p>
|
||
<button class="close-info">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Breakout -->
|
||
<div class="game" data-game="breakout">
|
||
<button class="info-btn">ℹ️</button>
|
||
<div class="icon">🧱</div>
|
||
<div class="title">Breakout</div>
|
||
<div class="subtitle">Brick-busting action</div>
|
||
<div class="info-modal hidden">
|
||
<div class="info-content">
|
||
<p><strong>Breakout</strong> challenges you to destroy all bricks by bouncing a ball off your paddle. Collect power-ups to survive!</p>
|
||
<button class="close-info">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<script>
|
||
// populate emoji background
|
||
const emojis=['🎮','🕹️','👾','🎧','🖥️','🎲','🏆','🎯','🔥','💥','🧩','⭐','⚔️','🛡️','🚀','🎉','🌟','⚡','💣'];
|
||
const bg=document.querySelector('.emoji-bg');
|
||
for(let i=0;i<400;i++){const s=document.createElement('span');s.textContent=emojis[i%emojis.length];bg.appendChild(s);}
|
||
// info modal toggles
|
||
document.querySelectorAll('.game').forEach(card=>{
|
||
const btn=card.querySelector('.info-btn');
|
||
const modal=card.querySelector('.info-modal');
|
||
btn.onclick=()=>modal.classList.toggle('hidden');
|
||
modal.querySelector('.close-info').onclick=()=>modal.classList.add('hidden');
|
||
// navigate on click outside info
|
||
card.addEventListener('click',e=>{ if(e.target===card){ const g=card.dataset.game; window.location=`?game=${g}`; }});
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|
||
|
||
<% } %>
|
||
|
||
|
||
|
||
|
||
|
||
<% if (game === "snake") { %>
|
||
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>POKE SNAKE</title>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
|
||
<style>
|
||
html, body {
|
||
margin: 0; padding: 0; background: #000;
|
||
font-family: 'Press Start 2P', monospace; color: #0f0;
|
||
overflow: hidden;
|
||
}
|
||
#game-container { position: relative; width:100vw; height:100vh; }
|
||
#snakeCanvas {
|
||
background: #111; display: block; margin: auto;
|
||
image-rendering: pixelated; border:4px solid #0f0;
|
||
box-shadow:0 0 20px #0f0;
|
||
}
|
||
#overlay {
|
||
position:absolute; top:0; left:0; width:100%; height:100%;
|
||
pointer-events:none;
|
||
background:
|
||
repeating-linear-gradient(transparent 0 2px, rgba(0,255,0,0.1) 2px 3px),
|
||
repeating-linear-gradient(90deg, transparent 0 2px, rgba(0,255,0,0.1) 2px 3px);
|
||
}
|
||
.screen {
|
||
position:absolute; top:0; left:0; width:100%; height:100%;
|
||
background:rgba(0,0,0,0.8);
|
||
display:flex; flex-direction:column; align-items:center;
|
||
justify-content:center; z-index:100; text-align:center; color:#0f0;
|
||
}
|
||
.hidden { display:none; }
|
||
.screen h1 { font-size:48px; margin:0 0 16px;
|
||
white-space:nowrap; text-shadow:0 0 10px #0f0; }
|
||
.screen p { font-size:14px; margin:4px 0; }
|
||
.screen button {
|
||
margin-top:12px; background:#0f0; color:#000;
|
||
border:none; padding:8px 16px; cursor:pointer;
|
||
}
|
||
#scoreboard {
|
||
position:absolute; top:16px; left:16px; z-index:50;
|
||
white-space:nowrap; text-shadow:0 0 5px #0f0;
|
||
}
|
||
#settingsBtn {
|
||
position:absolute; top:16px; right:16px; font-size:24px;
|
||
cursor:pointer; z-index:50; user-select:none;
|
||
}
|
||
#settingsModal {
|
||
position:absolute; top:50%; left:50%;
|
||
transform:translate(-50%,-50%);
|
||
background:#222; border:2px solid #0f0;
|
||
padding:16px; display:none; z-index:200; width:auto;height:auto;
|
||
}
|
||
#settingsModal h2 { margin-top:0; text-align:center; }
|
||
#settingsModal .section { margin-bottom:12px; }
|
||
#settingsModal label {
|
||
display:block; font-size:12px; margin:6px 0; color:#0f0;
|
||
}
|
||
#settingsModal input, #settingsModal select {
|
||
width:100%; margin-top:4px;
|
||
background:#000; color:#0f0; border:1px solid #0f0;
|
||
font-family:inherit; font-size:12px; padding:4px;
|
||
}
|
||
#settingsModal button {
|
||
margin:8px 4px 0 0; background:#0f0; color:#000;
|
||
border:none; padding:6px 12px; cursor:pointer;
|
||
}
|
||
/* DEBUG MENU */
|
||
#debugMenu {
|
||
position:absolute; top:48px; left:16px;
|
||
color:#fff;
|
||
font-size:12px; line-height:1.2;
|
||
white-space:pre; z-index:300; display:none;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="game-container">
|
||
<div id="titleScreen" class="screen">
|
||
<h1>POKE SNAKE</h1>
|
||
<p>Press Any Key to Start</p>
|
||
</div>
|
||
<div id="scoreboard">Score: 0 High: 0 Level: 1</div>
|
||
<div id="settingsBtn">⚙️</div>
|
||
<canvas id="snakeCanvas"></canvas>
|
||
<div id="overlay"></div>
|
||
<div id="gameOverScreen" class="screen hidden">
|
||
<h1 id="gameOverText">GAME OVER!</h1>
|
||
<p>Press R to Retry</p>
|
||
<button id="backToTitle">Back to Title</button>
|
||
</div>
|
||
<div id="settingsModal">
|
||
<h2>Settings</h2>
|
||
<div class="section">
|
||
<label>Speed
|
||
<select id="paramSpeed">
|
||
<option value="150">Slow</option>
|
||
<option value="100" selected>Normal</option>
|
||
<option value="50">Fast</option>
|
||
</select>
|
||
</label>
|
||
<label>Sound FX
|
||
<select id="paramSfx">
|
||
<option value="on" selected>On</option>
|
||
<option value="off">Off</option>
|
||
</select>
|
||
</label>
|
||
</div>
|
||
<div class="section">
|
||
<label>Start Length
|
||
<input type="number" id="paramStartLength" min="1" max="20" value="1">
|
||
</label>
|
||
<label>Wall Mode
|
||
<select id="paramWallMode">
|
||
<option value="kill" selected>Die on Hit</option>
|
||
<option value="wrap">Wrap Around</option>
|
||
</select>
|
||
</label>
|
||
<label>Grid Lines
|
||
<select id="paramGrid">
|
||
<option value="false" selected>Off</option>
|
||
<option value="true">On</option>
|
||
</select>
|
||
</label>
|
||
</div>
|
||
<div class="section">
|
||
<h3>Advanced Knobs</h3>
|
||
<label>SFX Volume
|
||
<input type="range" id="paramSfxVolume" min="0" max="1" step="0.1" value="1">
|
||
</label>
|
||
<label>Snake Head Color
|
||
<input type="color" id="paramHeadColor" value="#ffff00">
|
||
</label>
|
||
<label>Snake Body Color
|
||
<input type="color" id="paramBodyColor" value="#00ffff">
|
||
</label>
|
||
</div>
|
||
<div class="section">
|
||
<p style="font-size:10px; text-align:center; color:#0f0;">
|
||
Press D to toggle Debug
|
||
</p>
|
||
</div>
|
||
<button id="saveSettings">Save</button>
|
||
<button id="closeSettings">Close</button>
|
||
</div>
|
||
<div id="debugMenu"></div>
|
||
</div>
|
||
|
||
<script>
|
||
const canvas = document.getElementById('snakeCanvas'),
|
||
ctx = canvas.getContext('2d'),
|
||
titleScreen = document.getElementById('titleScreen'),
|
||
gameOverScreen = document.getElementById('gameOverScreen'),
|
||
gameOverText = document.getElementById('gameOverText'),
|
||
scoreboard = document.getElementById('scoreboard'),
|
||
settingsBtn = document.getElementById('settingsBtn'),
|
||
settingsModal = document.getElementById('settingsModal'),
|
||
saveBtn = document.getElementById('saveSettings'),
|
||
closeBtn = document.getElementById('closeSettings'),
|
||
backToTitle = document.getElementById('backToTitle'),
|
||
debugMenu = document.getElementById('debugMenu');
|
||
|
||
let params = {
|
||
speed:100, sfx:true, startLength:1,
|
||
wallMode:'kill', grid:false,
|
||
sfxVolume:1,
|
||
headColor:'#ffff00', bodyColor:'#00ffff'
|
||
};
|
||
|
||
let snake, dir, food, score, highScore=0, level, gameInterval;
|
||
let state='start', startTime=0;
|
||
const endMessages=["Skill issue!","Snake bit ya!","Sssorry, try again!"];
|
||
let fps=0, frameCount=0, lastFpsTime=Date.now();
|
||
let movesCount=0, framesSinceFood=0;
|
||
|
||
function resizeCanvas() {
|
||
canvas.width = Math.floor(window.innerWidth/20)*20;
|
||
canvas.height= Math.floor(window.innerHeight/20)*20;
|
||
}
|
||
window.addEventListener('resize', resizeCanvas);
|
||
resizeCanvas();
|
||
|
||
function pickFood() {
|
||
const cols=canvas.width/20, rows=canvas.height/20;
|
||
food={ x:Math.floor(Math.random()*cols), y:Math.floor(Math.random()*rows) };
|
||
framesSinceFood=0;
|
||
}
|
||
|
||
function initGame() {
|
||
snake=[]; for(let i=0;i<params.startLength;i++){
|
||
snake.push({x:10,y:10+i});
|
||
}
|
||
dir={x:0,y:-1}; pickFood();
|
||
score=0; level=1; movesCount=0; startTime=Date.now();
|
||
updateScoreboard();
|
||
clearInterval(gameInterval);
|
||
gameInterval=setInterval(gameLoop, params.speed);
|
||
}
|
||
|
||
function drawGrid() {
|
||
if(!params.grid) return;
|
||
ctx.strokeStyle='rgba(0,255,0,0.2)';
|
||
for(let x=0;x<canvas.width;x+=20){
|
||
ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x,canvas.height); ctx.stroke();
|
||
}
|
||
for(let y=0;y<canvas.height;y+=20){
|
||
ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(canvas.width,y); ctx.stroke();
|
||
}
|
||
}
|
||
|
||
function draw(){
|
||
ctx.fillStyle='#111'; ctx.fillRect(0,0,canvas.width,canvas.height);
|
||
drawGrid();
|
||
ctx.fillStyle='#f00'; ctx.fillRect(food.x*20,food.y*20,20,20);
|
||
snake.forEach((seg,i)=>{
|
||
ctx.fillStyle = i===0 ? params.headColor : params.bodyColor;
|
||
ctx.fillRect(seg.x*20,seg.y*20,20,20);
|
||
});
|
||
}
|
||
|
||
function recordFrame(){
|
||
const now=Date.now();
|
||
frameCount++;
|
||
framesSinceFood++;
|
||
if(now-lastFpsTime>=1000){
|
||
fps=frameCount; frameCount=0; lastFpsTime=now;
|
||
}
|
||
}
|
||
|
||
function updateDebugMenu(){
|
||
if(debugMenu.style.display!=='block') return;
|
||
const head=snake[0], tail=snake[snake.length-1],
|
||
cols=canvas.width/20, rows=canvas.height/20,
|
||
cells=cols*rows,
|
||
elapsedMs=Date.now()-startTime,
|
||
elapsedS=Math.floor(elapsedMs/1000),
|
||
avgRate=elapsedS>0?(score/elapsedS).toFixed(2):'N/A',
|
||
frameTime = fps>0?(1000/fps).toFixed(1):'N/A',
|
||
cores= navigator.hardwareConcurrency||'N/A',
|
||
ua=navigator.userAgent,
|
||
vis= document.visibilityState,
|
||
focus = document.hasFocus(),
|
||
heap = performance.memory
|
||
? (performance.memory.usedJSHeapSize/1024/1024).toFixed(2)+'/'+
|
||
(performance.memory.jsHeapSizeLimit/1024/1024).toFixed(2)+'MB'
|
||
: 'N/A',
|
||
diff = params.speed>=150?'Easy':params.speed>=100?'Normal':'Hard';
|
||
|
||
// color-coded FPS
|
||
let fpsColor = fps>=8?'#0f0': fps>=4?'#ffa500':'#f00';
|
||
|
||
debugMenu.innerHTML =
|
||
`Poke Snake 1.1.2 on ${diff} \n\n` +
|
||
`<span style="color:${fpsColor}">FPS: ${fps}</span>\n` +
|
||
`FrameTime: ${frameTime}ms\n` +
|
||
`Head: ${head.x},${head.y}\n` +
|
||
`Tail: ${tail.x},${tail.y}\n` +
|
||
`Dir: ${dir.x},${dir.y}\n` +
|
||
`DistToFood: ${Math.abs(head.x-food.x)+Math.abs(head.y-food.y)}\n` +
|
||
`FramesSinceFood: ${framesSinceFood}\n` +
|
||
`Moves: ${movesCount}\n` +
|
||
`Moves/s: ${elapsedS>0?(movesCount/elapsedS).toFixed(2):'N/A'}\n` +
|
||
`SnakeLen: ${snake.length}\n` +
|
||
`Occupancy: ${(snake.length/cells*100).toFixed(1)}%\n` +
|
||
`Cells: ${cells}\n` +
|
||
`Canvas: ${cols}×${rows}\n` +
|
||
`Time: ${elapsedS}s\n` +
|
||
`AvgRate: ${avgRate} pts/s\n` +
|
||
`Score: ${score}\n` +
|
||
`Level: ${level}\n` +
|
||
`HighScore: ${highScore}\n` +
|
||
`Difficulty: ${diff}\n` +
|
||
`Speed(ms): ${params.speed}\n` +
|
||
`StartLen: ${params.startLength}\n` +
|
||
`Wall: ${params.wallMode}\n` +
|
||
`Grid: ${params.grid}\n` +
|
||
`SFXVol: ${params.sfxVolume}\n` +
|
||
`Heap: ${heap}\n` +
|
||
`Cores: ${cores}\n` +
|
||
`VisState: ${vis}\n` +
|
||
`HasFocus: ${focus}\n` +
|
||
`Resolution: ${window.innerWidth}×${window.innerHeight}\n` +
|
||
`UA: ${ua}`;
|
||
}
|
||
|
||
function update(){
|
||
const head={x:snake[0].x+dir.x,y:snake[0].y+dir.y},
|
||
cols=canvas.width/20, rows=canvas.height/20;
|
||
if(params.wallMode==='wrap'){
|
||
head.x=(head.x+cols)%cols; head.y=(head.y+rows)%rows;
|
||
} else if(head.x<0||head.x>=cols||head.y<0||head.y>=rows){
|
||
return endGame();
|
||
}
|
||
if(snake.some((s,i)=>i>0&&s.x===head.x&&s.y===head.y)){
|
||
return endGame();
|
||
}
|
||
snake.unshift(head);
|
||
movesCount++;
|
||
if(head.x===food.x&&head.y===food.y){
|
||
score++; if(score%5===0) level++;
|
||
updateScoreboard(); pickFood();
|
||
if(params.sfx) playBeep(440,0.1);
|
||
} else snake.pop();
|
||
}
|
||
|
||
function gameLoop(){
|
||
update(); draw(); recordFrame(); updateDebugMenu();
|
||
}
|
||
|
||
function endGame(){
|
||
clearInterval(gameInterval); state='gameover';
|
||
gameOverText.textContent=endMessages[Math.floor(Math.random()*endMessages.length)];
|
||
gameOverScreen.classList.remove('hidden');
|
||
}
|
||
|
||
function updateScoreboard(){
|
||
highScore=Math.max(highScore,score);
|
||
scoreboard.textContent=`Score: ${score} High: ${highScore} Level: ${level}`;
|
||
}
|
||
|
||
function playBeep(freq,dur){
|
||
const ac=new (window.AudioContext||window.webkitAudioContext)(),
|
||
gain=ac.createGain(), osc=ac.createOscillator();
|
||
gain.gain.value=params.sfxVolume;
|
||
osc.frequency.value=freq;
|
||
osc.connect(gain).connect(ac.destination);
|
||
osc.start(); osc.stop(ac.currentTime+dur);
|
||
}
|
||
|
||
document.addEventListener('keydown', e=>{
|
||
if(e.key.toLowerCase()==='d'){
|
||
debugMenu.style.display=debugMenu.style.display==='block'?'none':'block';
|
||
return;
|
||
}
|
||
if(state==='start'){
|
||
titleScreen.classList.add('hidden'); initGame(); state='playing';
|
||
} else if(state==='playing'){
|
||
const M={ArrowUp:[0,-1],ArrowDown:[0,1],ArrowLeft:[-1,0],ArrowRight:[1,0]};
|
||
if(M[e.key]&&(M[e.key][0]!==-dir.x||M[e.key][1]!==-dir.y)){
|
||
dir={x:M[e.key][0],y:M[e.key][1]};
|
||
}
|
||
} else if(state==='gameover'&&e.key.toLowerCase()==='r'){
|
||
gameOverScreen.classList.add('hidden');
|
||
titleScreen.classList.remove('hidden');
|
||
state='start';
|
||
}
|
||
});
|
||
|
||
settingsBtn.addEventListener('click', ()=>{
|
||
if(state==='playing') clearInterval(gameInterval);
|
||
document.getElementById('paramSpeed').value = params.speed;
|
||
document.getElementById('paramSfx').value = params.sfx?'on':'off';
|
||
document.getElementById('paramStartLength').value = params.startLength;
|
||
document.getElementById('paramWallMode').value = params.wallMode;
|
||
document.getElementById('paramGrid').value = params.grid;
|
||
document.getElementById('paramSfxVolume').value = params.sfxVolume;
|
||
document.getElementById('paramHeadColor').value = params.headColor;
|
||
document.getElementById('paramBodyColor').value = params.bodyColor;
|
||
settingsModal.style.display='block';
|
||
});
|
||
|
||
closeBtn.addEventListener('click', ()=>{
|
||
settingsModal.style.display='none';
|
||
if(state==='playing') gameInterval=setInterval(gameLoop,params.speed);
|
||
});
|
||
|
||
saveBtn.addEventListener('click', ()=>{
|
||
params.speed = parseInt(document.getElementById('paramSpeed').value,10);
|
||
params.sfx = document.getElementById('paramSfx').value==='on';
|
||
params.startLength = parseInt(document.getElementById('paramStartLength').value,10);
|
||
params.wallMode = document.getElementById('paramWallMode').value;
|
||
params.grid = document.getElementById('paramGrid').value==='true';
|
||
params.sfxVolume = parseFloat(document.getElementById('paramSfxVolume').value);
|
||
params.headColor = document.getElementById('paramHeadColor').value;
|
||
params.bodyColor = document.getElementById('paramBodyColor').value;
|
||
settingsModal.style.display='none';
|
||
if(state==='playing') initGame();
|
||
});
|
||
|
||
backToTitle.addEventListener('click', ()=>{
|
||
gameOverScreen.classList.add('hidden');
|
||
titleScreen.classList.remove('hidden');
|
||
state='start';
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|
||
|
||
<% } %>
|
||
|
||
|
||
<% if (game === "tic-tac-toe") { %>
|
||
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>POKE TIC-TAC-TOE</title>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root {
|
||
--accent: #00ff99;
|
||
--board-bg: rgba(0, 0, 0, 0.5);
|
||
--cell-bg: rgba(255,255,255,0.05);
|
||
--cell-hover: rgba(255,255,255,0.1);
|
||
--glow: 0 0 8px var(--accent), 0 0 16px var(--accent);
|
||
}
|
||
* { box-sizing: border-box; }
|
||
html, body {
|
||
margin: 0; padding: 0;
|
||
width: 100%; height: 100%;
|
||
background: radial-gradient(circle at center, #111, #000);
|
||
font-family: 'Press Start 2P', monospace;
|
||
color: var(--accent);
|
||
}
|
||
#game {
|
||
width: 100vw; height: 100vh;
|
||
display: flex; flex-direction: column;
|
||
align-items: center; justify-content: flex-start;
|
||
padding-top: 20px;
|
||
}
|
||
h1 { margin: 0; font-size: 1.5em; text-shadow: var(--glow); }
|
||
#scoreboard, #timer { font-size: 1em; text-shadow: var(--glow); margin: 0.3em 0; }
|
||
#message { font-size: 1.2em; text-shadow: var(--glow); margin: 0.3em 0; }
|
||
#board {
|
||
position: relative;
|
||
display: grid;
|
||
grid-template: repeat(3, 1fr) / repeat(3, 1fr);
|
||
gap: 8px;
|
||
background: var(--board-bg);
|
||
padding: 8px;
|
||
box-shadow: var(--glow);
|
||
width: min(90vw, 360px);
|
||
aspect-ratio: 1;
|
||
margin: 0 auto;
|
||
}
|
||
.cell {
|
||
background: var(--cell-bg);
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 2.5em; cursor: pointer;
|
||
transition: background 0.2s;
|
||
user-select: none;
|
||
}
|
||
.cell:hover { background: var(--cell-hover); }
|
||
#lineCanvas {
|
||
position: absolute; top: 8px; left: 8px;
|
||
width: calc(100% - 16px);
|
||
height: calc(100% - 16px);
|
||
pointer-events: none;
|
||
}
|
||
#controls { margin-top: 1em; display: flex; gap: 0.5em; }
|
||
button {
|
||
background: var(--board-bg);
|
||
color: var(--accent);
|
||
border: none; padding: 0.5em 1em;
|
||
cursor: pointer; font-size: 0.9em;
|
||
border-radius: 4px; box-shadow: var(--glow);
|
||
transition: background 0.2s;
|
||
}
|
||
button:hover { background: var(--cell-hover); }
|
||
#settingsBtn {
|
||
position: absolute; top: 10px; right: 10px;
|
||
font-size: 1.2em; background: none; border: none;
|
||
cursor: pointer; color: var(--accent); text-shadow: var(--glow);
|
||
}
|
||
#history {
|
||
margin-top: 1em; font-size: 0.8em; max-height: 100px;
|
||
overflow-y: auto; width: min(90vw,360px);
|
||
text-align: left; padding-left: 10px;
|
||
}
|
||
/* Settings Modal */
|
||
#settingsModal {
|
||
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
||
background: #111; border: 2px solid var(--accent);
|
||
padding: 1em; display: none; z-index: 10;
|
||
width: 90vw; max-width: 320px; border-radius: 6px;
|
||
box-shadow: var(--glow);
|
||
}
|
||
#settingsModal h2 { margin-top: 0; text-shadow: var(--glow); }
|
||
.setting { margin: 0.6em 0; font-size: 0.8em; }
|
||
.setting label { display: block; margin-bottom: 0.3em; }
|
||
.setting select, .setting input[type="color"], .setting input[type="number"] {
|
||
width: 100%; padding: 0.4em; background: #000;
|
||
color: var(--accent); border: 1px solid var(--accent);
|
||
font-family: inherit; box-shadow: inset 0 0 4px var(--accent);
|
||
}
|
||
#settingsModal .buttons { display: flex; justify-content: flex-end; gap: 0.5em; margin-top: 0.5em; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="game">
|
||
<button id="settingsBtn">⚙️</button>
|
||
<h1>POKE TIC-TAC-TOE</h1>
|
||
<div id="scoreboard">X: 0 | O: 0 | D: 0</div>
|
||
<div id="timer">Time Left: 10s</div>
|
||
<div id="message">Player X’s turn</div>
|
||
<div id="board">
|
||
<canvas id="lineCanvas"></canvas>
|
||
</div>
|
||
<div id="controls">
|
||
<button id="undoBtn">Undo</button>
|
||
<button id="redoBtn">Redo</button>
|
||
<button id="resetBtn">Restart</button>
|
||
</div>
|
||
<ol id="history"></ol>
|
||
|
||
<div id="settingsModal">
|
||
<h2>Settings</h2>
|
||
<div class="setting">
|
||
<label for="themeColor">Accent Color</label>
|
||
<input type="color" id="themeColor" value="#00ff99">
|
||
</div>
|
||
<div class="setting">
|
||
<label for="firstPlayer">First Player</label>
|
||
<select id="firstPlayer"><option>X</option><option>O</option></select>
|
||
</div>
|
||
<div class="setting">
|
||
<label for="modeSelect">Mode</label>
|
||
<select id="modeSelect">
|
||
<option value="pvp">PvP</option>
|
||
<option value="pvc">PvC</option>
|
||
<option value="ai">AI vs AI</option>
|
||
</select>
|
||
</div>
|
||
<div class="setting">
|
||
<label for="aiDifficulty">AI Difficulty</label>
|
||
<select id="aiDifficulty"><option>easy</option><option selected>normal</option><option>hard</option></select>
|
||
</div>
|
||
<div class="setting">
|
||
<label for="gridToggle">
|
||
<input type="checkbox" id="gridToggle" checked> Show Grid Lines
|
||
</label>
|
||
</div>
|
||
<div class="setting">
|
||
<label for="presetTheme">Theme Preset</label>
|
||
<select id="presetTheme">
|
||
<option value="#00ff99">Neon Green</option>
|
||
<option value="#ff0099">Neon Pink</option>
|
||
<option value="#0099ff">Neon Blue</option>
|
||
</select>
|
||
</div>
|
||
<div class="buttons">
|
||
<button id="saveSettings">Save</button>
|
||
<button id="cancelSettings">Cancel</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const boardEl = document.getElementById('board');
|
||
const msgEl = document.getElementById('message');
|
||
const scoreEl = document.getElementById('scoreboard');
|
||
const timerEl = document.getElementById('timer');
|
||
const historyEl = document.getElementById('history');
|
||
const resetBtn = document.getElementById('resetBtn');
|
||
const undoBtn = document.getElementById('undoBtn');
|
||
const redoBtn = document.getElementById('redoBtn');
|
||
const settingsBtn= document.getElementById('settingsBtn');
|
||
const settingsModal= document.getElementById('settingsModal');
|
||
const saveSet = document.getElementById('saveSettings');
|
||
const cancelSet = document.getElementById('cancelSettings');
|
||
const themeColor = document.getElementById('themeColor');
|
||
const presetTheme= document.getElementById('presetTheme');
|
||
const firstPlayer= document.getElementById('firstPlayer');
|
||
const aiDiff = document.getElementById('aiDifficulty');
|
||
const modeSelect = document.getElementById('modeSelect');
|
||
const gridToggle = document.getElementById('gridToggle');
|
||
const lineCanvas = document.getElementById('lineCanvas');
|
||
|
||
let ctxLine;
|
||
let params = {
|
||
accent: '#00ff99', first: 'X', mode: 'pvc', diff: 'normal', showGrid: true
|
||
};
|
||
|
||
let board = Array(9).fill('');
|
||
let history = [];
|
||
let future = [];
|
||
let current, winner, moveCount;
|
||
let scoreX=0, scoreO=0, scoreD=0;
|
||
let timer, timeLeft;
|
||
|
||
const wins = [[0,1,2],[3,4,5],[6,7,8],[0,3,6],[1,4,7],[2,5,8],[0,4,8],[2,4,6]];
|
||
|
||
function applyTheme() {
|
||
document.documentElement.style.setProperty('--accent', params.accent);
|
||
}
|
||
function updateScores() {
|
||
scoreEl.textContent = `X: ${scoreX} | O: ${scoreO} | D: ${scoreD}`;
|
||
}
|
||
function startTimer() {
|
||
clearInterval(timer);
|
||
timeLeft=10;
|
||
timerEl.textContent = `Time Left: ${timeLeft}s`;
|
||
timer = setInterval(()=>{
|
||
timeLeft--;
|
||
if(timeLeft<=0) { clearInterval(timer); skipTurn(); }
|
||
timerEl.textContent = `Time Left: ${timeLeft}s`;
|
||
},1000);
|
||
}
|
||
function skipTurn(){
|
||
if(params.mode==='pvp') switchTurn();
|
||
else if(params.mode==='pvc' && current!=='O') switchTurn();
|
||
}
|
||
function initCanvas(){
|
||
lineCanvas.width = boardEl.clientWidth;
|
||
lineCanvas.height= boardEl.clientHeight;
|
||
ctxLine = lineCanvas.getContext('2d');
|
||
}
|
||
function drawWinLine(combo){
|
||
ctxLine.clearRect(0,0,lineCanvas.width,lineCanvas.height);
|
||
const cellSize = (lineCanvas.width)/3;
|
||
const [a,,b,,c] = combo; // actually combo indices
|
||
const [i0,i1,i2] = combo;
|
||
const toXY = i=>({
|
||
x: (i%3 +0.5)*cellSize,
|
||
y: (Math.floor(i/3)+0.5)*cellSize
|
||
});
|
||
const p0 = toXY(i0), p2=toXY(i2);
|
||
ctxLine.strokeStyle = params.accent;
|
||
ctxLine.lineWidth = 6;
|
||
ctxLine.shadowColor = params.accent;
|
||
ctxLine.shadowBlur = 12;
|
||
ctxLine.beginPath();
|
||
ctxLine.moveTo(p0.x, p0.y);
|
||
ctxLine.lineTo(p2.x, p2.y);
|
||
ctxLine.stroke();
|
||
}
|
||
function clearWinLine(){ ctxLine.clearRect(0,0,lineCanvas.width,lineCanvas.height); }
|
||
|
||
function init() {
|
||
board.fill(''); history=[]; future=[];
|
||
moveCount=0; winner=null;
|
||
current=params.first;
|
||
updateScores(); msgEl.textContent=`Player ${current}’s turn`;
|
||
initCanvas(); render(); startTimer();
|
||
}
|
||
function render() {
|
||
boardEl.innerHTML = '';
|
||
wins.forEach(c=>{ if(checkLine(c)) drawWinLine(c); });
|
||
if(!winner) clearWinLine();
|
||
board.forEach((v,i)=>{
|
||
const cell=document.createElement('div');
|
||
cell.className='cell'; cell.textContent = v;
|
||
if(params.showGrid) cell.style.border='1px solid rgba(255,255,255,0.1)';
|
||
else cell.style.border='none';
|
||
cell.onclick = ()=>clickCell(i);
|
||
boardEl.appendChild(cell);
|
||
});
|
||
historyEl.innerHTML = history.map((m,i) => `<li>${i+1}. ${m.player} -> (${Math.floor(m.idx/3)+1},${m.idx%3+1})</li>`).join('');
|
||
}
|
||
function clickCell(i){
|
||
if(board[i]||winner) return;
|
||
history.push({player:current,idx:i}); future=[];
|
||
board[i]=current; moveCount++;
|
||
playBeep( current==='X'?440:660,0.1 );
|
||
checkGame(); render();
|
||
if(!winner){
|
||
switchTurn();
|
||
if(params.mode==='pvc' && current==='O') setTimeout(aiMove,300);
|
||
else if(params.mode==='ai') setTimeout(aiMove,300);
|
||
}
|
||
}
|
||
function switchTurn(){
|
||
clearInterval(timer);
|
||
current = current==='X'?'O':'X';
|
||
msgEl.textContent=`Player ${current}’s turn`;
|
||
startTimer();
|
||
}
|
||
function aiMove(){
|
||
let idx;
|
||
if(params.diff==='hard') idx=minimax(board,'O').idx;
|
||
else if(params.diff==='normal'&&Math.random()<0.7) idx=minimax(board,'O').idx;
|
||
else { const e=board.map((v,i)=>v===''?i:null).filter(i=>i!==null); idx=e[Math.floor(Math.random()*e.length)]; }
|
||
if(idx!=null) clickCell(idx);
|
||
}
|
||
function checkGame(){
|
||
for(const c of wins){ const [a,b,c2]=c;
|
||
if(board[a]&&board[a]===board[b]&&board[a]===board[c2]){
|
||
winner=board[a]; msgEl.textContent=`Player ${winner} wins!`;
|
||
if(winner==='X') scoreX++; else scoreO++;
|
||
playBeep(880,0.2);
|
||
updateScores(); return;
|
||
}
|
||
}
|
||
if(moveCount===9){ winner='draw'; msgEl.textContent=`It’s a draw!`;
|
||
scoreD++; playBeep(220,0.2); updateScores();
|
||
}
|
||
}
|
||
function undo(){ if(history.length===0) return;
|
||
const last=history.pop(); future.push(last);
|
||
board[last.idx]=''; moveCount--; current=last.player;
|
||
clearWinLine(); render(); clearInterval(timer); startTimer();
|
||
}
|
||
function redo(){ if(future.length===0) return;
|
||
const next=future.pop(); history.push(next);
|
||
board[next.idx]=next.player; moveCount++;
|
||
current= next.player==='X'?'O':'X'; clearWinLine(); render(); clearInterval(timer); startTimer();
|
||
}
|
||
function minimax(bd,player){ const avail=bd.map((v,i)=>v===''?i:null).filter(i=>i!==null);
|
||
if(checkWin(bd,'X'))return{score:-1}; if(checkWin(bd,'O'))return{score:1}; if(!avail.length)return{score:0};
|
||
let best = player==='O'?{score:-Infinity}:{score:Infinity};
|
||
for(let i of avail){ const nb=bd.slice(); nb[i]=player;
|
||
const res=minimax(nb, player==='O'?'X':'O');
|
||
if(player==='O'?res.score>best.score:res.score<best.score){ best={idx:i,score:res.score}; }
|
||
}
|
||
return best;
|
||
}
|
||
function checkWin(bd,p){ return wins.some(([a,b,c])=>bd[a]===p&&bd[b]===p&&bd[c]===p); }
|
||
function checkLine(c){ const [a,b,c2]=c; return board[a]&&board[a]===board[b]&&board[a]===board[c2]; }
|
||
function playBeep(freq,dur){ const ac=new (window.AudioContext||window.webkitAudioContext)(); const gain=ac.createGain(),osc=ac.createOscillator();
|
||
gain.gain.value=0.2; osc.frequency.value=freq; osc.connect(gain).connect(ac.destination); osc.start(); osc.stop(ac.currentTime+dur);
|
||
}
|
||
// Settings
|
||
settingsBtn.onclick=()=>settingsModal.style.display='block'; cancelSet.onclick=()=>settingsModal.style.display='none';
|
||
saveSet.onclick=()=>{
|
||
params.accent=themeColor.value; params.first=firstPlayer.value;
|
||
params.mode=modeSelect.value; params.diff=aiDiff.value;
|
||
params.showGrid=gridToggle.checked; params.accent=presetTheme.value;
|
||
applyTheme(); settingsModal.style.display='none'; init();
|
||
};
|
||
undoBtn.onclick=undo; redoBtn.onclick=redo; resetBtn.onclick=init;
|
||
window.addEventListener('resize', initCanvas);
|
||
applyTheme(); updateScores(); init();
|
||
</script>
|
||
</body>
|
||
</html>
|
||
|
||
<% } %>
|
||
|
||
|
||
<% if (game === "sudoku") { %>
|
||
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>POKE SUDOKU</title>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root {
|
||
--accent: #00ff99;
|
||
--bg: #111;
|
||
--cell-bg: #222;
|
||
--given-bg: #333;
|
||
--error-bg: #550000;
|
||
--highlight-bg: #333333aa;
|
||
--grid-color: #00ff99;
|
||
}
|
||
* { box-sizing: border-box; }
|
||
body {
|
||
margin: 0;
|
||
height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: radial-gradient(circle at center, #2c3e50, #34495e);
|
||
font-family: 'Press Start 2P', monospace;
|
||
color: var(--accent);
|
||
}
|
||
#controls {
|
||
margin-bottom: 10px;
|
||
display: flex;
|
||
gap: 10px;
|
||
}
|
||
button {
|
||
background: var(--accent);
|
||
color: var(--bg);
|
||
border: none;
|
||
padding: 8px 12px;
|
||
cursor: pointer;
|
||
box-shadow: 0 0 8px var(--accent);
|
||
}
|
||
button:hover { opacity: 0.8; }
|
||
#settingsBtn {
|
||
position: absolute;
|
||
top: 10px;
|
||
right: 10px;
|
||
font-size: 1.2em;
|
||
background: none;
|
||
border: none;
|
||
cursor: pointer;
|
||
color: var(--accent);
|
||
text-shadow: 0 0 8px var(--accent);
|
||
}
|
||
#settingsModal {
|
||
position: fixed;
|
||
top: 50%; left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
background: #111;
|
||
border: 2px solid var(--accent);
|
||
padding: 1em;
|
||
display: none;
|
||
z-index: 10;
|
||
width: 90vw; max-width: 300px;
|
||
border-radius: 6px;
|
||
box-shadow: 0 0 16px var(--accent);
|
||
}
|
||
#settingsModal h2 {
|
||
margin-top: 0; text-shadow: 0 0 8px var(--accent);
|
||
}
|
||
.setting {
|
||
margin: 0.6em 0; font-size: 0.8em; text-align: left;
|
||
}
|
||
.setting label { display: block; margin-bottom: 0.3em; }
|
||
.setting input[type="color"],
|
||
.setting input[type="checkbox"],
|
||
.setting select {
|
||
margin-top: 0.3em;
|
||
width: 100%; padding: 0.4em;
|
||
background: #000; color: var(--accent);
|
||
border: 1px solid var(--accent);
|
||
font-family: inherit;
|
||
box-shadow: inset 0 0 4px var(--accent);
|
||
}
|
||
#settingsModal .buttons {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 0.5em;
|
||
margin-top: 0.5em;
|
||
}
|
||
#board {
|
||
display: grid;
|
||
grid-template: repeat(9, 40px) / repeat(9, 40px);
|
||
gap: 2px;
|
||
background: var(--grid-color);
|
||
box-shadow: 0 0 16px var(--accent);
|
||
}
|
||
.cell {
|
||
position: relative;
|
||
background: var(--cell-bg);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 1.2em;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
}
|
||
.given { background: var(--given-bg); cursor: default; }
|
||
.cell.error { background: var(--error-bg); }
|
||
.cell.highlight { background: var(--highlight-bg); }
|
||
.cell:nth-child(3n) { border-right: 3px solid var(--grid-color); }
|
||
.cell:nth-child(n+19):nth-child(-n+27),
|
||
.cell:nth-child(n+46):nth-child(-n+54) { border-bottom: 3px solid var(--grid-color); }
|
||
#palette {
|
||
margin-top: 10px; display: flex; gap: 6px;
|
||
}
|
||
#palette div {
|
||
width: 36px; height: 36px;
|
||
background: var(--accent);
|
||
color: var(--bg);
|
||
display: flex; align-items: center; justify-content: center;
|
||
cursor: pointer;
|
||
box-shadow: 0 0 8px var(--accent);
|
||
}
|
||
#palette div:hover { opacity: 0.8; }
|
||
#timer { margin-top: 10px; font-size: 1em; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<button id="settingsBtn">⚙️</button>
|
||
<div id="controls">
|
||
<button id="newGameBtn">New Game</button>
|
||
<button id="solveBtn">Solve</button>
|
||
<button id="clearBtn">Clear</button>
|
||
</div>
|
||
|
||
<div id="board"></div>
|
||
<div id="palette"></div>
|
||
<div id="timer">Time: 00:00</div>
|
||
|
||
<div id="settingsModal">
|
||
<h2>Settings</h2>
|
||
<div class="setting">
|
||
<label for="themeColor">Accent Color</label>
|
||
<input type="color" id="themeColor" value="#00ff99">
|
||
</div>
|
||
<div class="setting">
|
||
<label><input type="checkbox" id="toggleGrid" checked> Show Grid Lines</label>
|
||
</div>
|
||
<div class="setting">
|
||
<label><input type="checkbox" id="toggleTimer" checked> Enable Timer</label>
|
||
</div>
|
||
<div class="setting">
|
||
<label for="presetTheme">Theme Preset</label>
|
||
<select id="presetTheme">
|
||
<option value="#00ff99">Neon Green</option>
|
||
<option value="#ff0099">Neon Pink</option>
|
||
<option value="#0099ff">Neon Blue</option>
|
||
</select>
|
||
</div>
|
||
<div class="buttons">
|
||
<button id="saveSettings">Save</button>
|
||
<button id="cancelSettings">Cancel</button>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const puzzle = [
|
||
[5,3,0,0,7,0,0,0,0],[6,0,0,1,9,5,0,0,0],[0,9,8,0,0,0,0,6,0],
|
||
[8,0,0,0,6,0,0,0,3],[4,0,0,8,0,3,0,0,1],[7,0,0,0,2,0,0,0,6],
|
||
[0,6,0,0,0,0,2,8,0],[0,0,0,4,1,9,0,0,5],[0,0,0,0,8,0,0,7,9]
|
||
];
|
||
let board=[], selected=null;
|
||
let startTime, timerInterval;
|
||
const boardEl=document.getElementById('board');
|
||
const paletteEl=document.getElementById('palette');
|
||
const timerEl=document.getElementById('timer');
|
||
const settingsBtn=document.getElementById('settingsBtn');
|
||
const settingsModal=document.getElementById('settingsModal');
|
||
const saveBtn=document.getElementById('saveSettings');
|
||
const cancelBtn=document.getElementById('cancelSettings');
|
||
|
||
let params={
|
||
accent:'#00ff99', showGrid:true, enableTimer:true, preset:'#00ff99'
|
||
};
|
||
|
||
function applySettings(){
|
||
document.documentElement.style.setProperty('--accent', params.preset);
|
||
document.getElementById('toggleGrid').checked=params.showGrid;
|
||
document.getElementById('toggleTimer').checked=params.enableTimer;
|
||
renderBoard();
|
||
if(!params.enableTimer) clearInterval(timerInterval);
|
||
else startTimer();
|
||
}
|
||
|
||
function initBoard(){
|
||
board=JSON.parse(JSON.stringify(puzzle));
|
||
selected=null;
|
||
renderBoard(); renderPalette();
|
||
applySettings();
|
||
}
|
||
|
||
function renderBoard(){
|
||
boardEl.innerHTML='';
|
||
for(let r=0;r<9;r++)for(let c=0;c<9;c++){
|
||
const val=board[r][c];
|
||
const cell=document.createElement('div');
|
||
cell.className='cell'+(puzzle[r][c]?' given':'');
|
||
if(!params.showGrid) cell.style.border='none';
|
||
cell.textContent=val||'';
|
||
cell.dataset.row=r;cell.dataset.col=c;
|
||
if(!puzzle[r][c])cell.onclick=()=>selectCell(r,c);
|
||
boardEl.appendChild(cell);
|
||
}
|
||
highlightCells();
|
||
}
|
||
|
||
function renderPalette(){
|
||
paletteEl.innerHTML='';
|
||
for(let n=1;n<=9;n++){const btn=document.createElement('div');btn.textContent=n;
|
||
btn.onclick=()=>fillNumber(n);paletteEl.appendChild(btn);}
|
||
const erase=document.createElement('div');erase.textContent='✕';
|
||
erase.onclick=()=>fillNumber(0);paletteEl.appendChild(erase);
|
||
}
|
||
|
||
function selectCell(r,c){ if(puzzle[r][c])return; selected={r,c}; highlightCells(); }
|
||
function highlightCells(){ document.querySelectorAll('.cell').forEach(el=>el.classList.remove('highlight'));
|
||
if(!selected)return; const {r,c}=selected;
|
||
document.querySelectorAll('.cell').forEach(el=>{
|
||
const rr=+el.dataset.row,cc=+el.dataset.col;
|
||
if(rr===r||cc===c|| (Math.floor(rr/3)===Math.floor(r/3)&&Math.floor(cc/3)===Math.floor(c/3)))
|
||
el.classList.add('highlight');
|
||
});
|
||
}
|
||
|
||
function fillNumber(n){ if(!selected)return; const {r,c}=selected;
|
||
board[r][c]=n; if(!validateMove(r,c)){
|
||
const idx=r*9+c; boardEl.children[idx].classList.add('error');
|
||
setTimeout(()=>boardEl.children[idx].classList.remove('error'),500);
|
||
board[r][c]=0;
|
||
}
|
||
renderBoard();
|
||
}
|
||
|
||
function validateMove(r,c){ const n=board[r][c]; if(!n)return true;
|
||
for(let i=0;i<9;i++)if(i!==c&&board[r][i]===n)return false;
|
||
for(let i=0;i<9;i++)if(i!==r&&board[i][c]===n)return false;
|
||
const br=Math.floor(r/3)*3,bc=Math.floor(c/3)*3;
|
||
for(let rr=br;rr<br+3;rr++)for(let cc=bc;cc<bc+3;cc++)
|
||
if((rr!==r||cc!==c)&&board[rr][cc]===n) return false;
|
||
return true;
|
||
}
|
||
|
||
function startTimer(){ if(!params.enableTimer)return;
|
||
clearInterval(timerInterval); startTime=Date.now(); timerInterval=setInterval(()=>{
|
||
const diff=Math.floor((Date.now()-startTime)/1000);
|
||
const m=String(Math.floor(diff/60)).padStart(2,'0');
|
||
const s=String(diff%60).padStart(2,'0');
|
||
timerEl.textContent=`Time: ${m}:${s}`;
|
||
},500);
|
||
}
|
||
|
||
function solve(){ /* omitted for brevity, same as before */ }
|
||
|
||
document.getElementById('newGameBtn').onclick=initBoard;
|
||
document.getElementById('solveBtn').onclick=solve;
|
||
document.getElementById('clearBtn').onclick=()=>{board=JSON.parse(JSON.stringify(puzzle));renderBoard();};
|
||
|
||
settingsBtn.onclick=()=>settingsModal.style.display='block';
|
||
cancelBtn.onclick=()=>settingsModal.style.display='none';
|
||
saveBtn.onclick=()=>{
|
||
params.preset=document.getElementById('presetTheme').value;
|
||
params.showGrid=document.getElementById('toggleGrid').checked;
|
||
params.enableTimer=document.getElementById('toggleTimer').checked;
|
||
applySettings(); settingsModal.style.display='none';
|
||
};
|
||
|
||
initBoard();
|
||
</script>
|
||
</body>
|
||
</html>
|
||
|
||
<% } %>
|
||
|
||
|
||
<% if (game === "pong") { %>
|
||
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>POKE PONG</title>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root { --bg1:#111; --bg2:#000; --accent:#00ff99; }
|
||
*{box-sizing:border-box;}
|
||
html,body{margin:0;padding:0;width:100%;height:100%;background:var(--bg1);font-family:'Press Start 2P',monospace;color:var(--accent);display:flex;align-items:center;justify-content:center;}
|
||
#game-container{position:relative;width:800px;max-width:100vw;height:400px;max-height:100vh;background:var(--bg2);overflow:hidden;box-shadow:0 0 16px var(--accent);}
|
||
canvas{position:absolute;top:0;left:0;width:100%;height:100%;}
|
||
#scanlines{mix-blend-mode:overlay;opacity:0.1;pointer-events:none;}
|
||
#crt{mix-blend-mode:overlay;opacity:0.2;pointer-events:none;}
|
||
#score{position:absolute;top:10px;left:50%;transform:translateX(-50%);font-size:1.5em;text-shadow:0 0 8px var(--accent);}
|
||
#settingsBtn{position:absolute;top:10px;right:10px;background:none;border:none;color:var(--accent);font-size:1.5em;cursor:pointer;text-shadow:0 0 8px var(--accent);}
|
||
#settingsModal{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--bg2);border:2px solid var(--accent);padding:1em;display:none;z-index:10;width:90vw;max-width:320px;border-radius:6px;box-shadow:0 0 16px var(--accent);}
|
||
#settingsModal h2{margin-top:0;}
|
||
.setting{margin:0.5em 0;font-size:0.8em;text-align:left;}
|
||
.setting label{display:block;margin-bottom:0.2em;}
|
||
.setting input[type=text],.setting input[type=color],.setting input[type=number],.setting select{width:100%;padding:0.4em;background:var(--bg1);color:var(--accent);border:1px solid var(--accent);font-family:inherit;font-size:0.8em;box-shadow:inset 0 0 8px var(--accent);}
|
||
.buttons{display:flex;justify-content:flex-end;gap:0.5em;margin-top:0.5em;}
|
||
.buttons button{background:var(--accent);color:var(--bg1);border:none;padding:0.5em 1em;cursor:pointer;font-size:0.8em;box-shadow:0 0 8px var(--accent);}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="game-container">
|
||
<canvas id="pongCanvas" width="800" height="400"></canvas>
|
||
<canvas id="scanlines" width="800" height="400"></canvas>
|
||
<canvas id="crt" width="800" height="400"></canvas>
|
||
<div id="score">0 : 0</div>
|
||
<button id="settingsBtn">⚙️</button>
|
||
<div id="settingsModal">
|
||
<h2>Settings</h2>
|
||
<div class="setting"><label>Paddle Speed<input type="range" id="paddleSpeed" min="2" max="20" value="5"></label></div>
|
||
<div class="setting"><label>Ball Speed<input type="range" id="ballSpeed" min="2" max="20" value="5"></label></div>
|
||
<div class="setting"><label>Max Score<input type="number" id="maxScore" min="1" max="20" value="5"></label></div>
|
||
<div class="setting"><label>Accent Color<input type="color" id="themeColor" value="#00ff99"></label></div>
|
||
<div class="setting"><label>AI Paddle<select id="aiPaddle"><option value="off">Off (2P)</option><option value="easy">Easy</option><option value="hard">Hard</option></select></label></div>
|
||
<div class="setting"><label>P1 Up Key<input type="text" id="keyP1Up" value="w" maxlength="1"></label></div>
|
||
<div class="setting"><label>P1 Down Key<input type="text" id="keyP1Down" value="s" maxlength="1"></label></div>
|
||
<div class="setting"><label>P2 Up Key<input type="text" id="keyP2Up" value="ArrowUp"></label></div>
|
||
<div class="setting"><label>P2 Down Key<input type="text" id="keyP2Down" value="ArrowDown"></label></div>
|
||
<div class="buttons"><button id="saveSettings">Save</button><button id="cancelSettings">Cancel</button></div>
|
||
</div>
|
||
</div>
|
||
<script>
|
||
const canvas=document.getElementById('pongCanvas'),ctx=canvas.getContext('2d');
|
||
const sctx=document.getElementById('scanlines').getContext('2d');
|
||
const cctx=document.getElementById('crt').getContext('2d');
|
||
const scoreEl=document.getElementById('score');
|
||
const settingsBtn=document.getElementById('settingsBtn');
|
||
const settingsModal=document.getElementById('settingsModal');
|
||
const saveBtn=document.getElementById('saveSettings');
|
||
const cancelBtn=document.getElementById('cancelSettings');
|
||
const paddleSpeedCtrl=document.getElementById('paddleSpeed');
|
||
const ballSpeedCtrl=document.getElementById('ballSpeed');
|
||
const maxScoreCtrl=document.getElementById('maxScore');
|
||
const themeColorCtrl=document.getElementById('themeColor');
|
||
const aiPaddleCtrl=document.getElementById('aiPaddle');
|
||
const keyP1UpCtrl=document.getElementById('keyP1Up');
|
||
const keyP1DownCtrl=document.getElementById('keyP1Down');
|
||
const keyP2UpCtrl=document.getElementById('keyP2Up');
|
||
const keyP2DownCtrl=document.getElementById('keyP2Down');
|
||
|
||
let settings={paddleSpeed:5,ballSpeed:5,maxScore:5,accent:'#00ff99',aiMode:'off',
|
||
keyP1Up:'w',keyP1Down:'s',keyP2Up:'ArrowUp',keyP2Down:'ArrowDown'};
|
||
let L=0,R=0,p1=170,p2=170,ball={},interval;
|
||
const audioCtx=new (window.AudioContext||window.webkitAudioContext)();
|
||
function playBeep(freq,dur){const osc=audioCtx.createOscillator(),gain=audioCtx.createGain();osc.frequency.value=freq;gain.gain.value=0.2;osc.connect(gain).connect(audioCtx.destination);osc.start();osc.stop(audioCtx.currentTime+dur);}
|
||
function init(){window.addEventListener('keydown',handleKeydown);resetBall();L=0;R=0;updateScore();drawOverlays();clearInterval(interval);interval=setInterval(loop,1000/60);}
|
||
function resetBall(){ball={x:400,y:200,vx:(Math.random()>0.5?1:-1)*settings.ballSpeed,vy:(Math.random()>0.5?1:-1)*settings.ballSpeed};}
|
||
function drawOverlays(){sctx.clearRect(0,0,800,400);sctx.fillStyle='#000';for(let y=0;y<400;y+=2)sctx.fillRect(0,y,800,1);cctx.clearRect(0,0,800,400);cctx.strokeStyle='rgba(0,255,153,0.2)';for(let i=0;i<20;i++){cctx.beginPath();cctx.arc(400,200,400-i*10,0,2*Math.PI);cctx.stroke();}}
|
||
function handleKeydown(e){if(e.key===settings.keyP1Up)pmove1(-settings.paddleSpeed);if(e.key===settings.keyP1Down)pmove1(settings.paddleSpeed);if(settings.aiMode==='off'){if(e.key===settings.keyP2Up)pmove2(-settings.paddleSpeed);if(e.key===settings.keyP2Down)pmove2(settings.paddleSpeed);}}
|
||
function pmove1(d){p1=Math.max(0,Math.min(340,p1+d));}
|
||
function pmove2(d){p2=Math.max(0,Math.min(340,p2+d));}
|
||
function loop(){update();draw();}
|
||
function update(){ball.x+=ball.vx;ball.y+=ball.vy;if(ball.y<0||ball.y>400){ball.vy*=-1;playBeep(600,0.05);}if(settings.aiMode!=='off'){const dir=ball.y-(p2+30);p2+=Math.sign(dir)*(settings.aiMode==='hard'?1:0.5)*settings.paddleSpeed;p2=Math.max(0,Math.min(340,p2));}if(ball.x<10&&ball.y>p1&&ball.y<p1+60){ball.vx*=-1;playBeep(1000,0.05);}if(ball.x>790&&ball.y>p2&&ball.y<p2+60){ball.vx*=-1;playBeep(1000,0.05);}if(ball.x<0){R++;playBeep(400,0.2);updateScore();resetBall();}if(ball.x>800){L++;playBeep(400,0.2);updateScore();resetBall();}if(L>=settings.maxScore||R>=settings.maxScore)init();}
|
||
function draw(){ctx.clearRect(0,0,800,400);ctx.fillStyle=settings.accent;ctx.fillRect(0,p1,10,60);ctx.fillRect(790,p2,10,60);ctx.beginPath();ctx.arc(ball.x,ball.y,8,0,2*Math.PI);ctx.fill();}
|
||
function updateScore(){scoreEl.textContent=`${L} : ${R}`;}
|
||
settingsBtn.onclick=()=>settingsModal.style.display='block';cancelBtn.onclick=()=>settingsModal.style.display='none';saveBtn.onclick=()=>{settings.paddleSpeed=parseInt(paddleSpeedCtrl.value);settings.ballSpeed=parseInt(ballSpeedCtrl.value);settings.maxScore=parseInt(maxScoreCtrl.value);settings.accent=themeColorCtrl.value;settings.aiMode=aiPaddleCtrl.value;settings.keyP1Up=keyP1UpCtrl.value;settings.keyP1Down=keyP1DownCtrl.value;settings.keyP2Up=keyP2UpCtrl.value;settings.keyP2Down=keyP2DownCtrl.value;document.documentElement.style.setProperty('--accent',settings.accent);settingsModal.style.display='none';init();};
|
||
init();
|
||
</script>
|
||
</body>
|
||
</html>
|
||
|
||
<% } %>
|
||
|
||
|
||
<% if (game === "minesweeper") { %>
|
||
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>POKE MINESWEEPER</title>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root {
|
||
--bg: #111;
|
||
--accent: #00ff99;
|
||
--cell-hidden: #333;
|
||
--cell-revealed: #ddd;
|
||
--cell-flag: #f22;
|
||
--cell-text: #000;
|
||
}
|
||
* { box-sizing: border-box; }
|
||
html, body {
|
||
margin: 0; padding: 0;
|
||
width: 100%; height: 100%;
|
||
background: radial-gradient(circle at center, #2c3e50, #34495e);
|
||
font-family: 'Press Start 2P', monospace;
|
||
color: var(--accent);
|
||
display: flex; align-items: center; justify-content: center;
|
||
}
|
||
#game {
|
||
display: flex; flex-direction: column; align-items: center;
|
||
gap: 8px; user-select: none;
|
||
}
|
||
#header {
|
||
display: flex; align-items: center; gap: 16px;
|
||
}
|
||
#mineCount, #timer {
|
||
background: var(--bg); padding: 4px 8px;
|
||
color: var(--accent); box-shadow: 0 0 8px var(--accent);
|
||
}
|
||
#resetBtn {
|
||
background: var(--bg); color: var(--accent);
|
||
border: none; width: 40px; height: 40px;
|
||
font-size: 1.5em; line-height: 1;
|
||
cursor: pointer; box-shadow: 0 0 8px var(--accent);
|
||
}
|
||
#difficulty {
|
||
background: var(--bg); color: var(--accent);
|
||
border: none; padding: 4px;
|
||
box-shadow: 0 0 8px var(--accent);
|
||
}
|
||
#board {
|
||
display: grid; background: var(--accent);
|
||
gap: 2px; touch-action: none;
|
||
}
|
||
.cell {
|
||
width: 30px; height: 30px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
background: var(--cell-hidden);
|
||
cursor: pointer; color: var(--cell-text);
|
||
font-size: 0.9em;
|
||
position: relative;
|
||
}
|
||
.cell.revealed { background: var(--cell-revealed); cursor: default; }
|
||
.cell.flagged { background: var(--cell-flag); }
|
||
.cell.mine { background: var(--cell-revealed); color: red; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="game">
|
||
<div id="header">
|
||
<div id="mineCount">Mines: 0</div>
|
||
<button id="resetBtn">🙂</button>
|
||
<div id="timer">Time: 000</div>
|
||
<select id="difficulty">
|
||
<option value="9x9:15">Easy (9×9,15)</option>
|
||
<option value="16x16:40" selected>Medium (16×16,40)</option>
|
||
<option value="30x16:99">Hard (30×16,99)</option>
|
||
</select>
|
||
</div>
|
||
<div id="board"></div>
|
||
</div>
|
||
<script>
|
||
const boardEl = document.getElementById('board');
|
||
const mineCountEl = document.getElementById('mineCount');
|
||
const timerEl = document.getElementById('timer');
|
||
const resetBtn = document.getElementById('resetBtn');
|
||
const diffSel = document.getElementById('difficulty');
|
||
|
||
let rows, cols, minesTotal;
|
||
let cells = [], mines = new Set(), revealedCount = 0;
|
||
let startTime, timerInterval;
|
||
let gameOver = false;
|
||
|
||
function parseDifficulty() {
|
||
const [rc, m] = diffSel.value.split(':');
|
||
[cols, rows] = rc.split('x').map(n=>parseInt(n));
|
||
minesTotal = parseInt(m);
|
||
boardEl.style.gridTemplate = `repeat(${rows}, 30px) / repeat(${cols}, 30px)`;
|
||
}
|
||
|
||
function init() {
|
||
gameOver = false;
|
||
clearInterval(timerInterval);
|
||
parseDifficulty();
|
||
cells = [];
|
||
mines.clear();
|
||
revealedCount = 0;
|
||
startTime = null;
|
||
mineCountEl.textContent = `Mines: ${minesTotal}`;
|
||
timerEl.textContent = 'Time: 000';
|
||
resetBtn.textContent = '🙂';
|
||
boardEl.innerHTML = '';
|
||
// place cells
|
||
for (let i = 0; i < rows*cols; i++) {
|
||
const cell = document.createElement('div');
|
||
cell.className = 'cell';
|
||
cell.dataset.idx = i;
|
||
cell.addEventListener('click', onReveal);
|
||
cell.addEventListener('contextmenu', onFlag);
|
||
// mobile flag: long press
|
||
let pressTimer;
|
||
cell.addEventListener('touchstart', e=>{ e.preventDefault(); pressTimer = setTimeout(()=>{onFlag(e);},500); });
|
||
cell.addEventListener('touchend', e=>{ clearTimeout(pressTimer); });
|
||
boardEl.appendChild(cell);
|
||
cells.push(cell);
|
||
}
|
||
// place mines
|
||
while (mines.size < minesTotal) mines.add(Math.floor(Math.random()*rows*cols));
|
||
startTimer();
|
||
}
|
||
|
||
function startTimer() {
|
||
startTime = Date.now();
|
||
timerInterval = setInterval(()=>{
|
||
const sec = Math.floor((Date.now()-startTime)/1000);
|
||
timerEl.textContent = 'Time: ' + String(sec).padStart(3,'0');
|
||
}, 200);
|
||
}
|
||
|
||
function onReveal(e) {
|
||
if (gameOver) return;
|
||
const idx = +e.currentTarget.dataset.idx;
|
||
if (!startTime) startTimer();
|
||
revealCell(idx);
|
||
}
|
||
|
||
function revealCell(idx) {
|
||
const cell = cells[idx];
|
||
if (cell.classList.contains('revealed')|| cell.classList.contains('flagged')) return;
|
||
cell.classList.add('revealed');
|
||
revealedCount++;
|
||
if (mines.has(idx)) {
|
||
cell.textContent = '💣'; cell.classList.add('mine');
|
||
endGame(false);
|
||
return;
|
||
}
|
||
const neighbors = getNeighbors(idx);
|
||
const count = neighbors.filter(n=>mines.has(n)).length;
|
||
if (count) {
|
||
cell.textContent = count;
|
||
} else {
|
||
neighbors.forEach(n=>revealCell(n));
|
||
}
|
||
checkWin();
|
||
}
|
||
|
||
function onFlag(e) {
|
||
e.preventDefault(); if (gameOver) return;
|
||
const idx = +e.currentTarget.dataset.idx;
|
||
const cell = cells[idx];
|
||
if (cell.classList.contains('revealed')) return;
|
||
cell.classList.toggle('flagged');
|
||
const flags = cells.filter(c=>c.classList.contains('flagged')).length;
|
||
mineCountEl.textContent = `Mines: ${minesTotal - flags}`;
|
||
}
|
||
|
||
function getNeighbors(i) {
|
||
const x = i % cols, y = Math.floor(i / cols);
|
||
const res = [];
|
||
for(let dy=-1;dy<=1;dy++) for(let dx=-1;dx<=1;dx++) {
|
||
const nx = x+dx, ny=y+dy;
|
||
if (nx>=0&&nx<cols&&ny>=0&&ny<rows&&(dx||dy)) res.push(ny*cols+nx);
|
||
}
|
||
return res;
|
||
}
|
||
|
||
function endGame(win) {
|
||
gameOver = true;
|
||
clearInterval(timerInterval);
|
||
resetBtn.textContent = win ? '😎' : '😵';
|
||
// reveal all mines
|
||
if (!win) mines.forEach(i=>{
|
||
const c = cells[i]; if(!c.classList.contains('revealed')){ c.textContent='💣'; c.classList.add('revealed'); }});
|
||
}
|
||
|
||
function checkWin() {
|
||
if (revealedCount === rows*cols - minesTotal) {
|
||
endGame(true);
|
||
}
|
||
}
|
||
|
||
resetBtn.addEventListener('click', init);
|
||
diffSel.addEventListener('change', init);
|
||
|
||
init();
|
||
</script>
|
||
</body>
|
||
</html>
|
||
|
||
<% } %>
|
||
|
||
|
||
<% if (game === "breakout") { %>
|
||
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>POKE BREAKOUT</title>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root { --bg1:#111; --bg2:#000; --accent:#00ff99; --paddle-color:#fff; --ball-color:#fff; --brick-color:#09f; --text-color:#fff; }
|
||
*{box-sizing:border-box;}
|
||
html,body{margin:0;padding:0;width:100%;height:100%;background:var(--bg1);font-family:'Press Start 2P',monospace;color:var(--text-color);display:flex;align-items:center;justify-content:center;overflow:hidden;}
|
||
#game-container{position:relative;width:800px;max-width:100vw;height:400px;max-height:100vh;background:var(--bg2);overflow:hidden;box-shadow:0 0 16px var(--accent);}
|
||
canvas{position:absolute;top:0;left:0;width:100%;height:100%;}
|
||
#scanlines{mix-blend-mode:overlay;opacity:0.1;pointer-events:none;}
|
||
#crt{mix-blend-mode:overlay;opacity:0.2;pointer-events:none;}
|
||
#ui{position:absolute;top:10px;left:50%;transform:translateX(-50%);display:flex;gap:20px;align-items:center;text-shadow:0 0 8px var(--accent);}
|
||
#ui div{background:var(--bg1);padding:4px 8px;}
|
||
#settingsBtn{position:absolute;top:10px;right:10px;background:none;border:none;color:var(--accent);font-size:1.2em;cursor:pointer;text-shadow:0 0 8px var(--accent);}
|
||
#settingsModal{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--bg2);border:2px solid var(--accent);padding:1em;display:none;z-index:10;width:90vw;max-width:320px;border-radius:6px;box-shadow:0 0 16px var(--accent);}
|
||
#settingsModal h2{margin-top:0;text-shadow:0 0 8px var(--accent);}
|
||
.setting{margin:0.5em 0;font-size:0.8em;text-align:left;}
|
||
.setting label{display:block;margin-bottom:0.2em;}
|
||
.setting input[type=range],.setting input[type=number],.setting input[type=color]{width:100%;padding:0.4em;background:var(--bg1);color:var(--accent);border:1px solid var(--accent);font-family:inherit;box-shadow:inset 0 0 8px var(--accent);}
|
||
.buttons{display:flex;justify-content:flex-end;gap:0.5em;margin-top:0.5em;}
|
||
.buttons button{background:var(--accent);color:var(--bg1);border:none;padding:0.5em 1em;cursor:pointer;font-size:0.8em;box-shadow:0 0 8px var(--accent);}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="game-container">
|
||
<canvas id="gameCanvas" width="800" height="400"></canvas>
|
||
<canvas id="scanlines" width="800" height="400"></canvas>
|
||
<canvas id="crt" width="800" height="400"></canvas>
|
||
<div id="ui">
|
||
<div id="score">Score: 0</div>
|
||
<div id="lives">Lives: 3</div>
|
||
<div id="level">Level: 1</div>
|
||
</div>
|
||
<button id="settingsBtn">⚙️</button>
|
||
<div id="settingsModal">
|
||
<h2>Settings</h2>
|
||
<div class="setting"><label>Paddle Width<input type="range" id="paddleWidth" min="50" max="200" value="100"></label></div>
|
||
<div class="setting"><label>Ball Speed<input type="range" id="ballSpeed" min="2" max="12" value="4"></label></div>
|
||
<div class="setting"><label>Brick Rows<input type="number" id="brickRows" min="1" max="10" value="5"></label></div>
|
||
<div class="setting"><label>Brick Columns<input type="number" id="brickCols" min="1" max="20" value="8"></label></div>
|
||
<div class="setting"><label>Multi-ball<input type="checkbox" id="toggleMulti"> On</label></div>
|
||
<div class="setting"><label>Power-ups<input type="checkbox" id="togglePower"> On</label></div>
|
||
<div class="setting"><label>Accent Color<input type="color" id="themeColor" value="#00ff99"></label></div>
|
||
<div class="setting"><label>Sound Effects<input type="checkbox" id="toggleSfx" checked> On</label></div>
|
||
<div class="buttons"><button id="saveSettings">Save</button><button id="cancelSettings">Cancel</button></div>
|
||
</div>
|
||
</div>
|
||
<script>
|
||
const canvas=document.getElementById('gameCanvas'),ctx=canvas.getContext('2d');
|
||
const sctx=document.getElementById('scanlines').getContext('2d'),cctx=document.getElementById('crt').getContext('2d');
|
||
const scoreEl=document.getElementById('score'),livesEl=document.getElementById('lives'),levelEl=document.getElementById('level');
|
||
const settingsBtn=document.getElementById('settingsBtn'),settingsModal=document.getElementById('settingsModal');
|
||
const saveBtn=document.getElementById('saveSettings'),cancelBtn=document.getElementById('cancelSettings');
|
||
const paddleWidthCtrl=document.getElementById('paddleWidth'),ballSpeedCtrl=document.getElementById('ballSpeed');
|
||
const brickRowsCtrl=document.getElementById('brickRows'),brickColsCtrl=document.getElementById('brickCols');
|
||
const toggleMultiCtrl=document.getElementById('toggleMulti'),togglePowerCtrl=document.getElementById('togglePower');
|
||
const themeColorCtrl=document.getElementById('themeColor'),toggleSfxCtrl=document.getElementById('toggleSfx');
|
||
|
||
let settings={paddleWidth:100,ballSpeed:4,brickRows:5,brickCols:8,multi:false,power:true,accent:'#00ff99',sfx:true};
|
||
let paddle,balls,bricks,score,lives,level,animation,powerUps=[];
|
||
const audioCtx=new(window.AudioContext||window.webkitAudioContext)();
|
||
function playBeep(f,d){if(!settings.sfx)return;let o=audioCtx.createOscillator(),g=audioCtx.createGain();o.frequency.value=f;g.gain.value=0.2;o.connect(g).connect(audioCtx.destination);o.start();o.stop(audioCtx.currentTime+d);}
|
||
|
||
function init(){
|
||
cancelAnimationFrame(animation); paddle={w:settings.paddleWidth,h:10,x:(canvas.width-settings.paddleWidth)/2,y:canvas.height-20};
|
||
balls=[{x:canvas.width/2,y:canvas.height/2,r:8,vx:settings.ballSpeed,vy:-settings.ballSpeed}];
|
||
score=0; lives=3; level=1; powerUps=[];
|
||
scoreEl.textContent='Score: 0';livesEl.textContent='Lives: 3';levelEl.textContent='Level: 1';
|
||
initBricks(); drawOverlays();
|
||
canvas.onmousemove=e=>{const r=canvas.getBoundingClientRect(); paddle.x=Math.min(canvas.width-paddle.w,Math.max(0,e.clientX-r.left-paddle.w/2));};
|
||
loop();
|
||
}
|
||
|
||
function nextLevel(){ level++; levelEl.textContent='Level: '+level; settings.brickRows++; settings.brickCols++; initBricks(); balls=[balls[0]]; resetBall(balls[0]); }
|
||
|
||
function resetBall(b){ b.x=canvas.width/2; b.y=canvas.height/2; b.vx=settings.ballSpeed*(Math.random()>0.5?1:-1); b.vy=-settings.ballSpeed; }
|
||
|
||
function initBricks(){ bricks=[]; const bw=(canvas.width-(settings.brickCols+1)*5)/settings.brickCols;
|
||
for(let r=0;r<settings.brickRows;r++)for(let c=0;c<settings.brickCols;c++)bricks.push({x:5+c*(bw+5),y:30+r*(20+5),w:bw,h:20,alive:true});
|
||
}
|
||
|
||
function spawnPower(x,y){ const types=['expand','slow','multi']; powerUps.push({x,y,type:types[Math.floor(Math.random()*types.length)],yv:2}); }
|
||
|
||
function applyPower(p){ if(p.type==='expand') paddle.w=Math.min(400,paddle.w+50);
|
||
if(p.type==='slow') balls.forEach(b=>{b.vx*=0.7; b.vy*=0.7;});
|
||
if(p.type==='multi'&&settings.multi){ let newBalls=balls.map(b=>({x:b.x,y:b.y,r:b.r,vx:-b.vx,vy:b.vy})); balls.push(...newBalls); }
|
||
}
|
||
|
||
function update(){
|
||
balls.forEach((b,i)=>{
|
||
b.x+=b.vx; b.y+=b.vy;
|
||
if(b.x<b.r||b.x>canvas.width-b.r){b.vx*=-1; playBeep(600,0.05);} if(b.y<b.r){b.vy*=-1; playBeep(600,0.05);}
|
||
if(b.y>canvas.height-b.r){balls.splice(i,1); if(balls.length===0){lives--;playBeep(300,0.2); if(lives<=0){init();return;} balls=[b]; resetBall(b); livesEl.textContent='Lives: '+lives;} }
|
||
if(b.y+ b.r>paddle.y&&b.x>paddle.x&&b.x<paddle.x+paddle.w){b.vy*=-1; b.y=paddle.y-b.r; playBeep(1000,0.05);}
|
||
bricks.forEach(br=>{ if(br.alive&&b.x>br.x&&b.x<br.x+br.w&&b.y>br.y&&b.y<br.y+br.h){ br.alive=false; b.vy*=-1; score++; playBeep(1200,0.05); scoreEl.textContent='Score: '+score; if(settings.power&&Math.random()<0.3) spawnPower(br.x+br.w/2, br.y+br.h/2); }});
|
||
});
|
||
powerUps.forEach((p,i)=>{ p.y+=p.yv; if(p.y>paddle.y&&p.x>paddle.x&&p.x<paddle.x+paddle.w){ applyPower(p); powerUps.splice(i,1);} else if(p.y>canvas.height) powerUps.splice(i,1); });
|
||
if(bricks.every(br=>!br.alive)) nextLevel();
|
||
}
|
||
|
||
function draw(){
|
||
ctx.clearRect(0,0,800,400);
|
||
ctx.fillStyle=settings.accent; ctx.fillRect(paddle.x,paddle.y,paddle.w,paddle.h);
|
||
balls.forEach(b=>{ ctx.beginPath(); ctx.arc(b.x,b.y,b.r,0,2*Math.PI); ctx.fill(); });
|
||
ctx.fillStyle='var(--brick-color)'; bricks.forEach(br=>{ if(br.alive)ctx.fillRect(br.x,br.y,br.w,br.h); });
|
||
powerUps.forEach(p=>{ ctx.fillStyle='#f0f'; ctx.fillRect(p.x-5,p.y-5,10,10); });
|
||
}
|
||
|
||
function drawOverlays(){ sctx.clearRect(0,0,800,400); sctx.fillStyle='#000'; for(let y=0;y<400;y+=2)sctx.fillRect(0,y,800,1);
|
||
cctx.clearRect(0,0,800,400); cctx.strokeStyle='rgba(0,255,153,0.2)'; for(let i=0;i<20;i++){cctx.beginPath();cctx.arc(400,200,400-i*10,0,2*Math.PI);cctx.stroke();} }
|
||
|
||
function loop(){ update(); draw(); animation=requestAnimationFrame(loop); }
|
||
|
||
settingsBtn.onclick=()=>settingsModal.style.display='block'; cancelBtn.onclick=()=>settingsModal.style.display='none'; saveSettings.onclick=()=>{ settings.paddleWidth=parseInt(paddleWidthCtrl.value); settings.ballSpeed=parseInt(ballSpeedCtrl.value); settings.brickRows=parseInt(brickRowsCtrl.value); settings.brickCols=parseInt(brickColsCtrl.value); settings.multi=toggleMultiCtrl.checked; settings.power=togglePowerCtrl.checked; settings.accent=themeColorCtrl.value; settings.sfx=toggleSfxCtrl.checked; document.documentElement.style.setProperty('--accent',settings.accent); settingsModal.style.display='none'; init(); };
|
||
|
||
init();
|
||
</script>
|
||
</body>
|
||
</html>
|
||
|
||
<% } %>
|