From 43c8c0e535c94d23fb56012bf22303412a30bc76 Mon Sep 17 00:00:00 2001 From: Reimar Date: Thu, 16 Oct 2025 11:03:41 +0200 Subject: [PATCH] add runtime type-checking --- src/gamelib.js | 279 +------------------------- src/gamelib_adapter.js | 402 ++++++++++++++++++++++++++++++++++++++ src/index.js | 6 +- src/playground_console.js | 14 +- 4 files changed, 420 insertions(+), 281 deletions(-) diff --git a/src/gamelib.js b/src/gamelib.js index 76a9cbc..6986f80 100644 --- a/src/gamelib.js +++ b/src/gamelib.js @@ -1,3 +1,5 @@ +import { GamelibAdapter } from "./gamelib_adapter.js"; + export class Gamelib { constructor(console, assetProvider, canvasElement) { this.console = console; @@ -274,280 +276,3 @@ export class Gamelib { return new GamelibAdapter(this); } } - -/** - * A function that is called whenever the Key it is registered to is pressed or released. - * @callback OnKeyEventHandler - */ - -/** - * A function that is called whenever the MouseButton it is registered to is pressed or released. - * @callback OnClickEventHandler - */ - -/** - * A function that is called whenever the mouse is moved. - * @callback OnMouseMoveEventHandler - * @param {number} positionX The mouse x position - * @param {number} positionY The mouse y position - * @param {number} deltaX The difference in x position between this and the last onMouseMove event - * @param {number} deltaY The difference in y position between this and the last onMouseMove event - */ - -/** - * A loaded sprite - * @typedef {object} Sprite - */ - -/** - * A html color - * @typedef {string} Color - */ - -/** - * A component of `[x, y]` of a path. - * @typedef {[number, number]} PathComponent - */ - -/** - * Text style - * @typedef TextStyle - * @type {object} - * @property {number} fontWeight Font weight - * @property {number} fontStyle !!!!!!!!!!!!!!!!!!!!! no idea what this does, needs better docs - * @property {number} fontSize Font size in pixels - * @property {string} fontFamily Font family - * @property {"left"|"center"|"right"} align Text alignment - * @property {string} baseline !!!!!!!!!!!!!!!!!!!!! no idea what this does, needs better docs - * @property {string} direction !!!!!!!!!!!!!!!!!!!!! no idea what this does, needs better docs - * @property {Color} color - */ - -export class GamelibAdapter { - /* Enum for MouseButton values - * @enum {number} - */ - MouseButton = { - Left: 0, - Right: 1, - Middle: 2, - }; - - constructor(gamelib) { - this.gamelib = gamelib; - } - - get width() { - return this.gamelib.width; - } - - get height() { - return this.gamelib.height; - } - - get mouseX() { - return this.gamelib.mouseX; - } - - get mouseY() { - return this.gamelib.mouseY; - } - - println(msg) { - this.gamelib.console.log(msg); - } - - startGameLoop(loopFunction) { - this.gamelib.startGameLoop(loopFunction); - } - - /** - * Returns whether or not `key` is currently pressed. - * `Key` can any one of [KeyBoardEvent.key](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values)'s values - * @param {Key} key - * @return {boolean} Whether `key` is pressed. - */ - isPressed(key) { - return this.gamelib.keysPressed.has(key); - } - - /** - * Registers a `handlerFunction` that is called when `key` is pressed. - * Key can be any of [KeyBoardEvent.key](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values)'s values - * @param {Key} key - * @param {OnKeyEventHandler} handlerFunction - * @return {void} - */ - onPress(key, handlerFunction) { - this.gamelib.keyPressHandlers.set(key, handlerFunction); - } - - /** - * Registers a `handlerFunction` that is called when `key` is pressed. - * Key can be any of [KeyBoardEvent.key](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values)'s values - * @param {Key} key - * @param {OnKeyEventHandler} handlerFunction - */ - onRelease(key, handlerFunction) { - this.gamelib.keyReleaseHandlers.set(key, handlerFunction); - } - - /** - * Registers a `handlerFunction` that is called when mouse is moved. - * @param {OnMouseMoveEventHandler} handlerFunction - */ - onMouseMove(handlerFunction) { - this.gamelib.mouseMoveHandler = handlerFunction; - } - - /** - * Returns whether or not `button` is currently clicked. - * MouseButton is accessed from `lib.MouseButton` - * @param {GamelibAdapter.MouseButton} button defaults to {GamelibAdapter.MouseButton.Left} - * @return {boolean} Whether `key` is pressed. - */ - isClicking(button = this.MouseButton.Left) { - return this.gamelib.mouseButtonsPressed.has(button); - } - - /** - * Registers a `handlerFunction` that is called when `button` is pressed. - * MouseButton is accessed from `lib.MouseButton` - * @param {OnClickEventHandler} handlerFunction - * @param {GamelibAdapter.MouseButton} button defaults to {GamelibAdapter.MouseButton.Left} - */ - onClick(handlerFunction, button = this.MouseButton.Left) { - this.gamelib.mouseDownHandlers.set(button, handlerFunction); - } - - /** - * Registers a `handlerFunction` that is called when `button` is released. - * MouseButton is accessed from `lib.MouseButton` - * @param {OnClickEventHandler} handlerFunction - * @param {GamelibAdapter.MouseButton} button defaults to {GamelibAdapter.MouseButton.Left} - */ - onClickRelease(handlerFunction, button = this.MouseButton.Left) { - this.gamelib.mouseUpHandlers.set(button, handlerFunction); - } - - /** - * Loads a sprite with `name`, rendered with specified `width` and `height` - * - * @param {string} name The sprite name - * @param {number} width - * @param {number} height - * @return {Promise} The loaded sprite - */ - async loadSprite(name, width, height) { - return await this.gamelib.loadSprite(name, width, height); - } - - /** - * Draws a sprite loaded by {GamelibAdapter.loadSprite} - * - * @param {number} x - * @param {number} y - * @param {Sprite} A loaded sprite - */ - drawSprite(x, y, sprite) { - this.gamelib.drawSprite(x, y, sprite); - } - - /** - * Draws a rotated sprite loaded by {GamelibAdapter.loadSprite} - * - * @param {number} x - * @param {number} y - * @param {Sprite} sprite A loaded sprite - * @param {number} angle An angle in radians - */ - drawSpriteRotated(x, y, sprite, angle) { - this.gamelib.drawSpriteRotated(x, y, sprite, angle); - } - - /** - * Generates an rgb Color. - * - * @param {number} red - * @param {number} green - * @param {number} blue - * @return {Color} Formatted {Color} string - */ - rgb(red, green, blue) { - return `rgb(${red}, ${green}, ${blue})`; - } - - /** - * Clears the screen with `color` - * @param {Color} color - */ - clear(color) { - this.gamelib.clear(color); - } - - /** - * Draws a rect at `(x,y)` with a size of `(width,height)` in `color` - * @param {number} x - * @param {number} y - * @param {number} width - * @param {number} height - * @param {Color} color - */ - drawRect(x, y, width, height, color) { - this.gamelib.drawRect(x, y, width, height, color); - } - - /** - * Draws a circle at `(x,y)` with a radius of `r` in `color` - * @param {number} x - * @param {number} y - * @param {number} r Radius of circle - * @param {Color} color - */ - drawCircle(x, y, r, color) { - this.gamelib.drawCircle(x, y, r, color); - } - - /** - * Draws a line from `(x0,y0)` to `(x1,y1)` with a thickness of `thickness` in `color` - * @param {number} x0 - * @param {number} y0 - * @param {number} x1 - * @param {number} y1 - * @param {number} thickness - * @param {Color} color - */ - drawLine(x0, y0, x1, y1, thickness, color) { - this.gamelib.drawLine(x0, y0, x1, y1, thickness, color); - } - - /** - * Draws a polygon in `color` based off of `path` components - * @param {PathComponent[]} path - * @param {Color} color - */ - drawPath(path, color) { - this.gamelib.drawPath(path, color); - } - - /** - * Draws an outline in `color` based off of `path` components, with a thickness of `thickness` - * @param {PathComponent[]} path - * @param {number} thickness - * @param {Color} color - */ - drawPathLine(path, thickness, color) { - this.gamelib.drawPathLine(path, thickness, color); - } - - /** - * Draw text - * @param {number} x - * @param {number} y - * @param {string} text - * @param {TextStyle} style - */ - drawText(x, y, text, style = {}) { - this.gamelib.drawText(x, y, text, style); - } -} diff --git a/src/gamelib_adapter.js b/src/gamelib_adapter.js index e69de29..5566e2e 100644 --- a/src/gamelib_adapter.js +++ b/src/gamelib_adapter.js @@ -0,0 +1,402 @@ +/** + * A function that is called whenever the Key it is registered to is pressed or released. + * @callback OnKeyEventHandler + */ + +/** + * A function that is called whenever the MouseButton it is registered to is pressed or released. + * @callback OnClickEventHandler + */ + +/** + * A function that is called whenever the mouse is moved. + * @callback OnMouseMoveEventHandler + * @param {number} positionX The mouse x position + * @param {number} positionY The mouse y position + * @param {number} deltaX The difference in x position between this and the last onMouseMove event + * @param {number} deltaY The difference in y position between this and the last onMouseMove event + */ + +/** + * A loaded sprite + * @typedef {object} Sprite + */ + +/** + * A html color + * @typedef {string} Color + */ + +/** + * A component of `[x, y]` of a path. + * @typedef {[number, number]} PathComponent + */ + +/** + * A keyboard key + * @typedef {string} Key + */ + +/** + * Text style + * @typedef TextStyle + * @type {object} + * @property {number|string} [fontWeight="normal"] Font weight as a number or a string, e.g. 400 or "bold" + * @property {"normal"|"italic"} [fontStyle="normal"] Style to apply to the font, e.g. "italic" + * @property {number} [fontSize=16] Font size in pixels + * @property {string} [fontFamily="inherit"] Font family, e.g. "sans-serif" or "serif" + * @property {"left"|"center"|"right"} [align="left"] Horizontal text alignment + * @property {"top"|"hanging"|"middle"|"alphabetic"|"ideographic"|"bottom"} [baseline="alphabetic"] Vertical text alignment + * @property {"ltr"|"rtl"} [direction="ltr"] Whether the text goes left-to-right or right-to-left + * @property {Color} [color="white"] Text color + */ + +export class GamelibAdapter { + /* Enum for MouseButton values + * @enum {number} + */ + MouseButton = { + Left: 0, + Right: 1, + Middle: 2, + }; + + constructor(gamelib) { + this.gamelib = gamelib; + } + + #error(message, func) { + const error = new Error(`${func.name}: ${message}`); + Error.captureStackTrace(error, func); + + this.gamelib.console.error(error); + throw error; + } + + #checkParams(func, args, expectedTypes) { + console.log(func, args, expectedTypes); + if (args.length > expectedTypes.length) { + this.#error( + `Too many arguments to function. Expected ${expectedTypes.length}, got ${args.length}`, + func, + ); + } + + const typeAliases = { + Sprite: OffscreenCanvas, + MouseButton: "number", + }; + + for (let i = 0; i < args.length; i++) { + const expectedType = typeAliases[expectedTypes[i]] ?? expectedTypes[i]; + + if (typeof expectedType === "string" && typeof args[i] !== expectedType) { + this.#error( + `Expected parameter #${ + i + 1 + } to be of type ${expectedType}, got ${typeof args[i]}`, + func, + ); + } + + if (expectedType instanceof Object && !(args[i] instanceof expectedType)) { + const expectedTypeName = expectedType.prototype.constructor.name; + + this.#error( + `Expected parameter #${ + i + 1 + } to be of type ${expectedTypeName}, got ${typeof args[i]}`, + func, + ); + } + } + } + + get width() { + return this.gamelib.width; + } + + get height() { + return this.gamelib.height; + } + + get mouseX() { + return this.gamelib.mouseX; + } + + get mouseY() { + return this.gamelib.mouseY; + } + + /** + * Prints a line to the console + * @param {any} msg The message or value to print + * @return {void} + */ + println(msg) { + this.gamelib.console.log(msg); + } + + startGameLoop(loopFunction) { + this.#checkParams(this.startGameLoop, [loopFunction], [Function]); + + this.gamelib.startGameLoop(loopFunction); + } + + /** + * Returns whether or not `key` is currently pressed. + * `Key` can any one of [KeyBoardEvent.key](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values)'s values + * @param {Key} key + * @return {boolean} Whether `key` is pressed. + */ + isPressed(key) { + this.#checkParams(this.isPressed, [key], ["string"]); + + return this.gamelib.keysPressed.has(key); + } + + /** + * Registers a `handlerFunction` that is called when `key` is pressed. + * Key can be any of [KeyBoardEvent.key](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values)'s values + * @param {Key} key + * @param {OnKeyEventHandler} handlerFunction + * @return {void} + */ + onPress(key, handlerFunction) { + this.#checkParams(this.onPress, [key, handlerFunction], ["string", Function]); + + this.gamelib.keyPressHandlers.set(key, handlerFunction); + } + + /** + * Registers a `handlerFunction` that is called when `key` is pressed. + * Key can be any of [KeyBoardEvent.key](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values)'s values + * @param {Key} key + * @param {OnKeyEventHandler} handlerFunction + */ + onRelease(key, handlerFunction) { + this.#checkParams(this.onRelease, [key, handlerFunction], ["string", Function]); + + this.gamelib.keyReleaseHandlers.set(key, handlerFunction); + } + + /** + * Registers a `handlerFunction` that is called when mouse is moved. + * @param {OnMouseMoveEventHandler} handlerFunction + */ + onMouseMove(handlerFunction) { + this.#checkParams(this.onMouseMove, [handlerFunction], [Function]); + + this.gamelib.mouseMoveHandler = handlerFunction; + } + + /** + * Returns whether or not `button` is currently clicked. + * MouseButton is accessed from `lib.MouseButton` + * @param {GamelibAdapter.MouseButton} button defaults to {GamelibAdapter.MouseButton.Left} + * @return {boolean} Whether `key` is pressed. + */ + isClicking(button = this.MouseButton.Left) { + this.#checkParams(this.isClicking, [button], ["MouseButton"]); + + return this.gamelib.mouseButtonsPressed.has(button); + } + + /** + * Registers a `handlerFunction` that is called when `button` is pressed. + * MouseButton is accessed from `lib.MouseButton` + * @param {OnClickEventHandler} handlerFunction + * @param {GamelibAdapter.MouseButton} button defaults to {GamelibAdapter.MouseButton.Left} + */ + onClick(handlerFunction, button = this.MouseButton.Left) { + this.#checkParams(this.onClick, [handlerFunction, button], [Function, "MouseButton"]); + + this.gamelib.mouseDownHandlers.set(button, handlerFunction); + } + + /** + * Registers a `handlerFunction` that is called when `button` is released. + * MouseButton is accessed from `lib.MouseButton` + * @param {OnClickEventHandler} handlerFunction + * @param {GamelibAdapter.MouseButton} button defaults to {GamelibAdapter.MouseButton.Left} + */ + onClickRelease(handlerFunction, button = this.MouseButton.Left) { + this.#checkParams(this.onClickRelease, [handlerFunction, button], [ + Function, + "MouseButton", + ]); + + this.gamelib.mouseUpHandlers.set(button, handlerFunction); + } + + /** + * Loads a sprite with `name`, rendered with specified `width` and `height` + * + * @param {string} name The sprite name + * @param {number} width + * @param {number} height + * @return {Promise} The loaded sprite + */ + async loadSprite(name, width, height) { + this.#checkParams(this.loadSprite, [name, width, height], ["string", "number", "number"]); + + return await this.gamelib.loadSprite(name, width, height); + } + + /** + * Draws a sprite loaded by {GamelibAdapter.loadSprite} + * + * @param {number} x + * @param {number} y + * @param {Sprite} sprite A loaded sprite + */ + drawSprite(x, y, sprite) { + this.#checkParams(this.drawSprite, [x, y, sprite], ["number", "number", "Sprite"]); + + this.gamelib.drawSprite(x, y, sprite); + } + + /** + * Draws a rotated sprite loaded by {GamelibAdapter.loadSprite} + * + * @param {number} x + * @param {number} y + * @param {Sprite} sprite A loaded sprite + * @param {number} angle An angle in radians + */ + drawSpriteRotated(x, y, sprite, angle) { + this.#checkParams(this.drawSpriteRotated, [x, y, sprite, angle], [ + "number", + "number", + "Sprite", + "number", + ]); + + this.gamelib.drawSpriteRotated(x, y, sprite, angle); + } + + /** + * Generates an rgb Color. + * + * @param {number} red + * @param {number} green + * @param {number} blue + * @return {Color} Formatted {Color} string + */ + rgb(red, green, blue) { + this.#checkParams(this.rgb, [red, green, blue], ["number", "number", "number"]); + + return `rgb(${red}, ${green}, ${blue})`; + } + + /** + * Clears the screen with `color` + * @param {Color} color + */ + clear(color) { + this.#checkParams(this.clear, [color], ["string"]); + + this.gamelib.clear(color); + } + + /** + * Draws a rect at `(x,y)` with a size of `(width,height)` in `color` + * @param {number} x + * @param {number} y + * @param {number} width + * @param {number} height + * @param {Color} color + */ + drawRect(x, y, width, height, color) { + this.#checkParams(this.drawRect, [x, y, width, height, color], [ + "number", + "number", + "number", + "number", + "string", + ]); + + this.gamelib.drawRect(x, y, width, height, color); + } + + /** + * Draws a circle at `(x,y)` with a radius of `r` in `color` + * @param {number} x + * @param {number} y + * @param {number} r Radius of circle + * @param {Color} color + */ + drawCircle(x, y, r, color) { + this.#checkParams(this.drawCircle, [x, y, r, color], [ + "number", + "number", + "number", + "string", + ]); + + this.gamelib.drawCircle(x, y, r, color); + } + + /** + * Draws a line from `(x0,y0)` to `(x1,y1)` with a thickness of `thickness` in `color` + * @param {number} x0 + * @param {number} y0 + * @param {number} x1 + * @param {number} y1 + * @param {number} thickness + * @param {Color} color + */ + drawLine(x0, y0, x1, y1, thickness, color) { + this.#checkParams(this.drawLine, [x0, y0, x1, y1, thickness, color], [ + "number", + "number", + "number", + "number", + "number", + "string", + ]); + + this.gamelib.drawLine(x0, y0, x1, y1, thickness, color); + } + + /** + * Draws a polygon in `color` based off of `path` components + * @param {PathComponent[]} path + * @param {Color} color + */ + drawPath(path, color) { + this.#checkParams(this.drawPath, [path, color], [Array, "string"]); + + this.gamelib.drawPath(path, color); + } + + /** + * Draws an outline in `color` based off of `path` components, with a thickness of `thickness` + * @param {PathComponent[]} path + * @param {number} thickness + * @param {Color} color + */ + drawPathLine(path, thickness, color) { + this.#checkParams(this.drawPathLine, [path, thickness, color], [Array, "number", "string"]); + + this.gamelib.drawPathLine(path, thickness, color); + } + + /** + * Draw text + * @param {number} x + * @param {number} y + * @param {string} text + * @param {TextStyle} style + */ + drawText(x, y, text, style = {}) { + this.#checkParams(this.drawText, [x, y, text, style], [ + "number", + "number", + "string", + Object, + ]); + + this.gamelib.drawText(x, y, text, style); + } +} diff --git a/src/index.js b/src/index.js index 18da293..e85fef0 100644 --- a/src/index.js +++ b/src/index.js @@ -30,13 +30,13 @@ const languageProvider = LanguageProvider.fromCdn( languageProvider.registerEditor(editor); languageProvider.setGlobalOptions("typescript", { extraLibs: { - "gamelib.js": { - content: await (await fetch("/src/gamelib.js")).text(), + "gamelib_adapter.js": { + content: await (await fetch("/src/gamelib_adapter.js")).text(), version: 1, }, "karlkoder.js": { content: ` - import { GamelibAdapter } from "./gamelib.js"; + import { GamelibAdapter } from "./gamelib_adapter.js"; declare global { const karlkoder: { lib: () => GamelibAdapter } } diff --git a/src/playground_console.js b/src/playground_console.js index 9b12e15..7b82ab7 100644 --- a/src/playground_console.js +++ b/src/playground_console.js @@ -64,7 +64,13 @@ export class PlaygroundConsole { details.appendChild(summary); if (arg instanceof Error) { - // Add error stack trace + // On Chrome, the first line of the stack trace is the error message repeated + if (globalThis.chrome) { + const trace = arg.stack.split("\n"); + trace.shift(); + arg.stack = trace.join("\n"); + } + const el = document.createElement("p"); el.innerHTML = this.formatStacktrace(arg.stack); details.appendChild(el); @@ -126,26 +132,32 @@ class PlaygroundConsoleAdapter { } log() { + console.log(...arguments); this.#console.addTopLevelEntry("log", ...arguments); } dir() { + console.dir(...arguments); this.#console.addTopLevelEntry("dir", ...arguments); } debug() { + console.debug(...arguments); this.#console.addTopLevelEntry("debug", ...arguments); } info() { + console.info(...arguments); this.#console.addTopLevelEntry("info", ...arguments); } warn() { + console.warn(...arguments); this.#console.addTopLevelEntry("warn", ...arguments); } error() { + console.error(...arguments); this.#console.addTopLevelEntry("error", ...arguments); }