1035 lines
32 KiB
HTML
1035 lines
32 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Securityfest 2026 Badge</title>
|
|
<style>
|
|
@import url('https://fonts.googleapis.com/css2?family=VT323&display=swap');
|
|
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
body {
|
|
background: #0a0a0a;
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-family: 'VT323', monospace;
|
|
color: #00ff41;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 2rem;
|
|
margin-bottom: 1rem;
|
|
text-shadow: 0 0 10px #00ff41;
|
|
}
|
|
|
|
.badge {
|
|
background: linear-gradient(135deg, #1a1a2e 0%, #0f0f1a 100%);
|
|
border: 3px solid #333;
|
|
border-radius: 20px;
|
|
padding: 20px;
|
|
box-shadow: 0 0 30px rgba(0, 255, 65, 0.2), inset 0 0 60px rgba(0, 0, 0, 0.5);
|
|
}
|
|
|
|
#matrix {
|
|
border: 2px solid #222;
|
|
border-radius: 5px;
|
|
margin-bottom: 15px;
|
|
image-rendering: pixelated;
|
|
}
|
|
|
|
.front-leds {
|
|
display: flex;
|
|
gap: 15px;
|
|
justify-content: center;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.front-led {
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
background: #1a1a1a;
|
|
border: 1px solid #333;
|
|
transition: background 0.05s, box-shadow 0.05s;
|
|
}
|
|
|
|
.front-led.on {
|
|
background: #ff0040;
|
|
box-shadow: 0 0 10px #ff0040, 0 0 20px #ff0040;
|
|
}
|
|
|
|
.flashlight {
|
|
display: flex;
|
|
justify-content: center;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.flashlight-led {
|
|
width: 30px;
|
|
height: 15px;
|
|
border-radius: 5px;
|
|
background: #1a1a1a;
|
|
border: 1px solid #333;
|
|
transition: background 0.05s, box-shadow 0.05s;
|
|
}
|
|
|
|
.flashlight-led.on {
|
|
background: #880000;
|
|
box-shadow: 0 0 20px #ff0000;
|
|
}
|
|
|
|
.controls {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 50px);
|
|
gap: 5px;
|
|
justify-content: center;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.btn {
|
|
width: 50px;
|
|
height: 50px;
|
|
border-radius: 50%;
|
|
background: linear-gradient(145deg, #2a2a2a, #1a1a1a);
|
|
border: 2px solid #444;
|
|
color: #888;
|
|
font-family: 'VT323', monospace;
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
user-select: none;
|
|
transition: all 0.1s;
|
|
}
|
|
|
|
.btn:active, .btn.pressed {
|
|
background: #00ff41;
|
|
color: #0a0a0a;
|
|
box-shadow: 0 0 15px #00ff41;
|
|
transform: scale(0.95);
|
|
}
|
|
|
|
.btn-a, .btn-b { border-radius: 8px; width: 60px; }
|
|
.btn-up { grid-column: 2; }
|
|
.btn-left { grid-column: 1; grid-row: 2; }
|
|
.btn-down { grid-column: 2; grid-row: 2; }
|
|
.btn-right { grid-column: 3; grid-row: 2; }
|
|
.btn-a { grid-column: 1; grid-row: 3; background: linear-gradient(145deg, #2a002a, #1a001a); border-color: #404; }
|
|
.btn-b { grid-column: 3; grid-row: 3; background: linear-gradient(145deg, #2a002a, #1a001a); border-color: #404; }
|
|
|
|
.sao {
|
|
display: flex;
|
|
gap: 20px;
|
|
justify-content: center;
|
|
margin-top: 10px;
|
|
padding-top: 10px;
|
|
border-top: 1px solid #222;
|
|
}
|
|
|
|
.sao-pin {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 5px;
|
|
}
|
|
|
|
.sao-led {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
background: #1a1a1a;
|
|
border: 1px solid #333;
|
|
transition: background 0.05s, box-shadow 0.05s;
|
|
}
|
|
|
|
.sao-led.on {
|
|
background: #00ff41;
|
|
box-shadow: 0 0 8px #00ff41;
|
|
}
|
|
|
|
.sao-pwm {
|
|
width: 30px;
|
|
height: 8px;
|
|
background: linear-gradient(90deg, #00ff41 var(--p), #333 var(--p));
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.instructions {
|
|
margin-top: 15px;
|
|
color: #444;
|
|
font-size: 12px;
|
|
text-align: center;
|
|
}
|
|
|
|
.instructions kbd {
|
|
background: #222;
|
|
padding: 2px 6px;
|
|
border-radius: 3px;
|
|
color: #666;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Securityfest 2026 Badge</h1>
|
|
|
|
<div class="badge">
|
|
<canvas id="matrix" width="270" height="270"></canvas>
|
|
|
|
<div class="front-leds">
|
|
<div class="front-led" id="led1"></div>
|
|
<div class="front-led" id="led2"></div>
|
|
<div class="front-led" id="led3"></div>
|
|
<div class="front-led" id="led4"></div>
|
|
</div>
|
|
|
|
<div class="flashlight">
|
|
<div class="flashlight-led" id="flashlight"></div>
|
|
</div>
|
|
|
|
<div class="controls">
|
|
<button class="btn btn-up" data-btn="up">▲</button>
|
|
<button class="btn btn-left" data-btn="left">◀</button>
|
|
<button class="btn btn-down" data-btn="down">▼</button>
|
|
<button class="btn btn-right" data-btn="right">▶</button>
|
|
<button class="btn btn-a" data-btn="a">A</button>
|
|
<button class="btn btn-b" data-btn="b">B</button>
|
|
</div>
|
|
|
|
<div class="sao">
|
|
<div class="sao-pin">
|
|
<div class="sao-led" id="sao0"></div>
|
|
<span>GPIO0</span>
|
|
</div>
|
|
<div class="sao-pin">
|
|
<div class="sao-pwm" id="sao1"></div>
|
|
<span>GPIO1 PWM</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="instructions">
|
|
<kbd>↑</kbd><kbd>↓</kbd><kbd>←</kbd><kbd>→</kbd> Navigate
|
|
<kbd>A</kbd> Select
|
|
<kbd>B</kbd> Back
|
|
</div>
|
|
|
|
<script>
|
|
// ==================== CONSTANTS ====================
|
|
const MATRIX_COLS = 9;
|
|
const MATRIX_ROWS = 9;
|
|
const PIXEL_SIZE = 30;
|
|
const IDLE_TIMEOUT = 5000;
|
|
|
|
const BUTTON_A = 0, BUTTON_B = 1, BUTTON_UP = 2, BUTTON_DOWN = 3, BUTTON_LEFT = 4, BUTTON_RIGHT = 5;
|
|
|
|
const MODE_MENU = 0;
|
|
const MODE_TETRIS = 1;
|
|
const MODE_SNAKE = 2;
|
|
const MODE_BRICK = 3;
|
|
const MODE_PONG = 4;
|
|
const MODE_TVBGONE = 5;
|
|
const MODE_IDLE = 6;
|
|
|
|
const RANDOM_MESSAGES = [
|
|
"Hack the planet!", "0x00 0x01 0x10", "Securityfest 2026!",
|
|
"RP2040 rocks!", "Buy the dip!", "Nice badge!", "#!.!/#!.!",
|
|
"0xDEAD 0xBEEF", "Pwned!", "1337 h4x0r", "Kernel panic!",
|
|
"sudo make me", "CTF{D3bug}", "0-day here", "Badgelife!"
|
|
];
|
|
|
|
// ==================== STATE ====================
|
|
let currentMode = MODE_MENU;
|
|
let currentMenuItem = 0;
|
|
let lastActivity = Date.now();
|
|
let matrixBuffer = [];
|
|
let canvas, ctx;
|
|
|
|
// ==================== INITIALIZATION ====================
|
|
function init() {
|
|
canvas = document.getElementById('matrix');
|
|
ctx = canvas.getContext('2d');
|
|
|
|
initMatrixBuffer();
|
|
setupControls();
|
|
|
|
showMenu();
|
|
lastActivity = Date.now();
|
|
|
|
requestAnimationFrame(gameLoop);
|
|
}
|
|
|
|
function initMatrixBuffer() {
|
|
matrixBuffer = [];
|
|
for (let y = 0; y < MATRIX_ROWS; y++) {
|
|
matrixBuffer[y] = [];
|
|
for (let x = 0; x < MATRIX_COLS; x++) {
|
|
matrixBuffer[y][x] = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
function setupControls() {
|
|
document.querySelectorAll('.btn').forEach(btn => {
|
|
btn.addEventListener('mousedown', () => {
|
|
btn.classList.add('pressed');
|
|
handleButton(btn.dataset.btn, true);
|
|
});
|
|
btn.addEventListener('mouseup', () => {
|
|
btn.classList.remove('pressed');
|
|
handleButton(btn.dataset.btn, false);
|
|
});
|
|
btn.addEventListener('mouseleave', () => {
|
|
btn.classList.remove('pressed');
|
|
handleButton(btn.dataset.btn, false);
|
|
});
|
|
});
|
|
|
|
document.addEventListener('keydown', e => {
|
|
const keyMap = { 'ArrowUp': 'up', 'ArrowDown': 'down', 'ArrowLeft': 'left', 'ArrowRight': 'right', 'a': 'a', 'b': 'b', 'A': 'a', 'B': 'b' };
|
|
const btn = keyMap[e.key];
|
|
if (btn && !e.repeat) {
|
|
const el = document.querySelector(`[data-btn="${btn}"]`);
|
|
if (el) { el.classList.add('pressed'); handleButton(btn, true); }
|
|
}
|
|
});
|
|
|
|
document.addEventListener('keyup', e => {
|
|
const keyMap = { 'ArrowUp': 'up', 'ArrowDown': 'down', 'ArrowLeft': 'left', 'ArrowRight': 'right', 'a': 'a', 'b': 'b', 'A': 'a', 'B': 'b' };
|
|
const btn = keyMap[e.key];
|
|
if (btn) {
|
|
const el = document.querySelector(`[data-btn="${btn}"]`);
|
|
if (el) { el.classList.remove('pressed'); handleButton(btn, false); }
|
|
}
|
|
});
|
|
}
|
|
|
|
let buttonStates = { up: false, down: false, left: false, right: false, a: false, b: false };
|
|
let buttonFirstPress = { up: false, down: false, left: false, right: false, a: false, b: false };
|
|
|
|
function handleButton(btn, pressed) {
|
|
buttonStates[btn] = pressed;
|
|
if (pressed) {
|
|
buttonFirstPress[btn] = true;
|
|
recordActivity();
|
|
}
|
|
}
|
|
|
|
function buttonPressed(btn) { return buttonFirstPress[btn]; }
|
|
|
|
function clearFirstPress(btn) { buttonFirstPress[btn] = false; }
|
|
|
|
function clearAllFirstPress() {
|
|
for (let k in buttonFirstPress) buttonFirstPress[k] = false;
|
|
}
|
|
|
|
function recordActivity() {
|
|
lastActivity = Date.now();
|
|
}
|
|
|
|
// ==================== MATRIX DISPLAY ====================
|
|
function renderMatrix() {
|
|
ctx.fillStyle = '#000';
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
for (let y = 0; y < MATRIX_ROWS; y++) {
|
|
for (let x = 0; x < MATRIX_COLS; x++) {
|
|
const brightness = matrixBuffer[y][x];
|
|
if (brightness > 0) {
|
|
const intensity = brightness / 255;
|
|
const r = Math.floor(40 + 80 * intensity);
|
|
const g = Math.floor(200 + 55 * intensity);
|
|
const b = Math.floor(40 + 80 * intensity);
|
|
ctx.fillStyle = `rgb(${r},${g},${b})`;
|
|
ctx.fillRect(x * PIXEL_SIZE + 1, y * PIXEL_SIZE + 1, PIXEL_SIZE - 2, PIXEL_SIZE - 2);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function ledMatrixSetPixel(x, y, brightness) {
|
|
if (x >= 0 && x < MATRIX_COLS && y >= 0 && y < MATRIX_ROWS) {
|
|
matrixBuffer[y][x] = brightness;
|
|
}
|
|
}
|
|
|
|
function ledMatrixClear() {
|
|
initMatrixBuffer();
|
|
}
|
|
|
|
// ==================== FONT (simple 3x5 pixel font) ====================
|
|
const FONT = {
|
|
'A': ["111", "101", "111", "101", "101"],
|
|
'B': ["111", "101", "111", "101", "101"],
|
|
'C': ["111", "100", "100", "100", "111"],
|
|
'D': ["111", "100", "100", "100", "111"],
|
|
'E': ["111", "101", "101", "100", "111"],
|
|
'F': ["111", "101", "101", "100", "100"],
|
|
'G': ["111", "100", "101", "101", "111"],
|
|
'H': ["111", "100", "100", "100", "111"],
|
|
'I': ["111", "010", "010", "010", "111"],
|
|
'J': ["001", "001", "001", "101", "010"],
|
|
'K': ["111", "100", "101", "100", "101"],
|
|
'L': ["111", "100", "100", "100", "100"],
|
|
'N': ["111", "100", "100", "100", "111"],
|
|
'O': ["111", "101", "101", "101", "111"],
|
|
'P': ["111", "101", "111", "100", "100"],
|
|
'R': ["111", "101", "111", "101", "101"],
|
|
'S': ["111", "101", "111", "001", "111"],
|
|
'T': ["111", "010", "010", "010", "010"],
|
|
'U': ["111", "100", "100", "100", "111"],
|
|
'V': ["111", "100", "100", "100", "010"],
|
|
'W': ["111", "100", "111", "100", "111"],
|
|
'X': ["111", "100", "010", "100", "111"],
|
|
'Y': ["111", "100", "010", "010", "010"],
|
|
'Z': ["111", "001", "010", "100", "111"],
|
|
'0': ["111", "101", "101", "101", "111"],
|
|
'1': ["010", "110", "010", "010", "111"],
|
|
'2': ["111", "001", "111", "100", "111"],
|
|
'3': ["111", "001", "111", "001", "111"],
|
|
'4': ["101", "101", "111", "001", "001"],
|
|
'5': ["111", "100", "111", "001", "111"],
|
|
'6': ["111", "100", "111", "101", "111"],
|
|
'7': ["111", "001", "001", "001", "001"],
|
|
'8': ["111", "101", "111", "101", "111"],
|
|
'9': ["111", "101", "111", "001", "111"],
|
|
'-': ["000", "000", "111", "000", "000"],
|
|
'!': ["010", "010", "010", "000", "010"],
|
|
'.': ["000", "000", "000", "000", "010"],
|
|
' ': ["000", "000", "000", "000", "000"],
|
|
':': ["000", "010", "000", "010", "000"],
|
|
'#': ["010", "111", "010", "111", "010"],
|
|
'/': ["001", "010", "100", "100", "100"],
|
|
'{': ["010", "100", "010", "001", "010"],
|
|
'}': ["010", "001", "010", "100", "010"],
|
|
};
|
|
|
|
function ledMatrixDrawChar(x, y, c, brightness) {
|
|
const charData = FONT[c];
|
|
if (!charData) return;
|
|
for (let col = 0; col < 3; col++) {
|
|
for (let row = 0; row < 5; row++) {
|
|
if (charData[row] && charData[row][col] === '1') {
|
|
ledMatrixSetPixel(x + col, y + row, brightness);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function drawText(text, x, y, brightness) {
|
|
let cursorX = x;
|
|
for (let i = 0; i < text.length; i++) {
|
|
ledMatrixDrawChar(cursorX, y, text[i], brightness);
|
|
cursorX += 4;
|
|
if (cursorX < -10) break;
|
|
}
|
|
}
|
|
|
|
function drawTextCentered(text, y, brightness) {
|
|
const len = text.length;
|
|
let x = Math.floor((MATRIX_COLS - len * 4) / 2);
|
|
if (x < 0) x = 0;
|
|
drawText(text, x, y, brightness);
|
|
}
|
|
|
|
// ==================== BACKGROUND FUNCTIONS ====================
|
|
let knightPos = 0, knightDir = true, knightLastUpdate = 0;
|
|
let saoToggleTime = 0, saoGPIO0State = false;
|
|
let saoPWMTime = 0, saoPWMDirection = true, saoPWMValue = 0;
|
|
let tvbgoneLastToggle = 0, tvbgoneOn = false;
|
|
|
|
function updateBackground(now) {
|
|
// Knight Rider sweep
|
|
if (now - knightLastUpdate > 80) {
|
|
knightLastUpdate = now;
|
|
const leds = [0, 0, 0, 0];
|
|
if (knightDir) {
|
|
knightPos++;
|
|
if (knightPos >= 4) { knightPos = 3; knightDir = false; }
|
|
} else {
|
|
knightPos--;
|
|
if (knightPos < 0) { knightPos = 1; knightDir = true; }
|
|
}
|
|
if (knightPos >= 0 && knightPos < 4) leds[knightPos] = 1;
|
|
|
|
document.getElementById('led1').classList.toggle('on', leds[0]);
|
|
document.getElementById('led2').classList.toggle('on', leds[1]);
|
|
document.getElementById('led3').classList.toggle('on', leds[2]);
|
|
document.getElementById('led4').classList.toggle('on', leds[3]);
|
|
}
|
|
|
|
// SAO GPIO0 - 1Hz toggle
|
|
if (now - saoToggleTime >= 1000) {
|
|
saoToggleTime = now;
|
|
saoGPIO0State = !saoGPIO0State;
|
|
document.getElementById('sao0').classList.toggle('on', saoGPIO0State);
|
|
}
|
|
|
|
// SAO GPIO1 - PWM bouncing 0-100% in 3 seconds
|
|
if (now - saoPWMTime >= 30) {
|
|
saoPWMTime = now;
|
|
if (saoPWMDirection) {
|
|
saoPWMValue++;
|
|
if (saoPWMValue >= 100) { saoPWMValue = 100; saoPWMDirection = false; }
|
|
} else {
|
|
saoPWMValue--;
|
|
if (saoPWMValue <= 0) { saoPWMValue = 0; saoPWMDirection = true; }
|
|
}
|
|
document.getElementById('sao1').style.setProperty('--p', saoPWMValue + '%');
|
|
}
|
|
|
|
// TV-B-Gone
|
|
if (currentMode === MODE_TVBGONE) {
|
|
if (now - tvbgoneLastToggle > 20) {
|
|
tvbgoneLastToggle = now;
|
|
tvbgoneOn = !tvbgoneOn;
|
|
document.getElementById('flashlight').classList.toggle('on', tvbgoneOn);
|
|
}
|
|
} else {
|
|
document.getElementById('flashlight').classList.remove('on');
|
|
}
|
|
}
|
|
|
|
// ==================== MENU ====================
|
|
function showMenu() {
|
|
ledMatrixClear();
|
|
const items = ["Tetris", "Snake", "Brick", "Pong", "TV-B-Gone"];
|
|
for (let i = 0; i < 5; i++) {
|
|
if (i === currentMenuItem) {
|
|
ledMatrixSetPixel(0, 1 + i * 2, 255);
|
|
ledMatrixSetPixel(1, 1 + i * 2, 150);
|
|
}
|
|
ledMatrixSetPixel(2, 1 + i * 2, (i === currentMenuItem) ? 255 : 80);
|
|
ledMatrixSetPixel(3, 1 + i * 2, 80);
|
|
}
|
|
}
|
|
|
|
function handleMenuInput() {
|
|
if (buttonPressed('up')) {
|
|
if (currentMenuItem > 0) currentMenuItem--;
|
|
clearFirstPress('up');
|
|
showMenu();
|
|
} else if (buttonPressed('down')) {
|
|
if (currentMenuItem < 4) currentMenuItem++;
|
|
clearFirstPress('down');
|
|
showMenu();
|
|
} else if (buttonPressed('a')) {
|
|
clearFirstPress('a');
|
|
const modes = [MODE_TETRIS, MODE_SNAKE, MODE_BRICK, MODE_PONG, MODE_TVBGONE];
|
|
currentMode = modes[currentMenuItem];
|
|
ledMatrixClear();
|
|
// Initialize the selected game
|
|
if (currentMode === MODE_TETRIS) tetris.reset();
|
|
else if (currentMode === MODE_SNAKE) snake.reset();
|
|
else if (currentMode === MODE_BRICK) brick.reset();
|
|
else if (currentMode === MODE_PONG) pong.reset();
|
|
}
|
|
}
|
|
|
|
// ==================== TETRIS ====================
|
|
const tetris = {
|
|
W: 7, H: 15,
|
|
field: [],
|
|
pieceX: 0, pieceY: 0, pieceType: 0, pieceRot: 0,
|
|
lastFall: 0, score: 0, gameOver: false,
|
|
|
|
pieces: [
|
|
[[0,1,0,0],[0,1,0,0],[0,1,0,0],[0,1,0,0]],
|
|
[[0,1,1,0],[0,1,0,0],[0,1,1,0],[0,0,0,0]],
|
|
[[0,1,1,0],[0,0,1,0],[0,1,1,0],[0,0,0,0]],
|
|
[[0,0,1,0],[0,0,1,0],[0,1,1,0],[0,0,0,0]],
|
|
[[0,1,0,0],[0,1,0,0],[0,1,1,0],[0,0,0,0]],
|
|
[[0,1,0,0],[0,1,0,0],[0,1,0,0],[0,1,0,0]],
|
|
[[0,0,0,0],[0,1,1,0],[0,1,1,0],[0,0,0,0]]
|
|
],
|
|
|
|
reset() {
|
|
this.field = [];
|
|
for (let y = 0; y < this.H; y++) {
|
|
this.field[y] = [];
|
|
for (let x = 0; x < this.W; x++) this.field[y][x] = 0;
|
|
}
|
|
this.score = 0;
|
|
this.gameOver = false;
|
|
this.newPiece();
|
|
},
|
|
|
|
newPiece() {
|
|
this.pieceType = Math.floor(Math.random() * 7);
|
|
this.pieceX = Math.floor(this.W / 2) - 2;
|
|
this.pieceY = 0;
|
|
this.pieceRot = 0;
|
|
},
|
|
|
|
getBlock(type, rot, x, y) {
|
|
const r = rot % 4;
|
|
const piece = this.pieces[type];
|
|
if (type === 0 && r === 0) {
|
|
if (y < 4 && x < 4) return ([1,1,1,1][y] || 0) && (x < 4);
|
|
}
|
|
if (y < piece.length && x < piece[y].length) return piece[y][x];
|
|
return 0;
|
|
},
|
|
|
|
canPlace(px, py, type, rot) {
|
|
for (let i = 0; i < 4; i++) {
|
|
for (let j = 0; j < 4; j++) {
|
|
if (this.getBlock(type, rot, j, i)) {
|
|
if (px + j < 0 || px + j >= this.W || py + i >= this.H || this.field[py + i][px + j]) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
},
|
|
|
|
lockPiece() {
|
|
for (let i = 0; i < 4; i++) {
|
|
for (let j = 0; j < 4; j++) {
|
|
if (this.getBlock(this.pieceType, this.pieceRot, j, i)) {
|
|
const fx = this.pieceX + j;
|
|
const fy = this.pieceY + i;
|
|
if (fy >= 0 && fy < this.H && fx >= 0 && fx < this.W) {
|
|
this.field[fy][fx] = (this.pieceType + 1) * 30;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
this.clearLines();
|
|
this.newPiece();
|
|
},
|
|
|
|
clearLines() {
|
|
for (let y = this.H - 1; y >= 0; y--) {
|
|
let full = true;
|
|
for (let x = 0; x < this.W; x++) {
|
|
if (!this.field[y][x]) { full = false; break; }
|
|
}
|
|
if (full) {
|
|
this.score += 100;
|
|
for (let yy = y; yy > 0; yy--) {
|
|
for (let x = 0; x < this.W; x++) this.field[yy][x] = this.field[yy - 1][x];
|
|
}
|
|
for (let x = 0; x < this.W; x++) this.field[0][x] = 0;
|
|
}
|
|
}
|
|
},
|
|
|
|
render() {
|
|
ledMatrixClear();
|
|
for (let y = 0; y < this.H && y < MATRIX_ROWS; y++) {
|
|
for (let x = 0; x < this.W && x < MATRIX_COLS; x++) {
|
|
if (this.field[y][x]) ledMatrixSetPixel(x, y, this.field[y][x]);
|
|
}
|
|
}
|
|
for (let i = 0; i < 4; i++) {
|
|
for (let j = 0; j < 4; j++) {
|
|
if (this.getBlock(this.pieceType, this.pieceRot, j, i)) {
|
|
const px = this.pieceX + j;
|
|
const py = this.pieceY + i;
|
|
if (py >= 0 && py < MATRIX_ROWS && px >= 0 && px < MATRIX_COLS) {
|
|
ledMatrixSetPixel(px, py, 255);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
update(now) {
|
|
if (this.gameOver) return;
|
|
if (now - this.lastFall > 500) {
|
|
this.lastFall = now;
|
|
if (this.canPlace(this.pieceX, this.pieceY + 1, this.pieceType, this.pieceRot)) {
|
|
this.pieceY++;
|
|
} else {
|
|
this.lockPiece();
|
|
if (!this.canPlace(this.pieceX, this.pieceY, this.pieceType, this.pieceRot)) {
|
|
this.gameOver = true;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
input() {
|
|
if (buttonPressed('left')) {
|
|
if (this.canPlace(this.pieceX - 1, this.pieceY, this.pieceType, this.pieceRot)) this.pieceX--;
|
|
clearFirstPress('left');
|
|
} else if (buttonPressed('right')) {
|
|
if (this.canPlace(this.pieceX + 1, this.pieceY, this.pieceType, this.pieceRot)) this.pieceX++;
|
|
clearFirstPress('right');
|
|
} else if (buttonPressed('up')) {
|
|
if (this.canPlace(this.pieceX, this.pieceY, this.pieceType, (this.pieceRot + 1) % 4)) this.pieceRot = (this.pieceRot + 1) % 4;
|
|
clearFirstPress('up');
|
|
} else if (buttonPressed('down')) {
|
|
if (this.canPlace(this.pieceX, this.pieceY + 1, this.pieceType, this.pieceRot)) this.pieceY++;
|
|
clearFirstPress('down');
|
|
} else if (buttonPressed('b')) {
|
|
clearFirstPress('b');
|
|
currentMode = MODE_MENU;
|
|
showMenu();
|
|
}
|
|
}
|
|
};
|
|
|
|
// ==================== SNAKE ====================
|
|
const snake = {
|
|
MAX_LEN: 30,
|
|
snakeX: [], snakeY: [], snakeLen: 3,
|
|
dirX: 0, dirY: 1, foodX: 0, foodY: 0,
|
|
lastMove: 0, score: 0, gameOver: false,
|
|
|
|
reset() {
|
|
this.snakeX = []; this.snakeY = [];
|
|
for (let i = 0; i < this.snakeLen; i++) {
|
|
this.snakeX[i] = 4;
|
|
this.snakeY[i] = 4 - i;
|
|
}
|
|
this.dirX = 0; this.dirY = 1;
|
|
this.score = 0;
|
|
this.gameOver = false;
|
|
this.spawnFood();
|
|
},
|
|
|
|
spawnFood() {
|
|
this.foodX = Math.floor(Math.random() * MATRIX_COLS);
|
|
this.foodY = Math.floor(Math.random() * MATRIX_ROWS);
|
|
for (let i = 0; i < this.snakeLen; i++) {
|
|
if (this.snakeX[i] === this.foodX && this.snakeY[i] === this.foodY) {
|
|
this.spawnFood();
|
|
return;
|
|
}
|
|
}
|
|
},
|
|
|
|
render() {
|
|
ledMatrixClear();
|
|
ledMatrixSetPixel(this.foodX, this.foodY, 255);
|
|
for (let i = 0; i < this.snakeLen; i++) {
|
|
ledMatrixSetPixel(this.snakeX[i], this.snakeY[i], i === 0 ? 255 : 150);
|
|
}
|
|
},
|
|
|
|
update(now) {
|
|
if (this.gameOver) return;
|
|
if (now - this.lastMove > 150) {
|
|
this.lastMove = now;
|
|
const headX = this.snakeX[0] + this.dirX;
|
|
const headY = this.snakeY[0] + this.dirY;
|
|
|
|
if (headX < 0 || headX >= MATRIX_COLS || headY < 0 || headY >= MATRIX_ROWS) {
|
|
this.gameOver = true;
|
|
return;
|
|
}
|
|
for (let i = 1; i < this.snakeLen; i++) {
|
|
if (this.snakeX[i] === headX && this.snakeY[i] === headY) {
|
|
this.gameOver = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
for (let i = this.snakeLen - 1; i > 0; i--) {
|
|
this.snakeX[i] = this.snakeX[i - 1];
|
|
this.snakeY[i] = this.snakeY[i - 1];
|
|
}
|
|
this.snakeX[0] = headX;
|
|
this.snakeY[0] = headY;
|
|
|
|
if (headX === this.foodX && headY === this.foodY) {
|
|
if (this.snakeLen < this.MAX_LEN) this.snakeLen++;
|
|
this.score += 10;
|
|
this.spawnFood();
|
|
}
|
|
}
|
|
},
|
|
|
|
input() {
|
|
if (buttonPressed('left')) {
|
|
if (this.dirX !== 1) { this.dirX = -1; this.dirY = 0; }
|
|
clearFirstPress('left');
|
|
} else if (buttonPressed('right')) {
|
|
if (this.dirX !== -1) { this.dirX = 1; this.dirY = 0; }
|
|
clearFirstPress('right');
|
|
} else if (buttonPressed('up')) {
|
|
if (this.dirY !== 1) { this.dirX = 0; this.dirY = -1; }
|
|
clearFirstPress('up');
|
|
} else if (buttonPressed('down')) {
|
|
if (this.dirY !== -1) { this.dirX = 0; this.dirY = 1; }
|
|
clearFirstPress('down');
|
|
} else if (buttonPressed('b')) {
|
|
clearFirstPress('b');
|
|
currentMode = MODE_MENU;
|
|
showMenu();
|
|
}
|
|
}
|
|
};
|
|
|
|
// ==================== BRICK BREAKER ====================
|
|
const brick = {
|
|
ROWS: 4, COLS: 5,
|
|
bricks: [],
|
|
ballX: 4, ballY: 4, ballVX: 1, ballVY: -1,
|
|
paddleX: 4, lastUpdate: 0, score: 0, gameOver: false,
|
|
|
|
reset() {
|
|
this.bricks = [];
|
|
for (let r = 0; r < this.ROWS; r++) {
|
|
this.bricks[r] = [];
|
|
for (let c = 0; c < this.COLS; c++) {
|
|
this.bricks[r][c] = (r + 1) * 40;
|
|
}
|
|
}
|
|
this.ballX = 4; this.ballY = 4; this.ballVX = 1; this.ballVY = -1;
|
|
this.paddleX = 4;
|
|
this.score = 0;
|
|
this.gameOver = false;
|
|
},
|
|
|
|
render() {
|
|
ledMatrixClear();
|
|
for (let r = 0; r < this.ROWS; r++) {
|
|
for (let c = 0; c < this.COLS; c++) {
|
|
if (this.bricks[r][c]) {
|
|
ledMatrixSetPixel(c, r + 1, this.bricks[r][c]);
|
|
}
|
|
}
|
|
}
|
|
ledMatrixSetPixel(this.ballX, this.ballY, 255);
|
|
ledMatrixSetPixel(this.paddleX, 8, 255);
|
|
if (this.paddleX > 0) ledMatrixSetPixel(this.paddleX - 1, 8, 100);
|
|
if (this.paddleX < 8) ledMatrixSetPixel(this.paddleX + 1, 8, 100);
|
|
},
|
|
|
|
update(now) {
|
|
if (this.gameOver) return;
|
|
if (now - this.lastUpdate > 200) {
|
|
this.lastUpdate = now;
|
|
this.ballX += this.ballVX;
|
|
this.ballY += this.ballVY;
|
|
|
|
if (this.ballX < 0 || this.ballX >= MATRIX_COLS) {
|
|
this.ballVX = -this.ballVX;
|
|
this.ballX += this.ballVX;
|
|
}
|
|
if (this.ballY < 0) {
|
|
this.ballVY = -this.ballVY;
|
|
this.ballY += this.ballVY;
|
|
}
|
|
if (this.ballY === 8) {
|
|
if (Math.abs(this.ballX - this.paddleX) <= 1) {
|
|
this.ballVY = -this.ballVY;
|
|
this.score += 10;
|
|
} else {
|
|
this.gameOver = true;
|
|
}
|
|
}
|
|
|
|
const by = this.ballY;
|
|
const bx = this.ballX;
|
|
if (by >= 1 && by <= this.ROWS && bx >= 0 && bx < this.COLS) {
|
|
if (this.bricks[by - 1][bx]) {
|
|
this.bricks[by - 1][bx] = 0;
|
|
this.ballVY = -this.ballVY;
|
|
this.score += 10;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
input() {
|
|
if (buttonPressed('left')) {
|
|
if (this.paddleX > 0) this.paddleX--;
|
|
clearFirstPress('left');
|
|
} else if (buttonPressed('right')) {
|
|
if (this.paddleX < 8) this.paddleX++;
|
|
clearFirstPress('right');
|
|
} else if (buttonPressed('b')) {
|
|
clearFirstPress('b');
|
|
currentMode = MODE_MENU;
|
|
showMenu();
|
|
}
|
|
}
|
|
};
|
|
|
|
// ==================== PONG ====================
|
|
const pong = {
|
|
leftY: 4, rightY: 4,
|
|
ballX: 4, ballY: 4, ballVX: 1, ballVY: 1,
|
|
leftScore: 0, rightScore: 0,
|
|
lastUpdate: 0, gameOver: false,
|
|
|
|
reset() {
|
|
this.leftY = 4; this.rightY = 4;
|
|
this.ballX = 4; this.ballY = 4;
|
|
this.ballVX = 1;
|
|
this.ballVY = Math.random() > 0.5 ? 1 : -1;
|
|
this.leftScore = 0; this.rightScore = 0;
|
|
this.gameOver = false;
|
|
},
|
|
|
|
render() {
|
|
ledMatrixClear();
|
|
ledMatrixSetPixel(0, this.leftY, 255);
|
|
ledMatrixSetPixel(8, this.rightY, 255);
|
|
ledMatrixSetPixel(this.ballX, this.ballY, 255);
|
|
},
|
|
|
|
update(now) {
|
|
if (this.gameOver) return;
|
|
if (now - this.lastUpdate > 200) {
|
|
this.lastUpdate = now;
|
|
this.ballX += this.ballVX;
|
|
this.ballY += this.ballVY;
|
|
|
|
if (this.ballY < 0 || this.ballY >= MATRIX_ROWS) {
|
|
this.ballVY = -this.ballVY;
|
|
}
|
|
|
|
if (this.ballX === 1 && Math.abs(this.ballY - this.leftY) <= 1) {
|
|
this.ballVX = -this.ballVX;
|
|
this.leftScore++;
|
|
}
|
|
if (this.ballX === 7 && Math.abs(this.ballY - this.rightY) <= 1) {
|
|
this.ballVX = -this.ballVX;
|
|
this.rightScore++;
|
|
}
|
|
|
|
if (this.ballX < 0 || this.ballX > 8) {
|
|
this.gameOver = true;
|
|
}
|
|
}
|
|
},
|
|
|
|
input() {
|
|
if (buttonPressed('left')) {
|
|
if (this.leftY > 0) this.leftY--;
|
|
clearFirstPress('left');
|
|
} else if (buttonPressed('right')) {
|
|
if (this.rightY > 0) this.rightY--;
|
|
clearFirstPress('right');
|
|
} else if (buttonPressed('up')) {
|
|
if (this.rightY > 0) this.rightY--;
|
|
clearFirstPress('up');
|
|
} else if (buttonPressed('down')) {
|
|
if (this.rightY < 8) this.rightY++;
|
|
clearFirstPress('down');
|
|
} else if (buttonPressed('a')) {
|
|
if (this.leftY < 8) this.leftY++;
|
|
clearFirstPress('a');
|
|
} else if (buttonPressed('b')) {
|
|
clearFirstPress('b');
|
|
currentMode = MODE_MENU;
|
|
showMenu();
|
|
}
|
|
}
|
|
};
|
|
|
|
// ==================== IDLE SCREEN ====================
|
|
let idleMessage = "";
|
|
let idleScrollX = MATRIX_COLS;
|
|
let idleScrollTime = 0;
|
|
|
|
function showIdleScreen() {
|
|
ledMatrixClear();
|
|
idleMessage = RANDOM_MESSAGES[Math.floor(Math.random() * RANDOM_MESSAGES.length)];
|
|
idleScrollX = MATRIX_COLS;
|
|
currentMode = MODE_IDLE;
|
|
lastActivity = Date.now();
|
|
}
|
|
|
|
function updateIdleScroll(now) {
|
|
if (currentMode !== MODE_IDLE) return;
|
|
if (now - idleScrollTime > 100) {
|
|
idleScrollTime = now;
|
|
idleScrollX--;
|
|
if (idleScrollX < -idleMessage.length * 4) {
|
|
idleScrollX = MATRIX_COLS;
|
|
idleMessage = RANDOM_MESSAGES[Math.floor(Math.random() * RANDOM_MESSAGES.length)];
|
|
}
|
|
ledMatrixClear();
|
|
drawText(idleMessage, idleScrollX, 3, 150);
|
|
}
|
|
}
|
|
|
|
// ==================== MAIN LOOP ====================
|
|
let lastModeSwitch = 0;
|
|
|
|
function gameLoop() {
|
|
const now = Date.now();
|
|
|
|
updateBackground(now);
|
|
updateBackground(now); // Call twice for faster updates
|
|
|
|
if (anyButtonPressed()) {
|
|
if (currentMode === MODE_IDLE) {
|
|
currentMode = MODE_MENU;
|
|
showMenu();
|
|
}
|
|
recordActivity();
|
|
}
|
|
|
|
if (now - lastActivity > IDLE_TIMEOUT) {
|
|
if (currentMode !== MODE_IDLE && currentMode !== MODE_TVBGONE) {
|
|
showIdleScreen();
|
|
}
|
|
}
|
|
|
|
switch (currentMode) {
|
|
case MODE_MENU:
|
|
handleMenuInput();
|
|
break;
|
|
case MODE_TETRIS:
|
|
tetris.input();
|
|
tetris.update(now);
|
|
tetris.render();
|
|
break;
|
|
case MODE_SNAKE:
|
|
snake.input();
|
|
snake.update(now);
|
|
snake.render();
|
|
break;
|
|
case MODE_BRICK:
|
|
brick.input();
|
|
brick.update(now);
|
|
brick.render();
|
|
break;
|
|
case MODE_PONG:
|
|
pong.input();
|
|
pong.update(now);
|
|
pong.render();
|
|
break;
|
|
case MODE_IDLE:
|
|
updateIdleScroll(now);
|
|
break;
|
|
case MODE_TVBGONE:
|
|
if (buttonPressed('b')) {
|
|
clearFirstPress('b');
|
|
currentMode = MODE_MENU;
|
|
showMenu();
|
|
}
|
|
break;
|
|
}
|
|
|
|
clearAllFirstPress();
|
|
renderMatrix();
|
|
|
|
requestAnimationFrame(gameLoop);
|
|
}
|
|
|
|
function anyButtonPressed() {
|
|
for (let k in buttonStates) {
|
|
if (buttonStates[k]) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Start
|
|
window.onload = init;
|
|
</script>
|
|
</body>
|
|
</html>
|