Secfest2026BadgeCode/rp2040_badge_primary/js/index.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>