From 7715c01656fd1060897af47feb28764c7882315b Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Mon, 30 Mar 2026 10:22:23 +0000
Subject: [PATCH 1/4] Add SPRITECOLLIDE, runtime error hints, tutorial
runOnEdit
- Wire SPRITECOLLIDE(slot_a,slot_b) to gfx_sprite_slots_overlap_aabb; fix
function_lookup length (13) and eval_factor dispatch for SPRITE* names
- Non-GFX builds parse args then error with a clear message
- runtime_error_hint for unknown function and type coercion hints
- tutorial-embed: optional runOnEdit with debounce; clear timer on destroy
- Docs: README, tutorial-embedding, CHANGELOG, to-do
Co-authored-by: Chris Garrett
---
CHANGELOG.md | 18 ++++++-
README.md | 1 +
basic.c | 100 +++++++++++++++++++++++++++++++++----
basic_api.h | 2 +
docs/tutorial-embedding.md | 2 +
gfx/gfx_raylib.c | 68 +++++++++++++++++++++++++
gfx/gfx_software_sprites.c | 48 ++++++++++++++++++
to-do.md | 4 +-
web/tutorial-embed.js | 30 ++++++++++-
9 files changed, 259 insertions(+), 14 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b741eb4..c3cdb8b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,22 @@
### Unreleased
+- **Sprites**: **`SPRITECOLLIDE(a, b)`** — returns **1** if two loaded, visible sprites’ axis-aligned bounding boxes overlap (basic-gfx + canvas WASM; **0** otherwise). Terminal **`./basic`** errors if used (requires **basic-gfx** or canvas WASM). **Runtime errors**: optional **`Hint:`** line for **unknown function** (shows name) and **`ensure_num` / `ensure_str`** type mismatches. **`tutorial-embed.js`**: optional **`runOnEdit`** / **`runOnEditMs`** for debounced auto-run after editing.
+
+- **Documentation**: **`docs/basic-to-c-transpiler-plan.md`** — design notes for a future **BASIC → C** backend (**cc65** / **z88dk**), recommended subset, and **explicit exclusions** (host I/O, `EVAL`, graphics, etc.).
+
+- **IF conditions with parentheses**: **`IF (J=F OR A$=" ") AND SF=0 THEN`** failed with **Missing THEN** because **`eval_simple_condition`** used **`eval_expr`** inside **`(`**, which does not parse relational **`=`**. Leading **`(`** now distinguishes **`(expr) relop rhs`** (e.g. **`(1+1)=2`**) from boolean groups **`(… OR …)`** via **`eval_condition`**. Regression: **`tests/if_paren_condition_test.bas`**.
+
+- **Canvas WASM `PEEK(56320+n)` keyboard**: **`key_state[]`** was never updated in the browser (only **basic-gfx** / Raylib did). **`canvas.html`** now drives **`wasm_gfx_key_state_set`** / **`wasm_gfx_key_state_clear`** on key up/down (A–Z, 0–9, arrows, Escape, Space, Enter, Tab) so **`examples/gfx_jiffy_game_demo.bas`**-style **`PEEK(KEYBASE+…)`** works.
+
+- **Canvas WASM `TI` / `TI$`**: **`GfxVideoState.ticks60`** was only advanced in the **Raylib** main loop, so **canvas** **`TI`** / **`TI$`** stayed frozen (especially in tight **`GOTO`** loops). **Canvas** now derives 60 Hz jiffies from **`emscripten_get_now()`** since **`basic_load_and_run_gfx`** ( **`gfx_video_advance_ticks60`** still drives **basic-gfx** each frame). **Terminal WASM** (`basic.js`, no **`GFX_VIDEO`**) still uses **`time()`** for **`TI`** / **`TI$`** (seconds + wall-clock `HHMMSS`), not C64 jiffies.
+
+- **WordPress**: New plugin **`wordpress/rgc-basic-tutorial-block`** — Gutenberg block **RGC-BASIC embed** with automatic script/style enqueue, **`copy-web-assets.sh`** to sync **`web/tutorial-embed.js`**, **`vfs-helpers.js`**, and modular WASM into **`assets/`**; optional **Settings → RGC-BASIC Tutorial** base URL.
+
+- **Canvas WASM `TAB`**: **`FN_TAB`** used **`OUTC`** for newline/spaces, which updates **`print_col`** but not the **GFX** cursor, so **`PRINT … TAB(n) …`** misaligned on the canvas. **GFX** path now uses **`gfx_newline`** + **`print_spaces`** (same as **`SPC`**). **`wasm_gfx_screen_screencode_at`** exported for **`wasm_browser_canvas_test`** regression.
+
+- **Canvas / basic-gfx INPUT line (`gfx_read_line`)**: Removed same-character time debounce (~80 ms) that dropped consecutive identical keys, so words like **"LOOK"** work. **Raylib `keyq_push`** no longer applies the same filter (parity with canvas).
+
- **WASM `run_program`**: Reset **`wasm_str_concat_budget`** each **`RUN`** (was missing; concat-yield counter could carry across runs).
- **WASM canvas string concat (`Q$=LEFT$+…`)**: **`trek.bas`** **`GOSUB 5440`** builds **`Q$`** with **`+`** ( **`eval_addsub`** ) without a **`MID$`** per assignment — no prior yield path. **Canvas WASM** now yields every **256** string concatenations (**`wasm_str_concat_budget`**, separate from **`MID$`** counter).
@@ -56,7 +72,7 @@
- **basic-gfx — hires bitmap (Phase 3)**
- `SCREEN 0` / `SCREEN 1` (text vs 320×200 monochrome); `PSET` / `PRESET` / `LINE x1,y1 TO x2,y2`; bitmap RAM at `GFX_BITMAP_BASE` (0x2000).
- **basic-gfx — PNG sprites**
- - `LOADSPRITE`, `UNLOADSPRITE`, `DRAWSPRITE` (persistent per-slot pose, z-order, optional source rect), `SPRITEVISIBLE`, `SPRITEW()` / `SPRITEH()`; alpha blending over PETSCII/bitmap; `gfx_set_sprite_base_dir` from program path.
+ - `LOADSPRITE`, `UNLOADSPRITE`, `DRAWSPRITE` (persistent per-slot pose, z-order, optional source rect), `SPRITEVISIBLE`, `SPRITEW()` / `SPRITEH()`, `SPRITECOLLIDE(a,b)`; alpha blending over PETSCII/bitmap; `gfx_set_sprite_base_dir` from program path.
- Examples: `examples/gfx_sprite_hud_demo.bas`, `examples/gfx_game_shell.bas` (+ `player.png`, `enemy.png`, `hud_panel.png`).
### 1.5.0 – 2026-03-20
diff --git a/README.md b/README.md
index 7fb0e1b..c1f1b0f 100644
--- a/README.md
+++ b/README.md
@@ -379,6 +379,7 @@ Releases include **basic-gfx** — a full graphical version of the interpreter b
- `DRAWSPRITE slot, x, y [, z [, sx, sy [, sw, sh ]]]` sets the **persistent** pose for that slot: the same image is drawn **every frame** until you call `DRAWSPRITE` again for that slot or the program exits (textures are freed when the window closes). **`x`, `y`** are **pixel** coordinates on the 320×200 framebuffer (not character rows): row *r* starts at **`y = r × 8`**. **`z`**: larger values paint **on top** (e.g. text/bitmap at 0, HUD at 200). Omit **`sx, sy`** to use the top-left of the image; omit **`sw, sh`** (or use ≤0) to use the rest of the texture from `(sx,sy)`. **Alpha** in the PNG is respected (transparency over text or bitmap).
- `SPRITEVISIBLE slot, 0|1` hides or shows a loaded sprite without unloading.
- `SPRITEW(slot)` / `SPRITEH(slot)` return pixel width/height after load (0 if not loaded yet).
+- `SPRITECOLLIDE(a, b)` returns **1** if the axis-aligned bounding boxes of two visible, drawn sprites overlap, else **0** (empty slots or hidden sprites never collide).
- Example: `./basic-gfx -petscii examples/gfx_sprite_hud_demo.bas`
- **Game shell** (`examples/gfx_game_shell.bas`): tile map from `DATA` with `POKE` (walls/floor/goal), **8×8 PNG** player (`player.png`) and enemy (`enemy.png`) via `DRAWSPRITE`, `INKEY$()` loop, HUD strip (`hud_panel.png`). Run: `./basic-gfx examples/gfx_game_shell.bas`
diff --git a/basic.c b/basic.c
index f27489d..88252b4 100644
--- a/basic.c
+++ b/basic.c
@@ -1904,6 +1904,7 @@ static void do_exec(const char *cmd, char *out, size_t out_size)
}
static void runtime_error(const char *msg);
+static void runtime_error_hint(const char *msg, const char *hint);
static void load_program(const char *path);
static void run_program(const char *script_path_arg, int nargs, char **args);
static int find_line_index(int number);
@@ -2022,14 +2023,16 @@ enum func_code {
FN_JSON = 42,
FN_EVAL = 43,
FN_SPRITEW = 44,
- FN_SPRITEH = 45
+ FN_SPRITEH = 45,
+ FN_SPRITECOLLIDE = 46
};
/* Report an error and halt further execution.
* If possible, include the BASIC line number (if present) and the
* original source text with a caret pointing near the error location.
+ * Optional hint gives a short fix suggestion (shown on its own line).
*/
-static void runtime_error(const char *msg)
+static void runtime_error_hint(const char *msg, const char *hint)
{
int line_no = 0;
const char *line_text = NULL;
@@ -2049,9 +2052,17 @@ static void runtime_error(const char *msg)
int n;
if (line_no > 0) {
- n = snprintf(buf, sizeof(buf), "Error on line %d: %s\n", line_no, msg);
+ if (hint && hint[0]) {
+ n = snprintf(buf, sizeof(buf), "Error on line %d: %s\n Hint: %s\n", line_no, msg, hint);
+ } else {
+ n = snprintf(buf, sizeof(buf), "Error on line %d: %s\n", line_no, msg);
+ }
} else {
- n = snprintf(buf, sizeof(buf), "Error: %s\n", msg);
+ if (hint && hint[0]) {
+ n = snprintf(buf, sizeof(buf), "Error: %s\n Hint: %s\n", msg, hint);
+ } else {
+ n = snprintf(buf, sizeof(buf), "Error: %s\n", msg);
+ }
}
if (n < 0) {
n = 0;
@@ -2152,6 +2163,11 @@ static void runtime_error(const char *msg)
halted = 1;
}
+static void runtime_error(const char *msg)
+{
+ runtime_error_hint(msg, NULL);
+}
+
/* Strip trailing newline from a buffer if present. */
static void trim_newline(char *s)
{
@@ -2241,7 +2257,7 @@ static const char *const reserved_words[] = {
"INKEY", "INPUT", "INSTR", "INT", "INDEXOF", "JSON", "LEFT", "LEN", "LET", "LINE", "LOAD", "LOADSPRITE", "LOCATE", "LOG",
"LASTINDEXOF", "LCASE", "FIELD", "LTRIM", "MEMCPY", "MEMSET", "MID", "MOD", "NEXT", "OFF", "ON", "OPEN", "OR", "PEEK", "POKE", "PLATFORM", "PRESET", "PSET", "PRINT",
"XOR",
- "READ", "REM", "REPLACE", "RESTORE", "RETURN", "RIGHT", "RND", "RTRIM", "RVS", "SCREEN", "SCREENCODES", "SPRITEVISIBLE",
+ "READ", "REM", "REPLACE", "RESTORE", "RETURN", "RIGHT", "RND", "RTRIM", "RVS", "SCREEN", "SCREENCODES", "SPRITECOLLIDE", "SPRITEVISIBLE",
"JOIN",
"SGN", "SIN", "SLEEP", "SORT", "SPC", "SPLIT", "SPRITEH", "SPRITEW", "SQR", "STEP", "STOP", "STR", "STRING",
"DRAWSPRITE", "SYSTEM", "TAB", "TAN", "TEXTAT", "THEN", "TI", "TO", "TRIM", "UCASE", "UNLOADSPRITE", "VAL", "WEND", "WHILE",
@@ -2314,7 +2330,7 @@ static struct value make_str(const char *s)
static void ensure_num(struct value *v)
{
if (v->type != VAL_NUM) {
- runtime_error("Numeric value required");
+ runtime_error_hint("Numeric value required", "Use a number here, or VAL(...) to convert a string.");
}
}
@@ -2330,7 +2346,7 @@ static void ensure_str(struct value *v)
*v = make_str(buf);
return;
}
- runtime_error("String value required");
+ runtime_error_hint("String value required", "Use a string expression, or STR$(...) to convert a number.");
}
/* Emit spaces and track current print column. */
@@ -3038,6 +3054,9 @@ static int function_lookup(const char *name, int len)
name[4] == 'T' && name[5] == 'E' && name[6] == 'W') return FN_SPRITEW;
if (len == 7 && name[0] == 'S' && name[1] == 'P' && name[2] == 'R' && name[3] == 'I' &&
name[4] == 'T' && name[5] == 'E' && name[6] == 'H') return FN_SPRITEH;
+ if (len == 13 && name[0] == 'S' && name[1] == 'P' && name[2] == 'R' && name[3] == 'I' &&
+ name[4] == 'T' && name[5] == 'E' && name[6] == 'C' && name[7] == 'O' && name[8] == 'L' &&
+ name[9] == 'L' && name[10] == 'I' && name[11] == 'D' && name[12] == 'E') return FN_SPRITECOLLIDE;
return FN_NONE;
case 'C':
if ((len == 3 && name[0] == 'C' && name[1] == 'H' && name[2] == 'R') ||
@@ -4179,6 +4198,32 @@ static struct value eval_function(const char *name, char **p)
return make_str("");
#endif
}
+#if !defined(GFX_VIDEO)
+ if (code == FN_SPRITECOLLIDE) {
+ struct value va, vb;
+ skip_spaces(p);
+ va = eval_expr(p);
+ (void)va;
+ skip_spaces(p);
+ if (**p != ',') {
+ runtime_error("SPRITECOLLIDE expects slot_a, slot_b");
+ return make_num(0.0);
+ }
+ (*p)++;
+ skip_spaces(p);
+ vb = eval_expr(p);
+ (void)vb;
+ skip_spaces(p);
+ if (**p != ')') {
+ runtime_error("Missing ')'");
+ return make_num(0.0);
+ }
+ (*p)++;
+ skip_spaces(p);
+ runtime_error("SPRITECOLLIDE requires basic-gfx or canvas WASM");
+ return make_num(0.0);
+ }
+#endif
#ifdef GFX_VIDEO
if (code == FN_SPRITEW || code == FN_SPRITEH) {
struct value vs;
@@ -4200,6 +4245,36 @@ static struct value eval_function(const char *name, char **p)
}
return make_num(0.0);
}
+ if (code == FN_SPRITECOLLIDE) {
+ struct value va, vb;
+ int sa, sb, hit;
+ skip_spaces(p);
+ va = eval_expr(p);
+ ensure_num(&va);
+ skip_spaces(p);
+ if (**p != ',') {
+ runtime_error("SPRITECOLLIDE expects slot_a, slot_b");
+ return make_num(0.0);
+ }
+ (*p)++;
+ skip_spaces(p);
+ vb = eval_expr(p);
+ ensure_num(&vb);
+ skip_spaces(p);
+ if (**p != ')') {
+ runtime_error("Missing ')'");
+ return make_num(0.0);
+ }
+ (*p)++;
+ skip_spaces(p);
+ sa = (int)va.num;
+ sb = (int)vb.num;
+ if (gfx_vs) {
+ hit = gfx_sprite_slots_overlap_aabb(sa, sb);
+ return make_num((double)hit);
+ }
+ return make_num(0.0);
+ }
#endif
if (code == FN_PLATFORM) {
if (**p != ')') {
@@ -5180,10 +5255,13 @@ static struct value eval_function(const char *name, char **p)
start = end + dlen;
}
}
- default:
- runtime_error("Unknown function");
+ default: {
+ char ubuf[96];
+ snprintf(ubuf, sizeof(ubuf), "Unknown function: %s", tmp);
+ runtime_error_hint(ubuf, "Check spelling; intrinsic names have no spaces.");
return make_num(0.0);
}
+ }
}
/* Normalize variable name to uppercase in dest, strip trailing $ and set is_string. */
@@ -5571,7 +5649,9 @@ static struct value eval_factor(char **p)
starts_with_kw(*p, "ENV") || starts_with_kw(*p, "EVAL") || starts_with_kw(*p, "PLATFORM") || starts_with_kw(*p, "JSON") ||
starts_with_kw(*p, "ARGC") || starts_with_kw(*p, "ARG") ||
starts_with_kw(*p, "SYSTEM") || starts_with_kw(*p, "EXEC") ||
- starts_with_kw(*p, "PEEK") || starts_with_kw(*p, "INKEY")) {
+ starts_with_kw(*p, "PEEK") || starts_with_kw(*p, "INKEY") ||
+ starts_with_kw(*p, "SPRITEW") || starts_with_kw(*p, "SPRITEH") ||
+ starts_with_kw(*p, "SPRITECOLLIDE")) {
char namebuf[32];
char *q;
q = *p;
diff --git a/basic_api.h b/basic_api.h
index 179fc00..61c3b40 100644
--- a/basic_api.h
+++ b/basic_api.h
@@ -60,6 +60,8 @@ void gfx_sprite_enqueue_visible(int slot, int on);
void gfx_sprite_enqueue_draw(int slot, float x, float y, int z, int sx, int sy, int sw, int sh);
int gfx_sprite_slot_width(int slot);
int gfx_sprite_slot_height(int slot);
+/* Axis-aligned bounding box overlap of last DRAWSPRITE rects (basic-gfx / canvas). */
+int gfx_sprite_slots_overlap_aabb(int slot_a, int slot_b);
#endif
#endif /* BASIC_API_H */
diff --git a/docs/tutorial-embedding.md b/docs/tutorial-embedding.md
index 9e5e0ae..e401679 100644
--- a/docs/tutorial-embedding.md
+++ b/docs/tutorial-embedding.md
@@ -110,6 +110,8 @@ Each embed gets its own **virtual filesystem** and **Asyncify** state; they do n
| `vfsExportPath` | string | `'/out.txt'` | Default path in the download field |
| `editorMinHeight` | string | `'120px'` | CSS min-height for textarea |
| `outputMinHeight` | string | `'100px'` | CSS min-height for output panel |
+| `runOnEdit` | boolean | `false` | If true, re-run the program after a short pause whenever the editor text changes (debounced) |
+| `runOnEditMs` | number | `600` | Debounce delay in milliseconds for `runOnEdit` |
## Returned API (`Promise`)
diff --git a/gfx/gfx_raylib.c b/gfx/gfx_raylib.c
index 6e55808..042c43d 100644
--- a/gfx/gfx_raylib.c
+++ b/gfx/gfx_raylib.c
@@ -58,6 +58,8 @@ typedef struct {
int sx, sy, sw, sh;
} GfxSpriteDraw;
+static void gfx_sprite_process_queue(void);
+
static pthread_mutex_t g_sprite_mutex = PTHREAD_MUTEX_INITIALIZER;
static char g_sprite_base_dir[GFX_SPRITE_PATH_MAX];
static GfxSpriteSlot g_sprite_slots[GFX_SPRITE_MAX_SLOTS];
@@ -195,6 +197,72 @@ int gfx_sprite_slot_height(int slot)
return h;
}
+int gfx_sprite_slots_overlap_aabb(int slot_a, int slot_b)
+{
+ float ax, ay, aw, ah, bx, by, bw, bh;
+ float ax2, ay2, bx2, by2;
+ GfxSpriteSlot *a;
+ GfxSpriteSlot *b;
+
+ gfx_sprite_process_queue();
+ if (slot_a < 0 || slot_a >= GFX_SPRITE_MAX_SLOTS ||
+ slot_b < 0 || slot_b >= GFX_SPRITE_MAX_SLOTS) {
+ return 0;
+ }
+ pthread_mutex_lock(&g_sprite_mutex);
+ a = &g_sprite_slots[slot_a];
+ b = &g_sprite_slots[slot_b];
+ if (!a->loaded || !a->visible || !a->draw_active ||
+ !b->loaded || !b->visible || !b->draw_active) {
+ pthread_mutex_unlock(&g_sprite_mutex);
+ return 0;
+ }
+ {
+ float swa, sha, swb, shb;
+ int twa, tha, twb, thb;
+ twa = a->tex.width;
+ tha = a->tex.height;
+ twb = b->tex.width;
+ thb = b->tex.height;
+ if (a->draw_sw <= 0 || a->draw_sh <= 0) {
+ swa = (float)(twa - a->draw_sx);
+ sha = (float)(tha - a->draw_sy);
+ } else {
+ swa = (float)a->draw_sw;
+ sha = (float)a->draw_sh;
+ }
+ if (b->draw_sw <= 0 || b->draw_sh <= 0) {
+ swb = (float)(twb - b->draw_sx);
+ shb = (float)(thb - b->draw_sy);
+ } else {
+ swb = (float)b->draw_sw;
+ shb = (float)b->draw_sh;
+ }
+ if (swa <= 0 || sha <= 0 || swb <= 0 || shb <= 0) {
+ pthread_mutex_unlock(&g_sprite_mutex);
+ return 0;
+ }
+ ax = a->draw_x;
+ ay = a->draw_y;
+ aw = swa;
+ ah = sha;
+ bx = b->draw_x;
+ by = b->draw_y;
+ bw = swb;
+ bh = shb;
+ }
+ pthread_mutex_unlock(&g_sprite_mutex);
+
+ ax2 = ax + aw;
+ ay2 = ay + ah;
+ bx2 = bx + bw;
+ by2 = by + bh;
+ if (ax2 <= bx || bx2 <= ax || ay2 <= by || by2 <= ay) {
+ return 0;
+ }
+ return 1;
+}
+
static int cmp_sprite_draw_z(const void *a, const void *b)
{
const GfxSpriteDraw *da = (const GfxSpriteDraw *)a;
diff --git a/gfx/gfx_software_sprites.c b/gfx/gfx_software_sprites.c
index 6dd9a62..e140398 100644
--- a/gfx/gfx_software_sprites.c
+++ b/gfx/gfx_software_sprites.c
@@ -175,6 +175,54 @@ int gfx_sprite_slot_height(int slot)
return g_sprite_slots[slot].h;
}
+int gfx_sprite_slots_overlap_aabb(int slot_a, int slot_b)
+{
+ GfxSpriteSlot *a, *b;
+ float ax, ay, aw, ah, bx, by, bw, bh;
+ float ax2, ay2, bx2, by2;
+
+ gfx_sprite_process_queue();
+ if (slot_a < 0 || slot_a >= GFX_SPRITE_MAX_SLOTS ||
+ slot_b < 0 || slot_b >= GFX_SPRITE_MAX_SLOTS) {
+ return 0;
+ }
+ a = &g_sprite_slots[slot_a];
+ b = &g_sprite_slots[slot_b];
+ if (!a->loaded || !a->visible || !a->draw_active ||
+ !b->loaded || !b->visible || !b->draw_active) {
+ return 0;
+ }
+ if (a->draw_sw <= 0 || a->draw_sh <= 0) {
+ aw = (float)(a->w - a->draw_sx);
+ ah = (float)(a->h - a->draw_sy);
+ } else {
+ aw = (float)a->draw_sw;
+ ah = (float)a->draw_sh;
+ }
+ if (b->draw_sw <= 0 || b->draw_sh <= 0) {
+ bw = (float)(b->w - b->draw_sx);
+ bh = (float)(b->h - b->draw_sy);
+ } else {
+ bw = (float)b->draw_sw;
+ bh = (float)b->draw_sh;
+ }
+ if (aw <= 0 || ah <= 0 || bw <= 0 || bh <= 0) {
+ return 0;
+ }
+ ax = a->draw_x;
+ ay = a->draw_y;
+ bx = b->draw_x;
+ by = b->draw_y;
+ ax2 = ax + aw;
+ ay2 = ay + ah;
+ bx2 = bx + bw;
+ by2 = by + bh;
+ if (ax2 <= bx || bx2 <= ax || ay2 <= by || by2 <= ay) {
+ return 0;
+ }
+ return 1;
+}
+
static int cmp_sprite_draw_z(const void *a, const void *b)
{
const GfxSpriteDraw *da = (const GfxSpriteDraw *)a;
diff --git a/to-do.md b/to-do.md
index 28d69ab..569137f 100644
--- a/to-do.md
+++ b/to-do.md
@@ -55,7 +55,7 @@
* **Bitmap graphics & sprites** (incremental; see `docs/bitmap-graphics-plan.md`, `docs/sprite-features-plan.md`)
1. ~**Bitmap mode**~ — `SCREEN 0`/`SCREEN 1` (320×200 hires at `GFX_BITMAP_BASE`), monochrome raylib renderer; `COLOR`/`BACKGROUND` as pen/paper. ~`PSET`/`PRESET`/`LINE`~; POKE still works.
- 2. ~**Sprites (minimal)**~ — `LOADSPRITE` / `UNLOADSPRITE` / `DRAWSPRITE` (persistent pose, z-order, optional `sx,sy,sw,sh` crop) / `SPRITEVISIBLE` / `SPRITEW`/`SPRITEH` in basic-gfx; PNG alpha over text/bitmap; paths relative to `.bas` directory. ~Worked example: `examples/gfx_game_shell.bas` (PETSCII map + PNG player/enemy/HUD), `examples/gfx_sprite_hud_demo.bas`, `examples/player.png`, `examples/enemy.png`, `examples/hud_panel.png`.~ Still open: `SPRITECOLLIDE`, tilemap `LOADSPRITE` mode.
+ 2. ~**Sprites (minimal)**~ — `LOADSPRITE` / `UNLOADSPRITE` / `DRAWSPRITE` (persistent pose, z-order, optional `sx,sy,sw,sh` crop) / `SPRITEVISIBLE` / `SPRITEW`/`SPRITEH` / `SPRITECOLLIDE` in basic-gfx + canvas WASM; PNG alpha over text/bitmap; paths relative to `.bas` directory. ~Worked example: `examples/gfx_game_shell.bas` (PETSCII map + PNG player/enemy/HUD), `examples/gfx_sprite_hud_demo.bas`, `examples/player.png`, `examples/enemy.png`, `examples/hud_panel.png`.~ Still open: tilemap `LOADSPRITE` mode.
3. **Joystick / joypad / controller input** — Poll gamepad via raylib; expose to BASIC as `JOY(port)` or similar read API for D-pad, buttons, axes. Support keyboards-as-joystick and real controllers.
4. **Graphic layers and scrolling** — Layer stack (background, tiles, sprites, text) with independent scroll offsets; `SCROLL x, y` or per-layer scroll; camera/viewport for games.
5. **Tilemap handling** — `LOADSPRITE slot, "path", "tilemap"`; define tile size (e.g. 16×16); render tile grid from sprite sheet. Efficient level/world rendering.
@@ -69,7 +69,7 @@
* ~**CI**~ — GitHub Actions WASM job uses **emsdk** (`install latest`), builds both targets, runs Playwright: `tests/wasm_browser_test.py`, `tests/wasm_browser_canvas_test.py`.
* ~**Deploy hygiene**~ — `canvas.html` pairs cache-bust query on `basic-canvas.js` and `basic-canvas.wasm`; optional `?debug=1` for console diagnostics (`wasm_canvas_build_stamp`, stack dumps).
* ~**Tutorial embedding**~ — `make basic-wasm-modular`; `web/tutorial-embed.js` + `RgcBasicTutorialEmbed.mount()` for **multiple** terminal instances per page. Guide: **`docs/tutorial-embedding.md`**. Example: `web/tutorial-example.html`. CI: `tests/wasm_tutorial_embed_test.py`, `make wasm-tutorial-test`.
- * **Still open — richer tutorial UX**: “Live” auto-run on edit, step-through debugging, or synchronized markdown blocks (beyond Run button + static program text).
+ * **Still open — richer tutorial UX**: step-through debugging or synchronized markdown blocks (beyond Run button + static program text). ~Optional **`runOnEdit`** in `tutorial-embed.js`~ for debounced auto-run after edits.
* ~~Subroutines and Functions~~
* **User-defined FUNCTIONS** implemented — `FUNCTION name[(params)]` … `RETURN [expr]` … `END FUNCTION`; call with `name()` or `name(a,b)`; recursion supported. See `docs/user-functions-plan.md`.
diff --git a/web/tutorial-embed.js b/web/tutorial-embed.js
index f80c39c..2c5302a 100644
--- a/web/tutorial-embed.js
+++ b/web/tutorial-embed.js
@@ -193,6 +193,8 @@
* @param {boolean} [opts.showEditor=true]
* @param {boolean} [opts.showPauseStop=true]
* @param {boolean} [opts.showVfsTools=true] - Upload/Download virtual FS (needs vfs-helpers.js beside basic-modular.js)
+ * @param {boolean} [opts.runOnEdit=false] - After edits, auto-run after a short debounce (handy for tutorials)
+ * @param {number} [opts.runOnEditMs=600] - Debounce delay for runOnEdit
* @returns {Promise<{ run: function, resetOutput: function, destroy: function, getModule: function }>}
*/
function mount(container, opts) {
@@ -207,6 +209,10 @@
var showEditor = opts.showEditor !== false;
var showPauseStop = opts.showPauseStop !== false;
var showVfsTools = opts.showVfsTools !== false;
+ var runOnEdit = opts.runOnEdit === true;
+ var runOnEditMs =
+ typeof opts.runOnEditMs === 'number' && opts.runOnEditMs >= 0 ? opts.runOnEditMs : 600;
+ var runOnEditTimer = null;
container.classList.add('rgc-tutorial-embed');
container.innerHTML = '';
@@ -421,7 +427,7 @@
}
}
- runBtn.onclick = function () {
+ function doRunFromEditor() {
clearOutput();
inputRow.style.display = 'none';
inputLine.value = '';
@@ -457,8 +463,26 @@
resumeBtn.disabled = true;
stopBtn.disabled = true;
});
+ }
+
+ runBtn.onclick = function () {
+ doRunFromEditor();
};
+ if (runOnEdit && showEditor) {
+ ta.addEventListener('input', function () {
+ if (runOnEditTimer) {
+ clearTimeout(runOnEditTimer);
+ }
+ runOnEditTimer = setTimeout(function () {
+ runOnEditTimer = null;
+ if (!destroyed && moduleRef) {
+ doRunFromEditor();
+ }
+ }, runOnEditMs);
+ });
+ }
+
pauseBtn.onclick = function () {
moduleRef.wasmPaused = 1;
pauseBtn.disabled = true;
@@ -516,6 +540,10 @@
},
destroy: function () {
destroyed = true;
+ if (runOnEditTimer) {
+ clearTimeout(runOnEditTimer);
+ runOnEditTimer = null;
+ }
if (vfsRemove) {
try {
vfsRemove();
From e416646a33260d160279428412159f810ce2aa64 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Mon, 30 Mar 2026 11:01:17 +0000
Subject: [PATCH 2/4] Tutorial: enable runOnEdit on playground embed only
- web/tutorial.html: debounced auto-run (550ms) for final playground; short note in copy
- docs/tutorial-embedding.md + CHANGELOG: document behavior
Co-authored-by: Chris Garrett
---
CHANGELOG.md | 2 +-
docs/tutorial-embedding.md | 2 +-
web/tutorial.html | 7 ++++++-
3 files changed, 8 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c3cdb8b..f35910d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,7 @@
### Unreleased
-- **Sprites**: **`SPRITECOLLIDE(a, b)`** — returns **1** if two loaded, visible sprites’ axis-aligned bounding boxes overlap (basic-gfx + canvas WASM; **0** otherwise). Terminal **`./basic`** errors if used (requires **basic-gfx** or canvas WASM). **Runtime errors**: optional **`Hint:`** line for **unknown function** (shows name) and **`ensure_num` / `ensure_str`** type mismatches. **`tutorial-embed.js`**: optional **`runOnEdit`** / **`runOnEditMs`** for debounced auto-run after editing.
+- **Sprites**: **`SPRITECOLLIDE(a, b)`** — returns **1** if two loaded, visible sprites’ axis-aligned bounding boxes overlap (basic-gfx + canvas WASM; **0** otherwise). Terminal **`./basic`** errors if used (requires **basic-gfx** or canvas WASM). **Runtime errors**: optional **`Hint:`** line for **unknown function** (shows name) and **`ensure_num` / `ensure_str`** type mismatches. **`tutorial-embed.js`**: optional **`runOnEdit`** / **`runOnEditMs`** for debounced auto-run after editing; **`web/tutorial.html`** enables this on the final playground embed only (**550** ms).
- **Documentation**: **`docs/basic-to-c-transpiler-plan.md`** — design notes for a future **BASIC → C** backend (**cc65** / **z88dk**), recommended subset, and **explicit exclusions** (host I/O, `EVAL`, graphics, etc.).
diff --git a/docs/tutorial-embedding.md b/docs/tutorial-embedding.md
index e401679..6ca347d 100644
--- a/docs/tutorial-embedding.md
+++ b/docs/tutorial-embedding.md
@@ -110,7 +110,7 @@ Each embed gets its own **virtual filesystem** and **Asyncify** state; they do n
| `vfsExportPath` | string | `'/out.txt'` | Default path in the download field |
| `editorMinHeight` | string | `'120px'` | CSS min-height for textarea |
| `outputMinHeight` | string | `'100px'` | CSS min-height for output panel |
-| `runOnEdit` | boolean | `false` | If true, re-run the program after a short pause whenever the editor text changes (debounced) |
+| `runOnEdit` | boolean | `false` | If true, re-run the program after a short pause whenever the editor text changes (debounced). **`web/tutorial.html`** enables this only on the final “playground” embed. |
| `runOnEditMs` | number | `600` | Debounce delay in milliseconds for `runOnEdit` |
## Returned API (`Promise`)
diff --git a/web/tutorial.html b/web/tutorial.html
index 1e6694c..dadb627 100644
--- a/web/tutorial.html
+++ b/web/tutorial.html
@@ -202,6 +202,7 @@ 13. What to try next
Edit the program below and experiment — for example, add a second PRINT or change the message.
+ After you stop typing for about half a second, the run updates automatically (you can still use Run anytime).
@@ -333,7 +334,11 @@ 13. What to try next
RgcBasicTutorialEmbed.mount(document.getElementById('tut-arrays'), { program: progs.arrays });
RgcBasicTutorialEmbed.mount(document.getElementById('tut-functions'), { program: progs.functions });
RgcBasicTutorialEmbed.mount(document.getElementById('tut-rem'), { program: progs.rem });
- RgcBasicTutorialEmbed.mount(document.getElementById('tut-playground'), { program: progs.playground });
+ RgcBasicTutorialEmbed.mount(document.getElementById('tut-playground'), {
+ program: progs.playground,
+ runOnEdit: true,
+ runOnEditMs: 550
+ });
})();