poke/html/gamehub.ejs
2025-04-27 17:13:14 +00:00

1695 lines
70 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!--
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 opponents 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 19.</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 Xs 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=`Its 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>
<% } %>