From d38c3cacaca45bc650897e8e56e52756272a84d4 Mon Sep 17 00:00:00 2001 From: Pabloader Date: Mon, 19 Aug 2024 14:37:20 +0000 Subject: [PATCH] OLC CodeJam 2024 entry --- build/build.ts | 4 +- build/html.ts | 9 +- build/server.ts | 13 +- bun.lockb | Bin 28841 -> 29555 bytes package.json | 1 + src/common/display/assets/brick.module.css | 8 + src/common/display/brick.tsx | 77 ++++++- src/common/input.ts | 14 +- src/games/brick-dungeon/data.ts | 2 +- src/games/olc-run-2024/assets/figures.png | Bin 0 -> 158 bytes src/games/olc-run-2024/assets/fill.ogg | Bin 0 -> 12067 bytes src/games/olc-run-2024/assets/place.ogg | Bin 0 -> 6090 bytes src/games/olc-run-2024/index.tsx | 229 +++++++++++++++++++++ 13 files changed, 338 insertions(+), 19 deletions(-) create mode 100644 src/games/olc-run-2024/assets/figures.png create mode 100644 src/games/olc-run-2024/assets/fill.ogg create mode 100644 src/games/olc-run-2024/assets/place.ogg create mode 100644 src/games/olc-run-2024/index.tsx diff --git a/build/build.ts b/build/build.ts index 20ae4df..85a51fc 100644 --- a/build/build.ts +++ b/build/build.ts @@ -13,13 +13,13 @@ let game = process.argv[2]; const publish = process.env.PUBLISH_LOCATION; while (!await isGame(game)) { - const game = await select({ + game = await select({ message: 'Game to build:', choices: (await getGames()).map(value => ({ value })), }); } -const html = await buildHTML(game, true); +const html = await buildHTML(game, { production: true }); if (!html) { process.exit(1); diff --git a/build/html.ts b/build/html.ts index 6194399..71fa9d3 100644 --- a/build/html.ts +++ b/build/html.ts @@ -10,7 +10,12 @@ import lightningcss from 'bun-lightningcss'; import { getGames } from './isGame'; import audioPlugin from './audioPlugin'; -export async function buildHTML(game: string, production = false, portable = false) { +interface Args { + production?: boolean; + portable?: boolean; + mobile?: boolean +} +export async function buildHTML(game: string, { production = false, portable = false, mobile = false }: Args = {}) { const html = await Bun.file(path.resolve(import.meta.dir, 'assets', 'index.html')).text(); const bundle = await Bun.build({ outdir: '/tmp', @@ -62,7 +67,7 @@ export async function buildHTML(game: string, production = false, portable = fal } else { script = minifyResult.code; } - } else { + } else if (mobile) { const eruda = await Bun.file(path.resolve(import.meta.dir, '..', 'node_modules', 'eruda', 'eruda.js')).text(); scriptPrefix = ``; } diff --git a/build/server.ts b/build/server.ts index f56c67f..c637e02 100644 --- a/build/server.ts +++ b/build/server.ts @@ -1,5 +1,6 @@ import Bun from 'bun'; import path from 'path'; +import browser from 'browser-detect'; import { buildHTML } from './html'; import { isGame } from './isGame'; @@ -15,10 +16,14 @@ Bun.serve({ case '/': case 'index.html': try { + const detectedBrowser = browser(req.headers.get('user-agent') ?? ''); const html = await buildHTML( - game, - url.searchParams.get('production') === 'true', // to debug production builds - url.searchParams.get('portable') === 'true', // to skip AssemblyScript compilation + game, + { + production: url.searchParams.get('production') === 'true', // to debug production builds + portable: url.searchParams.get('portable') === 'true', // to skip AssemblyScript compilation, + mobile: detectedBrowser.mobile, + } ); if (html) { return new Response(html, { @@ -33,7 +38,7 @@ Bun.serve({ } return new Response(`Error building HTML`, { status: 500 }); default: - console.log(`Pathname: ${pathname}`); + console.log(`[Dev Server] requested unknown pathname: ${pathname}`); return new Response(null, { status: 404 }); } } diff --git a/bun.lockb b/bun.lockb index 45cc0dff0fec7707b071fe2384e5dbe3b9876cbb..a12c8c06747e325dfc5e87447328772d6257ed40 100755 GIT binary patch delta 4414 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⪼e zkOPzjLJl{&Ks+p-3U|-b;OTI8 zOswZs=VjwI)9i3}FZyU)#V_cSibuxqh8TsH7R%P*_E&xoG3R6;TDlzB4qnh2%3Pqy zIx1SA*_VOGf_9{K-#J*jq12_wT!+(<1G5d(yufKUFUVb09izKuWo=gEPYd7%KZ6lc_$Kr@4s^t~fq$TvX4<(>vzyoO-lU>`Eh*d82P^oB=+pvu$<21;xEuv_)C(_0k9PCMy{~FtSFo?Po~;}OJmNM= zSg1*(N4t4Xf4{Qvr1Has9k6maa(LXk=*q&BQ8_}GZ-S}|UyWJcoal#1o+Fn{j7iBW z^earwcp+c`2bRl2EkBvj|DYqyZoVXH)(K(b8y2BP+*a!+=--(i)?Z=ZL86wk5<-bZu4$GHTIPY6RoG;yZ4Ft3fMc4 zweJ13AM#SUFm`lyg9a8kN0_gPLwyJOCteX%&_m1mfoLkLvLQMes+@N7k#WY=dg<%_ zmj-v&Fbh&Q^yZ`DaD2Q{wC>1ra04y7L3XLu|xu@Aio$zeFNe*V!pS-wrg}A5h zq4VhfDt!qn^GWmE`MVdMU3qrtJu4ff`!u)t@^J2|U*Frg2eU{S_Tby(%c4NwquhC_ zF>;q9zaX!aR`@hz&n-SDUKz)In!|kWG{1T_p>1o~D~a+Hyr>8bP`S=2P&`U;QqvCu86|W|8g4<3KjLQO>2%6MmcBip0pn z-P`x-y4}xw+^ER5M;+LPY?%GrAC9@0bDYjmcGS>m|1?Uh&6ed ))} @@ -209,7 +215,7 @@ export class BrickDisplay {
))} @@ -224,6 +230,9 @@ export class BrickDisplay {
Level
+
+ {this.helpText} +
Pause
Game over
@@ -282,4 +291,64 @@ export class BrickDisplay { return result; } + + static autoCrop(image: BrickDisplayImage): BrickDisplayImage { + let minX = image.width; + let minY = image.height; + let maxX = 0; + let maxY = 0; + + for (let y = 0; y < image.height; y++) { + for (let x = 0; x < image.width; x++) { + const i = y * image.width + x; + if (image.image[i]) { + minX = Math.min(minX, x); + maxX = Math.max(maxX, x); + minY = Math.min(minY, y); + maxY = Math.max(maxY, y); + } + } + } + + return this.extractSprite(image, minX, minY, maxX - minX + 1, maxY - minY + 1); + } + + static copySprite(image: BrickDisplayImage): BrickDisplayImage { + return this.extractSprite(image, 0, 0, image.width, image.height); + } + + static rotateSprite(image: BrickDisplayImage, angle: 0 | 90 | 180 | 270): BrickDisplayImage { + if (angle === 0) return this.copySprite(image); + + const newImage: BrickDisplayImage = { + image: new Array(image.width * image.height), + width: angle === 180 ? image.width : image.height, + height: angle === 180 ? image.height : image.width, + } + + for (let j = 0; j < image.height; j++) { + for (let i = 0; i < image.width; i++) { + const originalPixel = image.image[j * image.width + i]; + let x = i; + let y = j; + + if (angle === 90) { + const tmp = y; + y = x; + x = image.height - tmp - 1; + } else if (angle === 180) { + x = image.width - x - 1; + y = image.height - y - 1; + } else if (angle === 270) { + const tmp = x; + x = y; + y = image.width - tmp - 1; + } + + newImage.image[y * newImage.width + x] = originalPixel + } + } + + return newImage; + } } \ No newline at end of file diff --git a/src/common/input.ts b/src/common/input.ts index d8c6303..a0a9740 100644 --- a/src/common/input.ts +++ b/src/common/input.ts @@ -6,10 +6,12 @@ interface IKeyState { pressed?: boolean; held?: boolean; } -const KEYS: Record = {}; +type KeyCode = 'ArrowUp' | 'ArrowDown' | 'ArrowLeft' | 'ArrowRight' | 'Space'; + +const KEYS: Partial> = {}; document.body.addEventListener('keydown', (e) => { - const keyId = e.code; + const keyId = e.code as KeyCode; console.debug(`[Input] Pressed ${keyId}`); if (KEYS[keyId]) { KEYS[keyId].state = true; @@ -19,16 +21,16 @@ document.body.addEventListener('keydown', (e) => { }); document.body.addEventListener('keyup', (e) => { - const keyId = e.code; + const keyId = e.code as KeyCode; console.debug(`[Input] Released ${keyId}`); if (KEYS[keyId]) { KEYS[keyId].state = false; } }); -export const isPressed = (key: string): boolean => KEYS[key]?.pressed ?? false; -export const isReleased = (key: string): boolean => KEYS[key]?.released ?? false; -export const isHeld = (key: string): boolean => KEYS[key]?.held ?? false; +export const isPressed = (key: KeyCode): boolean => KEYS[key]?.pressed ?? false; +export const isReleased = (key: KeyCode): boolean => KEYS[key]?.released ?? false; +export const isHeld = (key: KeyCode): boolean => KEYS[key]?.held ?? false; export function updateKeys() { for (const key of Object.values(KEYS)) { diff --git a/src/games/brick-dungeon/data.ts b/src/games/brick-dungeon/data.ts index 1332a4d..ae3c9d7 100644 --- a/src/games/brick-dungeon/data.ts +++ b/src/games/brick-dungeon/data.ts @@ -1,4 +1,4 @@ -import { BrickDisplay, type BrickDisplayImage } from "@common/display"; +import { BrickDisplay, type BrickDisplayImage } from "@common/display/brick"; import { choice, shuffle } from "@common/utils"; const emptySprite: BrickDisplayImage = { image: [], width: 0, height: 0 }; diff --git a/src/games/olc-run-2024/assets/figures.png b/src/games/olc-run-2024/assets/figures.png new file mode 100644 index 0000000000000000000000000000000000000000..6b75daea44d090c2c7921263fcf6ee752a1010c7 GIT binary patch literal 158 zcmeAS@N?(olHy`uVBq!ia0vp^LO{&M!3HGHb)LHgq!^2X+?^QKos)S9-Q;C-_%(b!Q6VJ?}7-!i2|cJ3@dczpFg70=De%&_VekR zP2%6aWnH(=DS2+x+M-k6_-=_m@!EF&gv8;Wtq=9Bp6+Eoo|k7U=&L^gXa|F*tDnm{ Hr-UW|p*lN7 literal 0 HcmV?d00001 diff --git a/src/games/olc-run-2024/assets/fill.ogg b/src/games/olc-run-2024/assets/fill.ogg new file mode 100644 index 0000000000000000000000000000000000000000..bd54c6639dd0f9a03742dd8aaf710d08ff0ef590 GIT binary patch literal 12067 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 I%nX441J%m(*8l(j literal 0 HcmV?d00001 diff --git a/src/games/olc-run-2024/index.tsx b/src/games/olc-run-2024/index.tsx new file mode 100644 index 0000000..bb53342 --- /dev/null +++ b/src/games/olc-run-2024/index.tsx @@ -0,0 +1,229 @@ +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'; + +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; +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; + } + } +} + +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; +} + +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); + } + } +} + +async function loop() { + 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); +} \ No newline at end of file