Files
vibing/ittybittytetris/index.php
thrhymes b049dded72 feat: Add WFW-Aushang web app with PWA support, offline caching, and dark mode
- Created index.html for the main application interface with responsive design and dark mode support.
- Added manifest.webmanifest for PWA configuration, including app icons and display settings.
- Implemented service worker (sw.js) for offline caching of assets and network-first strategy for versioning.
- Introduced version.json to manage app versioning.
2025-12-24 16:59:51 +01:00

443 lines
15 KiB
PHP
Raw Permalink 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.
<?php /* Simple Tetris clone in a single index.php (HTML + JS). Now with WebAudio sound effects (line clear, drop/lock, game over) + mute toggle. */ ?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Vibe Coding Test Tetris (with SFX)</title>
<style>
:root { --bg:#0b0f14; --fg:#e6ebf0; --muted:#8a99a8; --panel:#121922; --accent:#4dd0e1; }
* { box-sizing:border-box; }
html,body { height:100%; }
body { margin:0; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji"; background:radial-gradient(1200px 700px at 20% -10%, #15202b 0, #0b0f14 60%); color:var(--fg); display:grid; place-items:center; }
.wrap { display:grid; grid-template-columns:auto 240px; gap:16px; padding:20px; }
.panel { background:var(--panel); border:1px solid #1d2631; border-radius:14px; padding:14px; box-shadow:0 10px 25px rgba(0,0,0,.25); }
canvas { display:block; background:#0c121a; border:1px solid #1d2631; border-radius:12px; box-shadow:inset 0 0 0 1px rgba(255,255,255,.03); }
h1 { margin:0 0 8px; font-size:18px; letter-spacing:.4px; color:#cfe6ff; display:flex; align-items:center; justify-content:space-between; gap:8px; }
.stats { display:grid; gap:10px; }
.stat { display:flex; justify-content:space-between; font-variant-numeric:tabular-nums; }
.controls { margin-top:10px; font-size:13px; color:var(--muted); line-height:1.4; }
.btns { display:flex; gap:8px; margin-top:10px; flex-wrap:wrap; }
button { background:#17212b; border:1px solid #243242; color:var(--fg); padding:8px 10px; border-radius:10px; cursor:pointer; }
button:hover { border-color:#2f455a; }
.next { margin-top:10px; display:grid; grid-template-columns:repeat(4, 22px); grid-template-rows:repeat(4, 22px); gap:2px; justify-content:flex-start; }
.cell { width:22px; height:22px; background:#0c121a; border:1px solid #1a2532; border-radius:4px; }
.badge { display:inline-block; background:#13202a; border:1px solid #2a404f; padding:2px 6px; border-radius:6px; font-size:12px; color:#bfe9ff; }
.footer { margin-top:8px; font-size:12px; color:#7f8fa1; }
.muted { opacity:.6; }
</style>
</head>
<body>
<div class="wrap">
<div class="panel">
<canvas id="board" width="300" height="600" aria-label="Tetris board" role="img"></canvas>
</div>
<div class="panel" style="width:240px">
<h1>
<span>TETRIS <span class="badge">vanilla JS</span></span>
<button id="btn-sound" title="Toggle sound">🔊</button>
</h1>
<div class="stats">
<div class="stat"><span>Score</span><strong id="score">0</strong></div>
<div class="stat"><span>Lines</span><strong id="lines">0</strong></div>
<div class="stat"><span>Level</span><strong id="level">1</strong></div>
</div>
<div style="margin-top:10px">
<div style="font-size:13px; color:#9fb3c6; margin-bottom:6px">Next</div>
<div id="next" class="next" aria-label="Next tetromino preview"></div>
</div>
<div class="btns">
<button id="btn-pause" title="P">Pause</button>
<button id="btn-restart" title="R">Restart</button>
</div>
<div class="controls">
<div>← → move, ↓ soft drop</div>
<div>↑ rotate, Space hard drop</div>
<div>P pause, R restart</div>
<div style="margin-top:6px; color:#9fb3c6">Tip: Some browsers need a key press to enable audio.</div>
</div>
<div class="footer">© <?php echo date('Y'); ?> VIBING test</div>
</div>
</div>
<script>
(() => {
// ====== Config ======
const COLS = 10, ROWS = 20, SIZE = 30; // canvas cell size
const GRAVITY_BASE_MS = 900; // level 1 drop interval
const LEVEL_ACCEL = 0.88; // per level speed multiplier
// Colors per piece type
const COLORS = {
I: '#66d9ff', J: '#85a5ff', L: '#ffc98b', O: '#f7f58a', S: '#9cff9c', T: '#e6a8ff', Z: '#ff9aae'
};
// Tetromino rotation states (4x4 matrices)
const SHAPES = {
I: [
[[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]],
[[0,0,1,0],[0,0,1,0],[0,0,1,0],[0,0,1,0]],
[[0,0,0,0],[0,0,0,0],[1,1,1,1],[0,0,0,0]],
[[0,1,0,0],[0,1,0,0],[0,1,0,0],[0,1,0,0]]
],
J: [
[[1,0,0],[1,1,1],[0,0,0]],
[[0,1,1],[0,1,0],[0,1,0]],
[[0,0,0],[1,1,1],[0,0,1]],
[[0,1,0],[0,1,0],[1,1,0]]
],
L: [
[[0,0,1],[1,1,1],[0,0,0]],
[[0,1,0],[0,1,0],[0,1,1]],
[[0,0,0],[1,1,1],[1,0,0]],
[[1,1,0],[0,1,0],[0,1,0]]
],
O: [
[[1,1],[1,1]],
[[1,1],[1,1]],
[[1,1],[1,1]],
[[1,1],[1,1]]
],
S: [
[[0,1,1],[1,1,0],[0,0,0]],
[[0,1,0],[0,1,1],[0,0,1]],
[[0,0,0],[0,1,1],[1,1,0]],
[[1,0,0],[1,1,0],[0,1,0]]
],
T: [
[[0,1,0],[1,1,1],[0,0,0]],
[[0,1,0],[0,1,1],[0,1,0]],
[[0,0,0],[1,1,1],[0,1,0]],
[[0,1,0],[1,1,0],[0,1,0]]
],
Z: [
[[1,1,0],[0,1,1],[0,0,0]],
[[0,0,1],[0,1,1],[0,1,0]],
[[0,0,0],[1,1,0],[0,1,1]],
[[0,1,0],[1,1,0],[1,0,0]]
]
};
// ====== Helpers ======
const $ = sel => document.querySelector(sel);
const board = $('#board');
const ctx = board.getContext('2d');
board.width = COLS * SIZE; board.height = ROWS * SIZE;
const ui = {
score: $('#score'), lines: $('#lines'), level: $('#level'),
next: $('#next'), pauseBtn: $('#btn-pause'), restartBtn: $('#btn-restart'), soundBtn: $('#btn-sound')
};
function emptyMatrix(w, h, v=0) { return Array.from({length:h}, () => Array(w).fill(v)); }
// 7-bag randomizer
class Bag {
constructor(){ this.bag = []; }
next(){
if (this.bag.length === 0) this.bag = shuffle(Object.keys(SHAPES));
return this.bag.pop();
}
}
function shuffle(a){ for(let i=a.length-1;i>0;i--){ const j=Math.floor(Math.random()*(i+1)); [a[i],a[j]]=[a[j],a[i]]; } return a; }
// Piece class
class Piece {
constructor(type){
this.type = type;
this.rot = 0;
this.shape = SHAPES[type][0];
this.x = Math.floor((COLS - this.shape[0].length)/2);
this.y = -this.shape.length; // spawn above
}
rotate(dir){ // dir: +1 cw, -1 ccw
const oldRot = this.rot;
this.rot = (this.rot + dir + 4) % 4;
this.shape = SHAPES[this.type][this.rot];
// simple wall-kick: try offsets
const kicks = [0, -1, 1, -2, 2];
for (const dx of kicks){ if (!collide(boardState, this, this.x+dx, this.y)) { this.x += dx; sfx.tick(); return true; } }
// revert
this.rot = oldRot; this.shape = SHAPES[this.type][this.rot];
return false;
}
}
// ====== Audio (WebAudio synth) ======
class SFX {
constructor(){
this.enabled = true;
this.ctx = null;
this.started = false;
const gesture = () => { this.start(); window.removeEventListener('keydown', gesture); window.removeEventListener('pointerdown', gesture); };
window.addEventListener('keydown', gesture);
window.addEventListener('pointerdown', gesture);
}
start(){
if (this.started) return;
const AC = window.AudioContext || window.webkitAudioContext;
if (!AC) return; // no audio support
this.ctx = new AC();
this.started = true;
}
toggle(){ this.enabled = !this.enabled; updateSoundButton(); }
// tiny helper to make beeps
beep(freq=440, dur=0.08, type='square', gain=0.03, when=0){
if (!this.enabled || !this.ctx) return;
const t = this.ctx.currentTime + when;
const o = this.ctx.createOscillator();
const g = this.ctx.createGain();
o.type = type; o.frequency.setValueAtTime(freq, t);
g.gain.setValueAtTime(0.0001, t);
g.gain.exponentialRampToValueAtTime(gain, t+0.005);
g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
o.connect(g).connect(this.ctx.destination);
o.start(t);
o.stop(t + dur + 0.02);
}
drop(){ this.beep(220, 0.06, 'sawtooth', 0.04); }
lock(){ this.beep(180, 0.07, 'triangle', 0.035); }
line(n){
const base = 520; // cheerful arpeggio
for (let i=0;i<n;i++) this.beep(base + i*120, 0.08, 'square', 0.04, i*0.05);
}
gameover(){
[440, 392, 349, 329].forEach((f,i)=> this.beep(f, 0.18, 'sawtooth', 0.035, i*0.12));
}
tick(){ this.beep(700, 0.03, 'square', 0.02); } // rotate/move
}
const sfx = new SFX();
function updateSoundButton(){
if (!ui.soundBtn) return;
ui.soundBtn.textContent = sfx.enabled ? '🔊' : '🔈';
ui.soundBtn.classList.toggle('muted', !sfx.enabled);
}
// ====== Game state ======
let boardState = emptyMatrix(COLS, ROWS, 0);
let bag = new Bag();
let current = new Piece(bag.next());
let nextPieceType = bag.next();
let score = 0, lines = 0, level = 1;
let dropTimer = 0, dropInterval = gravityFor(level), lastTime = 0;
let paused = false, gameOver = false;
drawNextPreview();
updateUI();
requestAnimationFrame(loop);
// ====== Core ======
function loop(time=0){
const dt = time - lastTime; lastTime = time;
if (!paused && !gameOver){
dropTimer += dt;
if (dropTimer >= dropInterval){
dropTimer = 0; softDrop(true);
}
}
render();
requestAnimationFrame(loop);
}
function gravityFor(level){ return GRAVITY_BASE_MS * Math.pow(LEVEL_ACCEL, Math.max(0, level-1)); }
function collide(mat, piece, ox = piece.x, oy = piece.y){
const s = piece.shape;
for (let y=0; y<s.length; y++){
for (let x=0; x<s[y].length; x++){
if (!s[y][x]) continue;
const px = ox + x, py = oy + y;
if (px < 0 || px >= COLS || py >= ROWS) return true;
if (py >= 0 && mat[py][px]) return true;
}
}
return false;
}
function lockPiece(){
const s = current.shape;
for (let y=0; y<s.length; y++){
for (let x=0; x<s[y].length; x++){
if (s[y][x]){
const px = current.x + x, py = current.y + y;
if (py < 0){ // topped out
gameOver = true; paused = false; sfx.gameover();
return;
}
boardState[py][px] = current.type;
}
}
}
sfx.lock();
const cleared = clearLines();
if (cleared > 0){
lines += cleared;
score += [0, 100, 300, 500, 800][cleared] * level;
level = 1 + Math.floor(lines / 10);
dropInterval = gravityFor(level);
sfx.line(cleared);
}
current = new Piece(nextPieceType);
nextPieceType = bag.next();
drawNextPreview();
}
function clearLines(){
let cleared = 0;
for (let y = ROWS-1; y >= 0; y--){
if (boardState[y].every(v => v)){
boardState.splice(y,1);
boardState.unshift(Array(COLS).fill(0));
cleared++; y++;
}
}
return cleared;
}
function softDrop(fromGravity=false){
if (!collide(boardState, current, current.x, current.y+1)){
current.y++;
if (!fromGravity) sfx.drop(); // manual soft drop feedback
} else {
lockPiece();
}
updateUI();
}
function hardDrop(){
let dist = 0;
while (!collide(boardState, current, current.x, current.y+1)){
current.y++; dist++;
}
score += 2 * dist; // small bonus
sfx.drop();
lockPiece();
updateUI();
}
function move(dx){
if (!collide(boardState, current, current.x+dx, current.y)) { current.x += dx; sfx.tick(); }
}
// ====== Rendering ======
function render(){
ctx.clearRect(0,0,board.width,board.height);
// grid (subtle)
ctx.globalAlpha = 0.05;
for(let x=0;x<COLS;x++){
for(let y=0;y<ROWS;y++){
ctx.strokeRect(x*SIZE, y*SIZE, SIZE, SIZE);
}
}
ctx.globalAlpha = 1;
// draw board
for (let y=0; y<ROWS; y++){
for (let x=0; x<COLS; x++){
const t = boardState[y][x];
if (!t) continue;
drawCell(x, y, COLORS[t]);
}
}
// draw current piece
const s = current.shape;
for (let y=0; y<s.length; y++){
for (let x=0; x<s[y].length; x++){
if (!s[y][x]) continue;
const px = current.x + x, py = current.y + y;
if (py >= 0) drawCell(px, py, COLORS[current.type]);
}
}
if (paused) overlayText('PAUSED');
if (gameOver) overlayText('GAME OVER');
}
function overlayText(text){
ctx.save();
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(0,0,board.width,board.height);
ctx.fillStyle = '#e6ebf0';
ctx.font = 'bold 28px system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial';
ctx.textAlign = 'center';
ctx.fillText(text, board.width/2, board.height/2);
ctx.restore();
}
function drawCell(x, y, color){
const px = x*SIZE, py = y*SIZE;
// base
ctx.fillStyle = color;
ctx.fillRect(px+1, py+1, SIZE-2, SIZE-2);
// glossy inset
ctx.fillStyle = 'rgba(255,255,255,.15)';
ctx.fillRect(px+3, py+3, SIZE-6, Math.floor((SIZE-6)/2));
// border
ctx.strokeStyle = 'rgba(0,0,0,.35)';
ctx.strokeRect(px+0.5, py+0.5, SIZE-1, SIZE-1);
}
function drawNextPreview(){
ui.next.innerHTML = '';
const grid = Array.from({length:16}, () => {
const d = document.createElement('div'); d.className = 'cell'; return d; });
grid.forEach(cell => ui.next.appendChild(cell));
const shape = SHAPES[nextPieceType][0];
// center in 4x4
const offX = Math.floor((4 - shape[0].length)/2);
const offY = Math.floor((4 - shape.length)/2);
for (let y=0;y<shape.length;y++){
for (let x=0;x<shape[y].length;x++){
if (shape[y][x]){
const idx = (offY+y)*4 + (offX+x);
grid[idx].style.background = COLORS[nextPieceType];
grid[idx].style.borderColor = '#2a3b4f';
}
}
}
}
function updateUI(){
ui.score.textContent = score;
ui.lines.textContent = lines;
ui.level.textContent = level;
}
// ====== Input ======
window.addEventListener('keydown', (e) => {
if (gameOver){ if (e.key.toLowerCase()==='r'){ restart(); } return; }
switch (e.key) {
case 'ArrowLeft': move(-1); break;
case 'ArrowRight': move(+1); break;
case 'ArrowDown': softDrop(); break;
case 'ArrowUp': current.rotate(+1); break;
case ' ': e.preventDefault(); hardDrop(); break;
case 'p': case 'P': togglePause(); break;
case 'r': case 'R': restart(); break;
}
});
ui.pauseBtn.addEventListener('click', togglePause);
ui.restartBtn.addEventListener('click', restart);
ui.soundBtn.addEventListener('click', () => { sfx.toggle(); });
function togglePause(){ if (gameOver) return; paused = !paused; }
function restart(){
boardState = emptyMatrix(COLS, ROWS, 0);
bag = new Bag();
current = new Piece(bag.next());
nextPieceType = bag.next();
score = 0; lines = 0; level = 1; dropInterval = gravityFor(level); dropTimer = 0; lastTime = 0;
gameOver = false; paused = false;
drawNextPreview(); updateUI();
}
// initialize sound button state
updateSoundButton();
})();
</script>
</body>
</html>