From 0d9f29aa7ad3c1b713add44a202b9206e3bf2a7e Mon Sep 17 00:00:00 2001 From: Eremey Valetov Date: Sat, 21 Mar 2026 02:48:17 -0400 Subject: [PATCH] Fix DRAW M/S/A bugs, add TA rotation, =variable; substitution, X substring execution Rewrites gfx_draw() as a recursive draw_engine() to support all DRAW mini-language features: Bug fixes: - M command parsing: skip generic arg parser so M100,50 correctly parses both coordinates instead of consuming x as a generic arg - S (scale) semantics: distance is now (arg ?: 1) * scale / 4, matching original GW-BASIC where S4 means 1 pixel per unit, not 4 - A (rotation): implements 90-degree rotation state with direction vector transform for all 8 direction commands New features: - TA n: arbitrary rotation angle (-360 to 360 degrees) via cos/sin - =variable;: numeric variable substitution in DRAW strings - X stringvar;: execute substring from string variable (recursive) - Scale factor applied to relative M coordinates --- docs/development.md | 4 +- docs/roadmap.md | 26 +-- src/graphics.c | 226 +++++++++++++++++++------- tests/expected/draw_commands.expected | 13 ++ tests/programs/draw_commands.bas | 43 +++++ 5 files changed, 227 insertions(+), 85 deletions(-) create mode 100644 tests/expected/draw_commands.expected create mode 100644 tests/programs/draw_commands.bas diff --git a/docs/development.md b/docs/development.md index 9732479..649450a 100644 --- a/docs/development.md +++ b/docs/development.md @@ -17,11 +17,11 @@ | 0.11.0 | | DEF SEG / PEEK / POKE virtual memory, GET/PUT graphics sprites, PRINT USING fixes | | 0.12.0 | | BSAVE/BLOAD, TUI ANSI 16-color rendering, CGA graphics framebuffer PEEK/POKE, keyboard shift flags | | 0.13.0 | | VIEW/WINDOW/PALETTE graphics, PMAP function, MBF float format for CVS/CVD/MKS$/MKD$ | -| 0.14.0 | | MBF binary file compatibility (IEEE↔MBF at save/load boundary), fix binary loader null-byte truncation | +| 0.14.0 | | MBF binary file compatibility (IEEE↔MBF at save/load boundary), fix binary loader null-byte truncation, fix DRAW M/S/A bugs, add TA/=var;/X substring | ## Tests -67 test programs in `tests/programs/`, plus 4 classic interactive programs in +68 test programs in `tests/programs/`, plus 4 classic interactive programs in `tests/classic/` (Hamurabi, Lunar Lander, Gunner, Diamond from David Ahl's *BASIC Computer Games*). diff --git a/docs/roadmap.md b/docs/roadmap.md index b64efbc..b4c12f1 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -8,7 +8,7 @@ But we've come this far, so why not? Likely approach: translate the token stream to C and lean on GCC/Clang for the heavy lifting. -## Next Up (v0.14.0) +## Next Up ### Hardware I/O Simulator @@ -38,30 +38,6 @@ following the established `virmem.c` dispatch-by-address pattern. - `INP(port)` → `portio_inp(port)` (currently returns 0) - `STICK(n)` / `STRIG(n)` → joystick state from port 0x201 -### DRAW Command Fixes - -The DRAW mini-language parser has three bugs and four missing features. - -**Bugs to fix:** - -1. **M command parsing** — the generic argument parser consumes digits that - belong to M's x-coordinate, so `DRAW "M100,50"` moves to (0, 50). Fix: - skip the generic parser when the command is M. -2. **S (scale) semantics** — scale is used as a default distance instead of a - multiplier. `DRAW "U"` moves 4 pixels instead of 1. Correct formula: - `dist = (has_arg ? arg : 1) * scale / 4`. -3. **A (90° rotation)** — recognized but no-op. Needs a rotation state - variable and direction vector transform for U/D/L/R/E/F/G/H. - -**Features to add:** - -- `TA n` — arbitrary rotation angle (degrees, −360 to 360); requires - multi-character command parsing and trigonometric direction vector rotation -- `=variable;` — numeric variable substitution in DRAW strings; requires - passing variable lookup capability into `gfx_draw()` -- `X subexpr;` — execute sub-string from a string variable (recursive parse) -- Scale applied to relative M coordinates - ## IDE and Notebook Integration - **Jupyter kernel for GW-BASIC** — a Jupyter Notebook kernel that runs diff --git a/src/graphics.c b/src/graphics.c index f02c377..ed12f48 100644 --- a/src/graphics.c +++ b/src/graphics.c @@ -1,4 +1,5 @@ #include "graphics.h" +#include "gwbasic.h" #include "hal.h" #include #include @@ -266,13 +267,53 @@ void gfx_paint(int x, int y, int fill_color, int border_color) free(stack); } -/* DRAW mini-language parser */ -void gfx_draw(const char *cmd) +/* Parse an integer from a DRAW string, advancing *pp. */ +static int draw_parse_int(const char **pp) { - if (!framebuf) return; - int x = last_x, y = last_y; - int draw_color = current_color; - int scale = 4; /* default scale factor (C4) */ + const char *p = *pp; + int neg = 0, val = 0; + if (*p == '-') { neg = 1; p++; } + else if (*p == '+') p++; + while (isdigit((unsigned char)*p)) { val = val * 10 + (*p - '0'); p++; } + *pp = p; + return neg ? -val : val; +} + +/* Parse a =variable; reference, returning the variable's integer value. */ +static int draw_parse_varref(const char **pp) +{ + const char *p = *pp; + if (*p != '=') return 0; + p++; + /* Parse variable name (1-2 chars + optional type suffix) */ + if (!isalpha((unsigned char)*p)) return 0; + char vname[2] = {toupper(*p), 0}; + p++; + if (isalpha((unsigned char)*p)) { vname[1] = toupper(*p); p++; } + gw_valtype_t vtype = VT_SNG; + if (*p == '%') { vtype = VT_INT; p++; } + else if (*p == '!') { vtype = VT_SNG; p++; } + else if (*p == '#') { vtype = VT_DBL; p++; } + if (*p == ';') p++; + *pp = p; + var_entry_t *var = gw_var_find_or_create(vname, vtype); + if (!var) return 0; + switch (var->val.type) { + case VT_INT: return var->val.ival; + case VT_SNG: return (int)var->val.fval; + case VT_DBL: return (int)var->val.dval; + default: return 0; + } +} + +/* Core DRAW engine, used by both gfx_draw() and X substring recursion. */ +static void draw_engine(const char *cmd, int *px, int *py, + int *pcolor, int *pscale, double *pangle) +{ + int x = *px, y = *py; + int draw_color = *pcolor; + int scale = *pscale; + double angle = *pangle; const char *p = cmd; while (*p) { @@ -280,10 +321,9 @@ void gfx_draw(const char *cmd) if (!*p) break; int blind = 0, no_update = 0; - /* Prefix modifiers */ - while (*p == 'B' || *p == 'b' || *p == 'N' || *p == 'n') { - if (*p == 'B' || *p == 'b') blind = 1; - if (*p == 'N' || *p == 'n') no_update = 1; + while (toupper(*p) == 'B' || toupper(*p) == 'N') { + if (toupper(*p) == 'B') blind = 1; + if (toupper(*p) == 'N') no_update = 1; p++; } @@ -291,78 +331,148 @@ void gfx_draw(const char *cmd) if (!ch) break; p++; - /* Parse optional numeric argument */ - int has_arg = 0, arg = 0; - if (*p == '-' || isdigit((unsigned char)*p)) { - int neg = 0; - if (*p == '-') { neg = 1; p++; } - while (isdigit((unsigned char)*p)) { - arg = arg * 10 + (*p - '0'); + /* Commands that handle their own argument parsing */ + if (ch == 'M' || ch == 'X' || ch == 'T') { + /* TA — multi-char command */ + if (ch == 'T' && toupper(*p) == 'A') { p++; + while (*p == ' ') p++; + int ta; + if (*p == '=') ta = draw_parse_varref(&p); + else ta = draw_parse_int(&p); + angle = ta * M_PI / 180.0; + while (*p == ' ' || *p == ';') p++; + continue; } - if (neg) arg = -arg; + if (ch == 'T') continue; /* unknown T command */ + + if (ch == 'X') { + /* X stringvar; — execute substring */ + while (*p == ' ') p++; + if (!isalpha((unsigned char)*p)) continue; + char vn[2] = {toupper(*p), 0}; + p++; + if (isalpha((unsigned char)*p)) { vn[1] = toupper(*p); p++; } + gw_valtype_t vt = VT_STR; + if (*p == '$') p++; + if (*p == ';') p++; + var_entry_t *sv = gw_var_find_or_create(vn, vt); + if (sv && sv->val.type == VT_STR && sv->val.sval.len > 0) { + char *sub = gw_str_to_cstr(&sv->val.sval); + draw_engine(sub, &x, &y, &draw_color, &scale, &angle); + free(sub); + } + continue; + } + + /* M x,y — move absolute or relative */ + while (*p == ' ') p++; + int relative = (*p == '+' || *p == '-'); + int mx, my; + if (*p == '=') mx = draw_parse_varref(&p); + else mx = draw_parse_int(&p); + while (*p == ' ' || *p == ',') p++; + if (*p == '=') my = draw_parse_varref(&p); + else my = draw_parse_int(&p); + + int nx, ny; + if (relative) { + mx = mx * scale / 4; + my = my * scale / 4; + nx = x + mx; + ny = y + my; + } else { + nx = mx; + ny = my; + } + + if (!blind) + draw_line(x, y, nx, ny, draw_color); + if (!no_update) { x = nx; y = ny; } + while (*p == ' ' || *p == ';') p++; + continue; + } + + /* Generic numeric argument (or =variable; reference) */ + while (*p == ' ') p++; + int has_arg = 0, arg = 0; + if (*p == '=') { + arg = draw_parse_varref(&p); + has_arg = 1; + } else if (*p == '-' || isdigit((unsigned char)*p)) { + arg = draw_parse_int(&p); has_arg = 1; } - int dist = has_arg ? arg : scale; - int nx = x, ny = y; - switch (ch) { - case 'U': ny = y - dist; break; - case 'D': ny = y + dist; break; - case 'L': nx = x - dist; break; - case 'R': nx = x + dist; break; - case 'E': nx = x + dist; ny = y - dist; break; - case 'F': nx = x + dist; ny = y + dist; break; - case 'G': nx = x - dist; ny = y + dist; break; - case 'H': nx = x - dist; ny = y - dist; break; - case 'M': { - /* M x,y or M +/-x, +/-y */ - int relative = 0; - while (*p == ' ') p++; - if (*p == '+' || *p == '-') relative = 1; - int mx = 0, my = 0; - int mneg = 0; - if (*p == '-') { mneg = 1; p++; } - else if (*p == '+') p++; - while (isdigit((unsigned char)*p)) { mx = mx * 10 + (*p - '0'); p++; } - if (mneg) mx = -mx; - while (*p == ' ' || *p == ',') p++; - mneg = 0; - if (*p == '-') { mneg = 1; p++; } - else if (*p == '+') p++; - while (isdigit((unsigned char)*p)) { my = my * 10 + (*p - '0'); p++; } - if (mneg) my = -my; - if (relative) { nx = x + mx; ny = y + my; } - else { nx = mx; ny = my; } - break; - } case 'C': draw_color = has_arg ? arg : 1; + while (*p == ' ' || *p == ';') p++; continue; case 'S': scale = has_arg ? arg : 4; + while (*p == ' ' || *p == ';') p++; continue; case 'A': - /* Angle: 0-3, we ignore rotation for simplicity */ - continue; - case ';': + angle = (has_arg ? (arg & 3) : 0) * M_PI / 2.0; + while (*p == ' ' || *p == ';') p++; continue; default: + break; + } + + /* Direction commands — compute base displacement */ + int dist = (has_arg ? arg : 1) * scale / 4; + int dx = 0, dy = 0; + + switch (ch) { + case 'U': dy = -dist; break; + case 'D': dy = dist; break; + case 'L': dx = -dist; break; + case 'R': dx = dist; break; + case 'E': dx = dist; dy = -dist; break; + case 'F': dx = dist; dy = dist; break; + case 'G': dx = -dist; dy = dist; break; + case 'H': dx = -dist; dy = -dist; break; + default: + while (*p == ' ' || *p == ';') p++; continue; } + /* Apply rotation */ + if (angle != 0.0) { + double c = cos(angle), s = sin(angle); + int rdx = (int)round(dx * c + dy * s); + int rdy = (int)round(-dx * s + dy * c); + dx = rdx; + dy = rdy; + } + + int nx = x + dx, ny = y + dy; if (!blind) draw_line(x, y, nx, ny, draw_color); - - if (!no_update) { - x = nx; - y = ny; - } + if (!no_update) { x = nx; y = ny; } while (*p == ' ' || *p == ';') p++; } + *px = x; *py = y; + *pcolor = draw_color; + *pscale = scale; + *pangle = angle; +} + +/* DRAW mini-language parser */ +void gfx_draw(const char *cmd) +{ + if (!framebuf) return; + int x = last_x, y = last_y; + int draw_color = current_color; + int scale = 4; + double angle = 0.0; + + draw_engine(cmd, &x, &y, &draw_color, &scale, &angle); + last_x = x; last_y = y; } diff --git a/tests/expected/draw_commands.expected b/tests/expected/draw_commands.expected new file mode 100644 index 0000000..608d6db --- /dev/null +++ b/tests/expected/draw_commands.expected @@ -0,0 +1,13 @@ +Pq#0;2;0;0;0#1;2;0;0;66#0}||zzvvnn^^!629~$#1@AACCGGOO__!629?-#0!11~}}||zzvvnn^^!617~$#1!11?@@AACCGGOO__!617?-#0!23~}}||zzvvnn^^!605~$#1!23?@@AACCGGOO__!605?-#0!35~}}||zzvvnn^^!593~$#1!35?@@AACCGGOO__!593?-#0!47~}}||zzvvnn^^!581~$#1!47?@@AACCGGOO__!581?-#0!59~}}||zzvvnn^^!569~$#1!59?@@AACCGGOO__!569?-#0!71~}}||zzvvnn^^!557~$#1!71?@@AACCGGOO__!557?-#0!83~}}||zzvvnn^^!545~$#1!83?@@AACCGGOO__!545?-#0!95~}}||zz!539~$#1!95?@@AACC!539?-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640B-\M abs: 1 +Pq#0;2;0;0;0#1;2;0;0;66#0}||zzvvnn^^!629~$#1@AACCGGOO__!629?-#0!10~n]]||zzvvnn^^!617~$#1!10?O``AACCGGOO__!617?-#0!13~}}||zzvvnn]]||zzvvnn^^!605~$#1!13?@@AACCGGOO``AACCGGOO__!605?-#0!25~}}||zz!4~}}||zzvvnn^^!593~$#1!25?@@AACC!4?@@AACCGGOO__!593?-#0!47~}}||zzvvnn^^!581~$#1!47?@@AACCGGOO__!581?-#0!59~}}||zzvvnn^^!569~$#1!59?@@AACCGGOO__!569?-#0!71~}}||zzvvnn^^!557~$#1!71?@@AACCGGOO__!557?-#0!83~}}||zzvvnn^^!545~$#1!83?@@AACCGGOO__!545?-#0!95~}}||zz!539~$#1!95?@@AACC!539?-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640B-\M rel: 1 +Pq#0;2;0;0;0#1;2;0;0;66#0}||zzvvnn^^!629~$#1@AACCGGOO__!629?-#0!10~n]]||zzvvnn^^!617~$#1!10?O``AACCGGOO__!617?-#0!13~}}||zzvvnn]]||zzvvnn^^!605~$#1!13?@@AACCGGOO``AACCGGOO__!605?-#0!25~}}||zz!4~}}||zzvvnn^^!593~$#1!25?@@AACC!4?@@AACCGGOO__!593?-#0!47~}}||zzvvnn^^!581~$#1!47?@@AACCGGOO__!581?-#0!59~}}||zzvvnn^^!569~$#1!59?@@AACCGGOO__!569?-#0!71~}}||zzvvnn^^!557~$#1!71?@@AACCGGOO__!557?-#0!83~}}||zzvvnn^^!545~$#1!83?@@AACCGGOO__!545?-#0!50~x!44~}}||zz!539~$#1!50?E!44?@@AACC!539?-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640B-\S4 U: 1 +Pq#0;2;0;0;0#1;2;0;0;66#0}||zzvvnn^^!629~$#1@AACCGGOO__!629?-#0!10~n]]||zzvvnn^^!617~$#1!10?O``AACCGGOO__!617?-#0!13~}}||zzvvnn]]||zzvvnn^^!605~$#1!13?@@AACCGGOO``AACCGGOO__!605?-#0!25~}}||zz!4~}}||zzvvnn^^!593~$#1!25?@@AACC!4?@@AACCGGOO__!593?-#0!47~}}||zzvvnn^^!581~$#1!47?@@AACCGGOO__!581?-#0!59~}}||zzvvnn^^!569~$#1!59?@@AACCGGOO__!569?-#0!71~}}||zzvvnn^^!557~$#1!71?@@AACCGGOO__!557?-#0!83~}}||zzvvnn^^!545~$#1!83?@@AACCGGOO__!545?-#0!50~w!44~}}||zz!539~$#1!50?F!44?@@AACC!539?-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640B-\S8 U: 1 +Pq#0;2;0;0;0#1;2;0;0;66#0}||zzvvnn^^!629~$#1@AACCGGOO__!629?-#0!10~n]]||zzvvnn^^!617~$#1!10?O``AACCGGOO__!617?-#0!13~}}||zzvvnn]]||zzvvnn^^!605~$#1!13?@@AACCGGOO``AACCGGOO__!605?-#0!25~}}||zz!4~}}||zzvvnn^^!593~$#1!25?@@AACC!4?@@AACCGGOO__!593?-#0!47~}}||zzvvnn^^!581~$#1!47?@@AACCGGOO__!581?-#0!59~}}||zzvvnn^^!569~$#1!59?@@AACCGGOO__!569?-#0!50~N!20~}}||zzvvnn^^!557~$#1!50?o!20?@@AACCGGOO__!557?-#0!50~?!32~}}||zzvvnn^^!545~$#1!50?~!32?@@AACCGGOO__!545?-#0!50~w!44~}}||zz!539~$#1!50?F!44?@@AACC!539?-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640B-\S8 U5: 1 +Pq#0;2;0;0;0#1;2;0;0;66#0}||zzvvnn^^!629~$#1@AACCGGOO__!629?-#0!10~n]]||zzvvnn^^!617~$#1!10?O``AACCGGOO__!617?-#0!13~}}||zzvvnn]]||zzvvnn^^!605~$#1!13?@@AACCGGOO``AACCGGOO__!605?-#0!25~}}||zz!4~}}||zzvvnn^^!593~$#1!25?@@AACC!4?@@AACCGGOO__!593?-#0!47~}}||zzvvnn^^!581~$#1!47?@@AACCGGOO__!581?-#0!59~}}||zzvvnn^^!569~$#1!59?@@AACCGGOO__!569?-#0!50~N!20~}}||zzvvnn^^!557~$#1!50?o!20?@@AACCGGOO__!557?-#0!50~?!32~}}||zzvvnn^^!545~$#1!50?~!32?@@AACCGGOO__!545?-#0!50~w!44~}}||zz!539~$#1!50?F!44?@@AACC!539?-#0!640~-#0!640~-#0!80~N!559~$#1!80?o!559?-#0!80~?!559~$#1!80?~!559?-#0!80~w!559~$#1!80?F!559?-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640B-\A1 R: 1 +Pq#0;2;0;0;0#1;2;0;0;66#0}||zzvvnn^^!629~$#1@AACCGGOO__!629?-#0!10~n]]||zzvvnn^^!617~$#1!10?O``AACCGGOO__!617?-#0!13~}}||zzvvnn]]||zzvvnn^^!605~$#1!13?@@AACCGGOO``AACCGGOO__!605?-#0!25~}}||zz!4~}}||zzvvnn^^!593~$#1!25?@@AACC!4?@@AACCGGOO__!593?-#0!47~}}||zzvvnn^^!581~$#1!47?@@AACCGGOO__!581?-#0!59~}}||zzvvnn^^!569~$#1!59?@@AACCGGOO__!569?-#0!50~N!20~}}||zzvvnn^^!557~$#1!50?o!20?@@AACCGGOO__!557?-#0!50~?!32~}}||zzvvnn^^!545~$#1!50?~!32?@@AACCGGOO__!545?-#0!50~w!44~}}||zz!539~$#1!50?F!44?@@AACC!539?-#0!640~-#0!640~-#0!80~N!559~$#1!80?o!559?-#0!80~?!559~$#1!80?~!559?-#0!70~!10zw!559~$#1!70?!10CF!559?-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640B-\A2 R: 1 +Pq#0;2;0;0;0#1;2;0;0;66#0}||zzvvnn^^!629~$#1@AACCGGOO__!629?-#0!10~n]]||zzvvnn^^!617~$#1!10?O``AACCGGOO__!617?-#0!13~}}||zzvvnn]]||zzvvnn^^!605~$#1!13?@@AACCGGOO``AACCGGOO__!605?-#0!25~}}||zz!4~}}||zzvvnn^^!593~$#1!25?@@AACC!4?@@AACCGGOO__!593?-#0!47~}}||zzvvnn^^!581~$#1!47?@@AACCGGOO__!581?-#0!59~}}||zzvvnn^^!569~$#1!59?@@AACCGGOO__!569?-#0!50~N!20~}}||zzvvnn^^!557~$#1!50?o!20?@@AACCGGOO__!557?-#0!50~?!32~}}||zzvvnn^^!545~$#1!50?~!32?@@AACCGGOO__!545?-#0!50~w!44~}}||zz!539~$#1!50?F!44?@@AACC!539?-#0!640~-#0!640~-#0!80~N!559~$#1!80?o!559?-#0!80~?!559~$#1!80?~!559?-#0!70~!10zw!559~$#1!70?!10CF!559?-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640B-\Pq#0;2;0;0;0#1;2;0;0;66#0}||zzvvnn^^!629~$#1@AACCGGOO__!629?-#0!10~n]]||zzvvnn^^!617~$#1!10?O``AACCGGOO__!617?-#0!13~}}||zzvvnn]]||zzvvnn^^!605~$#1!13?@@AACCGGOO``AACCGGOO__!605?-#0!25~}}||zz!4~}}||zzvvnn^^!593~$#1!25?@@AACC!4?@@AACCGGOO__!593?-#0!47~}}||zzvvnn^^!581~$#1!47?@@AACCGGOO__!581?-#0!59~}}||zzvvnn^^!569~$#1!59?@@AACCGGOO__!569?-#0!50~N!20~}}||zzvvnn^^!557~$#1!50?o!20?@@AACCGGOO__!557?-#0!50~?!32~}}||zzvvnn^^!545~$#1!50?~!32?@@AACCGGOO__!545?-#0!50~w!9~B!34~}}||zz!539~$#1!50?F!9?{!34?@@AACC!539?-#0!60~?!579~$#1!60?~!579?-#0!60~}!579~$#1!60?@!579?-#0!80~N!559~$#1!80?o!559?-#0!80~?!559~$#1!80?~!559?-#0!70~!10zw!559~$#1!70?!10CF!559?-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640B-\TA90 R: 1 +Pq#0;2;0;0;0#1;2;0;0;66#0}||zzvvnn^^!629~$#1@AACCGGOO__!629?-#0!10~n]]||zzvvnn^^!617~$#1!10?O``AACCGGOO__!617?-#0!13~}}||zzvvnn]]||zzvvnn^^!605~$#1!13?@@AACCGGOO``AACCGGOO__!605?-#0!25~}}||zz!4~}}||zzvvnn^^!593~$#1!25?@@AACC!4?@@AACCGGOO__!593?-#0!47~}}||zzvvnn^^!581~$#1!47?@@AACCGGOO__!581?-#0!59~}}||zzvvnn^^!569~$#1!59?@@AACCGGOO__!569?-#0!50~N!20~}}||zzvvnn^^!557~$#1!50?o!20?@@AACCGGOO__!557?-#0!50~?!32~}}||zzvvnn^^!545~$#1!50?~!32?@@AACCGGOO__!545?-#0!50~w!9~B!34~}}||zz!539~$#1!50?F!9?{!34?@@AACC!539?-#0!60~?!579~$#1!60?~!579?-#0!60~}!579~$#1!60?@!579?-#0!80~N!559~$#1!80?o!559?-#0!80~?!559~$#1!80?~!559?-#0!70~!10zw!559~$#1!70?!10CF!559?-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640B-\Pq#0;2;0;0;0#1;2;0;0;66#0}||zzvvnn^^!629~$#1@AACCGGOO__!629?-#0!10~n]]||zzvvnn^^!617~$#1!10?O``AACCGGOO__!617?-#0!13~}}||zzvvnn]]||zzvvnn^^!605~$#1!13?@@AACCGGOO``AACCGGOO__!605?-#0!25~}}||zz!4~}}||zzvvnn^^!593~$#1!25?@@AACC!4?@@AACCGGOO__!593?-#0!47~}}||zzvvnn^^!581~$#1!47?@@AACCGGOO__!581?-#0!59~}}||zzvvnn^^!569~$#1!59?@@AACCGGOO__!569?-#0!50~N!20~}}||zzvvnn^^!557~$#1!50?o!20?@@AACCGGOO__!557?-#0!50~?!32~}}||zzvvnn^^!545~$#1!50?~!32?@@AACCGGOO__!545?-#0!50~w!9~B!34~}}||zz!539~$#1!50?F!9?{!34?@@AACC!539?-#0!60~?!579~$#1!60?~!579?-#0!60~}!579~$#1!60?@!579?-#0!80~N!559~$#1!80?o!559?-#0!80~?!559~$#1!80?~!559?-#0!70~!10zw!559~$#1!70?!10CF!559?-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!10~!21}!609~$#1!10?!21@!609?-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640B-\=var: 1 +Pq#0;2;0;0;0#1;2;0;0;66#0}||zzvvnn^^!629~$#1@AACCGGOO__!629?-#0!10~n]]||zzvvnn^^!617~$#1!10?O``AACCGGOO__!617?-#0!13~}}||zzvvnn]]||zzvvnn^^!605~$#1!13?@@AACCGGOO``AACCGGOO__!605?-#0!25~}}||zz!4~}}||zzvvnn^^!593~$#1!25?@@AACC!4?@@AACCGGOO__!593?-#0!47~}}||zzvvnn^^!581~$#1!47?@@AACCGGOO__!581?-#0!59~}}||zzvvnn^^!569~$#1!59?@@AACCGGOO__!569?-#0!50~N!20~}}||zzvvnn^^!557~$#1!50?o!20?@@AACCGGOO__!557?-#0!50~?!32~}}||zzvvnn^^!545~$#1!50?~!32?@@AACCGGOO__!545?-#0!50~w!9~B!34~}}||zz!539~$#1!50?F!9?{!34?@@AACC!539?-#0!60~?!579~$#1!60?~!579?-#0!60~}!579~$#1!60?@!579?-#0!80~N!559~$#1!80?o!559?-#0!80~?!559~$#1!80?~!559?-#0!70~!10zw!559~$#1!70?!10CF!559?-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!10~!21}!609~$#1!10?!21@!609?-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640B-\Pq#0;2;0;0;0#1;2;0;0;66#0}||zzvvnn^^!629~$#1@AACCGGOO__!629?-#0!10~n]]||zzvvnn^^!617~$#1!10?O``AACCGGOO__!617?-#0!13~}}||zzvvnn]]||zzvvnn^^!605~$#1!13?@@AACCGGOO``AACCGGOO__!605?-#0!25~}}||zz!4~}}||zzvvnn^^!593~$#1!25?@@AACC!4?@@AACCGGOO__!593?-#0!47~}}||zzvvnn^^!581~$#1!47?@@AACCGGOO__!581?-#0!59~}}||zzvvnn^^!569~$#1!59?@@AACCGGOO__!569?-#0!50~N!20~}}||zzvvnn^^!557~$#1!50?o!20?@@AACCGGOO__!557?-#0!50~?!32~}}||zzvvnn^^!545~$#1!50?~!32?@@AACCGGOO__!545?-#0!50~w!9~B!34~}}||zz!539~$#1!50?F!9?{!34?@@AACC!539?-#0!60~?!579~$#1!60?~!579?-#0!60~}!579~$#1!60?@!579?-#0!80~N!559~$#1!80?o!559?-#0!80~?!559~$#1!80?~!559?-#0!70~!10zw!559~$#1!70?!10CF!559?-#0!640~-#0!640~-#0!640~-#0!640~-#0!120~B!9zB!509~$#1!120?{!9C{!509?-#0!120~?!9~?!509~$#1!120?~!9?~!509?-#0!120~!11}!509~$#1!120?!11@!509?-#0!640~-#0!640~-#0!640~-#0!640~-#0!10~!21}!609~$#1!10?!21@!609?-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640B-\X sub: 1 +X sub: 1 +Pq#0;2;0;0;0#1;2;0;0;66#0}||zzvvnn^^!629~$#1@AACCGGOO__!629?-#0!10~n]]||zzvvnn^^!617~$#1!10?O``AACCGGOO__!617?-#0!13~}}||zzvvnn]]||zzvvnn^^!605~$#1!13?@@AACCGGOO``AACCGGOO__!605?-#0!25~}}||zz!4~}}||zzvvnn^^!593~$#1!25?@@AACC!4?@@AACCGGOO__!593?-#0!47~}}||zzvvnn^^!581~$#1!47?@@AACCGGOO__!581?-#0!59~}}||zzvvnn^^!569~$#1!59?@@AACCGGOO__!569?-#0!50~N!20~}}||zzvvnn^^!557~$#1!50?o!20?@@AACCGGOO__!557?-#0!50~?!32~}}||zzvvnn^^!545~$#1!50?~!32?@@AACCGGOO__!545?-#0!50~w!9~B!34~}}||zz!539~$#1!50?F!9?{!34?@@AACC!539?-#0!60~?!579~$#1!60?~!579?-#0!60~}!579~$#1!60?@!579?-#0!80~N!559~$#1!80?o!559?-#0!80~?!559~$#1!80?~!559?-#0!70~!10zw!559~$#1!70?!10CF!559?-#0!640~-#0!640~-#0!50~!21n!569~$#1!50?!21O!569?-#0!640~-#0!120~B!9zB!509~$#1!120?{!9C{!509?-#0!120~?!9~?!509~$#1!120?~!9?~!509?-#0!120~!11}!509~$#1!120?!11@!509?-#0!640~-#0!640~-#0!640~-#0!640~-#0!10~!21}!609~$#1!10?!21@!609?-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640B-\S M+: 1 +Pq#0;2;0;0;0#1;2;0;0;66#0}||zzvvnn^^!629~$#1@AACCGGOO__!629?-#0!10~n]]||zzvvnn^^!617~$#1!10?O``AACCGGOO__!617?-#0!13~}}||zzvvnn]]||zzvvnn^^!605~$#1!13?@@AACCGGOO``AACCGGOO__!605?-#0!25~}}||zz!4~}}||zzvvnn^^!593~$#1!25?@@AACC!4?@@AACCGGOO__!593?-#0!47~}}||zzvvnn^^!581~$#1!47?@@AACCGGOO__!581?-#0!59~}}||zzvvnn^^!569~$#1!59?@@AACCGGOO__!569?-#0!50~N!20~}}||zzvvnn^^!557~$#1!50?o!20?@@AACCGGOO__!557?-#0!50~?!32~}}||zzvvnn^^!545~$#1!50?~!32?@@AACCGGOO__!545?-#0!50~w!9~B!34~}}||zz!539~$#1!50?F!9?{!34?@@AACC!539?-#0!60~?!579~$#1!60?~!579?-#0!60~}!579~$#1!60?@!579?-#0!80~N!559~$#1!80?o!559?-#0!80~?!559~$#1!80?~!559?-#0!70~!10zw!559~$#1!70?!10CF!559?-#0!640~-#0!640~-#0!50~!21n!569~$#1!50?!21O!569?-#0!640~-#0!120~B!9zB!509~$#1!120?{!9C{!509?-#0!120~?!9~?!509~$#1!120?~!9?~!509?-#0!120~!11}!509~$#1!120?!11@!509?-#0!640~-#0!640~-#0!640~-#0!640~-#0!10~!21}!609~$#1!10?!21@!609?-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640~-#0!640B-\DONE diff --git a/tests/programs/draw_commands.bas b/tests/programs/draw_commands.bas new file mode 100644 index 0000000..68011f0 --- /dev/null +++ b/tests/programs/draw_commands.bas @@ -0,0 +1,43 @@ +10 REM DRAW command comprehensive test +20 SCREEN 2 +30 REM --- Bug 1: M command parsing (absolute) --- +40 DRAW "M100,50" +50 PRINT "M abs:"; POINT(100,50) +60 REM --- Bug 1: M command parsing (relative) --- +70 DRAW "BM10,10 M+20,+10" +80 PRINT "M rel:"; POINT(30,20) +90 REM --- Bug 2: S scale semantics --- +100 REM Default S4: U with no arg moves 1 pixel +110 DRAW "BM50,50 U" +120 PRINT "S4 U:"; POINT(50,49) +130 REM S8: U with no arg moves 2 pixels +140 DRAW "BM50,50 S8 U" +150 PRINT "S8 U:"; POINT(50,48) +160 REM S8: U5 moves 10 pixels (5*8/4=10) +170 DRAW "BM50,50 S8 U5" +180 PRINT "S8 U5:"; POINT(50,40) +190 REM --- Bug 3: A rotation --- +200 DRAW "BM80,80 A1 R10" +210 PRINT "A1 R:"; POINT(80,70) +220 DRAW "A2 BM80,80 R10" +230 PRINT "A2 R:"; POINT(70,80) +240 DRAW "A0" +250 REM --- TA arbitrary rotation --- +260 DRAW "BM60,60 TA90 R10" +270 PRINT "TA90 R:"; POINT(60,50) +280 DRAW "TA0" +290 REM --- =variable; substitution --- +300 D%=20 +310 DRAW "BM10,150 R=D%;" +320 PRINT "=var:"; POINT(15,150) +330 REM --- X substring execution --- +340 A$="U10R10D10L10" +350 DRAW "BM120,120" +360 DRAW "XA$;" +370 PRINT "X sub:"; POINT(120,110) +380 PRINT "X sub:"; POINT(130,120) +390 REM --- Scale on relative M --- +400 DRAW "S8 BM50,100 M+10,+0" +410 PRINT "S M+:"; POINT(70,100) +420 DRAW "S4" +430 PRINT "DONE"