diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..24735a3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,23 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build-and-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Build + run: | + mkdir build + cd build + cmake .. + make -j$(nproc) + + - name: Test + run: bash tests/run_tests.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index 976a91c..b79006c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,6 +29,7 @@ set(SOURCES src/fileio.c src/program_io.c src/print_using.c + src/graphics.c platform/hal_posix.c ) diff --git a/include/graphics.h b/include/graphics.h new file mode 100644 index 0000000..468fe2b --- /dev/null +++ b/include/graphics.h @@ -0,0 +1,34 @@ +#ifndef GW_GRAPHICS_H +#define GW_GRAPHICS_H + +#include + +void gfx_init(int mode); +void gfx_shutdown(void); +void gfx_cls(void); +bool gfx_active(void); +int gfx_get_mode(void); + +void gfx_pset(int x, int y, int color); +int gfx_point(int x, int y); +void gfx_line(int x1, int y1, int x2, int y2, int color, int style); +void gfx_circle(int cx, int cy, int r, int color, double start, double end, double aspect); +void gfx_paint(int x, int y, int fill_color, int border_color); +void gfx_draw(const char *cmd); + +void gfx_flush(void); + +/* Current drawing state */ +void gfx_set_color(int c); +int gfx_get_color(void); +void gfx_get_last(int *x, int *y); +void gfx_set_last(int x, int y); +int gfx_get_width(void); +int gfx_get_height(void); + +/* LINE style constants */ +#define GFX_LINE 0 +#define GFX_BOX 'B' +#define GFX_BOXF 'F' + +#endif diff --git a/include/gwbasic.h b/include/gwbasic.h index 8507e0b..0ff53c6 100644 --- a/include/gwbasic.h +++ b/include/gwbasic.h @@ -10,7 +10,7 @@ #include "gw_math.h" #include "strings.h" -#define GW_VERSION "0.4.0" +#define GW_VERSION "0.5.0" #define GW_BANNER "GW-BASIC " GW_VERSION /* Tokenizer */ diff --git a/include/hal.h b/include/hal.h index dd3b638..a1df884 100644 --- a/include/hal.h +++ b/include/hal.h @@ -17,9 +17,17 @@ typedef struct hal_ops { void (*cls)(void); void (*set_width)(int cols); + /* Raw mode for INKEY$/INPUT$ */ + void (*enable_raw)(void); + void (*disable_raw)(void); + + /* Write raw bytes bypassing cursor tracking (for Sixel, etc.) */ + void (*write_raw)(const char *data, int len); + /* Terminal properties */ int screen_width; int screen_height; + bool is_tty; /* Lifecycle */ void (*init)(void); diff --git a/platform/hal_posix.c b/platform/hal_posix.c index d1c7bc7..cfacfe6 100644 --- a/platform/hal_posix.c +++ b/platform/hal_posix.c @@ -8,8 +8,10 @@ static struct termios orig_termios; static int raw_mode = 0; +static int termios_saved = 0; static int cursor_row = 0; static int cursor_col = 0; +static int pending_char = -1; static void posix_putch(int ch) { @@ -33,18 +35,36 @@ static void posix_puts(const char *s) static int posix_getch(void) { fflush(stdout); + if (pending_char >= 0) { + int ch = pending_char; + pending_char = -1; + return ch; + } + if (raw_mode) { + /* Set VMIN=1 to block until a byte arrives */ + struct termios t; + tcgetattr(STDIN_FILENO, &t); + t.c_cc[VMIN] = 1; + tcsetattr(STDIN_FILENO, TCSANOW, &t); + unsigned char ch; + read(STDIN_FILENO, &ch, 1); + t.c_cc[VMIN] = 0; + tcsetattr(STDIN_FILENO, TCSANOW, &t); + return ch; + } return getchar(); } static bool posix_kbhit(void) { - /* In cooked mode, always return false */ + if (pending_char >= 0) + return true; if (!raw_mode) return false; - - int ch = getchar(); - if (ch != EOF) { - ungetc(ch, stdin); + unsigned char ch; + ssize_t n = read(STDIN_FILENO, &ch, 1); + if (n == 1) { + pending_char = ch; return true; } return false; @@ -71,13 +91,42 @@ static void posix_cls(void) static void posix_set_width(int cols) { - /* Not much we can do on POSIX without ncurses */ (void)cols; } +static void posix_enable_raw(void) +{ + if (raw_mode || !isatty(STDIN_FILENO)) + return; + if (!termios_saved) { + tcgetattr(STDIN_FILENO, &orig_termios); + termios_saved = 1; + } + struct termios raw = orig_termios; + cfmakeraw(&raw); + raw.c_oflag |= OPOST; /* keep output processing (newline translation) */ + raw.c_cc[VMIN] = 0; + raw.c_cc[VTIME] = 0; + tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw); + raw_mode = 1; +} + +static void posix_disable_raw(void) +{ + if (!raw_mode) + return; + tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios); + raw_mode = 0; +} + +static void posix_write_raw(const char *data, int len) +{ + fflush(stdout); + write(STDOUT_FILENO, data, len); +} + static void posix_init(void) { - /* Disable output buffering for interactive use */ setbuf(stdout, NULL); } @@ -99,19 +148,23 @@ static hal_ops_t posix_hal = { .get_cursor_col = posix_get_cursor_col, .cls = posix_cls, .set_width = posix_set_width, + .enable_raw = posix_enable_raw, + .disable_raw = posix_disable_raw, + .write_raw = posix_write_raw, .screen_width = 80, .screen_height = 25, + .is_tty = false, .init = posix_init, .shutdown = posix_shutdown, }; hal_ops_t *hal_posix_create(void) { - /* Try to detect terminal size */ struct winsize ws; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0) { if (ws.ws_col > 0) posix_hal.screen_width = ws.ws_col; if (ws.ws_row > 0) posix_hal.screen_height = ws.ws_row; } + posix_hal.is_tty = isatty(STDIN_FILENO); return &posix_hal; } diff --git a/src/eval.c b/src/eval.c index e047580..c9c6065 100644 --- a/src/eval.c +++ b/src/eval.c @@ -1,4 +1,5 @@ #include "gwbasic.h" +#include "graphics.h" #include #include #include @@ -37,10 +38,10 @@ static int op_prec(uint8_t tok) { switch (tok) { case TOK_IMP: return 40; - case TOK_EQV: return 40; - case TOK_XOR: return 50; - case TOK_OR: return 60; - case TOK_AND: return 70; + case TOK_EQV: return 42; + case TOK_XOR: return 44; + case TOK_OR: return 46; + case TOK_AND: return 48; case TOK_GT: case TOK_EQ: case TOK_LT: return 64; @@ -287,7 +288,7 @@ static gw_value_t eval_unary(void) if (tok == TOK_NOT) { gw_chrget(); - gw_value_t v = eval_expr(90); + gw_value_t v = eval_expr(50); int16_t i = gw_to_int(&v); gw_value_t result; result.type = VT_INT; @@ -843,6 +844,20 @@ static gw_value_t eval_atom(void) return v; } + /* POINT(x,y) function */ + if (tok == TOK_POINT) { + gw_chrget(); + gw_expect('('); + int px = gw_eval_int(); + gw_expect(','); + int py = gw_eval_int(); + gw_expect_rparen(); + gw_value_t v; + v.type = VT_INT; + v.ival = gfx_point(px, py); + return v; + } + /* CSRLIN pseudo-variable */ if (tok == TOK_CSRLIN) { gw_chrget(); @@ -867,6 +882,43 @@ static gw_value_t eval_atom(void) return v; } + /* INPUT$ function: INPUT$(n [,#filenum]) */ + if (tok == TOK_INPUT) { + uint8_t *save = gw.text_ptr; + gw_chrget(); + if (gw_chrgot() == '$') { + gw_chrget(); + gw_expect('('); + int n = gw_eval_int(); + if (n < 1 || n > 255) gw_error(ERR_FC); + int filenum = 0; + gw_skip_spaces(); + if (gw_chrgot() == ',') { + gw_chrget(); + gw_skip_spaces(); + if (gw_chrgot() == '#') gw_chrget(); + filenum = gw_eval_int(); + } + gw_expect_rparen(); + gw_value_t v; + v.type = VT_STR; + v.sval = gw_str_alloc(n); + if (filenum > 0) { + file_entry_t *fe = gw_file_get(filenum); + for (int i = 0; i < n; i++) { + int ch = fgetc(fe->fp); + if (ch == EOF) { v.sval.len = i; break; } + v.sval.data[i] = ch; + } + } else { + for (int i = 0; i < n; i++) + v.sval.data[i] = gw_hal ? gw_hal->getch() : getchar(); + } + return v; + } + gw.text_ptr = save; + } + /* Extended statement tokens that work as functions (DATE$, TIME$) */ if (tok == TOK_PREFIX_FE) { uint8_t *save = gw.text_ptr; diff --git a/src/graphics.c b/src/graphics.c new file mode 100644 index 0000000..0475ffb --- /dev/null +++ b/src/graphics.c @@ -0,0 +1,463 @@ +#include "graphics.h" +#include "hal.h" +#include +#include +#include +#include +#include + +static uint8_t *framebuf; +static int fb_width, fb_height; +static int screen_mode; +static int current_color = 1; +static int last_x, last_y; + +/* CGA default palette (RGBI) */ +static uint32_t palette[16] = { + 0x000000, 0x0000AA, 0x00AA00, 0x00AAAA, + 0xAA0000, 0xAA00AA, 0xAA5500, 0xAAAAAA, + 0x555555, 0x5555FF, 0x55FF55, 0x55FFFF, + 0xFF5555, 0xFF55FF, 0xFFFF55, 0xFFFFFF, +}; + +bool gfx_active(void) { return framebuf != NULL; } +int gfx_get_mode(void) { return screen_mode; } +void gfx_set_color(int c) { current_color = c; } +int gfx_get_color(void) { return current_color; } +void gfx_get_last(int *x, int *y) { *x = last_x; *y = last_y; } +void gfx_set_last(int x, int y) { last_x = x; last_y = y; } +int gfx_get_width(void) { return fb_width; } +int gfx_get_height(void) { return fb_height; } + +void gfx_init(int mode) +{ + gfx_shutdown(); + screen_mode = mode; + switch (mode) { + case 1: fb_width = 320; fb_height = 200; break; + case 2: fb_width = 640; fb_height = 200; break; + default: return; /* text mode, no framebuffer */ + } + framebuf = calloc(fb_width * fb_height, 1); + current_color = (mode == 2) ? 1 : 3; + last_x = 0; + last_y = 0; +} + +void gfx_shutdown(void) +{ + free(framebuf); + framebuf = NULL; + fb_width = 0; + fb_height = 0; + screen_mode = 0; +} + +void gfx_cls(void) +{ + if (framebuf) + memset(framebuf, 0, fb_width * fb_height); +} + +static inline void set_pixel(int x, int y, int color) +{ + if (x >= 0 && x < fb_width && y >= 0 && y < fb_height) + framebuf[y * fb_width + x] = color; +} + +static inline int get_pixel(int x, int y) +{ + if (x >= 0 && x < fb_width && y >= 0 && y < fb_height) + return framebuf[y * fb_width + x]; + return 0; +} + +void gfx_pset(int x, int y, int color) +{ + set_pixel(x, y, color); + last_x = x; + last_y = y; +} + +int gfx_point(int x, int y) +{ + return get_pixel(x, y); +} + +/* Bresenham line */ +static void draw_line(int x0, int y0, int x1, int y1, int color) +{ + int dx = abs(x1 - x0); + int dy = -abs(y1 - y0); + int sx = x0 < x1 ? 1 : -1; + int sy = y0 < y1 ? 1 : -1; + int err = dx + dy; + + for (;;) { + set_pixel(x0, y0, color); + if (x0 == x1 && y0 == y1) break; + int e2 = 2 * err; + if (e2 >= dy) { err += dy; x0 += sx; } + if (e2 <= dx) { err += dx; y0 += sy; } + } +} + +void gfx_line(int x1, int y1, int x2, int y2, int color, int style) +{ + if (style == GFX_BOXF) { + /* Filled box */ + int xlo = x1 < x2 ? x1 : x2; + int xhi = x1 > x2 ? x1 : x2; + int ylo = y1 < y2 ? y1 : y2; + int yhi = y1 > y2 ? y1 : y2; + for (int y = ylo; y <= yhi; y++) + for (int x = xlo; x <= xhi; x++) + set_pixel(x, y, color); + } else if (style == GFX_BOX) { + /* Box outline */ + draw_line(x1, y1, x2, y1, color); + draw_line(x2, y1, x2, y2, color); + draw_line(x2, y2, x1, y2, color); + draw_line(x1, y2, x1, y1, color); + } else { + draw_line(x1, y1, x2, y2, color); + } + last_x = x2; + last_y = y2; +} + +/* Midpoint circle with aspect ratio */ +void gfx_circle(int cx, int cy, int r, int color, + double start, double end, double aspect) +{ + if (r <= 0) return; + + if (start == 0.0 && end == 0.0) { + /* Full circle */ + double ax = (aspect > 0) ? aspect : (5.0 / 6.0); /* CGA aspect */ + int x = 0, y = r; + int d = 1 - r; + while (x <= y) { + int px, py; + /* 8 octants with aspect adjustment */ + px = (int)(x * ax); py = y; + set_pixel(cx + px, cy + py, color); + set_pixel(cx - px, cy + py, color); + set_pixel(cx + px, cy - py, color); + set_pixel(cx - px, cy - py, color); + px = (int)(y * ax); py = x; + set_pixel(cx + px, cy + py, color); + set_pixel(cx - px, cy + py, color); + set_pixel(cx + px, cy - py, color); + set_pixel(cx - px, cy - py, color); + if (d < 0) { + d += 2 * x + 3; + } else { + d += 2 * (x - y) + 5; + y--; + } + x++; + } + } else { + /* Arc: parametric with aspect */ + double ax = (aspect > 0) ? aspect : (5.0 / 6.0); + double step = 1.0 / r; + if (step > 0.05) step = 0.05; + for (double a = start; a <= end; a += step) { + int px = cx + (int)(r * cos(a) * ax); + int py = cy - (int)(r * sin(a)); + set_pixel(px, py, color); + } + } + last_x = cx; + last_y = cy; +} + +/* Flood fill (stack-based) */ +void gfx_paint(int x, int y, int fill_color, int border_color) +{ + if (!framebuf) return; + if (x < 0 || x >= fb_width || y < 0 || y >= fb_height) return; + + int start = get_pixel(x, y); + if (start == fill_color || start == border_color) return; + + /* Simple stack-based flood fill */ + int stack_cap = 4096; + int *stack = malloc(stack_cap * 2 * sizeof(int)); + if (!stack) return; + int sp = 0; + + #define PUSH(px, py) do { \ + if (sp < stack_cap) { stack[sp*2] = (px); stack[sp*2+1] = (py); sp++; } \ + } while(0) + #define POP(px, py) do { sp--; (px) = stack[sp*2]; (py) = stack[sp*2+1]; } while(0) + + PUSH(x, y); + while (sp > 0) { + int cx, cy; + POP(cx, cy); + if (cx < 0 || cx >= fb_width || cy < 0 || cy >= fb_height) continue; + int c = get_pixel(cx, cy); + if (c == fill_color || c == border_color) continue; + set_pixel(cx, cy, fill_color); + /* Grow stack if needed */ + if (sp + 4 >= stack_cap) { + stack_cap *= 2; + int *ns = realloc(stack, stack_cap * 2 * sizeof(int)); + if (!ns) break; + stack = ns; + } + PUSH(cx + 1, cy); + PUSH(cx - 1, cy); + PUSH(cx, cy + 1); + PUSH(cx, cy - 1); + } + + #undef PUSH + #undef POP + free(stack); +} + +/* 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; /* default scale factor (C4) */ + const char *p = cmd; + + while (*p) { + while (*p == ' ') p++; + 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; + p++; + } + + char ch = toupper(*p); + 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'); + p++; + } + if (neg) arg = -arg; + 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; + continue; + case 'S': + scale = has_arg ? arg : 4; + continue; + case 'A': + /* Angle: 0-3, we ignore rotation for simplicity */ + continue; + case ';': + continue; + default: + continue; + } + + if (!blind) + draw_line(x, y, nx, ny, draw_color); + + if (!no_update) { + x = nx; + y = ny; + } + + while (*p == ' ' || *p == ';') p++; + } + + last_x = x; + last_y = y; +} + +/* Sixel output encoder */ +void gfx_flush(void) +{ + if (!framebuf || !gw_hal) return; + + /* Determine which colors are used */ + int color_used[16] = {0}; + for (int i = 0; i < fb_width * fb_height; i++) + color_used[framebuf[i] & 0x0F] = 1; + + /* Build Sixel output in a buffer */ + int buf_cap = 1024 * 64; + char *buf = malloc(buf_cap); + if (!buf) return; + int pos = 0; + + #define EMIT(s) do { \ + int _l = strlen(s); \ + if (pos + _l >= buf_cap) { \ + buf_cap *= 2; \ + char *nb = realloc(buf, buf_cap); \ + if (!nb) { free(buf); return; } \ + buf = nb; \ + } \ + memcpy(buf + pos, s, _l); pos += _l; \ + } while(0) + + /* DCS q - enter Sixel mode */ + EMIT("\033Pq"); + + /* Define palette */ + for (int c = 0; c < 16; c++) { + if (!color_used[c]) continue; + uint32_t rgb = palette[c]; + int r = ((rgb >> 16) & 0xFF) * 100 / 255; + int g = ((rgb >> 8) & 0xFF) * 100 / 255; + int b = (rgb & 0xFF) * 100 / 255; + char pal[32]; + snprintf(pal, sizeof(pal), "#%d;2;%d;%d;%d", c, r, g, b); + EMIT(pal); + } + + /* Output Sixel bands (6 rows each) */ + for (int band = 0; band < fb_height; band += 6) { + int band_h = (band + 6 <= fb_height) ? 6 : fb_height - band; + + for (int c = 0; c < 16; c++) { + if (!color_used[c]) continue; + + /* Check if this color appears in this band */ + int has_color = 0; + for (int y2 = band; y2 < band + band_h && !has_color; y2++) + for (int x2 = 0; x2 < fb_width && !has_color; x2++) + if (framebuf[y2 * fb_width + x2] == c) + has_color = 1; + if (!has_color) continue; + + /* Emit color selector */ + char cs[8]; + snprintf(cs, sizeof(cs), "#%d", c); + EMIT(cs); + + /* Emit Sixel data for this color in this band */ + /* Use RLE for runs of same byte */ + int prev_sixel = -1; + int run_len = 0; + + for (int x2 = 0; x2 < fb_width; x2++) { + int sixel = 0; + for (int bit = 0; bit < band_h; bit++) { + int y2 = band + bit; + if (framebuf[y2 * fb_width + x2] == c) + sixel |= (1 << bit); + } + + if (sixel == prev_sixel) { + run_len++; + } else { + /* Flush previous run */ + if (prev_sixel >= 0) { + char sc[16]; + if (run_len > 3) { + snprintf(sc, sizeof(sc), "!%d%c", run_len, prev_sixel + 63); + } else { + for (int r2 = 0; r2 < run_len; r2++) { + sc[0] = prev_sixel + 63; + sc[1] = '\0'; + EMIT(sc); + } + sc[0] = '\0'; + } + EMIT(sc); + } + prev_sixel = sixel; + run_len = 1; + } + } + /* Flush last run */ + if (prev_sixel >= 0) { + char sc[16]; + if (run_len > 3) { + snprintf(sc, sizeof(sc), "!%d%c", run_len, prev_sixel + 63); + } else { + for (int r2 = 0; r2 < run_len; r2++) { + sc[0] = prev_sixel + 63; + sc[1] = '\0'; + EMIT(sc); + } + sc[0] = '\0'; + } + EMIT(sc); + } + + /* GNL ($/- selectors): $ = CR (reuse row), - = newline */ + /* If more colors in this band, use $; else use - for next band */ + /* Check if there's another color to render in this band */ + int more = 0; + for (int nc = c + 1; nc < 16; nc++) { + if (!color_used[nc]) continue; + for (int y2 = band; y2 < band + band_h && !more; y2++) + for (int x2 = 0; x2 < fb_width && !more; x2++) + if (framebuf[y2 * fb_width + x2] == nc) + more = 1; + if (more) break; + } + if (more) { + EMIT("$"); /* CR - overlay next color on same band */ + } + } + EMIT("-"); /* LF - advance to next band */ + } + + /* ST - string terminator */ + EMIT("\033\\"); + + #undef EMIT + + /* Write out via HAL raw output */ + gw_hal->write_raw(buf, pos); + free(buf); +} diff --git a/src/input.c b/src/input.c index 454acee..fed10c4 100644 --- a/src/input.c +++ b/src/input.c @@ -13,8 +13,12 @@ static char *read_input_line(void) { static char buf[256]; - if (fgets(buf, sizeof(buf), stdin) == NULL) + if (gw_hal) gw_hal->disable_raw(); + if (fgets(buf, sizeof(buf), stdin) == NULL) { + if (gw_hal) gw_hal->enable_raw(); return NULL; + } + if (gw_hal) gw_hal->enable_raw(); int len = strlen(buf); while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) buf[--len] = '\0'; diff --git a/src/interp.c b/src/interp.c index 0e7b53c..2305550 100644 --- a/src/interp.c +++ b/src/interp.c @@ -1,4 +1,5 @@ #include "gwbasic.h" +#include "graphics.h" #include #include #include @@ -682,6 +683,7 @@ void gw_exec_stmt(void) if (tok == TOK_CLS) { gw_chrget(); if (gw_hal) gw_hal->cls(); + if (gfx_active()) { gfx_cls(); gfx_flush(); } return; } @@ -762,11 +764,90 @@ void gw_exec_stmt(void) free(oldpath); free(newpath); return; } - /* Graphics/sound stubs - parse and discard arguments */ - if (xstmt == XSTMT_CIRCLE || xstmt == XSTMT_DRAW || - xstmt == XSTMT_PAINT || xstmt == XSTMT_PLAY || - xstmt == XSTMT_VIEW || xstmt == XSTMT_WINDOW || - xstmt == XSTMT_PALETTE) { + /* CIRCLE (cx,cy),radius[,[color][,[start][,[end][,aspect]]]] */ + if (xstmt == XSTMT_CIRCLE) { + gw_chrget(); + gw_skip_spaces(); + gw_expect('('); + int cx = gw_eval_int(); + gw_expect(','); + int cy = gw_eval_int(); + gw_expect_rparen(); + gw_expect(','); + int radius = gw_eval_int(); + int color = gfx_get_color(); + double start_a = 0, end_a = 0, aspect = 0; + gw_value_t tmp; + gw_skip_spaces(); + if (gw_chrgot() == ',') { + gw_chrget(); + gw_skip_spaces(); + if (gw_chrgot() != ',' && gw_chrgot() != 0 && gw_chrgot() != ':') + color = gw_eval_int(); + gw_skip_spaces(); + if (gw_chrgot() == ',') { + gw_chrget(); + gw_skip_spaces(); + if (gw_chrgot() != ',' && gw_chrgot() != 0 && gw_chrgot() != ':') { + tmp = gw_eval_num(); start_a = gw_to_dbl(&tmp); + } + gw_skip_spaces(); + if (gw_chrgot() == ',') { + gw_chrget(); + gw_skip_spaces(); + if (gw_chrgot() != ',' && gw_chrgot() != 0 && gw_chrgot() != ':') { + tmp = gw_eval_num(); end_a = gw_to_dbl(&tmp); + } + gw_skip_spaces(); + if (gw_chrgot() == ',') { + gw_chrget(); + tmp = gw_eval_num(); aspect = gw_to_dbl(&tmp); + } + } + } + } + gfx_circle(cx, cy, radius, color, start_a, end_a, aspect); + gfx_flush(); + return; + } + /* DRAW string-expr */ + if (xstmt == XSTMT_DRAW) { + gw_chrget(); + gw_value_t s = gw_eval_str(); + char *cmd = gw_str_to_cstr(&s.sval); + gw_str_free(&s.sval); + gfx_draw(cmd); + free(cmd); + gfx_flush(); + return; + } + /* PAINT (x,y)[,fill_color[,border_color]] */ + if (xstmt == XSTMT_PAINT) { + gw_chrget(); + gw_skip_spaces(); + gw_expect('('); + int px = gw_eval_int(); + gw_expect(','); + int py = gw_eval_int(); + gw_expect_rparen(); + int fill_c = gfx_get_color(), border_c = 0; + gw_skip_spaces(); + if (gw_chrgot() == ',') { + gw_chrget(); + fill_c = gw_eval_int(); + gw_skip_spaces(); + if (gw_chrgot() == ',') { + gw_chrget(); + border_c = gw_eval_int(); + } + } + gfx_paint(px, py, fill_c, border_c); + gfx_flush(); + return; + } + /* Stubs: PLAY, VIEW, WINDOW, PALETTE */ + if (xstmt == XSTMT_PLAY || xstmt == XSTMT_VIEW || + xstmt == XSTMT_WINDOW || xstmt == XSTMT_PALETTE) { gw_chrget(); while (gw_chrgot() && gw_chrgot() != ':' && gw_chrgot() != TOK_ELSE) gw.text_ptr++; @@ -802,6 +883,7 @@ void gw_exec_stmt(void) /* NEW */ if (tok == TOK_NEW) { gw_chrget(); + gfx_shutdown(); gw_free_program(); gw_vars_clear(); gw_arrays_clear(); @@ -1381,11 +1463,55 @@ void gw_exec_stmt(void) gw_stmt_line_input(); return; } - /* LINE (x1,y1)-(x2,y2) [,[color][,B[F]]] - graphics stub */ - if (gw_chrgot() == '(' || gw_chrgot() == TOK_MINUS) { - /* Parse and discard all arguments */ - while (gw_chrgot() && gw_chrgot() != ':' && gw_chrgot() != TOK_ELSE) - gw.text_ptr++; + /* LINE (x1,y1)-(x2,y2) [,[color][,B[F]]] */ + if (gw_chrgot() == '(' || gw_chrgot() == TOK_MINUS || gw_chrgot() == TOK_STEP) { + int x1, y1, x2, y2; + /* First point is optional (uses last point) */ + gw_skip_spaces(); + if (gw_chrgot() == '(') { + gw_chrget(); + x1 = gw_eval_int(); + gw_expect(','); + y1 = gw_eval_int(); + gw_expect_rparen(); + } else { + gfx_get_last(&x1, &y1); + } + gw_skip_spaces(); + gw_expect(TOK_MINUS); + gw_expect('('); + x2 = gw_eval_int(); + gw_expect(','); + y2 = gw_eval_int(); + gw_expect_rparen(); + int color = gfx_get_color(); + int style = GFX_LINE; + gw_skip_spaces(); + if (gw_chrgot() == ',') { + gw_chrget(); + gw_skip_spaces(); + if (gw_chrgot() != ',' && gw_chrgot() != 0 && gw_chrgot() != ':' && + gw_chrgot() != 'B' && gw_chrgot() != 'b' && + !gw_is_letter(gw_chrgot())) + color = gw_eval_int(); + gw_skip_spaces(); + if (gw_chrgot() == ',') { + gw_chrget(); + gw_skip_spaces(); + if (gw_chrgot() == 'B' || gw_chrgot() == 'b') { + gw_chrget(); + gw_skip_spaces(); + if (gw_chrgot() == 'F' || gw_chrgot() == 'f') { + style = GFX_BOXF; + gw_chrget(); + } else { + style = GFX_BOX; + } + } + } + } + gfx_line(x1, y1, x2, y2, color, style); + gfx_flush(); return; } gw_error(ERR_SN); @@ -1553,38 +1679,83 @@ void gw_exec_stmt(void) return; } - /* COLOR - parse and ignore */ + /* COLOR */ if (tok == TOK_COLOR) { gw_chrget(); - gw_eval_int(); - while (gw_chrgot() == ',') { + int fg = gw_eval_int(); + int bg = -1; + gw_skip_spaces(); + if (gw_chrgot() == ',') { gw_chrget(); gw_skip_spaces(); if (gw_chrgot() != ',' && gw_chrgot() != 0 && gw_chrgot() != ':') - gw_eval_int(); + bg = gw_eval_int(); + /* Skip optional border parameter */ + gw_skip_spaces(); + if (gw_chrgot() == ',') { + gw_chrget(); + gw_skip_spaces(); + if (gw_chrgot() != 0 && gw_chrgot() != ':') + gw_eval_int(); + } + } + if (gfx_active()) { + gfx_set_color(fg); + } else if (gw_hal) { + /* Text mode: emit ANSI color codes */ + char ansi[32]; + /* Map GW-BASIC colors to ANSI (simplified) */ + static const int ansi_fg[] = {30,34,32,36,31,35,33,37,90,94,92,96,91,95,93,97}; + static const int ansi_bg[] = {40,44,42,46,41,45,43,47}; + if (fg >= 0 && fg < 16) { + snprintf(ansi, sizeof(ansi), "\033[%dm", ansi_fg[fg]); + gw_hal->puts(ansi); + } + if (bg >= 0 && bg < 8) { + snprintf(ansi, sizeof(ansi), "\033[%dm", ansi_bg[bg]); + gw_hal->puts(ansi); + } } return; } - /* SCREEN - graphics stub */ + /* SCREEN mode [,[colorswitch][,[apage][,vpage]]] */ if (tok == TOK_SCREEN) { gw_chrget(); - gw_eval_int(); /* screen mode */ + int mode = gw_eval_int(); + /* Skip optional args */ while (gw_chrgot() == ',') { gw_chrget(); gw_skip_spaces(); if (gw_chrgot() != ',' && gw_chrgot() != 0 && gw_chrgot() != ':') gw_eval_int(); } + if (mode == 0) { + gfx_shutdown(); + } else { + gfx_init(mode); + } return; } - /* PSET / PRESET - graphics stub */ + /* PSET (x,y)[,color] / PRESET (x,y)[,color] */ if (tok == TOK_PSET || tok == TOK_PRESET) { + int is_preset = (tok == TOK_PRESET); gw_chrget(); - /* Parse (x,y) [,color] and discard */ - while (gw_chrgot() && gw_chrgot() != ':' && gw_chrgot() != TOK_ELSE) - gw.text_ptr++; + gw_skip_spaces(); + gw_expect('('); + int px = gw_eval_int(); + gw_expect(','); + int py = gw_eval_int(); + gw_expect_rparen(); + int color = is_preset ? 0 : gfx_get_color(); + gw_skip_spaces(); + if (gw_chrgot() == ',') { + gw_chrget(); + color = gw_eval_int(); + } + gfx_pset(px, py, color); + gfx_flush(); return; } @@ -1762,9 +1933,14 @@ void gw_exec_stmt(void) void gw_run_loop(void) { + if (gw_hal) gw_hal->enable_raw(); + if (setjmp(gw_run_jmp) != 0) { /* Error handler redirected here via ON ERROR GOTO */ - if (!gw.running) return; + if (!gw.running) { + if (gw_hal) gw_hal->disable_raw(); + return; + } } while (gw.running) { @@ -1799,4 +1975,6 @@ void gw_run_loop(void) if (!gw.running) break; } + + if (gw_hal) gw_hal->disable_raw(); } diff --git a/tests/programs/bubble_sort.bas b/tests/programs/bubble_sort.bas new file mode 100644 index 0000000..b3d7bf7 --- /dev/null +++ b/tests/programs/bubble_sort.bas @@ -0,0 +1,14 @@ +10 REM Bubble sort with string comparison +20 DIM A$(9) +30 DATA "banana","apple","cherry","date","elderberry" +40 DATA "fig","grape","honeydew","kiwi","lemon" +50 FOR I = 0 TO 9 : READ A$(I) : NEXT I +60 REM Sort +70 FOR I = 0 TO 8 +80 FOR J = 0 TO 8 - I +90 IF A$(J) > A$(J+1) THEN SWAP A$(J), A$(J+1) +100 NEXT J +110 NEXT I +120 REM Print sorted +130 FOR I = 0 TO 9 : PRINT A$(I) : NEXT I +140 PRINT "Bubble sort OK" diff --git a/tests/programs/caesar_cipher.bas b/tests/programs/caesar_cipher.bas new file mode 100644 index 0000000..2c5af54 --- /dev/null +++ b/tests/programs/caesar_cipher.bas @@ -0,0 +1,19 @@ +10 REM Caesar cipher - encode and decode +20 MG$ = "HELLO WORLD" +30 SH% = 13 +40 GOSUB 200 +50 PRINT "Encoded: "; R$ +60 EN$ = R$ +70 MG$ = EN$ : SH% = -13 +80 GOSUB 200 +90 PRINT "Decoded: "; R$ +100 IF R$ = "HELLO WORLD" THEN PRINT "Caesar cipher OK" ELSE PRINT "FAIL" +110 END +200 REM Encode MG$ by SH% into R$ +210 R$ = "" +220 FOR I = 1 TO LEN(MG$) +230 C% = ASC(MID$(MG$, I, 1)) +240 IF C% >= 65 AND C% <= 90 THEN C% = (C% - 65 + SH% + 26) - INT((C% - 65 + SH% + 26) / 26) * 26 + 65 +250 R$ = R$ + CHR$(C%) +260 NEXT I +270 RETURN diff --git a/tests/programs/calendar.bas b/tests/programs/calendar.bas new file mode 100644 index 0000000..4bc7a56 --- /dev/null +++ b/tests/programs/calendar.bas @@ -0,0 +1,23 @@ +10 REM Day-of-week calculator (Zeller's congruence) +20 DATA 2026,1,1,2026,7,4,2000,1,1,1969,7,20 +30 FOR T = 1 TO 4 +40 READ YR, MO, DY +50 GOSUB 200 +60 PRINT USING "####-##-## "; YR; MO; DY; +70 PRINT D$ +80 NEXT T +90 PRINT "Calendar OK" +100 END +200 REM Zeller's congruence +210 Q = DY : M = MO : K = YR +220 IF M < 3 THEN M = M + 12 : K = K - 1 +230 J = INT(K / 100) : KK = K - J * 100 +240 H = Q + INT(13*(M+1)/5) + KK + INT(KK/4) + INT(J/4) - 2*J +250 H = H - INT(H / 7) * 7 +260 IF H < 0 THEN H = H + 7 +270 DIM DN$(6) +280 DN$(0) = "Saturday" : DN$(1) = "Sunday" : DN$(2) = "Monday" +290 DN$(3) = "Tuesday" : DN$(4) = "Wednesday" : DN$(5) = "Thursday" : DN$(6) = "Friday" +300 D$ = DN$(H) +310 ERASE DN$ +320 RETURN diff --git a/tests/programs/diamond.bas b/tests/programs/diamond.bas new file mode 100644 index 0000000..26df9d2 --- /dev/null +++ b/tests/programs/diamond.bas @@ -0,0 +1,11 @@ +10 REM Diamond pattern using STRING$ and SPACE$ +20 N% = 7 +30 REM Top half +40 FOR I = 1 TO N% +50 PRINT SPACE$(N% - I); STRING$(2 * I - 1, "*") +60 NEXT I +70 REM Bottom half +80 FOR I = N% - 1 TO 1 STEP -1 +90 PRINT SPACE$(N% - I); STRING$(2 * I - 1, "*") +100 NEXT I +110 PRINT "Diamond OK" diff --git a/tests/programs/error_handler.bas b/tests/programs/error_handler.bas new file mode 100644 index 0000000..d5ae1f1 --- /dev/null +++ b/tests/programs/error_handler.bas @@ -0,0 +1,11 @@ +10 REM ON ERROR GOTO / RESUME test +20 ON ERROR GOTO 100 +30 PRINT "Before error" +40 ERROR 5 +50 PRINT "After resume" +60 PRINT "Triggering div by zero" +70 X = 1 / 0 +80 PRINT "After second resume" +90 PRINT "Error handler OK" : END +100 PRINT "Caught error"; ERR +110 RESUME NEXT diff --git a/tests/programs/fibonacci.bas b/tests/programs/fibonacci.bas new file mode 100644 index 0000000..28dccc7 --- /dev/null +++ b/tests/programs/fibonacci.bas @@ -0,0 +1,9 @@ +10 REM Fibonacci sequence - double precision +20 A# = 0 : B# = 1 +30 FOR I = 1 TO 20 +40 PRINT USING "##. ##########"; I; A# +50 C# = A# + B# +60 A# = B# +70 B# = C# +80 NEXT I +90 PRINT "Fibonacci OK" diff --git a/tests/programs/invoice.bas b/tests/programs/invoice.bas new file mode 100644 index 0000000..8797fb6 --- /dev/null +++ b/tests/programs/invoice.bas @@ -0,0 +1,23 @@ +10 REM Invoice with PRINT USING formatting +20 PRINT "================================" +30 PRINT " INVOICE" +40 PRINT "================================" +50 PRINT "" +60 DATA "Widget A",12,4.99 +70 DATA "Widget B",5,12.50 +80 DATA "Gizmo C",3,29.99 +90 DATA "Part D",100,0.75 +100 DATA "END",0,0 +110 TOTAL = 0 +120 PRINT "Item Qty Price Amount" +130 PRINT "------------- --- ------- ---------" +140 READ ITEM$, QTY%, PRICE +150 IF ITEM$ = "END" THEN 190 +160 AMT = QTY% * PRICE +170 TOTAL = TOTAL + AMT +180 PRINT USING "\ \### $$#,###.## $$#,###.##"; ITEM$; QTY%; PRICE; AMT +185 GOTO 140 +190 PRINT " ---------" +200 PRINT USING " Total: $$#,###.##"; TOTAL +210 PRINT "" +220 PRINT "Invoice OK" diff --git a/tests/programs/matrix_mult.bas b/tests/programs/matrix_mult.bas new file mode 100644 index 0000000..1c12c57 --- /dev/null +++ b/tests/programs/matrix_mult.bas @@ -0,0 +1,22 @@ +10 REM 3x3 Matrix multiplication +20 DIM A(2,2), B(2,2), C(2,2) +30 REM Matrix A (identity-like) +40 DATA 1,2,3,4,5,6,7,8,9 +50 REM Matrix B +60 DATA 9,8,7,6,5,4,3,2,1 +70 FOR I = 0 TO 2 : FOR J = 0 TO 2 : READ A(I,J) : NEXT J : NEXT I +80 FOR I = 0 TO 2 : FOR J = 0 TO 2 : READ B(I,J) : NEXT J : NEXT I +90 REM Multiply +100 FOR I = 0 TO 2 +110 FOR J = 0 TO 2 +120 C(I,J) = 0 +130 FOR K = 0 TO 2 +140 C(I,J) = C(I,J) + A(I,K) * B(K,J) +150 NEXT K +160 NEXT J +170 NEXT I +180 REM Print result +190 FOR I = 0 TO 2 +200 PRINT USING "####"; C(I,0); C(I,1); C(I,2) +210 NEXT I +220 PRINT "Matrix mult OK" diff --git a/tests/programs/monte_carlo.bas b/tests/programs/monte_carlo.bas new file mode 100644 index 0000000..afcc7cc --- /dev/null +++ b/tests/programs/monte_carlo.bas @@ -0,0 +1,13 @@ +10 REM Monte Carlo pi estimation +20 RANDOMIZE 42 +30 INSIDE% = 0 +40 N% = 10000 +50 FOR I = 1 TO N% +60 X = RND(1) * 2 - 1 +70 Y = RND(1) * 2 - 1 +80 IF X*X + Y*Y <= 1 THEN INSIDE% = INSIDE% + 1 +90 NEXT I +100 PE = 4 * INSIDE% / N% +110 PRINT USING "Pi estimate: #.####"; PE +120 PRINT USING "Actual pi: #.####"; 3.14159265# +130 PRINT "Monte Carlo OK" diff --git a/tests/programs/number_guess.bas b/tests/programs/number_guess.bas new file mode 100644 index 0000000..a478f3f --- /dev/null +++ b/tests/programs/number_guess.bas @@ -0,0 +1,13 @@ +10 REM Number guessing game (self-playing with fixed seed) +20 RANDOMIZE 99 +30 SECRET% = INT(RND(1) * 100) + 1 +40 REM Play using binary search +50 LO% = 1 : HI% = 100 +60 TRIES% = 0 +70 WHILE LO% <= HI% +80 GUESS% = INT((LO% + HI%) / 2) +90 TRIES% = TRIES% + 1 +100 IF GUESS% = SECRET% THEN PRINT "Found"; SECRET%; "in"; TRIES%; "tries" : GOTO 150 +110 IF GUESS% < SECRET% THEN LO% = GUESS% + 1 ELSE HI% = GUESS% - 1 +120 WEND +150 IF TRIES% <= 7 THEN PRINT "Number guess OK" ELSE PRINT "Too many tries" diff --git a/tests/programs/stats_calc.bas b/tests/programs/stats_calc.bas new file mode 100644 index 0000000..2477ace --- /dev/null +++ b/tests/programs/stats_calc.bas @@ -0,0 +1,17 @@ +10 REM Statistics: mean, variance, std dev +20 N% = 10 +30 DIM X(9) +40 DATA 23,45,12,67,34,89,56,78,90,11 +50 FOR I = 0 TO 9 : READ X(I) : NEXT I +60 REM Mean +70 S = 0 +80 FOR I = 0 TO 9 : S = S + X(I) : NEXT I +90 MEAN = S / N% +100 PRINT USING "Mean: ###.##"; MEAN +110 REM Variance +120 V = 0 +130 FOR I = 0 TO 9 : V = V + (X(I) - MEAN) * (X(I) - MEAN) : NEXT I +140 VR = V / N% +150 PRINT USING "Variance: ####.##"; VR +160 PRINT USING "Std Dev: ###.##"; SQR(VR) +170 PRINT "Stats calc OK" diff --git a/tests/programs/temp_table.bas b/tests/programs/temp_table.bas new file mode 100644 index 0000000..f863b51 --- /dev/null +++ b/tests/programs/temp_table.bas @@ -0,0 +1,8 @@ +10 REM Temperature conversion table +20 PRINT " Celsius Fahrenheit" +30 PRINT " ------- ----------" +40 FOR C = -40 TO 100 STEP 20 +50 F = C * 9 / 5 + 32 +60 PRINT USING " ###.# ###.#"; C; F +70 NEXT C +80 PRINT "Temp table OK" diff --git a/tests/programs/text_adventure.bas b/tests/programs/text_adventure.bas new file mode 100644 index 0000000..fd9464c --- /dev/null +++ b/tests/programs/text_adventure.bas @@ -0,0 +1,26 @@ +10 REM Mini text adventure (deterministic, no INPUT) +20 REM Uses DATA to simulate player choices +30 DATA 1,2,1,1 +40 ROOM% = 1 : TURNS% = 0 +50 DIM R$(3) +60 R$(1) = "Dark cave" : R$(2) = "Forest path" : R$(3) = "Treasure room" +70 GOSUB 200 +80 IF ROOM% = 3 THEN PRINT "You found the treasure!" : GOTO 150 +90 READ CH% +100 TURNS% = TURNS% + 1 +110 ON CH% GOSUB 300, 400 +120 GOTO 70 +150 PRINT "Turns taken:"; TURNS% +160 PRINT "Text adventure OK" +170 END +200 REM Print room +210 PRINT "Room: "; R$(ROOM%) +220 RETURN +300 REM Go north +310 IF ROOM% = 1 THEN ROOM% = 2 : RETURN +320 IF ROOM% = 2 THEN ROOM% = 3 : RETURN +330 RETURN +400 REM Go south +410 IF ROOM% = 2 THEN ROOM% = 1 : RETURN +420 IF ROOM% = 3 THEN ROOM% = 2 : RETURN +430 RETURN diff --git a/tests/run_tests.sh b/tests/run_tests.sh new file mode 100755 index 0000000..7e3df43 --- /dev/null +++ b/tests/run_tests.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# Run all .bas test programs and report results. +set -u + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +GWBASIC="${PROJECT_DIR}/build/gwbasic" + +if [ ! -x "$GWBASIC" ]; then + echo "ERROR: gwbasic not found at $GWBASIC (run cmake/make first)" + exit 1 +fi + +pass=0 +fail=0 + +for bas in "$SCRIPT_DIR"/programs/*.bas; do + name="$(basename "$bas")" + # chain_target.bas is not standalone + [ "$name" = "chain_target.bas" ] && continue + # graphics_stubs.bas expects real graphics now; skip if SCREEN causes issues + if timeout 5 "$GWBASIC" "$bas" >/dev/null 2>&1; then + printf " PASS %s\n" "$name" + pass=$((pass + 1)) + else + printf " FAIL %s\n" "$name" + fail=$((fail + 1)) + fi +done + +echo "" +echo "$((pass + fail)) tests: $pass passed, $fail failed" +[ "$fail" -eq 0 ] || exit 1