mirror of
https://codeberg.org/ashley/poke
synced 2025-05-30 02:59:43 +00:00
1131 lines
37 KiB
Plaintext
1131 lines
37 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; }
|
||
#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">
|
||
<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>
|
||
<% } %>
|