242 lines
7.5 KiB
TypeScript
242 lines
7.5 KiB
TypeScript
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<number>();
|
|
const colsToClear = new Set<number>();
|
|
|
|
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 = <>
|
|
<div>Try not to</div>
|
|
<div>Run Out Of Space</div>
|
|
<div>Arrows - move</div>
|
|
<div>Space - rotate on bottom</div>
|
|
<div>Space - place on top</div>
|
|
</>;
|
|
|
|
document.addEventListener('keydown', onKeyDown);
|
|
requestAnimationFrame(loop);
|
|
} |