mirror of
https://codeberg.org/ashley/poke
synced 2025-05-30 02:59:43 +00:00
1086 lines
34 KiB
Plaintext
1086 lines
34 KiB
Plaintext
<!--
|
||
This Source Code Form is subject to the terms of the GNU General Public License:
|
||
|
||
Copyright (C) 2021-2025 PokeTube (https://codeberg.org/Ashley/poketube)
|
||
|
||
This program is free software: you can redistribute it and/or modify
|
||
it under the terms of the GNU General Public License as published by
|
||
the Free Software Foundation, either version 3 of the License, or
|
||
(at your option) any later version.
|
||
|
||
This program is distributed in the hope that it will be useful,
|
||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
GNU General Public License for more details.
|
||
|
||
You should have received a copy of the GNU General Public License
|
||
along with this program. If not, see https://www.gnu.org/licenses/.
|
||
--><!--//--><![CDATA[//><!--
|
||
/**
|
||
* @licstart The following is the entire license notice for the JavaScript
|
||
* code in this page.
|
||
*
|
||
* Copyright (C) 2021-2024 POKETUBE (https://codeberg.org/Ashley/poketube)
|
||
*
|
||
* The JavaScript code in this page is free software: you can redistribute
|
||
* it and/or modify it under the terms of the GNU General Public License
|
||
* (GNU GPL) as published by the Free Software Foundation, either version 3
|
||
* of the License, or (at your option) any later version. The code is
|
||
* distributed WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU GPL
|
||
* for more details.
|
||
*
|
||
* As additional permission under GNU GPL version 3 section 7, you may
|
||
* distribute non-source (e.g., minimized or compacted) forms of that code
|
||
* without the copy of the GNU GPL normally required by section 4, provided
|
||
* you include this license notice and a URL through which recipients can
|
||
* access the Corresponding Source.
|
||
*
|
||
* @licend The above is the entire license notice for the JavaScript code
|
||
* in this page.
|
||
*/
|
||
|
||
//--><!]]>
|
||
<% if (!game) { %>
|
||
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<link href="/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 funnyMessages = [
|
||
"You got scaled!", "Snake bit ya!", "Sssorry, try again!", "Slither fail!", "Curveball!"
|
||
];
|
||
|
||
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 = funnyMessages[Math.floor(Math.random()*funnyMessages.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 X’s 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>
|
||
<% } %>
|