300 lines
15 KiB
PHP
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>
|