poke/html/gamehub.ejs
2025-04-27 12:52:28 +00:00

1086 lines
34 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

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="/css/yt-ukraine.svg?v=4" rel="icon">
<link rel="manifest" href="/manifest.json">
<meta property="og:title" content="▶▶ Poke Games Hub">
<meta property="twitter:description" content="Free software gaming on poke!">
<meta property="og:image" content="https://cdn.glitch.global/.../hub-image.png">
<meta name="twitter:card" content="summary_large_image">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Poke! Games Hub</title>
<style>
:root {
--bg-start: #1a1a2e;
--bg-end: #16213e;
--accent: #e94560;
--font-main: 'PokeTube Flex', sans-serif;
--card-bg: rgba(255,255,255,0.1);
--card-border: rgba(255,255,255,0.5);
--card-hover: rgba(255,255,255,0.2);
--shadow: rgba(0,0,0,0.4);
}
* { box-sizing: border-box; margin:0; padding:0; }
body {
font-family: var(--font-main);
background: linear-gradient(135deg, var(--bg-start), var(--bg-end));
color: #fff;
overflow: hidden;
}
@font-face {
font-family: "PokeTube Flex";
src: url("https://p.poketube.fun/https://cdn.glitch.global/43b6691a-c8db-41d4-921c-8cf6aa0d9108/robotoflex.ttf?v=1668343428681");
font-style: normal;
font-stretch: 1% 800%;
font-display: swap;
}
/* FULL-PAGE EMOJI GRID */
.emoji-bg {
position: fixed; top: 0; left: 0;
width: 100vw; height: 100vh;
display: grid;
/* make a responsive grid of ~25 columns */
grid-template-columns: repeat(auto-fill, minmax(3rem, 1fr));
grid-auto-rows: 3rem;
pointer-events: none;
z-index: 0;
font-size: 2.5rem;
}
.emoji-bg span {
display: flex;
align-items: center;
justify-content: center;
opacity: 0.08;
animation: float var(--dur) ease-in-out infinite alternate;
}
/* randomize durations a bit */
.emoji-bg span:nth-child(odd) { --dur: 6s; }
.emoji-bg span:nth-child(even) { --dur: 9s; }
@keyframes float {
to { transform: translateY(-20px) rotate(15deg); }
}
/* Main container */
.wrapper {
position: relative; z-index: 1;
max-width: 1000px; margin: 3rem auto;
padding: 2rem;
background: var(--card-bg);
border: 2px solid var(--card-border);
border-radius: 20px;
box-shadow: 0 8px 32px var(--shadow);
backdrop-filter: blur(12px);
animation: fadeIn 1s ease forwards;
opacity: 0;
}
@keyframes fadeIn { to { opacity: 1; } }
h1 {
font-size: 3.5rem;
text-align: center;
margin-bottom: 2rem;
background: linear-gradient(90deg, #ff2e63, #08d9d6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: 1000;
font-stretch: ultra-expanded;
font-family: "Poketube flex";
font-style: italic;
}
.game-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 1.5rem;
}
.game {
position: relative;
background: var(--card-bg);
border: 2px solid var(--card-border);
border-radius: 16px;
padding: 1.5rem;
text-align: center;
color: #fff;
text-decoration: none;
overflow: hidden;
transition: transform 0.3s, box-shadow 0.3s, background 0.3s;
}
.game::before {
content: '';
position: absolute; top:0; left:0; width:100%; height:100%;
background: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.2), transparent 70%);
opacity: 0; transition: opacity 0.3s;
}
.game:hover {
transform: translateY(-8px) rotate(-1deg);
box-shadow: 0 12px 48px var(--shadow);
background: var(--card-hover);
}
.game:hover::before { opacity: 1; }
.game .icon {
font-size: 3rem;
margin-bottom: 0.5rem;
animation: popIn 0.5s ease forwards;
opacity: 0;
}
@keyframes popIn { to { opacity:1; transform: scale(1); } }
.game h2 {
font-size: 1.4rem;
margin-bottom: 0.3rem;
text-shadow: 1px 1px 4px var(--shadow);
}
/* hide game UIs until selected */
canvas, .board { display: none; }
</style>
</head>
<body>
<!-- empty grid; JS will populate -->
<div class="emoji-bg"></div>
<div class="wrapper">
<h1>Poke! Games Hub</h1>
<div class="game-container">
<a href="?game=snake" class="game">
<div class="icon">🐍</div>
<h2>Snake</h2>
</a>
<a href="?game=tic-tac-toe" class="game">
<div class="icon">❌⭕</div>
<h2>Tic-Tac-Toe</h2>
</a>
<a href="?game=sudoku" class="game">
<div class="icon">🧮</div>
<h2>Sudoku</h2>
</a>
<a href="?game=pong" class="game">
<div class="icon">🏓</div>
<h2>Ping-Pong</h2>
</a>
<a href="?game=minesweeper" class="game">
<div class="icon">💣</div>
<h2>Minesweeper</h2>
</a>
<a href="?game=breakout" class="game">
<div class="icon">🧱</div>
<h2>Breakout</h2>
</a>
</div>
</div>
<script src="/static/data-mobile.js?v=6000"></script>
<script>
const emojis = ['🎮','🕹️','👾','🎧','🖥️','🎲','🏆','🎯','🔥','💥','🧩','⭐','⚔️','🛡️','🚀','🎉','🌟','⚡','💣'];
const bg = document.querySelector('.emoji-bg');
for (let i = 0; i < 400; i++) {
const span = document.createElement('span');
span.textContent = emojis[i % emojis.length];
// randomize animation speed slightly
span.style.setProperty('--dur', `${6 + Math.random()*4}s`);
bg.appendChild(span);
}
</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; }
/* Canvas */
#snakeCanvas {
background: #111;
display: block;
margin: auto;
image-rendering: pixelated;
border: 4px solid #0f0;
box-shadow: 0 0 20px #0f0;
}
/* Overlay Grid */
#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);
}
/* Title & GameOver Screens */
.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;
}
.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;
font-family: 'Press Start 2P', monospace;
cursor: pointer;
}
/* Scoreboard */
#scoreboard {
position:absolute; top:16px; left:16px;
z-index:50;
white-space: nowrap;
text-shadow:0 0 5px #0f0;
}
/* Settings Cog */
#settingsBtn {
position:absolute; top:16px; right:16px;
font-size:24px;
cursor:pointer;
z-index:50;
user-select:none;
}
/* Settings Modal */
#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; }
#settingsModal input[type="number"],
#settingsModal input[type="range"],
#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; }
</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>
<!-- Game Over Screen -->
<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>
<!-- Settings Modal -->
<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>
<button id="saveSettings">Save</button>
<button id="closeSettings">Close</button>
</div>
</div>
<script>
// Elements
const canvas = document.getElementById('snakeCanvas'), ctx = canvas.getContext('2d');
const titleScreen = document.getElementById('titleScreen');
const gameOverScreen = document.getElementById('gameOverScreen');
const gameOverText = document.getElementById('gameOverText');
const scoreboard = document.getElementById('scoreboard');
const settingsBtn = document.getElementById('settingsBtn');
const settingsModal = document.getElementById('settingsModal');
const saveBtn = document.getElementById('saveSettings');
const closeBtn = document.getElementById('closeSettings');
const backToTitle = document.getElementById('backToTitle');
// Params
let params = {
speed: 100,
sfx: true,
startLength: 1,
wallMode: 'kill',
grid: false,
sfxVolume: 1,
headColor: '#ffff00',
bodyColor: '#00ffff'
};
// Game state
let snake, dir, food, score, highScore = 0, level, gameInterval;
let state = 'start'; // 'start', 'playing', 'gameover'
const endMessages = [
"Skill issue!", "Snake bit ya!", "Sssorry, try again!",
];
function resizeCanvas() {
canvas.width = Math.floor(window.innerWidth / 20) * 20;
canvas.height = Math.floor(window.innerHeight / 20) * 20;
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
function initGame() {
// snake init
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;
updateScoreboard();
// start loop
clearInterval(gameInterval);
gameInterval = setInterval(gameLoop, params.speed);
}
function pickFood() {
const cols = canvas.width / 20, rows = canvas.height / 20;
food = { x: Math.floor(Math.random() * cols), y: Math.floor(Math.random() * rows) };
}
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();
// food
ctx.fillStyle = '#f00'; ctx.fillRect(food.x*20, food.y*20, 20, 20);
// snake
snake.forEach((seg,i) => {
ctx.fillStyle = i===0 ? params.headColor : params.bodyColor;
ctx.fillRect(seg.x*20, seg.y*20, 20, 20);
});
}
function playBeep(freq, duration) {
if (!params.sfx) return;
const ac = new (window.AudioContext || window.webkitAudioContext)();
const gain = ac.createGain();
gain.gain.value = params.sfxVolume;
const osc = ac.createOscillator();
osc.frequency.value = freq;
osc.connect(gain).connect(ac.destination);
osc.start(); osc.stop(ac.currentTime + duration);
}
function update() {
const head = { x: snake[0].x + dir.x, y: snake[0].y + dir.y };
const cols = canvas.width / 20, rows = canvas.height / 20;
// wall
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();
}
// self-collide
if (snake.some((s,i) => i>0 && s.x===head.x && s.y===head.y)) return endGame();
snake.unshift(head);
if (head.x===food.x && head.y===food.y) {
score++; if (score % 5 === 0) level++; updateScoreboard(); pickFood(); playBeep(440,0.1);
} else snake.pop();
}
function gameLoop() {
update(); draw();
}
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}`;
}
// Event handlers
document.addEventListener('keydown', e => {
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') {
if (e.key.toLowerCase() === 'r') {
gameOverScreen.classList.add('hidden'); titleScreen.classList.remove('hidden'); state = 'start';
}
}
});
// Settings
settingsBtn.addEventListener('click', () => {
// pause
if (state === 'playing') clearInterval(gameInterval);
// populate
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', () => {
// save and reinit if playing
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">
<link href="/css/yt-ukraine.svg?v=4" rel="icon">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Tic-Tac-Toe</title>
<style>
body {
margin: 0;
font-family: 'PokeTube Flex', sans-serif;
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
background: linear-gradient(135deg, #2c3e50, #34495e);
color: #fff;
}
#board {
display: grid;
grid-template: repeat(3, 100px) / repeat(3, 100px);
gap: 5px;
}
.cell {
display: flex;
align-items: center;
justify-content: center;
font-size: 2em;
background: rgba(255,255,255,0.2);
cursor: pointer;
}
#message {
text-align: center;
margin-bottom: 1rem;
}
</style>
</head>
<body>
<div>
<div id="message">Player Xs turn :3</div>
<div id="board"></div>
</div>
<script>
const boardElement = document.getElementById("board");
const messageElement = document.getElementById("message");
let currentPlayer = "X";
let board = ["", "", "", "", "", "", "", "", ""];
function checkWinner() {
const winningCombinations = [
[0, 1, 2], [3, 4, 5], [6, 7, 8], // Rows
[0, 3, 6], [1, 4, 7], [2, 5, 8], // Columns
[0, 4, 8], [2, 4, 6] // Diagonals
];
for (const combination of winningCombinations) {
const [a, b, c] = combination;
if (board[a] && board[a] === board[b] && board[a] === board[c]) {
return board[a];
}
}
return null;
}
function checkDraw() {
return !board.includes("");
}
function handleClick(index) {
if (board[index] === "" && !checkWinner() && !checkDraw()) {
board[index] = currentPlayer;
renderBoard();
const winner = checkWinner();
if (winner) {
messageElement.textContent = `Player ${winner} won!!!!!! woaah`;
} else if (checkDraw()) {
messageElement.textContent = "It's a draw! oh welp >~<";
} else {
currentPlayer = currentPlayer === "X" ? "O" : "X";
messageElement.textContent = `Player ${currentPlayer}'s turn :3`;
if (currentPlayer === "O") {
setTimeout(makeComputerMove, 500); // AI waits for 0.5 second
}
}
}
}
function makeComputerMove() {
// Look for a winning move, then look to block player, otherwise, choose a random move
const winningMove = findWinningMove();
const blockingMove = findBlockingMove();
if (winningMove !== null) {
board[winningMove] = currentPlayer;
} else if (blockingMove !== null) {
board[blockingMove] = currentPlayer;
} else {
// Randomly choose an empty cell for the computer's move
const emptyCells = board.reduce((acc, value, index) => {
if (value === "") {
acc.push(index);
}
return acc;
}, []);
if (emptyCells.length > 0) {
const randomIndex = Math.floor(Math.random() * emptyCells.length);
const computerMove = emptyCells[randomIndex];
board[computerMove] = currentPlayer;
}
}
renderBoard();
const winner = checkWinner();
if (winner) {
messageElement.textContent = `Player ${winner} won!!!!!! woaah`;
} else if (checkDraw()) {
messageElement.textContent = "It's a draw! oh welp >~<";
} else {
currentPlayer = currentPlayer === "X" ? "O" : "X";
messageElement.textContent = `Player ${currentPlayer}'s turn :3`;
}
}
function findWinningMove() {
for (let i = 0; i < board.length; i++) {
if (board[i] === "") {
board[i] = currentPlayer;
if (checkWinner() === currentPlayer) {
board[i] = ""; // Reset the move
return i;
}
board[i] = ""; // Reset the move
}
}
return null;
}
function findBlockingMove() {
const opponent = currentPlayer === "X" ? "O" : "X";
for (let i = 0; i < board.length; i++) {
if (board[i] === "") {
board[i] = opponent;
if (checkWinner() === opponent) {
board[i] = ""; // Reset the move
return i;
}
board[i] = ""; // Reset the move
}
}
return null;
}
function renderBoard() {
boardElement.innerHTML = "";
board.forEach((value, index) => {
const cell = document.createElement("div");
cell.classList.add("cell");
cell.textContent = value;
cell.addEventListener("click", () => handleClick(index));
boardElement.appendChild(cell);
});
}
function resetGame() {
currentPlayer = "X";
board = ["", "", "", "", "", "", "", "", ""];
messageElement.textContent = `Player ${currentPlayer}'s turn :3`;
renderBoard();
// If AI is the starting player, make the first move
if (currentPlayer === "O") {
setTimeout(makeComputerMove, 500); // AI waits for 0.5 second
}
}
// Initial setup
resetGame();
</script>
</body>
</html>
<% } %>
<% if (game === "sudoku") { %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link href="/css/yt-ukraine.svg?v=4" rel="icon">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Sudoku</title>
<style>
body{margin:0;display:flex;align-items:center;justify-content:center;height:100vh;background:linear-gradient(135deg,#2c3e50,#34495e);font-family:Arial;}
#sudokuBoard{display:grid;grid-template:repeat(9,40px)/repeat(9,40px);gap:1px;}
.cell{display:flex;align-items:center;justify-content:center;background:#ddd;font-weight:bold;cursor:pointer;user-select:none;}
.given{background:#ccc;} .error{background:#fbb;}
#overlay, #popup{display:none;position:fixed;width:100%;height:100%;}
#overlay{background:rgba(0,0,0,0.5);z-index:1;}
#popup{z-index:2;background:#fff;padding:2rem;border-radius:1em;top:50%;left:50%;transform:translate(-50%,-50%);}
</style>
</head>
<body>
<div id="overlay"></div>
<div id="popup"><h2>U did a incorrect move :sob:</h2><button onclick="closePopup()">Oki</button></div>
<div id="sudokuBoard"></div>
<script>
// GPL header omitted—see top of file
const base=[[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=JSON.parse(JSON.stringify(base));
const cont=document.getElementById('sudokuBoard'), pop=document.getElementById('popup'), ov=document.getElementById('overlay');
function closePopup(){pop.style.display=ov.style.display='none';}
function render(){
cont.innerHTML='';
board.forEach((r,ri)=>r.forEach((v,ci)=>{
const c=document.createElement('div');
c.className='cell'+(base[ri][ci]?' given':'');
c.textContent=v||'';
if(!base[ri][ci])c.onclick=()=>clickCell(ri,ci);
cont.append(c);
}));
}
function clickCell(r,c){
const n=parseInt(prompt('Enter 1-9:'),10);
if(n>=1&&n<=9){board[r][c]=n; if(!valid()){showErr();board[r][c]=0;} render();}
}
function valid(){
for(let i=0;i<9;i++){
let rs=new Set(), cs=new Set();
for(let j=0;j<9;j++){
if(board[i][j]&&rs.has(board[i][j]))return false;
rs.add(board[i][j]);
if(board[j][i]&&cs.has(board[j][i]))return false;
cs.add(board[j][i]);
}
}
for(let br=0;br<9;br+=3)for(let bc=0;bc<9;bc+=3){
let bs=new Set();
for(let r=br;r<br+3;r++)for(let c=bc;c<bc+3;c++){
if(board[r][c]&&bs.has(board[r][c]))return false;
bs.add(board[r][c]);
}
}
return true;
}
function showErr(){pop.style.display=ov.style.display='block';setTimeout(closePopup,1000);}
render();
</script>
</body>
</html>
<% } %>
<% if (game === "pong") { %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link href="/css/yt-ukraine.svg?v=4" rel="icon">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Pong</title>
<style>
body{margin:0;font-family:Arial;background:linear-gradient(135deg,#2c3e50,#34495e);color:#fff;display:flex;justify-content:center;align-items:center;height:100vh;}
#pongCanvas{border:1px solid #fff;}
#score{position:absolute;top:10px;left:50%;transform:translateX(-50%);font-size:1.2em;}
</style>
</head>
<body>
<div id="score">0 : 0</div>
<canvas id="pongCanvas" width="800" height="400"></canvas>
<script>
// GPL header omitted—see top of file
const c=document.getElementById('pongCanvas'),ctx=c.getContext('2d');
let L=0,R=0,ball={x:400,y:200,vx:5,vy:3},p1=170,p2=170,P={w:10,h:60},max=5;
function draw(){ctx.clearRect(0,0,800,400);ctx.fillStyle='#fff';ctx.fillRect(0,p1,P.w,P.h);ctx.fillRect(800-P.w,p2,P.w,P.h);ctx.beginPath();ctx.arc(ball.x,ball.y,8,0,2*Math.PI);ctx.fill();document.getElementById('score').textContent=`${L} : ${R}`;}
function update(){
ball.x+=ball.vx;ball.y+=ball.vy;
if(ball.y<0||ball.y>400)ball.vy*=-1;
if(ball.x<P.w&&ball.y>p1&&ball.y<p1+P.h)ball.vx*=-1;
if(ball.x>800-P.w&&ball.y>p2&&ball.y<p2+P.h)ball.vx*=-1;
if(ball.x<0){R++;reset();}
if(ball.x>800){L++;reset();}
p1+=(ball.y-(p1+P.h/2))*0.02;
}
function reset(){ball={x:400,y:200,vx:5*(Math.random()>0.5?1:-1),vy:3*(Math.random()>0.5?1:-1)};if(L>=max||R>=max){L=R=0;}}
window.addEventListener('keydown',e=>{if(e.key==='w'&&p2>0)p2-=20;if(e.key==='s'&&p2<340)p2+=20;});
setInterval(()=>{update();draw();},1000/60);
</script>
</body>
</html>
<% } %>
<% if (game === "minesweeper") { %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link href="/css/yt-ukraine.svg?v=4" rel="icon">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Minesweeper</title>
<style>
body{margin:0;display:flex;justify-content:center;align-items:center;height:100vh;background:linear-gradient(135deg,#2c3e50,#34495e);font-family:Arial;color:#fff;}
#board{display:grid;grid-template:repeat(10,30px)/repeat(10,30px);gap:2px;}
.cell{width:30px;height:30px;background:#aaa;display:flex;align-items:center;justify-content:center;cursor:pointer;user-select:none;}
.revealed{background:#ddd;cursor:default;} .flag{background:#f22;}
</style>
</head>
<body>
<div id="board"></div>
<script>
// GPL header omitted—see top of file
const R=10,C=10,M=15,boardEl=document.getElementById('board');
let cells=[],mines=new Set();
function init(){
while(mines.size<M)mines.add(Math.floor(Math.random()*R*C));
for(let i=0;i<R*C;i++){
const d=document.createElement('div');d.className='cell';d.dataset.i=i;
d.oncontextmenu=e=>{e.preventDefault();d.classList.toggle('flag');};
d.onclick=()=>reveal(d);boardEl.append(d);cells.push(d);
}
}
function nbr(i){const a=[];const x=i%C,y=Math.floor(i/C);
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<C&&ny>=0&&ny<R&&(dx||dy))a.push(ny*C+nx);
}return a;
}
function reveal(d){
const i=+d.dataset.i; if(d.classList.contains('flag')||d.classList.contains('revealed'))return;
d.classList.add('revealed');
if(mines.has(i)){d.textContent='💣';alert('Game Over!');reset();return;}
const cnt=nbr(i).filter(n=>mines.has(n)).length;
if(cnt)d.textContent=cnt; else nbr(i).forEach(n=>reveal(cells[n]));
if(cells.filter(c=>!c.classList.contains('revealed')).length===M){alert('You Win!');reset();}
}
function reset(){boardEl.innerHTML='';cells=[];mines.clear();init();}
init();
</script>
</body>
</html>
<% } %>
<% if (game === "breakout") { %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link href="/css/yt-ukraine.svg?v=4" rel="icon">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Breakout</title>
<style>
body {
margin: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: linear-gradient(135deg,#2c3e50,#34495e);
font-family: Arial;
color: #fff;
}
#breakoutCanvas {
border: 1px solid #fff;
}
</style>
</head>
<body>
<canvas id="breakoutCanvas" width="800" height="400"></canvas>
<script>
// GPL header omitted—see top of file
const c = document.getElementById('breakoutCanvas');
const ctx = c.getContext('2d');
// Paddle
const P = {
w: 100,
h: 10,
x: (c.width - 100) / 2,
y: c.height - 20
};
// Ball (now with x & y!)
const B = {
x: c.width / 2,
y: c.height / 2,
r: 8,
dx: 4,
dy: -4
};
// Bricks
const brickRow = 5;
const brickCol = 8;
const brickW = 90;
const brickH = 20;
const bricks = [];
let score = 0;
for (let row = 0; row < brickRow; row++) {
for (let col = 0; col < brickCol; col++) {
bricks.push({
x: col * (brickW + 10) + 20,
y: row * (brickH + 10) + 30,
alive: true
});
}
}
// Paddle follow mouse
document.addEventListener('mousemove', e => {
const rect = c.getBoundingClientRect();
P.x = Math.min(
c.width - P.w,
Math.max(0, e.clientX - rect.left - P.w / 2)
);
});
function draw() {
ctx.clearRect(0, 0, c.width, c.height);
// Draw paddle
ctx.fillStyle = '#fff';
ctx.fillRect(P.x, P.y, P.w, P.h);
// Draw ball
ctx.beginPath();
ctx.arc(B.x, B.y, B.r, 0, Math.PI * 2);
ctx.fill();
// Draw bricks
bricks.forEach(b => {
if (b.alive) {
ctx.fillStyle = '#09f';
ctx.fillRect(b.x, b.y, brickW, brickH);
}
});
// Draw score
ctx.fillStyle = '#fff';
ctx.fillText(`Score: ${score}`, 10, c.height - 10);
}
function update() {
// Move ball
B.x += B.dx;
B.y += B.dy;
// Wall collisions
if (B.x - B.r < 0 || B.x + B.r > c.width) B.dx *= -1;
if (B.y - B.r < 0) B.dy *= -1;
// Bottom out
if (B.y - B.r > c.height) {
alert('Game Over!');
location.reload();
}
// Paddle collision
if (
B.y + B.r > P.y &&
B.x > P.x &&
B.x < P.x + P.w
) {
B.dy *= -1;
B.y = P.y - B.r; // avoid sticking
}
// Brick collisions
bricks.forEach(b => {
if (
b.alive &&
B.x > b.x &&
B.x < b.x + brickW &&
B.y > b.y &&
B.y < b.y + brickH
) {
B.dy *= -1;
b.alive = false;
score++;
}
});
}
function loop() {
update();
draw();
requestAnimationFrame(loop);
}
loop();
</script>
</body>
</html>
<% } %>