diff --git a/build/assets/include/graphics.h b/build/assets/include/graphics.h index 9c59b95..e0bc1cc 100644 --- a/build/assets/include/graphics.h +++ b/build/assets/include/graphics.h @@ -29,8 +29,8 @@ void image_draw_point(image_data_t image, int16_t x, int16_t y, image_color_t co void image_draw_hline(image_data_t image, int16_t x1, int16_t x2, int16_t y, image_color_t color); void image_draw_vline(image_data_t image, int16_t x, int16_t y1, int16_t y2, image_color_t color); void image_draw_line(image_data_t image, int16_t x1, int16_t y1, int16_t x2, int16_t y2, image_color_t color); -void image_draw_rect(image_data_t image, int16_t x, int16_t y, uint16_t w, uint16_t h, image_color_t color); -void image_fill_rect(image_data_t image, int16_t x, int16_t y, uint16_t w, uint16_t h, image_color_t color); +void image_draw_rect(image_data_t image, int16_t x, int16_t y, int16_t w, int16_t h, image_color_t color); +void image_fill_rect(image_data_t image, int16_t x, int16_t y, int16_t w, int16_t h, image_color_t color); #ifdef __cplusplus } diff --git a/build/assets/index-itch.html b/build/assets/index-local.html similarity index 100% rename from build/assets/index-itch.html rename to build/assets/index-local.html diff --git a/build/assets/lib/graphics.c b/build/assets/lib/graphics.c index 6b79659..433adf9 100644 --- a/build/assets/lib/graphics.c +++ b/build/assets/lib/graphics.c @@ -1,12 +1,13 @@ #include #include +#include #include -static inline int max(int a, int b) { +static inline int32_t max(int32_t a, int32_t b) { return a > b ? a : b; } -static inline int min(int a, int b) { +static inline int32_t min(int32_t a, int32_t b) { return a < b ? a : b; } @@ -28,7 +29,7 @@ void image_free(image_data_t image) { } } -static inline void set_point(image_data_t image, uint16_t x, uint16_t y, image_color_t color) { +static inline void set_point(image_data_t image, int16_t x, int16_t y, image_color_t color) { image.pixels[x + y * image.width] = color; } @@ -44,15 +45,15 @@ void image_draw_hline(image_data_t image, int16_t x1, int16_t x2, int16_t y, ima return; } if (x1 > x2) { - uint16_t temp = x1; + int16_t temp = x1; x1 = x2; x2 = temp; } if (x1 > image.width || x2 < 0) { return; } - x1 = max(0, x1); - x2 = min(x2, image.width); + x1 = (int16_t)max(0, x1); + x2 = (int16_t)min(x2, image.width); do { set_point(image, x1++, y, color); @@ -64,15 +65,15 @@ void image_draw_vline(image_data_t image, int16_t x, int16_t y1, int16_t y2, ima return; } if (y1 > y2) { - uint16_t temp = y1; + int16_t temp = y1; y1 = y2; y2 = temp; } if (y1 > image.width || y2 < 0) { return; } - y1 = max(0, y1); - y2 = min(y2, image.height); + y1 = (int16_t)max(0, y1); + y2 = (int16_t)min(y2, image.height); do { set_point(image, x, y1++, color); @@ -110,13 +111,13 @@ void image_draw_line(image_data_t image, int16_t x1, int16_t y1, int16_t x2, int } } -void image_draw_rect(image_data_t image, int16_t x, int16_t y, uint16_t w, uint16_t h, image_color_t color) { - if (w == 0 || h == 0) { +void image_draw_rect(image_data_t image, int16_t x, int16_t y, int16_t w, int16_t h, image_color_t color) { + if (w <= 0 || h <= 0) { return; } - w = min(image.width - x, w); - h = min(image.height - y, h); + w = (int16_t)min(image.width - x, w); + h = (int16_t)min(image.height - y, h); image_draw_hline(image, x, x + w - 1, y, color); image_draw_hline(image, x, x + w, y + h - 1, color); @@ -125,14 +126,14 @@ void image_draw_rect(image_data_t image, int16_t x, int16_t y, uint16_t w, uint1 image_draw_vline(image, x + w - 1, y, y + h - 1, color); } -void image_fill_rect(image_data_t image, int16_t x, int16_t y, uint16_t w, uint16_t h, image_color_t color) { - if (w == 0 || h == 0) { +void image_fill_rect(image_data_t image, int16_t x, int16_t y, int16_t w, int16_t h, image_color_t color) { + if (w <= 0 || h <= 0) { return; } - x = max(0, x); - y = max(0, y); - w = min(image.width - x, w); - h = min(image.height - y, h); + x = (int16_t)max(0, x); + y = (int16_t)max(0, y); + w = (int16_t)min(image.width - x, w); + h = (int16_t)min(image.height - y, h); while (h--) { image_draw_hline(image, x, x + w, y++, color); diff --git a/build/build.ts b/build/build.ts index f8e7a59..9438d75 100644 --- a/build/build.ts +++ b/build/build.ts @@ -12,7 +12,7 @@ await fs.mkdir(outDir, { recursive: true }); const publish = process.env.PUBLISH_LOCATION; const args = process.argv.slice(2); -const itch = args.includes('--itch'); +const local = args.includes('--local'); let game = args.find(a => !a.startsWith('-')) ?? ''; while (!await isGame(game)) { @@ -22,7 +22,7 @@ while (!await isGame(game)) { }); } console.log(`Building ${game}...`); -const html = await buildHTML(game, { production: true, itch }); +const html = await buildHTML(game, { production: true, local }); if (!html) { process.exit(1); @@ -30,7 +30,7 @@ if (!html) { const filePath = path.resolve(outDir, `${game}.html`); await Bun.write(filePath, html); -if (publish && !itch) { +if (publish && !local) { console.log(`Publishing ${game}...`); const result = await $`scp "${filePath}" "${publish}${game}.html"`; if (result.exitCode === 0) { diff --git a/build/html.ts b/build/html.ts index b24b4b0..fa1c496 100644 --- a/build/html.ts +++ b/build/html.ts @@ -1,3 +1,4 @@ +import type { BunFile } from 'bun'; import path from 'path'; import { minify } from 'html-minifier'; import UglifyJS from 'uglify-js'; @@ -10,17 +11,27 @@ import filePlugin from './filePlugin'; import { getGames } from './isGame'; +const b64 = async (file: string | BunFile) => ( + typeof file === 'string' + ? Buffer.from(file) + : Buffer.from(await file.arrayBuffer()) +).toString('base64'); + interface Args { production?: boolean; - portable?: boolean; mobile?: boolean; - itch?: boolean; + local?: boolean; } -export async function buildHTML(game: string, { production = false, portable = false, mobile = false, itch = false }: Args = {}) { - const html = await Bun.file(path.resolve(import.meta.dir, 'assets', itch ? 'index-itch.html' : 'index.html')).text(); +export async function buildHTML(game: string, { production = false, mobile = false, local = false }: Args = {}) { + const assetsDir = path.resolve(import.meta.dir, 'assets'); + const srcDir = path.resolve(import.meta.dir, '..', 'src'); + const gameDir = path.resolve(srcDir, 'games', game); + const gameAssetsDir = path.resolve(gameDir, 'assets'); + + const html = await Bun.file(path.resolve(assetsDir, local ? 'index-local.html' : 'index.html')).text(); const bundle = await Bun.build({ outdir: '/tmp', - entrypoints: [path.resolve(import.meta.dir, '..', 'src', 'index.ts')], + entrypoints: [path.resolve(srcDir, 'index.ts')], sourcemap: production ? 'none' : 'inline', define: { global: 'window', @@ -31,7 +42,7 @@ export async function buildHTML(game: string, { production = false, portable = f imagePlugin, audioPlugin, fontPlugin, - wasmPlugin({ production, portable }), + wasmPlugin({ production }), filePlugin, ] }); @@ -42,24 +53,24 @@ export async function buildHTML(game: string, { production = false, portable = f console.error('No entry point found:', bundle.outputs); return; } - const iconFile = Bun.file(path.resolve(import.meta.dir, '..', 'src', 'games', game, 'assets', 'favicon.ico')); + const iconFile = Bun.file(path.resolve(gameAssetsDir, 'favicon.ico')); let icon = ''; if (await iconFile.exists()) { - icon = ``; + icon = ``; } let style = ''; - let styleFile = bundle.outputs.find(a => a.kind === 'asset' && a.path.endsWith('.css')); - if (styleFile) { - style = await styleFile.text(); + let styleFiles = bundle.outputs.filter(a => a.kind === 'asset' && a.path.endsWith('.css')); + for (let styleFile of styleFiles) { + style += await styleFile.text(); } const title = game[0].toUpperCase() + game.slice(1).toLowerCase(); - let pwaIconFile = Bun.file(path.resolve(import.meta.dir, '..', 'src', 'games', game, 'assets', 'pwa_icon.png')); + let pwaIconFile = Bun.file(path.resolve(gameAssetsDir, 'pwa_icon.png')); if (!await pwaIconFile.exists()) { - pwaIconFile = Bun.file(path.resolve(import.meta.dir, 'assets', 'pwa_icon.png')); + pwaIconFile = Bun.file(path.resolve(assetsDir, 'pwa_icon.png')); } let manifest = ''; - if (production) { - const pwaIcon = `data:;base64,${Buffer.from(await pwaIconFile.arrayBuffer()).toString('base64')}`; + if (production && !local) { + const pwaIcon = `data:;base64,${await b64(pwaIconFile)}`; const publishURL = process.env.PUBLISH_URL ? `${process.env.PUBLISH_URL}${game}` : '.'; const manifestJSON = JSON.stringify({ name: title, @@ -76,7 +87,7 @@ export async function buildHTML(game: string, { production = false, portable = f type: 'image/png' }] }); - manifest = ``; + manifest = ``; } let script = await scriptFile.text(); const inits = new Set(); @@ -106,8 +117,10 @@ export async function buildHTML(game: string, { production = false, portable = f scriptPrefix = ``; } + // function to avoid $& being replaced + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_string_as_the_replacement const resultHTML = html - .replace('', () => `${scriptPrefix}`) // to avoid $& being replaced + .replace('', () => `${scriptPrefix}`) .replace('', () => title) .replace('', () => icon) .replace('', () => manifest) diff --git a/build/server.ts b/build/server.ts index aa95834..3b5badf 100644 --- a/build/server.ts +++ b/build/server.ts @@ -21,8 +21,8 @@ Bun.serve({ game, { production: url.searchParams.get('production') === 'true', // to debug production builds - portable: url.searchParams.get('portable') === 'true', // to skip AssemblyScript compilation, mobile: detectedBrowser.mobile || url.searchParams.get('mobile') === 'true', + local: true, } ); if (html) { diff --git a/build/wasmPlugin.ts b/build/wasmPlugin.ts index 51245c4..1a057e1 100644 --- a/build/wasmPlugin.ts +++ b/build/wasmPlugin.ts @@ -151,7 +151,6 @@ const wasmPlugin = ({ production }: WasmLoaderConfig = {}): BunPlugin => { 'nontrapping-fptoint', 'reference-types', 'multivalue', - ].map(f => `-m${f}`); const flags = [ diff --git a/src/common/errors.ts b/src/common/errors.ts new file mode 100644 index 0000000..5b933b9 --- /dev/null +++ b/src/common/errors.ts @@ -0,0 +1,18 @@ +export const formatError = (error: unknown, message: string = ''): string => { + const prefix = message ? `${message}: ` : ''; + const suffix = (error && typeof error === 'object' ) ? (('stack' in error) ? `\n${error.stack}` : '') : ''; + + const errorMessage = formatErrorMessage(error).trim(); + + return `${prefix}${errorMessage}${suffix}`; +} + +export const formatErrorMessage = (error: unknown): string => { + if (error && typeof error === 'object' && 'message' in error) { + return `${error.message}`; + } else if (error) { + return error.toString(); + } else { + return 'Unknown error'; + } +} \ No newline at end of file diff --git a/src/common/game.ts b/src/common/game.ts index 0d6632c..7bdfcd7 100644 --- a/src/common/game.ts +++ b/src/common/game.ts @@ -1,8 +1,9 @@ +import { formatError, formatErrorMessage } from "./errors"; import Input from "./input"; import { nextFrame } from "./utils"; type Setup | void> = () => Promise | T; -type Frame | void> = (dt: number, state: T) => Promise | void; +type Frame | void> = (dt: number, state: T) => Promise | T | void; type GameMain = () => void; export function gameLoop | void>(frame: Frame): GameMain; @@ -10,24 +11,39 @@ export function gameLoop | void>(setup: Setup< export function gameLoop | void>(setupOrFrame: Setup | Frame, frame?: Frame): GameMain { return async () => { let state: T; - if (frame) { - state = await (setupOrFrame as Setup)(); - } else { - frame = setupOrFrame as Frame; - state = {} as T; + try { + if (frame) { + state = await (setupOrFrame as Setup)(); + } else { + frame = setupOrFrame as Frame; + state = {} as T; + } + } catch (e) { + console.error(formatError(e, 'Error in game setup')); + alert(formatErrorMessage(e)); + return; } - let prevFrame = performance.now(); - while (true) { - await nextFrame(); - Input.updateKeys(); + try { + let prevFrame = performance.now(); + while (true) { + await nextFrame(); + Input.updateKeys(); - const now = performance.now(); - const dt = (now - prevFrame) / 1000; + const now = performance.now(); + const dt = (now - prevFrame) / 1000; - await frame(dt, state); + const newState = await frame(dt, state); + if (newState) { + state = newState; + } - prevFrame = performance.now(); + prevFrame = performance.now(); + } + } catch (e) { + console.error(formatError(e, 'Error in game loop')); + alert(formatErrorMessage(e)); + return; } } }; diff --git a/src/common/utils.ts b/src/common/utils.ts index ca4051a..07297e9 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -1,4 +1,5 @@ export const nextFrame = async (): Promise => new Promise((resolve) => requestAnimationFrame(resolve)); +export const delay = async (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); export const randInt = (min: number, max: number) => Math.round(min + (max - min - 1) * Math.random()); export const randBool = () => Math.random() < 0.5; diff --git a/src/games/olc-run-2024/assets/figures.png b/src/games/olc-2024-run/assets/figures.png similarity index 100% rename from src/games/olc-run-2024/assets/figures.png rename to src/games/olc-2024-run/assets/figures.png diff --git a/src/games/olc-run-2024/assets/fill.ogg b/src/games/olc-2024-run/assets/fill.ogg similarity index 100% rename from src/games/olc-run-2024/assets/fill.ogg rename to src/games/olc-2024-run/assets/fill.ogg diff --git a/src/games/olc-run-2024/assets/place.ogg b/src/games/olc-2024-run/assets/place.ogg similarity index 100% rename from src/games/olc-run-2024/assets/place.ogg rename to src/games/olc-2024-run/assets/place.ogg diff --git a/src/games/olc-run-2024/assets/wrong.ogg b/src/games/olc-2024-run/assets/wrong.ogg similarity index 100% rename from src/games/olc-run-2024/assets/wrong.ogg rename to src/games/olc-2024-run/assets/wrong.ogg diff --git a/src/games/olc-run-2024/index.tsx b/src/games/olc-2024-run/index.tsx similarity index 99% rename from src/games/olc-run-2024/index.tsx rename to src/games/olc-2024-run/index.tsx index e4ce9b2..a90a94d 100644 --- a/src/games/olc-run-2024/index.tsx +++ b/src/games/olc-2024-run/index.tsx @@ -207,7 +207,6 @@ async function loop() { } rowsToClear.clear(); colsToClear.clear(); - } display.clear(); diff --git a/src/games/olc-2025-shapes/index.ts b/src/games/olc-2025-shapes/index.ts new file mode 100644 index 0000000..07248f4 --- /dev/null +++ b/src/games/olc-2025-shapes/index.ts @@ -0,0 +1,26 @@ +import { createCanvas } from "@common/display/canvas"; +import { gameLoop } from "@common/game"; + +type State = ReturnType; + +const setup = () => { + const canvas = createCanvas(800, 600); + const ctx = canvas.getContext("2d"); + + if (!ctx) { + throw new Error("Failed to get canvas context"); + } + + return { + canvas, + ctx, + }; +}; + +const frame = (dt: number, state: State) => { + const { ctx } = state; + ctx.fillStyle = "blue"; + ctx.fillRect(0, 0, 800, 600); +}; + +export default gameLoop(setup, frame); \ No newline at end of file