import { BrickDisplay, type BrickDisplayImage } from "@common/display/brick"; import { choice, delay, randInt, range } from "@common/utils"; import figuresImage from './assets/figures.png'; import placeSound from './assets/place.ogg'; import fillSound from './assets/fill.ogg'; import wrongSound from './assets/wrong.ogg'; let display: BrickDisplay; const field: BrickDisplayImage = { image: [], width: 8, height: 8, }; const figuresSpritesheet = BrickDisplay.convertImage(figuresImage); const figures = extractFigures(figuresSpritesheet); let currentFigure: BrickDisplayImage = generateFigure(); let nextFigure: BrickDisplayImage = generateFigure(); let currentFigureX = 4; let currentFigureY = 14; let currentFigureBlink: boolean; let frame: number = 0; const rowsToClear = new Set(); const colsToClear = new Set(); function extractFigures(spritesheet: BrickDisplayImage): BrickDisplayImage[] { const figures: BrickDisplayImage[] = []; for (let j = 0; j < spritesheet.height; j += 3) { for (let i = 0; i < spritesheet.width; i += 3) { const figure = BrickDisplay.extractSprite(spritesheet, i, j, 3, 3); figures.push(BrickDisplay.autoCrop(figure)); } } return figures; } function generateFigure() { return BrickDisplay.rotateSprite(choice(figures), randInt(0, 4) * 90 as any); } function tryToPlace() { if (canPlaceFigure(currentFigure, currentFigureX - 1, currentFigureY - 1)) { let pixelsCount = 0; for (let y = currentFigureY; y < currentFigureY + currentFigure.height; y++) { for (let x = currentFigureX; x < currentFigureX + currentFigure.width; x++) { const fieldX = x - 1; const fieldY = y - 1; const figureX = x - currentFigureX; const figureY = y - currentFigureY; const px = currentFigure.image[figureY * currentFigure.width + figureX]; if (px) { pixelsCount++; field.image[fieldY * field.width + fieldX] = true; } } } for (let y = currentFigureY; y < currentFigureY + currentFigure.height; y++) { let lineFilled = true; for (let fieldX = 0; fieldX < field.width; fieldX++) { const fieldY = y - 1; const px = field.image[fieldY * field.width + fieldX]; if (!px) { lineFilled = false; break; } } if (lineFilled) { rowsToClear.add(y - 1); } } for (let x = currentFigureX; x < currentFigureX + currentFigure.width; x++) { let lineFilled = true; for (let fieldY = 0; fieldY < field.height; fieldY++) { const fieldX = x - 1; const px = field.image[fieldY * field.width + fieldX]; if (!px) { lineFilled = false; break; } } if (lineFilled) { colsToClear.add(x - 1); } } if (rowsToClear.size > 0 || colsToClear.size > 0) { fillSound.currentTime = 0; fillSound.play(); } else { placeSound.currentTime = 0; placeSound.play(); } currentFigure = nextFigure; nextFigure = generateFigure() currentFigureX = 4; currentFigureY = 14; display.score += pixelsCount; if (!canPlaceAnywhere(currentFigure)) { display.gameOver = true; wrongSound.currentTime = 0; wrongSound.play(); } } else { wrongSound.currentTime = 0; wrongSound.play(); } } function canPlaceFigure(figure: BrickDisplayImage, checkX: number, checkY: number): boolean { for (let y = checkY; y < checkY + figure.height; y++) { for (let x = checkX; x < checkX + figure.width; x++) { if (colsToClear.has(x) || rowsToClear.has(y)) continue; const figureX = x - checkX; const figureY = y - checkY; const figurePx = figure.image[figureY * figure.width + figureX]; const fieldPx = field.image[y * field.width + x]; if (figurePx && fieldPx) { return false; } } } return true; } function canPlaceAnywhere(figure: BrickDisplayImage): boolean { for (let y = 0; y <= field.height - figure.height; y++) { for (let x = 0; x <= field.width - figure.width; x++) { if (range(4).some(i => canPlaceFigure(BrickDisplay.rotateSprite(figure, (i * 90) as any), x, y))) { return true; } } } return false; } function reset() { currentFigure = generateFigure(); nextFigure = generateFigure(); currentFigureX = 4; currentFigureY = 14; field.image = []; display.score = 0; display.gameOver = false; } function onKeyDown(e: KeyboardEvent) { if (rowsToClear.size > 0 || colsToClear.size > 0) { return; } const isUp = e.code === 'ArrowUp' || e.code === 'KeyW'; const isDown = e.code === 'ArrowDown' || e.code === 'KeyS'; const isLeft = e.code === 'ArrowLeft' || e.code === 'KeyA'; const isRight = e.code === 'ArrowRight' || e.code === 'KeyD'; if (display.gameOver) { if (e.code === 'Space') { reset(); } } else if (isUp && currentFigureY > 1) { currentFigureY--; } else if (isDown && currentFigureY < (20 - currentFigure.height)) { currentFigureY++; } else if (isLeft && currentFigureX > 1) { currentFigureX--; } else if (isRight && currentFigureX < (9 - currentFigure.width)) { currentFigureX++; } else if (e.code === 'Space') { if (currentFigureY <= (9 - currentFigure.height)) { tryToPlace(); } else { currentFigure = BrickDisplay.rotateSprite(currentFigure, 90); } } e.stopPropagation(); e.preventDefault(); return false; } async function loop() { frame++; if (frame % 6 === 0) { currentFigureBlink = !currentFigureBlink; } if (rowsToClear.size > 0 || colsToClear.size > 0) { display.score += Math.pow(rowsToClear.size + colsToClear.size, 2) * 100; for (let i = 0; i < field.width; i++) { for (const y of rowsToClear) { field.image[y * field.width + i] = false; } for (const x of colsToClear) { field.image[i * field.width + x] = false; } display.fillRect(1, 1, field.width, field.height, false); display.drawImage(field, 1, 1); display.update(); await delay(20); } rowsToClear.clear(); colsToClear.clear(); } display.clear(); display.drawRect(0, 0, 9, 9); display.drawImage(field, 1, 1); if (currentFigureBlink) { display.drawImage(currentFigure, currentFigureX, currentFigureY, false, true); } display.clear(true); display.drawImage(nextFigure, 0, 0, true); display.update(); requestAnimationFrame(loop); } export default async function main() { display = new BrickDisplay(); display.init(); display.helpText = <>
Try not to
Run Out Of Space
Arrows - move
Space - rotate on bottom
Space - place on top
; document.addEventListener('keydown', onKeyDown); requestAnimationFrame(loop); }