mirror of
https://codeberg.org/ashley/poke
synced 2025-05-30 02:59:43 +00:00
430 lines
17 KiB
Plaintext
430 lines
17 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: #2c3e50;
|
||
--bg-end: #34495e;
|
||
--accent: #ffabcc;
|
||
--card-bg: rgba(255,255,255,0.1);
|
||
--card-hover: rgba(255,255,255,0.2);
|
||
--font-main: 'PokeTube Flex', sans-serif;
|
||
}
|
||
* { 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;
|
||
display:flex; align-items:center; justify-content:center;
|
||
min-height:100vh;
|
||
}
|
||
h1 {
|
||
font-weight:900; font-stretch:ultra-expanded; font-style:italic;
|
||
color: var(--accent); text-align:center; margin-bottom:1rem;
|
||
}
|
||
.game-container {
|
||
display:grid; grid-template-columns:repeat(auto-fit,minmax(240px,1fr));
|
||
gap:1rem; width:90%; max-width:1000px;
|
||
}
|
||
.game {
|
||
background: var(--card-bg);
|
||
border:2px solid #fff; border-radius:10px;
|
||
text-decoration:none; color:#fff; padding:1rem;
|
||
text-align:center; transition:background .3s;
|
||
}
|
||
.game:hover { background: var(--card-hover); }
|
||
.game h2 { margin-bottom: .5rem; }
|
||
canvas, .board { display:none; }
|
||
@font-face {
|
||
font-family: var(--font-main);
|
||
src: url("https://p.poketube.fun/.../robotoflex.ttf");
|
||
font-display: swap;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div>
|
||
<h1>Poke! Games Hub</h1>
|
||
<div class="game-container">
|
||
<a href="?game=snake" class="game"><h2>Snake</h2><canvas id="snakeCanvas"></canvas></a>
|
||
<a href="?game=tic-tac-toe" class="game"><h2>Tic-Tac-Toe</h2><div id="message"></div><div class="board" id="board"></div></a>
|
||
<a href="?game=sudoku" class="game"><h2>Sudoku</h2><div class="board" id="sudokuBoard"></div></a>
|
||
<a href="?game=pong" class="game"><h2>Ping-Pong</h2><canvas id="pongCanvas"></canvas></a>
|
||
<a href="?game=minesweeper" class="game"><h2>Minesweeper</h2><div class="board" id="minesweeperBoard"></div></a>
|
||
<a href="?game=breakout" class="game"><h2>Breakout</h2><canvas id="breakoutCanvas"></canvas></a>
|
||
</div>
|
||
</div>
|
||
<script src="/static/data-mobile.js?v=6000"></script>
|
||
</body>
|
||
</html>
|
||
<% } %>
|
||
|
||
|
||
<% if (game === "snake") { %>
|
||
<!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>Snake</title>
|
||
<style>
|
||
html,body{margin:0;padding:0;background:linear-gradient(135deg,#2c3e50,#34495e);overflow:hidden;}
|
||
#snakeCanvas{display:block;border:1px solid #000;position:absolute;}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<canvas id="snakeCanvas"></canvas>
|
||
<script>
|
||
// GPL header omitted—see top of file
|
||
const canvas = document.getElementById('snakeCanvas');
|
||
const ctx = canvas.getContext('2d');
|
||
const scale = 20;
|
||
let snake = [{x:10,y:10}], dir={x:0,y:1}, food=randomFood();
|
||
function resize(){
|
||
canvas.width=Math.floor(window.innerWidth/scale)*scale;
|
||
canvas.height=Math.floor(window.innerHeight/scale)*scale;
|
||
}
|
||
window.addEventListener('resize',resize);
|
||
resize();
|
||
function randomFood(){ return { x:Math.floor(Math.random()*(canvas.width/scale)), y:Math.floor(Math.random()*(canvas.height/scale)) }; }
|
||
function draw(){
|
||
ctx.clearRect(0,0,canvas.width,canvas.height);
|
||
ctx.fillStyle='#f00'; ctx.fillRect(food.x*scale,food.y*scale,scale,scale);
|
||
ctx.fillStyle='#00f'; snake.forEach(s=>ctx.fillRect(s.x*scale,s.y*scale,scale,scale));
|
||
}
|
||
function update(){
|
||
const head={x:snake[0].x+dir.x,y:snake[0].y+dir.y};
|
||
head.x=(head.x+(canvas.width/scale))%(canvas.width/scale);
|
||
head.y=(head.y+(canvas.height/scale))%(canvas.height/scale);
|
||
if(snake.some((seg,i)=>i&&seg.x===head.x&&seg.y===head.y)) return reset();
|
||
snake.unshift(head);
|
||
if(head.x===food.x&&head.y===food.y) food=randomFood(); else snake.pop();
|
||
}
|
||
function reset(){ snake=[{x:10,y:10}]; dir={x:0,y:1}; food=randomFood(); }
|
||
document.addEventListener('keydown',e=>{
|
||
if(e.key.startsWith('Arrow')){
|
||
const map={Up:[0,-1],Down:[0,1],Left:[-1,0],Right:[1,0]};
|
||
const [dx,dy]=map[e.key.replace('Arrow','')];
|
||
if(dx!==-dir.x&&dy!==-dir.y) dir={x:dx,y:dy};
|
||
}
|
||
});
|
||
setInterval(()=>{update();draw();},100);
|
||
</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';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:#fff2;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>
|
||
// GPL header omitted—see top of file
|
||
const boardEl=document.getElementById('board'), msgEl=document.getElementById('message');
|
||
let board=Array(9).fill(''), player='X';
|
||
function render(){
|
||
boardEl.innerHTML='';
|
||
board.forEach((v,i)=>{
|
||
const c=document.createElement('div');
|
||
c.className='cell'; c.textContent=v;
|
||
c.onclick=()=>move(i);
|
||
boardEl.append(c);
|
||
});
|
||
}
|
||
function move(i){
|
||
if(board[i]||win()||draw())return;
|
||
board[i]=player;
|
||
if(win()){ msgEl.textContent=`Player ${player} won!!!!!! woaah`; }
|
||
else if(draw()){ msgEl.textContent="It's a draw! oh welp >~<"; }
|
||
else{ player=player==='X'?'O':'X'; msgEl.textContent=`Player ${player}’s turn :3`; if(player==='O')setTimeout(ai,300); }
|
||
render();
|
||
}
|
||
function win(){ return [[0,1,2],[3,4,5],[6,7,8],[0,3,6],[1,4,7],[2,5,8],[0,4,8],[2,4,6]].some(l=>l.every(i=>board[i]&&board[i]===board[l[0]])); }
|
||
function draw(){ return board.every(v=>v); }
|
||
function ai(){
|
||
const tryMv=p=>board.findIndex((v,i)=>!v&&(board[i]=p,win())&&(board[i]='',true));
|
||
let idx=tryMv('O')||tryMv('X');
|
||
if(idx<0) idx=board.map((v,i)=>v?'':i).filter(i=>i)[0];
|
||
move(idx);
|
||
}
|
||
render();
|
||
</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'),ctx=c.getContext('2d');
|
||
const P={w:100,h:10,x:350,y:380},B={r:8,dx:4,dy:-4},br=[];let score=0;
|
||
for(let r=0;r<5;r++)for(let co=0;co<8;co++)br.push({x:co*100+35,y:r*30+30,alive:true});
|
||
document.addEventListener('mousemove',e=>P.x=Math.min(c.width-P.w,Math.max(0,e.clientX-c.offsetLeft-P.w/2)));
|
||
function draw(){
|
||
ctx.clearRect(0,0,c.width,c.height);
|
||
ctx.fillStyle='#fff';ctx.fillRect(P.x,P.y,P.w,P.h);
|
||
ctx.beginPath();ctx.arc(B.x,B.y,B.r,0,2*Math.PI);ctx.fill();
|
||
br.forEach(b=>b.alive&&(ctx.fillStyle='#09f',ctx.fillRect(b.x,b.y,90,20)));
|
||
ctx.fillText(`Score: ${score}`,10,c.height-10);
|
||
}
|
||
function update(){
|
||
B.x+=B.dx;B.y+=B.dy;
|
||
if(B.x<B.r||B.x>c.width-B.r)B.dx*=-1;
|
||
if(B.y<B.r)B.dy*=-1;
|
||
if(B.y>c.height)return alert('Game Over'),location.reload();
|
||
if(B.y+ B.r>P.y&&B.x>P.x&&B.x<P.x+P.w)B.dy*=-1;
|
||
br.forEach(b=>b.alive&&B.x>b.x&&B.x<b.x+90&&B.y>b.y&&B.y<b.y+20&&(B.dy*=-1,b.alive=false,score++));
|
||
}
|
||
function loop(){update();draw();requestAnimationFrame(loop);}
|
||
loop();
|
||
</script>
|
||
</body>
|
||
</html>
|
||
<% } %>
|