Initial commit: AdGuard Tetris blocking page
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
.env
|
||||||
Binary file not shown.
@@ -0,0 +1,65 @@
|
|||||||
|
import sqlite3
|
||||||
|
import os
|
||||||
|
from flask import Flask, render_template, request, jsonify
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
DB_PATH = '/opt/flaskapp/scores.db'
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
return conn
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
with get_db() as conn:
|
||||||
|
conn.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS scores (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
score INTEGER NOT NULL,
|
||||||
|
level INTEGER NOT NULL,
|
||||||
|
lines INTEGER NOT NULL,
|
||||||
|
domain TEXT,
|
||||||
|
ts DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
init_db()
|
||||||
|
|
||||||
|
@app.route("/", defaults={"path": ""})
|
||||||
|
@app.route("/<path:path>")
|
||||||
|
def blocked(path):
|
||||||
|
if path == "leaderboard":
|
||||||
|
return leaderboard()
|
||||||
|
domain = request.host or request.args.get("domain", "that site")
|
||||||
|
domain = domain.split(":")[0]
|
||||||
|
return render_template("blocked.html", domain=domain, reason="blocked by network policy")
|
||||||
|
|
||||||
|
@app.route("/score", methods=["POST"])
|
||||||
|
def submit_score():
|
||||||
|
data = request.get_json()
|
||||||
|
name = (data.get("name") or "ANONYMOUS")[:16].upper().strip()
|
||||||
|
score = int(data.get("score", 0))
|
||||||
|
level = int(data.get("level", 1))
|
||||||
|
lines = int(data.get("lines", 0))
|
||||||
|
domain = (data.get("domain") or "unknown")[:64]
|
||||||
|
with get_db() as conn:
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO scores (name, score, level, lines, domain) VALUES (?,?,?,?,?)",
|
||||||
|
(name, score, level, lines, domain)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return jsonify({"ok": True})
|
||||||
|
|
||||||
|
@app.route("/leaderboard")
|
||||||
|
def leaderboard():
|
||||||
|
with get_db() as conn:
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT name, score, level, lines, domain, ts FROM scores ORDER BY score DESC LIMIT 20"
|
||||||
|
).fetchall()
|
||||||
|
return render_template("leaderboard.html", scores=rows)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(host="0.0.0.0", port=80, debug=True)
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import sys
|
||||||
|
sys.path.insert(0,'/opt/flaskapp')
|
||||||
|
|
||||||
|
from app import app as application
|
||||||
@@ -0,0 +1,729 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<title>ACCESS DENIED :: {{ domain }}</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=VT323&display=swap" rel="stylesheet"/>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--green: #00ff41;
|
||||||
|
--green-dim: #00aa2a;
|
||||||
|
--green-glow: #00ff4155;
|
||||||
|
--green-dark: #003a0e;
|
||||||
|
--bg: #020c04;
|
||||||
|
--amber: #ffb000;
|
||||||
|
--red: #ff2222;
|
||||||
|
--grid: #0a1f0d;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--green);
|
||||||
|
font-family: 'Share Tech Mono', monospace;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scanline overlay */
|
||||||
|
body::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
0deg,
|
||||||
|
transparent,
|
||||||
|
transparent 2px,
|
||||||
|
rgba(0,0,0,0.15) 2px,
|
||||||
|
rgba(0,0,0,0.15) 4px
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CRT flicker */
|
||||||
|
body::after {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: radial-gradient(ellipse at center, transparent 60%, rgba(0,0,0,0.5) 100%);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 99;
|
||||||
|
animation: flicker 8s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes flicker {
|
||||||
|
0%, 95%, 100% { opacity: 1; }
|
||||||
|
96% { opacity: 0.85; }
|
||||||
|
97% { opacity: 1; }
|
||||||
|
98% { opacity: 0.9; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header terminal block */
|
||||||
|
.terminal-header {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 900px;
|
||||||
|
padding: 20px 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.term-bar {
|
||||||
|
border: 1px solid var(--green-dim);
|
||||||
|
border-bottom: none;
|
||||||
|
padding: 4px 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--green-dim);
|
||||||
|
background: var(--green-dark);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.term-body {
|
||||||
|
border: 1px solid var(--green-dim);
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: rgba(0,10,2,0.8);
|
||||||
|
box-shadow: 0 0 20px var(--green-glow), inset 0 0 40px rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blink { animation: blink 1s step-end infinite; }
|
||||||
|
@keyframes blink { 50% { opacity: 0; } }
|
||||||
|
|
||||||
|
.prompt { color: var(--green-dim); }
|
||||||
|
.cmd { color: var(--green); }
|
||||||
|
|
||||||
|
.blocked-msg {
|
||||||
|
font-family: 'VT323', monospace;
|
||||||
|
font-size: 42px;
|
||||||
|
color: var(--red);
|
||||||
|
text-shadow: 0 0 10px var(--red), 0 0 20px var(--red);
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
margin: 8px 0 4px;
|
||||||
|
animation: glitch 4s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glitch {
|
||||||
|
0%, 90%, 100% { transform: none; text-shadow: 0 0 10px var(--red), 0 0 20px var(--red); }
|
||||||
|
91% { transform: skewX(4deg) translateX(3px); text-shadow: -3px 0 cyan, 3px 0 magenta; }
|
||||||
|
92% { transform: skewX(-2deg) translateX(-2px); }
|
||||||
|
93% { transform: none; text-shadow: 0 0 10px var(--red); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.domain-line {
|
||||||
|
color: var(--amber);
|
||||||
|
font-size: 13px;
|
||||||
|
margin: 4px 0;
|
||||||
|
text-shadow: 0 0 6px var(--amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-line {
|
||||||
|
color: var(--green-dim);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 8px;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag { color: var(--green); }
|
||||||
|
|
||||||
|
/* Main layout */
|
||||||
|
.main {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 16px 24px 24px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 900px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tetris game */
|
||||||
|
.game-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--green-dim);
|
||||||
|
letter-spacing: 0.15em;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tetris-canvas {
|
||||||
|
border: 1px solid var(--green-dim);
|
||||||
|
box-shadow: 0 0 16px var(--green-glow), inset 0 0 20px rgba(0,255,65,0.03);
|
||||||
|
background: var(--grid);
|
||||||
|
display: block;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
.sidebar {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
border: 1px solid var(--green-dim);
|
||||||
|
padding: 12px 14px;
|
||||||
|
background: rgba(0,10,2,0.6);
|
||||||
|
box-shadow: 0 0 8px rgba(0,255,65,0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-title {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--green-dim);
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-bottom: 1px solid var(--green-dark);
|
||||||
|
padding-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 13px;
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label { color: var(--green-dim); }
|
||||||
|
.stat-val {
|
||||||
|
color: var(--green);
|
||||||
|
text-shadow: 0 0 6px var(--green-glow);
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#next-canvas {
|
||||||
|
border: 1px solid var(--green-dark);
|
||||||
|
background: var(--grid);
|
||||||
|
display: block;
|
||||||
|
margin: 4px auto 0;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls-list {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 2;
|
||||||
|
color: var(--green-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.key {
|
||||||
|
color: var(--green);
|
||||||
|
background: var(--green-dark);
|
||||||
|
border: 1px solid var(--green-dim);
|
||||||
|
padding: 0 5px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(2,12,4,0.88);
|
||||||
|
font-family: 'VT323', monospace;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-title {
|
||||||
|
font-size: 36px;
|
||||||
|
color: var(--green);
|
||||||
|
text-shadow: 0 0 12px var(--green);
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-sub {
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--green-dim);
|
||||||
|
margin-top: 8px;
|
||||||
|
font-family: 'Share Tech Mono', monospace;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-wrap {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-log {
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.9;
|
||||||
|
color: var(--green-dim);
|
||||||
|
max-height: 120px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-ok { color: var(--green-dim); }
|
||||||
|
.log-warn { color: var(--amber); }
|
||||||
|
.log-err { color: var(--red); }
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.main { flex-direction: column; align-items: center; }
|
||||||
|
.sidebar { width: 100%; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="terminal-header">
|
||||||
|
<div class="term-bar">
|
||||||
|
<span>NETGUARD-OS v4.1.2 :: SECURITY TERMINAL</span>
|
||||||
|
<span id="clock"></span>
|
||||||
|
</div>
|
||||||
|
<div class="term-body">
|
||||||
|
<div class="info-line">
|
||||||
|
<span class="prompt">root@netguard:~$ </span><span class="cmd">resolve --host "{{ domain }}"</span>
|
||||||
|
</div>
|
||||||
|
<div class="blocked-msg">[ ACCESS DENIED ]</div>
|
||||||
|
<div class="domain-line">
|
||||||
|
TARGET HOST: <strong>{{ domain }}</strong> | STATUS: BLOCKED | POLICY: {{ reason }}
|
||||||
|
</div>
|
||||||
|
<div class="info-line">
|
||||||
|
<span class="tag">[SYSTEM]</span> Request intercepted by network policy enforcement layer.<br/>
|
||||||
|
<span class="tag">[SYSTEM]</span> Your browsing attempt has been logged. Timestamp recorded.<br/>
|
||||||
|
<span class="tag">[INFO] </span> While you wait, the system has deployed a cognitive recalibration protocol.<br/>
|
||||||
|
<span class="tag">[INFO] </span> Estimated recalibration duration: however long it takes. <span class="blink">_</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main">
|
||||||
|
<!-- Tetris -->
|
||||||
|
<div class="game-panel">
|
||||||
|
<div class="game-label">// RECALIBRATION MODULE :: TETRIS ENGINE //</div>
|
||||||
|
<div class="game-wrap">
|
||||||
|
<canvas id="tetris-canvas" width="200" height="400"></canvas>
|
||||||
|
<div class="overlay" id="overlay">
|
||||||
|
<div class="overlay-title">TETRIS.EXE</div>
|
||||||
|
<div class="overlay-sub">press <strong>[ENTER]</strong> to initialize</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-title">// SYSTEM METRICS</div>
|
||||||
|
<div class="stat-row"><span class="stat-label">SCORE</span><span class="stat-val" id="score">000000</span></div>
|
||||||
|
<div class="stat-row"><span class="stat-label">LEVEL</span><span class="stat-val" id="level">01</span></div>
|
||||||
|
<div class="stat-row"><span class="stat-label">LINES</span><span class="stat-val" id="lines">000</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-title">// NEXT PIECE</div>
|
||||||
|
<canvas id="next-canvas" width="80" height="80"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-title">// INPUT BINDINGS</div>
|
||||||
|
<div class="controls-list">
|
||||||
|
<span class="key">ENTER</span> START / RESTART<br/>
|
||||||
|
<span class="key">←</span><span class="key">→</span> MOVE<br/>
|
||||||
|
<span class="key">↑</span> ROTATE<br/>
|
||||||
|
<span class="key">↓</span> SOFT DROP<br/>
|
||||||
|
<span class="key">SPACE</span> HARD DROP<br/>
|
||||||
|
<span class="key">P</span> PAUSE
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-title">// NETWORK LOG</div>
|
||||||
|
<div class="network-log" id="net-log"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Clock
|
||||||
|
function updateClock() {
|
||||||
|
const now = new Date();
|
||||||
|
document.getElementById('clock').textContent =
|
||||||
|
now.toISOString().replace('T', ' ').substring(0, 19) + ' UTC';
|
||||||
|
}
|
||||||
|
setInterval(updateClock, 1000);
|
||||||
|
updateClock();
|
||||||
|
|
||||||
|
// Network log flavor text
|
||||||
|
const logMessages = [
|
||||||
|
['warn', 'DNS query for {{ domain }} intercepted'],
|
||||||
|
['err', 'Connection to {{ domain }} refused by policy'],
|
||||||
|
['ok', 'Firewall rule #4471 matched: BLOCK'],
|
||||||
|
['ok', 'Logging event to audit trail... done'],
|
||||||
|
['warn', 'Retry attempt 1/3 detected... blocked'],
|
||||||
|
['warn', 'Retry attempt 2/3 detected... blocked'],
|
||||||
|
['err', 'Retry attempt 3/3 detected... blocked'],
|
||||||
|
['ok', 'Packet disposal complete. Have a nice day.'],
|
||||||
|
];
|
||||||
|
let logIdx = 0;
|
||||||
|
const logEl = document.getElementById('net-log');
|
||||||
|
function appendLog() {
|
||||||
|
if (logIdx >= logMessages.length) return;
|
||||||
|
const [type, msg] = logMessages[logIdx++];
|
||||||
|
const span = document.createElement('div');
|
||||||
|
span.className = `log-${type}`;
|
||||||
|
const prefix = type === 'err' ? '[ERR] ' : type === 'warn' ? '[WARN]' : '[OK] ';
|
||||||
|
span.textContent = prefix + ' ' + msg;
|
||||||
|
logEl.appendChild(span);
|
||||||
|
if (logIdx < logMessages.length) setTimeout(appendLog, 900 + Math.random() * 600);
|
||||||
|
}
|
||||||
|
setTimeout(appendLog, 800);
|
||||||
|
|
||||||
|
// ── TETRIS ─────────────────────────────────────────────
|
||||||
|
const COLS = 10, ROWS = 20, BLOCK = 20;
|
||||||
|
const canvas = document.getElementById('tetris-canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const nextCanvas = document.getElementById('next-canvas');
|
||||||
|
const nCtx = nextCanvas.getContext('2d');
|
||||||
|
|
||||||
|
const GREEN = '#00ff41';
|
||||||
|
const DIMGREEN = '#00aa2a';
|
||||||
|
const DARKGREEN = '#003a0e';
|
||||||
|
const GRID = '#0a1f0d';
|
||||||
|
|
||||||
|
const PIECES = [
|
||||||
|
{ shape: [[1,1,1,1]], color: '#00ff41' }, // I
|
||||||
|
{ shape: [[1,1],[1,1]], color: '#00cc33' }, // O
|
||||||
|
{ shape: [[0,1,0],[1,1,1]], color: '#00ff66' }, // T
|
||||||
|
{ shape: [[1,0,0],[1,1,1]], color: '#00dd44' }, // L
|
||||||
|
{ shape: [[0,0,1],[1,1,1]], color: '#00bb22' }, // J
|
||||||
|
{ shape: [[0,1,1],[1,1,0]], color: '#00ee55' }, // S
|
||||||
|
{ shape: [[1,1,0],[0,1,1]], color: '#009911' }, // Z
|
||||||
|
];
|
||||||
|
|
||||||
|
let board, piece, nextPiece, score, level, lines, gameOver, paused, running, dropTimer, dropInterval, animFrame;
|
||||||
|
|
||||||
|
function newBoard() {
|
||||||
|
return Array.from({length: ROWS}, () => Array(COLS).fill(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomPiece() {
|
||||||
|
const p = PIECES[Math.floor(Math.random() * PIECES.length)];
|
||||||
|
return {
|
||||||
|
shape: p.shape.map(r => [...r]),
|
||||||
|
color: p.color,
|
||||||
|
x: Math.floor(COLS / 2) - Math.floor(p.shape[0].length / 2),
|
||||||
|
y: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function rotate(shape) {
|
||||||
|
const R = shape.length, C = shape[0].length;
|
||||||
|
const out = Array.from({length: C}, () => Array(R).fill(0));
|
||||||
|
for (let r = 0; r < R; r++)
|
||||||
|
for (let c = 0; c < C; c++)
|
||||||
|
out[c][R - 1 - r] = shape[r][c];
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function valid(s, dx, dy) {
|
||||||
|
for (let r = 0; r < s.shape.length; r++)
|
||||||
|
for (let c = 0; c < s.shape[r].length; c++)
|
||||||
|
if (s.shape[r][c]) {
|
||||||
|
const nx = s.x + c + dx, ny = s.y + r + dy;
|
||||||
|
if (nx < 0 || nx >= COLS || ny >= ROWS) return false;
|
||||||
|
if (ny >= 0 && board[ny][nx]) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function place() {
|
||||||
|
for (let r = 0; r < piece.shape.length; r++)
|
||||||
|
for (let c = 0; c < piece.shape[r].length; c++)
|
||||||
|
if (piece.shape[r][c] && piece.y + r >= 0)
|
||||||
|
board[piece.y + r][piece.x + c] = piece.color;
|
||||||
|
|
||||||
|
// Clear lines
|
||||||
|
let cleared = 0;
|
||||||
|
for (let r = ROWS - 1; r >= 0; r--) {
|
||||||
|
if (board[r].every(c => c)) {
|
||||||
|
board.splice(r, 1);
|
||||||
|
board.unshift(Array(COLS).fill(0));
|
||||||
|
cleared++;
|
||||||
|
r++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cleared) {
|
||||||
|
const pts = [0, 100, 300, 500, 800];
|
||||||
|
score += (pts[cleared] || 800) * level;
|
||||||
|
lines += cleared;
|
||||||
|
level = Math.floor(lines / 10) + 1;
|
||||||
|
dropInterval = Math.max(100, 1000 - (level - 1) * 90);
|
||||||
|
updateHUD();
|
||||||
|
}
|
||||||
|
|
||||||
|
piece = nextPiece;
|
||||||
|
nextPiece = randomPiece();
|
||||||
|
if (!valid(piece, 0, 0)) {
|
||||||
|
gameOver = true;
|
||||||
|
showOverlay('GAME OVER', 'press [ENTER] to retry');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateHUD() {
|
||||||
|
document.getElementById('score').textContent = String(score).padStart(6, '0');
|
||||||
|
document.getElementById('level').textContent = String(level).padStart(2, '0');
|
||||||
|
document.getElementById('lines').textContent = String(lines).padStart(3, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawBlock(context, x, y, color, size) {
|
||||||
|
context.fillStyle = color;
|
||||||
|
context.fillRect(x * size + 1, y * size + 1, size - 2, size - 2);
|
||||||
|
// highlight
|
||||||
|
context.fillStyle = 'rgba(255,255,255,0.12)';
|
||||||
|
context.fillRect(x * size + 1, y * size + 1, size - 2, 3);
|
||||||
|
context.fillRect(x * size + 1, y * size + 1, 3, size - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawBoard() {
|
||||||
|
ctx.fillStyle = GRID;
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
// grid lines
|
||||||
|
ctx.strokeStyle = '#0d1f10';
|
||||||
|
ctx.lineWidth = 0.5;
|
||||||
|
for (let r = 0; r < ROWS; r++) {
|
||||||
|
ctx.beginPath(); ctx.moveTo(0, r * BLOCK); ctx.lineTo(canvas.width, r * BLOCK); ctx.stroke();
|
||||||
|
}
|
||||||
|
for (let c = 0; c < COLS; c++) {
|
||||||
|
ctx.beginPath(); ctx.moveTo(c * BLOCK, 0); ctx.lineTo(c * BLOCK, canvas.height); ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// placed blocks
|
||||||
|
for (let r = 0; r < ROWS; r++)
|
||||||
|
for (let c = 0; c < COLS; c++)
|
||||||
|
if (board[r][c]) drawBlock(ctx, c, r, board[r][c], BLOCK);
|
||||||
|
|
||||||
|
// ghost piece
|
||||||
|
if (piece && !gameOver) {
|
||||||
|
let ghost = { ...piece, shape: piece.shape.map(r => [...r]) };
|
||||||
|
while (valid(ghost, 0, 1)) ghost.y++;
|
||||||
|
for (let r = 0; r < ghost.shape.length; r++)
|
||||||
|
for (let c = 0; c < ghost.shape[r].length; c++)
|
||||||
|
if (ghost.shape[r][c] && ghost.y + r >= 0) {
|
||||||
|
ctx.fillStyle = 'rgba(0,255,65,0.12)';
|
||||||
|
ctx.fillRect((ghost.x + c) * BLOCK + 1, (ghost.y + r) * BLOCK + 1, BLOCK - 2, BLOCK - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// active piece
|
||||||
|
for (let r = 0; r < piece.shape.length; r++)
|
||||||
|
for (let c = 0; c < piece.shape[r].length; c++)
|
||||||
|
if (piece.shape[r][c] && piece.y + r >= 0)
|
||||||
|
drawBlock(ctx, piece.x + c, piece.y + r, piece.color, BLOCK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawNext() {
|
||||||
|
nCtx.fillStyle = GRID;
|
||||||
|
nCtx.fillRect(0, 0, 80, 80);
|
||||||
|
if (!nextPiece) return;
|
||||||
|
const s = nextPiece.shape;
|
||||||
|
const offX = Math.floor((4 - s[0].length) / 2);
|
||||||
|
const offY = Math.floor((4 - s.length) / 2);
|
||||||
|
for (let r = 0; r < s.length; r++)
|
||||||
|
for (let c = 0; c < s[r].length; c++)
|
||||||
|
if (s[r][c]) drawBlock(nCtx, offX + c, offY + r, nextPiece.color, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
const CURRENT_DOMAIN = "{{ domain }}";
|
||||||
|
let awaitingName = false;
|
||||||
|
let nameInput = '';
|
||||||
|
|
||||||
|
function showOverlay(title, sub, extra) {
|
||||||
|
const el = document.getElementById('overlay');
|
||||||
|
el.innerHTML = `<div class="overlay-title">${title}</div><div class="overlay-sub">${sub}</div>${extra || ''}`;
|
||||||
|
el.style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideOverlay() {
|
||||||
|
document.getElementById('overlay').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNamePrompt(finalScore, finalLevel, finalLines) {
|
||||||
|
awaitingName = true;
|
||||||
|
nameInput = '';
|
||||||
|
const el = document.getElementById('overlay');
|
||||||
|
el.innerHTML = `
|
||||||
|
<div class="overlay-title" style="color:var(--amber);text-shadow:0 0 12px var(--amber)">GAME OVER</div>
|
||||||
|
<div class="overlay-sub" style="margin-top:6px">SCORE: <span style="color:var(--green)">${String(finalScore).padStart(6,'0')}</span></div>
|
||||||
|
<div class="overlay-sub" style="margin-top:14px;font-size:13px">ENTER YOUR NAME:</div>
|
||||||
|
<div id="name-display" style="font-family:'VT323',monospace;font-size:32px;color:var(--green);
|
||||||
|
text-shadow:0 0 8px var(--green);letter-spacing:0.2em;margin-top:6px;min-width:160px;
|
||||||
|
text-align:center;border-bottom:1px solid var(--green-dim);padding-bottom:4px"> </div>
|
||||||
|
<div class="overlay-sub" style="margin-top:10px;font-size:11px">[ENTER] submit [ESC] skip</div>
|
||||||
|
<a href="/leaderboard" style="margin-top:14px;font-size:11px;color:var(--green-dim);
|
||||||
|
font-family:'Share Tech Mono',monospace;text-decoration:none;">view leaderboard →</a>
|
||||||
|
`;
|
||||||
|
el.style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNameDisplay() {
|
||||||
|
const el = document.getElementById('name-display');
|
||||||
|
if (el) el.textContent = (nameInput || '') + '|';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitScore(name, finalScore, finalLevel, finalLines) {
|
||||||
|
try {
|
||||||
|
await fetch('/score', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: name || 'ANONYMOUS',
|
||||||
|
score: finalScore,
|
||||||
|
level: finalLevel,
|
||||||
|
lines: finalLines,
|
||||||
|
domain: CURRENT_DOMAIN
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} catch(e) { /* silent fail */ }
|
||||||
|
showOverlay(
|
||||||
|
'SCORE SAVED',
|
||||||
|
`rank recorded for ${name || 'ANONYMOUS'}`,
|
||||||
|
`<a href="/leaderboard" style="margin-top:14px;font-size:13px;color:var(--green-dim);
|
||||||
|
font-family:'Share Tech Mono',monospace;text-decoration:none;">view leaderboard →</a>
|
||||||
|
<div class="overlay-sub" style="margin-top:10px">[ENTER] play again</div>`
|
||||||
|
);
|
||||||
|
awaitingName = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startGame() {
|
||||||
|
board = newBoard();
|
||||||
|
score = 0; level = 1; lines = 0; gameOver = false; paused = false;
|
||||||
|
awaitingName = false; nameInput = '';
|
||||||
|
dropInterval = 1000;
|
||||||
|
piece = randomPiece();
|
||||||
|
nextPiece = randomPiece();
|
||||||
|
updateHUD();
|
||||||
|
hideOverlay();
|
||||||
|
running = true;
|
||||||
|
let last = 0;
|
||||||
|
dropTimer = 0;
|
||||||
|
|
||||||
|
function loop(ts) {
|
||||||
|
if (!running) return;
|
||||||
|
const dt = ts - last; last = ts;
|
||||||
|
if (!paused && !gameOver) {
|
||||||
|
dropTimer += dt;
|
||||||
|
if (dropTimer >= dropInterval) {
|
||||||
|
dropTimer = 0;
|
||||||
|
if (valid(piece, 0, 1)) piece.y++;
|
||||||
|
else place();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drawBoard();
|
||||||
|
drawNext();
|
||||||
|
animFrame = requestAnimationFrame(loop);
|
||||||
|
}
|
||||||
|
if (animFrame) cancelAnimationFrame(animFrame);
|
||||||
|
animFrame = requestAnimationFrame(loop);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap place() to trigger name prompt on game over
|
||||||
|
const _origPlace = place;
|
||||||
|
place = function() {
|
||||||
|
for (let r = 0; r < piece.shape.length; r++)
|
||||||
|
for (let c = 0; c < piece.shape[r].length; c++)
|
||||||
|
if (piece.shape[r][c] && piece.y + r >= 0)
|
||||||
|
board[piece.y + r][piece.x + c] = piece.color;
|
||||||
|
|
||||||
|
let cleared = 0;
|
||||||
|
for (let r = ROWS - 1; r >= 0; r--) {
|
||||||
|
if (board[r].every(c => c)) {
|
||||||
|
board.splice(r, 1);
|
||||||
|
board.unshift(Array(COLS).fill(0));
|
||||||
|
cleared++;
|
||||||
|
r++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cleared) {
|
||||||
|
const pts = [0, 100, 300, 500, 800];
|
||||||
|
score += (pts[cleared] || 800) * level;
|
||||||
|
lines += cleared;
|
||||||
|
level = Math.floor(lines / 10) + 1;
|
||||||
|
dropInterval = Math.max(100, 1000 - (level - 1) * 90);
|
||||||
|
updateHUD();
|
||||||
|
}
|
||||||
|
|
||||||
|
piece = nextPiece;
|
||||||
|
nextPiece = randomPiece();
|
||||||
|
if (!valid(piece, 0, 0)) {
|
||||||
|
gameOver = true;
|
||||||
|
const fs = score, fl = level, fn = lines;
|
||||||
|
setTimeout(() => showNamePrompt(fs, fl, fn), 300);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', e => {
|
||||||
|
if (awaitingName) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
const saved = score, slevel = level, slines = lines;
|
||||||
|
submitScore(nameInput, saved, slevel, slines);
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
awaitingName = false;
|
||||||
|
showOverlay('GAME OVER', 'press [ENTER] to retry',
|
||||||
|
`<a href="/leaderboard" style="margin-top:14px;font-size:11px;color:var(--green-dim);
|
||||||
|
font-family:'Share Tech Mono',monospace;text-decoration:none;">view leaderboard →</a>`
|
||||||
|
);
|
||||||
|
} else if (e.key === 'Backspace') {
|
||||||
|
nameInput = nameInput.slice(0, -1);
|
||||||
|
updateNameDisplay();
|
||||||
|
} else if (e.key.length === 1 && nameInput.length < 16) {
|
||||||
|
nameInput += e.key.toUpperCase();
|
||||||
|
updateNameDisplay();
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
if (!running || gameOver) { startGame(); return; }
|
||||||
|
}
|
||||||
|
if (!running || gameOver || paused) {
|
||||||
|
if (e.key === 'p' || e.key === 'P') {
|
||||||
|
if (paused) { paused = false; hideOverlay(); }
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (e.key) {
|
||||||
|
case 'ArrowLeft': if (valid(piece, -1, 0)) piece.x--; break;
|
||||||
|
case 'ArrowRight': if (valid(piece, 1, 0)) piece.x++; break;
|
||||||
|
case 'ArrowDown':
|
||||||
|
if (valid(piece, 0, 1)) { piece.y++; score += 1; updateHUD(); }
|
||||||
|
dropTimer = 0;
|
||||||
|
break;
|
||||||
|
case 'ArrowUp': {
|
||||||
|
const rot = { ...piece, shape: rotate(piece.shape) };
|
||||||
|
if (valid(rot, 0, 0)) piece.shape = rot.shape;
|
||||||
|
else if (valid(rot, 1, 0)) { piece.shape = rot.shape; piece.x++; }
|
||||||
|
else if (valid(rot, -1, 0)) { piece.shape = rot.shape; piece.x--; }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ' ':
|
||||||
|
while (valid(piece, 0, 1)) { piece.y++; score += 2; }
|
||||||
|
place(); updateHUD();
|
||||||
|
break;
|
||||||
|
case 'p': case 'P':
|
||||||
|
paused = true;
|
||||||
|
showOverlay('PAUSED', 'press [P] to resume');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
// initial draw
|
||||||
|
board = newBoard();
|
||||||
|
drawBoard();
|
||||||
|
drawNext();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,729 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<title>ACCESS DENIED :: {{ domain }}</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=VT323&display=swap" rel="stylesheet"/>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--green: #00ff41;
|
||||||
|
--green-dim: #00aa2a;
|
||||||
|
--green-glow: #00ff4155;
|
||||||
|
--green-dark: #003a0e;
|
||||||
|
--bg: #020c04;
|
||||||
|
--amber: #ffb000;
|
||||||
|
--red: #ff2222;
|
||||||
|
--grid: #0a1f0d;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--green);
|
||||||
|
font-family: 'Share Tech Mono', monospace;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scanline overlay */
|
||||||
|
body::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
0deg,
|
||||||
|
transparent,
|
||||||
|
transparent 2px,
|
||||||
|
rgba(0,0,0,0.15) 2px,
|
||||||
|
rgba(0,0,0,0.15) 4px
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CRT flicker */
|
||||||
|
body::after {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: radial-gradient(ellipse at center, transparent 60%, rgba(0,0,0,0.5) 100%);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 99;
|
||||||
|
animation: flicker 8s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes flicker {
|
||||||
|
0%, 95%, 100% { opacity: 1; }
|
||||||
|
96% { opacity: 0.85; }
|
||||||
|
97% { opacity: 1; }
|
||||||
|
98% { opacity: 0.9; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header terminal block */
|
||||||
|
.terminal-header {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 900px;
|
||||||
|
padding: 20px 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.term-bar {
|
||||||
|
border: 1px solid var(--green-dim);
|
||||||
|
border-bottom: none;
|
||||||
|
padding: 4px 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--green-dim);
|
||||||
|
background: var(--green-dark);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.term-body {
|
||||||
|
border: 1px solid var(--green-dim);
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: rgba(0,10,2,0.8);
|
||||||
|
box-shadow: 0 0 20px var(--green-glow), inset 0 0 40px rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blink { animation: blink 1s step-end infinite; }
|
||||||
|
@keyframes blink { 50% { opacity: 0; } }
|
||||||
|
|
||||||
|
.prompt { color: var(--green-dim); }
|
||||||
|
.cmd { color: var(--green); }
|
||||||
|
|
||||||
|
.blocked-msg {
|
||||||
|
font-family: 'VT323', monospace;
|
||||||
|
font-size: 42px;
|
||||||
|
color: var(--red);
|
||||||
|
text-shadow: 0 0 10px var(--red), 0 0 20px var(--red);
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
margin: 8px 0 4px;
|
||||||
|
animation: glitch 4s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glitch {
|
||||||
|
0%, 90%, 100% { transform: none; text-shadow: 0 0 10px var(--red), 0 0 20px var(--red); }
|
||||||
|
91% { transform: skewX(4deg) translateX(3px); text-shadow: -3px 0 cyan, 3px 0 magenta; }
|
||||||
|
92% { transform: skewX(-2deg) translateX(-2px); }
|
||||||
|
93% { transform: none; text-shadow: 0 0 10px var(--red); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.domain-line {
|
||||||
|
color: var(--amber);
|
||||||
|
font-size: 13px;
|
||||||
|
margin: 4px 0;
|
||||||
|
text-shadow: 0 0 6px var(--amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-line {
|
||||||
|
color: var(--green-dim);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 8px;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag { color: var(--green); }
|
||||||
|
|
||||||
|
/* Main layout */
|
||||||
|
.main {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 16px 24px 24px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 900px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tetris game */
|
||||||
|
.game-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--green-dim);
|
||||||
|
letter-spacing: 0.15em;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tetris-canvas {
|
||||||
|
border: 1px solid var(--green-dim);
|
||||||
|
box-shadow: 0 0 16px var(--green-glow), inset 0 0 20px rgba(0,255,65,0.03);
|
||||||
|
background: var(--grid);
|
||||||
|
display: block;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
.sidebar {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
border: 1px solid var(--green-dim);
|
||||||
|
padding: 12px 14px;
|
||||||
|
background: rgba(0,10,2,0.6);
|
||||||
|
box-shadow: 0 0 8px rgba(0,255,65,0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-title {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--green-dim);
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-bottom: 1px solid var(--green-dark);
|
||||||
|
padding-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 13px;
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label { color: var(--green-dim); }
|
||||||
|
.stat-val {
|
||||||
|
color: var(--green);
|
||||||
|
text-shadow: 0 0 6px var(--green-glow);
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#next-canvas {
|
||||||
|
border: 1px solid var(--green-dark);
|
||||||
|
background: var(--grid);
|
||||||
|
display: block;
|
||||||
|
margin: 4px auto 0;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls-list {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 2;
|
||||||
|
color: var(--green-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.key {
|
||||||
|
color: var(--green);
|
||||||
|
background: var(--green-dark);
|
||||||
|
border: 1px solid var(--green-dim);
|
||||||
|
padding: 0 5px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(2,12,4,0.88);
|
||||||
|
font-family: 'VT323', monospace;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-title {
|
||||||
|
font-size: 36px;
|
||||||
|
color: var(--green);
|
||||||
|
text-shadow: 0 0 12px var(--green);
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-sub {
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--green-dim);
|
||||||
|
margin-top: 8px;
|
||||||
|
font-family: 'Share Tech Mono', monospace;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-wrap {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-log {
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.9;
|
||||||
|
color: var(--green-dim);
|
||||||
|
max-height: 120px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-ok { color: var(--green-dim); }
|
||||||
|
.log-warn { color: var(--amber); }
|
||||||
|
.log-err { color: var(--red); }
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.main { flex-direction: column; align-items: center; }
|
||||||
|
.sidebar { width: 100%; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="terminal-header">
|
||||||
|
<div class="term-bar">
|
||||||
|
<span>NETGUARD-OS v4.1.2 :: SECURITY TERMINAL</span>
|
||||||
|
<span id="clock"></span>
|
||||||
|
</div>
|
||||||
|
<div class="term-body">
|
||||||
|
<div class="info-line">
|
||||||
|
<span class="prompt">root@netguard:~$ </span><span class="cmd">resolve --host "{{ domain }}"</span>
|
||||||
|
</div>
|
||||||
|
<div class="blocked-msg">[ ACCESS DENIED ]</div>
|
||||||
|
<div class="domain-line">
|
||||||
|
TARGET HOST: <strong>{{ domain }}</strong> | STATUS: BLOCKED | POLICY: {{ reason }}
|
||||||
|
</div>
|
||||||
|
<div class="info-line">
|
||||||
|
<span class="tag">[SYSTEM]</span> Request intercepted by network policy enforcement layer.<br/>
|
||||||
|
<span class="tag">[SYSTEM]</span> Your browsing attempt has been logged. Timestamp recorded.<br/>
|
||||||
|
<span class="tag">[INFO] </span> While you wait, the system has deployed a cognitive recalibration protocol.<br/>
|
||||||
|
<span class="tag">[INFO] </span> Estimated recalibration duration: however long it takes. <span class="blink">_</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main">
|
||||||
|
<!-- Tetris -->
|
||||||
|
<div class="game-panel">
|
||||||
|
<div class="game-label">// RECALIBRATION MODULE :: TETRIS ENGINE //</div>
|
||||||
|
<div class="game-wrap">
|
||||||
|
<canvas id="tetris-canvas" width="200" height="400"></canvas>
|
||||||
|
<div class="overlay" id="overlay">
|
||||||
|
<div class="overlay-title">TETRIS.EXE</div>
|
||||||
|
<div class="overlay-sub">press <strong>[ENTER]</strong> to initialize</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-title">// SYSTEM METRICS</div>
|
||||||
|
<div class="stat-row"><span class="stat-label">SCORE</span><span class="stat-val" id="score">000000</span></div>
|
||||||
|
<div class="stat-row"><span class="stat-label">LEVEL</span><span class="stat-val" id="level">01</span></div>
|
||||||
|
<div class="stat-row"><span class="stat-label">LINES</span><span class="stat-val" id="lines">000</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-title">// NEXT PIECE</div>
|
||||||
|
<canvas id="next-canvas" width="80" height="80"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-title">// INPUT BINDINGS</div>
|
||||||
|
<div class="controls-list">
|
||||||
|
<span class="key">ENTER</span> START / RESTART<br/>
|
||||||
|
<span class="key">←</span><span class="key">→</span> MOVE<br/>
|
||||||
|
<span class="key">↑</span> ROTATE<br/>
|
||||||
|
<span class="key">↓</span> SOFT DROP<br/>
|
||||||
|
<span class="key">SPACE</span> HARD DROP<br/>
|
||||||
|
<span class="key">P</span> PAUSE
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-title">// NETWORK LOG</div>
|
||||||
|
<div class="network-log" id="net-log"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Clock
|
||||||
|
function updateClock() {
|
||||||
|
const now = new Date();
|
||||||
|
document.getElementById('clock').textContent =
|
||||||
|
now.toISOString().replace('T', ' ').substring(0, 19) + ' UTC';
|
||||||
|
}
|
||||||
|
setInterval(updateClock, 1000);
|
||||||
|
updateClock();
|
||||||
|
|
||||||
|
// Network log flavor text
|
||||||
|
const logMessages = [
|
||||||
|
['warn', 'DNS query for {{ domain }} intercepted'],
|
||||||
|
['err', 'Connection to {{ domain }} refused by policy'],
|
||||||
|
['ok', 'Firewall rule #4471 matched: BLOCK'],
|
||||||
|
['ok', 'Logging event to audit trail... done'],
|
||||||
|
['warn', 'Retry attempt 1/3 detected... blocked'],
|
||||||
|
['warn', 'Retry attempt 2/3 detected... blocked'],
|
||||||
|
['err', 'Retry attempt 3/3 detected... blocked'],
|
||||||
|
['ok', 'Packet disposal complete. Have a nice day.'],
|
||||||
|
];
|
||||||
|
let logIdx = 0;
|
||||||
|
const logEl = document.getElementById('net-log');
|
||||||
|
function appendLog() {
|
||||||
|
if (logIdx >= logMessages.length) return;
|
||||||
|
const [type, msg] = logMessages[logIdx++];
|
||||||
|
const span = document.createElement('div');
|
||||||
|
span.className = `log-${type}`;
|
||||||
|
const prefix = type === 'err' ? '[ERR] ' : type === 'warn' ? '[WARN]' : '[OK] ';
|
||||||
|
span.textContent = prefix + ' ' + msg;
|
||||||
|
logEl.appendChild(span);
|
||||||
|
if (logIdx < logMessages.length) setTimeout(appendLog, 900 + Math.random() * 600);
|
||||||
|
}
|
||||||
|
setTimeout(appendLog, 800);
|
||||||
|
|
||||||
|
// ── TETRIS ─────────────────────────────────────────────
|
||||||
|
const COLS = 10, ROWS = 20, BLOCK = 20;
|
||||||
|
const canvas = document.getElementById('tetris-canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const nextCanvas = document.getElementById('next-canvas');
|
||||||
|
const nCtx = nextCanvas.getContext('2d');
|
||||||
|
|
||||||
|
const GREEN = '#00ff41';
|
||||||
|
const DIMGREEN = '#00aa2a';
|
||||||
|
const DARKGREEN = '#003a0e';
|
||||||
|
const GRID = '#0a1f0d';
|
||||||
|
|
||||||
|
const PIECES = [
|
||||||
|
{ shape: [[1,1,1,1]], color: '#00ff41' }, // I
|
||||||
|
{ shape: [[1,1],[1,1]], color: '#00cc33' }, // O
|
||||||
|
{ shape: [[0,1,0],[1,1,1]], color: '#00ff66' }, // T
|
||||||
|
{ shape: [[1,0,0],[1,1,1]], color: '#00dd44' }, // L
|
||||||
|
{ shape: [[0,0,1],[1,1,1]], color: '#00bb22' }, // J
|
||||||
|
{ shape: [[0,1,1],[1,1,0]], color: '#00ee55' }, // S
|
||||||
|
{ shape: [[1,1,0],[0,1,1]], color: '#009911' }, // Z
|
||||||
|
];
|
||||||
|
|
||||||
|
let board, piece, nextPiece, score, level, lines, gameOver, paused, running, dropTimer, dropInterval, animFrame;
|
||||||
|
|
||||||
|
function newBoard() {
|
||||||
|
return Array.from({length: ROWS}, () => Array(COLS).fill(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomPiece() {
|
||||||
|
const p = PIECES[Math.floor(Math.random() * PIECES.length)];
|
||||||
|
return {
|
||||||
|
shape: p.shape.map(r => [...r]),
|
||||||
|
color: p.color,
|
||||||
|
x: Math.floor(COLS / 2) - Math.floor(p.shape[0].length / 2),
|
||||||
|
y: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function rotate(shape) {
|
||||||
|
const R = shape.length, C = shape[0].length;
|
||||||
|
const out = Array.from({length: C}, () => Array(R).fill(0));
|
||||||
|
for (let r = 0; r < R; r++)
|
||||||
|
for (let c = 0; c < C; c++)
|
||||||
|
out[c][R - 1 - r] = shape[r][c];
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function valid(s, dx, dy) {
|
||||||
|
for (let r = 0; r < s.shape.length; r++)
|
||||||
|
for (let c = 0; c < s.shape[r].length; c++)
|
||||||
|
if (s.shape[r][c]) {
|
||||||
|
const nx = s.x + c + dx, ny = s.y + r + dy;
|
||||||
|
if (nx < 0 || nx >= COLS || ny >= ROWS) return false;
|
||||||
|
if (ny >= 0 && board[ny][nx]) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function place() {
|
||||||
|
for (let r = 0; r < piece.shape.length; r++)
|
||||||
|
for (let c = 0; c < piece.shape[r].length; c++)
|
||||||
|
if (piece.shape[r][c] && piece.y + r >= 0)
|
||||||
|
board[piece.y + r][piece.x + c] = piece.color;
|
||||||
|
|
||||||
|
// Clear lines
|
||||||
|
let cleared = 0;
|
||||||
|
for (let r = ROWS - 1; r >= 0; r--) {
|
||||||
|
if (board[r].every(c => c)) {
|
||||||
|
board.splice(r, 1);
|
||||||
|
board.unshift(Array(COLS).fill(0));
|
||||||
|
cleared++;
|
||||||
|
r++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cleared) {
|
||||||
|
const pts = [0, 100, 300, 500, 800];
|
||||||
|
score += (pts[cleared] || 800) * level;
|
||||||
|
lines += cleared;
|
||||||
|
level = Math.floor(lines / 10) + 1;
|
||||||
|
dropInterval = Math.max(100, 1000 - (level - 1) * 90);
|
||||||
|
updateHUD();
|
||||||
|
}
|
||||||
|
|
||||||
|
piece = nextPiece;
|
||||||
|
nextPiece = randomPiece();
|
||||||
|
if (!valid(piece, 0, 0)) {
|
||||||
|
gameOver = true;
|
||||||
|
showOverlay('GAME OVER', 'press [ENTER] to retry');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateHUD() {
|
||||||
|
document.getElementById('score').textContent = String(score).padStart(6, '0');
|
||||||
|
document.getElementById('level').textContent = String(level).padStart(2, '0');
|
||||||
|
document.getElementById('lines').textContent = String(lines).padStart(3, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawBlock(context, x, y, color, size) {
|
||||||
|
context.fillStyle = color;
|
||||||
|
context.fillRect(x * size + 1, y * size + 1, size - 2, size - 2);
|
||||||
|
// highlight
|
||||||
|
context.fillStyle = 'rgba(255,255,255,0.12)';
|
||||||
|
context.fillRect(x * size + 1, y * size + 1, size - 2, 3);
|
||||||
|
context.fillRect(x * size + 1, y * size + 1, 3, size - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawBoard() {
|
||||||
|
ctx.fillStyle = GRID;
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
// grid lines
|
||||||
|
ctx.strokeStyle = '#0d1f10';
|
||||||
|
ctx.lineWidth = 0.5;
|
||||||
|
for (let r = 0; r < ROWS; r++) {
|
||||||
|
ctx.beginPath(); ctx.moveTo(0, r * BLOCK); ctx.lineTo(canvas.width, r * BLOCK); ctx.stroke();
|
||||||
|
}
|
||||||
|
for (let c = 0; c < COLS; c++) {
|
||||||
|
ctx.beginPath(); ctx.moveTo(c * BLOCK, 0); ctx.lineTo(c * BLOCK, canvas.height); ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// placed blocks
|
||||||
|
for (let r = 0; r < ROWS; r++)
|
||||||
|
for (let c = 0; c < COLS; c++)
|
||||||
|
if (board[r][c]) drawBlock(ctx, c, r, board[r][c], BLOCK);
|
||||||
|
|
||||||
|
// ghost piece
|
||||||
|
if (piece && !gameOver) {
|
||||||
|
let ghost = { ...piece, shape: piece.shape.map(r => [...r]) };
|
||||||
|
while (valid(ghost, 0, 1)) ghost.y++;
|
||||||
|
for (let r = 0; r < ghost.shape.length; r++)
|
||||||
|
for (let c = 0; c < ghost.shape[r].length; c++)
|
||||||
|
if (ghost.shape[r][c] && ghost.y + r >= 0) {
|
||||||
|
ctx.fillStyle = 'rgba(0,255,65,0.12)';
|
||||||
|
ctx.fillRect((ghost.x + c) * BLOCK + 1, (ghost.y + r) * BLOCK + 1, BLOCK - 2, BLOCK - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// active piece
|
||||||
|
for (let r = 0; r < piece.shape.length; r++)
|
||||||
|
for (let c = 0; c < piece.shape[r].length; c++)
|
||||||
|
if (piece.shape[r][c] && piece.y + r >= 0)
|
||||||
|
drawBlock(ctx, piece.x + c, piece.y + r, piece.color, BLOCK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawNext() {
|
||||||
|
nCtx.fillStyle = GRID;
|
||||||
|
nCtx.fillRect(0, 0, 80, 80);
|
||||||
|
if (!nextPiece) return;
|
||||||
|
const s = nextPiece.shape;
|
||||||
|
const offX = Math.floor((4 - s[0].length) / 2);
|
||||||
|
const offY = Math.floor((4 - s.length) / 2);
|
||||||
|
for (let r = 0; r < s.length; r++)
|
||||||
|
for (let c = 0; c < s[r].length; c++)
|
||||||
|
if (s[r][c]) drawBlock(nCtx, offX + c, offY + r, nextPiece.color, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
const CURRENT_DOMAIN = "{{ domain }}";
|
||||||
|
let awaitingName = false;
|
||||||
|
let nameInput = '';
|
||||||
|
|
||||||
|
function showOverlay(title, sub, extra) {
|
||||||
|
const el = document.getElementById('overlay');
|
||||||
|
el.innerHTML = `<div class="overlay-title">${title}</div><div class="overlay-sub">${sub}</div>${extra || ''}`;
|
||||||
|
el.style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideOverlay() {
|
||||||
|
document.getElementById('overlay').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNamePrompt(finalScore, finalLevel, finalLines) {
|
||||||
|
awaitingName = true;
|
||||||
|
nameInput = '';
|
||||||
|
const el = document.getElementById('overlay');
|
||||||
|
el.innerHTML = `
|
||||||
|
<div class="overlay-title" style="color:var(--amber);text-shadow:0 0 12px var(--amber)">GAME OVER</div>
|
||||||
|
<div class="overlay-sub" style="margin-top:6px">SCORE: <span style="color:var(--green)">${String(finalScore).padStart(6,'0')}</span></div>
|
||||||
|
<div class="overlay-sub" style="margin-top:14px;font-size:13px">ENTER YOUR NAME:</div>
|
||||||
|
<div id="name-display" style="font-family:'VT323',monospace;font-size:32px;color:var(--green);
|
||||||
|
text-shadow:0 0 8px var(--green);letter-spacing:0.2em;margin-top:6px;min-width:160px;
|
||||||
|
text-align:center;border-bottom:1px solid var(--green-dim);padding-bottom:4px"> </div>
|
||||||
|
<div class="overlay-sub" style="margin-top:10px;font-size:11px">[ENTER] submit [ESC] skip</div>
|
||||||
|
<a href="/leaderboard" style="margin-top:14px;font-size:11px;color:var(--green-dim);
|
||||||
|
font-family:'Share Tech Mono',monospace;text-decoration:none;">view leaderboard →</a>
|
||||||
|
`;
|
||||||
|
el.style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNameDisplay() {
|
||||||
|
const el = document.getElementById('name-display');
|
||||||
|
if (el) el.textContent = (nameInput || '') + '|';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitScore(name, finalScore, finalLevel, finalLines) {
|
||||||
|
try {
|
||||||
|
await fetch('/score', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: name || 'ANONYMOUS',
|
||||||
|
score: finalScore,
|
||||||
|
level: finalLevel,
|
||||||
|
lines: finalLines,
|
||||||
|
domain: CURRENT_DOMAIN
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} catch(e) { /* silent fail */ }
|
||||||
|
showOverlay(
|
||||||
|
'SCORE SAVED',
|
||||||
|
`rank recorded for ${name || 'ANONYMOUS'}`,
|
||||||
|
`<a href="/leaderboard" style="margin-top:14px;font-size:13px;color:var(--green-dim);
|
||||||
|
font-family:'Share Tech Mono',monospace;text-decoration:none;">view leaderboard →</a>
|
||||||
|
<div class="overlay-sub" style="margin-top:10px">[ENTER] play again</div>`
|
||||||
|
);
|
||||||
|
awaitingName = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startGame() {
|
||||||
|
board = newBoard();
|
||||||
|
score = 0; level = 1; lines = 0; gameOver = false; paused = false;
|
||||||
|
awaitingName = false; nameInput = '';
|
||||||
|
dropInterval = 1000;
|
||||||
|
piece = randomPiece();
|
||||||
|
nextPiece = randomPiece();
|
||||||
|
updateHUD();
|
||||||
|
hideOverlay();
|
||||||
|
running = true;
|
||||||
|
let last = 0;
|
||||||
|
dropTimer = 0;
|
||||||
|
|
||||||
|
function loop(ts) {
|
||||||
|
if (!running) return;
|
||||||
|
const dt = ts - last; last = ts;
|
||||||
|
if (!paused && !gameOver) {
|
||||||
|
dropTimer += dt;
|
||||||
|
if (dropTimer >= dropInterval) {
|
||||||
|
dropTimer = 0;
|
||||||
|
if (valid(piece, 0, 1)) piece.y++;
|
||||||
|
else place();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drawBoard();
|
||||||
|
drawNext();
|
||||||
|
animFrame = requestAnimationFrame(loop);
|
||||||
|
}
|
||||||
|
if (animFrame) cancelAnimationFrame(animFrame);
|
||||||
|
animFrame = requestAnimationFrame(loop);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap place() to trigger name prompt on game over
|
||||||
|
const _origPlace = place;
|
||||||
|
place = function() {
|
||||||
|
for (let r = 0; r < piece.shape.length; r++)
|
||||||
|
for (let c = 0; c < piece.shape[r].length; c++)
|
||||||
|
if (piece.shape[r][c] && piece.y + r >= 0)
|
||||||
|
board[piece.y + r][piece.x + c] = piece.color;
|
||||||
|
|
||||||
|
let cleared = 0;
|
||||||
|
for (let r = ROWS - 1; r >= 0; r--) {
|
||||||
|
if (board[r].every(c => c)) {
|
||||||
|
board.splice(r, 1);
|
||||||
|
board.unshift(Array(COLS).fill(0));
|
||||||
|
cleared++;
|
||||||
|
r++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cleared) {
|
||||||
|
const pts = [0, 100, 300, 500, 800];
|
||||||
|
score += (pts[cleared] || 800) * level;
|
||||||
|
lines += cleared;
|
||||||
|
level = Math.floor(lines / 10) + 1;
|
||||||
|
dropInterval = Math.max(100, 1000 - (level - 1) * 90);
|
||||||
|
updateHUD();
|
||||||
|
}
|
||||||
|
|
||||||
|
piece = nextPiece;
|
||||||
|
nextPiece = randomPiece();
|
||||||
|
if (!valid(piece, 0, 0)) {
|
||||||
|
gameOver = true;
|
||||||
|
const fs = score, fl = level, fn = lines;
|
||||||
|
setTimeout(() => showNamePrompt(fs, fl, fn), 300);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', e => {
|
||||||
|
if (awaitingName) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
const saved = score, slevel = level, slines = lines;
|
||||||
|
submitScore(nameInput, saved, slevel, slines);
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
awaitingName = false;
|
||||||
|
showOverlay('GAME OVER', 'press [ENTER] to retry',
|
||||||
|
`<a href="/leaderboard" style="margin-top:14px;font-size:11px;color:var(--green-dim);
|
||||||
|
font-family:'Share Tech Mono',monospace;text-decoration:none;">view leaderboard →</a>`
|
||||||
|
);
|
||||||
|
} else if (e.key === 'Backspace') {
|
||||||
|
nameInput = nameInput.slice(0, -1);
|
||||||
|
updateNameDisplay();
|
||||||
|
} else if (e.key.length === 1 && nameInput.length < 16) {
|
||||||
|
nameInput += e.key.toUpperCase();
|
||||||
|
updateNameDisplay();
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
if (!running || gameOver) { startGame(); return; }
|
||||||
|
}
|
||||||
|
if (!running || gameOver || paused) {
|
||||||
|
if (e.key === 'p' || e.key === 'P') {
|
||||||
|
if (paused) { paused = false; hideOverlay(); }
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (e.key) {
|
||||||
|
case 'ArrowLeft': if (valid(piece, -1, 0)) piece.x--; break;
|
||||||
|
case 'ArrowRight': if (valid(piece, 1, 0)) piece.x++; break;
|
||||||
|
case 'ArrowDown':
|
||||||
|
if (valid(piece, 0, 1)) { piece.y++; score += 1; updateHUD(); }
|
||||||
|
dropTimer = 0;
|
||||||
|
break;
|
||||||
|
case 'ArrowUp': {
|
||||||
|
const rot = { ...piece, shape: rotate(piece.shape) };
|
||||||
|
if (valid(rot, 0, 0)) piece.shape = rot.shape;
|
||||||
|
else if (valid(rot, 1, 0)) { piece.shape = rot.shape; piece.x++; }
|
||||||
|
else if (valid(rot, -1, 0)) { piece.shape = rot.shape; piece.x--; }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ' ':
|
||||||
|
while (valid(piece, 0, 1)) { piece.y++; score += 2; }
|
||||||
|
place(); updateHUD();
|
||||||
|
break;
|
||||||
|
case 'p': case 'P':
|
||||||
|
paused = true;
|
||||||
|
showOverlay('PAUSED', 'press [P] to resume');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
// initial draw
|
||||||
|
board = newBoard();
|
||||||
|
drawBoard();
|
||||||
|
drawNext();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user