Compare commits

...

3 Commits

Author SHA1 Message Date
bmartin 29220010f9 Update README.md 2026-06-03 16:29:02 -05:00
bmartin a811216bb1 Merge branch 'master' of https://git.umbrellapants.xyz/bmartin/Adguard_Block 2026-06-03 21:25:41 +00:00
bmartin 1896807165 Initial commit: AdGuard Tetris blocking page 2026-06-03 21:23:35 +00:00
8 changed files with 1598 additions and 1 deletions
+1
View File
@@ -0,0 +1 @@
.env
+70 -1
View File
@@ -1,2 +1,71 @@
# Adguard_Block # AdGuard Block Page
A custom AdGuard Home blocking page that notifies users when a domain has been blocked and gives them a Tetris game to pass the time.
## Features
- Displays the blocked domain name to the user
- Retro terminal aesthetic
- Fully playable Tetris game embedded in the page
- Leaderboard to track high scores
## Stack
- Python / Flask
- SQLite (leaderboard scores)
- Apache with mod_wsgi
## Setup
### Prerequisites
- Python 3
- Apache with mod_wsgi
- AdGuard Home configured to redirect blocked domains to this page
### Installation
```bash
sudo mkdir -p /opt/flaskapp
sudo python3 -m venv /opt/flaskapp/venv
sudo /opt/flaskapp/venv/bin/pip install flask
```
Copy files:
```bash
sudo cp app.py /opt/flaskapp/
sudo cp app.wsgi /opt/flaskapp/
sudo cp -r templates /opt/flaskapp/
```
### Apache Configuration
Add a `WSGIScriptAlias` in your Apache config pointing to `app.wsgi`:
```apache
WSGIScriptAlias /blocked /opt/flaskapp/app.wsgi
<Directory /opt/flaskapp>
WSGIProcessGroup flaskapp
WSGIApplicationGroup %{GLOBAL}
Require all granted
</Directory>
```
### AdGuard Home Configuration
In AdGuard Home, set the **Custom blocking page** URL to:
```
http://192.168.1.189/blocked?domain=%DOMAIN%
```
## Project Structure
```
flaskapp/
├── app.py # Flask app
├── app.wsgi # Apache mod_wsgi entry point
└── templates/
└── blocked.html # Blocking page with Tetris
```
Binary file not shown.
+65
View File
@@ -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)
+4
View File
@@ -0,0 +1,4 @@
import sys
sys.path.insert(0,'/opt/flaskapp')
from app import app as application
BIN
View File
Binary file not shown.
+729
View File
@@ -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> &nbsp;|&nbsp; STATUS: BLOCKED &nbsp;|&nbsp; 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">&nbsp;</div>
<div class="overlay-sub" style="margin-top:10px;font-size:11px">[ENTER] submit &nbsp; [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 &rarr;</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 &rarr;</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 &rarr;</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>
+729
View File
@@ -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> &nbsp;|&nbsp; STATUS: BLOCKED &nbsp;|&nbsp; 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">&nbsp;</div>
<div class="overlay-sub" style="margin-top:10px;font-size:11px">[ENTER] submit &nbsp; [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 &rarr;</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 &rarr;</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 &rarr;</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>