39321-vm/index.php
2026-03-25 17:19:10 +00:00

300 lines
15 KiB
PHP

<?php
declare(strict_types=1);
@date_default_timezone_set('UTC');
$projectName = trim((string)($_SERVER['PROJECT_NAME'] ?? ''));
$projectDescription = trim((string)($_SERVER['PROJECT_DESCRIPTION'] ?? ''));
$projectImageUrl = trim((string)($_SERVER['PROJECT_IMAGE_URL'] ?? ''));
$appName = $projectName !== '' ? $projectName : 'Classic Tetris';
$pageTitle = $appName . ' — Classic Tetris';
$metaDescription = $projectDescription !== ''
? $projectDescription
: 'Play a focused browser-based Tetris clone with keyboard controls, scoring, levels, next piece preview, pause, restart, and local best score tracking.';
$cssVersion = (string)@filemtime(__DIR__ . '/assets/css/custom.css');
$jsVersion = (string)@filemtime(__DIR__ . '/assets/js/main.js');
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= htmlspecialchars($pageTitle) ?></title>
<meta name="description" content="<?= htmlspecialchars($metaDescription) ?>">
<meta name="author" content="Flatlogic">
<meta property="og:title" content="<?= htmlspecialchars($pageTitle) ?>">
<meta property="og:description" content="<?= htmlspecialchars($metaDescription) ?>">
<meta property="twitter:title" content="<?= htmlspecialchars($pageTitle) ?>">
<meta property="twitter:description" content="<?= htmlspecialchars($metaDescription) ?>">
<?php if ($projectImageUrl): ?>
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link rel="stylesheet" href="assets/css/custom.css?v=<?= urlencode($cssVersion !== '' ? $cssVersion : (string)time()) ?>">
</head>
<body class="tetris-body">
<noscript>
<div class="alert alert-danger rounded-0 mb-0 text-center">JavaScript is required to play this Tetris app.</div>
</noscript>
<div class="page-shell">
<header class="app-header border-bottom border-secondary-subtle">
<div class="container-xxl d-flex flex-wrap align-items-center justify-content-between gap-3 py-3">
<div>
<p class="eyebrow mb-1">Arcade game web app</p>
<h1 class="app-title mb-0">Classic Tetris</h1>
</div>
<div class="d-flex flex-wrap align-items-center gap-2">
<span id="game-state-badge" class="badge status-badge state-ready">Ready</span>
<button id="pause-button" class="btn btn-outline-light btn-sm control-btn" type="button">Pause</button>
<button id="restart-button" class="btn btn-light btn-sm control-btn" type="button">Restart</button>
</div>
</div>
</header>
<main class="container-xxl py-3 py-lg-4">
<div class="row g-3 g-xl-4 align-items-start">
<div class="col-xl-8">
<section class="panel game-panel h-100" aria-labelledby="playfield-title">
<div class="panel-header d-flex flex-wrap align-items-center justify-content-between gap-3">
<div>
<p class="panel-label mb-1">Playfield</p>
<h2 id="playfield-title" class="panel-title mb-0">Pure gameplay, zero filler</h2>
</div>
<div class="panel-inline-stats d-flex flex-wrap gap-2">
<span class="inline-chip">Best <strong id="best-score-inline">0</strong></span>
<span class="inline-chip">Time <strong id="elapsed-time-inline">00:00</strong></span>
</div>
</div>
<div class="board-layout">
<div class="board-frame">
<canvas id="tetris-board" width="300" height="600" aria-label="Tetris playfield" role="img"></canvas>
<div class="board-overlay" id="board-overlay">
<div>
<p class="overlay-label mb-2">Press Enter to start</p>
<h3 class="overlay-title mb-3">Stack clean. Clear fast.</h3>
<p class="overlay-copy mb-0">Controls: move with arrow keys, rotate with ↑ or X, hard drop with Space.</p>
</div>
</div>
</div>
<div class="stats-grid" aria-label="Game statistics">
<article class="stat-card">
<span class="stat-label">Score</span>
<strong id="score-value" class="stat-value">0</strong>
</article>
<article class="stat-card">
<span class="stat-label">Lines</span>
<strong id="lines-value" class="stat-value">0</strong>
</article>
<article class="stat-card">
<span class="stat-label">Level</span>
<strong id="level-value" class="stat-value">1</strong>
</article>
<article class="stat-card">
<span class="stat-label">Drop speed</span>
<strong id="speed-value" class="stat-value">0.90s</strong>
</article>
</div>
</div>
</section>
</div>
<div class="col-xl-4">
<div class="sidebar-stack d-grid gap-3 gap-xl-4">
<section class="panel compact-panel" aria-labelledby="next-piece-title">
<div class="panel-header">
<p class="panel-label mb-1">Preview</p>
<h2 id="next-piece-title" class="panel-title mb-0">Next piece</h2>
</div>
<div class="preview-wrap">
<canvas id="next-piece" width="144" height="144" aria-label="Next tetromino preview" role="img"></canvas>
</div>
</section>
<section class="panel compact-panel" aria-labelledby="multiplayer-title">
<div class="panel-header">
<p class="panel-label mb-1">Multiplayer</p>
<h2 id="multiplayer-title" class="panel-title mb-0">Play with a friend</h2>
</div>
<div class="multiplayer-block">
<div class="form-floating mb-1">
<input id="player-name" class="form-control form-control-sm bg-dark text-light border-secondary-subtle" placeholder="Your name">
<label for="player-name">Your name</label>
</div>
<div class="field-help mb-2">Used for room play and the database scoreboard.</div>
<div class="d-flex gap-2 mb-2">
<button id="create-room-btn" class="btn btn-light btn-sm flex-grow-1" type="button">Create room</button>
<button id="leave-room-btn" class="btn btn-outline-light btn-sm flex-grow-1 d-none" type="button">Leave</button>
</div>
<div id="room-code-card" class="room-code-card mb-2 d-none">
<div>
<span class="room-code-label">Room code</span>
<div class="room-code-value" id="room-code-value">—</div>
</div>
<button id="copy-room-btn" class="btn btn-outline-light btn-sm" type="button">Copy</button>
</div>
<div class="join-row d-flex gap-2">
<input id="room-code-input" class="form-control form-control-sm bg-dark text-light border-secondary-subtle" placeholder="Enter code">
<button id="join-room-btn" class="btn btn-outline-light btn-sm" type="button">Join</button>
</div>
<div class="room-status mt-2 d-flex align-items-center gap-2">
<span id="room-status-badge" class="badge status-badge state-ready">Offline</span>
<span id="room-status-text" class="small text-secondary">Create a room to start.</span>
</div>
</div>
</section>
<section class="panel compact-panel" aria-labelledby="opponent-title">
<div class="panel-header">
<p class="panel-label mb-1">Opponent</p>
<h2 id="opponent-title" class="panel-title mb-0">Live board</h2>
</div>
<div class="opponent-wrap">
<canvas id="opponent-board" width="200" height="400" aria-label="Opponent playfield" role="img"></canvas>
<div class="opponent-meta">
<div id="opponent-name" class="opponent-name">Waiting for opponent…</div>
<div id="opponent-stats" class="opponent-stats small text-secondary">Score — · Level — · Lines —</div>
<div id="opponent-status" class="opponent-status small text-secondary">Room idle</div>
</div>
</div>
</section>
<section class="panel compact-panel" aria-labelledby="session-title">
<div class="panel-header">
<p class="panel-label mb-1">Current run</p>
<h2 id="session-title" class="panel-title mb-0">Session detail</h2>
</div>
<dl class="detail-grid mb-0">
<div>
<dt>Status</dt>
<dd id="detail-status">Ready to start</dd>
</div>
<div>
<dt>Pieces placed</dt>
<dd id="detail-pieces">0</dd>
</div>
<div>
<dt>Lines cleared</dt>
<dd id="detail-lines">0</dd>
</div>
<div>
<dt>Duration</dt>
<dd id="detail-duration">00:00</dd>
</div>
</dl>
</section>
<section class="panel compact-panel" aria-labelledby="controls-title">
<div class="panel-header">
<p class="panel-label mb-1">Keyboard</p>
<h2 id="controls-title" class="panel-title mb-0">Controls</h2>
</div>
<ul class="controls-list list-unstyled mb-0">
<li><span>Move</span><kbd>←</kbd><kbd>→</kbd></li>
<li><span>Soft drop</span><kbd>↓</kbd></li>
<li><span>Rotate</span><kbd>↑</kbd><kbd>X</kbd><kbd>Z</kbd></li>
<li><span>Hard drop</span><kbd>Space</kbd></li>
<li><span>Pause</span><kbd>P</kbd></li>
<li><span>Restart</span><kbd>R</kbd></li>
</ul>
</section>
<section class="panel compact-panel" aria-labelledby="scoreboard-title">
<div class="panel-header d-flex justify-content-between align-items-end gap-2">
<div>
<p class="panel-label mb-1">Database records</p>
<h2 id="scoreboard-title" class="panel-title mb-0">Scoreboard</h2>
</div>
<span id="scoreboard-status" class="small text-secondary">Loading…</span>
</div>
<div id="scoreboard-empty" class="empty-state">No database scores yet. Finish a game to post the first result.</div>
<div id="scoreboard-list" class="scoreboard-list" aria-live="polite"></div>
</section>
<section class="panel compact-panel" aria-labelledby="history-title">
<div class="panel-header d-flex justify-content-between align-items-end gap-2">
<div>
<p class="panel-label mb-1">Local records</p>
<h2 id="history-title" class="panel-title mb-0">Recent runs</h2>
</div>
<span class="small text-secondary">Saved in this browser</span>
</div>
<div id="history-empty" class="empty-state">No completed runs yet. Finish a game and your latest result will appear here.</div>
<div id="history-list" class="history-list"></div>
<div id="history-detail" class="history-detail d-none">
<div class="history-detail-header">Selected run</div>
<dl class="detail-grid compact mb-0">
<div><dt>Score</dt><dd id="selected-score">0</dd></div>
<div><dt>Level</dt><dd id="selected-level">1</dd></div>
<div><dt>Lines</dt><dd id="selected-lines">0</dd></div>
<div><dt>Duration</dt><dd id="selected-duration">00:00</dd></div>
<div class="full"><dt>Finished</dt><dd id="selected-finished">—</dd></div>
</dl>
</div>
</section>
</div>
</div>
</div>
</main>
</div>
<div class="toast-container position-fixed top-0 end-0 p-3">
<div id="game-toast" class="toast text-bg-dark border border-secondary-subtle" role="status" aria-live="polite" aria-atomic="true">
<div class="toast-header bg-dark text-light border-bottom border-secondary-subtle">
<strong class="me-auto">Tetris</strong>
<small id="toast-timestamp">now</small>
<button type="button" class="btn-close btn-close-white ms-2 mb-1" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body" id="toast-body">Ready.</div>
</div>
</div>
<div class="modal fade" id="gameOverModal" tabindex="-1" aria-labelledby="gameOverTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content bg-dark text-light border border-secondary-subtle">
<div class="modal-header border-secondary-subtle">
<div>
<p class="panel-label mb-1">Run complete</p>
<h2 class="modal-title fs-5" id="gameOverTitle">Game over</h2>
</div>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="summary-grid">
<div>
<span class="summary-label">Score</span>
<strong id="final-score">0</strong>
</div>
<div>
<span class="summary-label">Lines</span>
<strong id="final-lines">0</strong>
</div>
<div>
<span class="summary-label">Level</span>
<strong id="final-level">1</strong>
</div>
<div>
<span class="summary-label">Duration</span>
<strong id="final-duration">00:00</strong>
</div>
</div>
<p class="modal-copy mb-0">Press restart or hit Enter to jump straight into a new run.</p>
</div>
<div class="modal-footer border-secondary-subtle">
<button type="button" class="btn btn-outline-light btn-sm" data-bs-dismiss="modal">Review board</button>
<button type="button" class="btn btn-light btn-sm" id="modal-restart-button">Play again</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<script src="assets/js/main.js?v=<?= urlencode($jsVersion !== '' ? $jsVersion : (string)time()) ?>" defer></script>
</body>
</html>