Implement DEF SEG/PEEK/POKE, GET/PUT sprites, fix PRINT USING, update to v0.11.0
This commit is contained in:
@@ -37,6 +37,7 @@ set(SOURCES
|
||||
src/program_io.c
|
||||
src/print_using.c
|
||||
src/graphics.c
|
||||
src/virmem.c
|
||||
src/sound.c
|
||||
src/tui.c
|
||||
platform/hal_posix.c
|
||||
|
||||
@@ -23,7 +23,7 @@ Interactive mode launches the authentic GW-BASIC full-screen editor:
|
||||
|
||||
```
|
||||
$ ./gwbasic
|
||||
GW-BASIC 2026 0.10.0
|
||||
GW-BASIC 2026 0.11.0
|
||||
(C) Eremey Valetov 2026. MIT License.
|
||||
Based on Microsoft GW-BASIC assembly source.
|
||||
Ok
|
||||
@@ -77,8 +77,9 @@ SPACE$, STRING$, HEX$, OCT$, INSTR, INPUT$
|
||||
| File management | KILL, NAME, FILES, MKDIR, RMDIR, CHDIR, SHELL |
|
||||
| Date/time | DATE$, TIME$, TIMER |
|
||||
| Screen | LOCATE, COLOR, WIDTH, SCREEN, KEY ON/OFF/LIST |
|
||||
| Graphics | PSET, PRESET, LINE, CIRCLE, DRAW, PAINT |
|
||||
| Graphics | PSET, PRESET, LINE, CIRCLE, DRAW, PAINT, GET/PUT (sprites) |
|
||||
| Sound | SOUND, BEEP, PLAY (MML) |
|
||||
| Memory | DEF SEG, PEEK, POKE |
|
||||
|
||||
### Binary and ASCII File Formats
|
||||
|
||||
@@ -144,6 +145,33 @@ CIRCLE (160,100), 80, 2
|
||||
PAINT (160,100), 3, 2
|
||||
```
|
||||
|
||||
`GET` and `PUT` capture and blit rectangular sprites using CGA-compatible
|
||||
packed pixel format. `PUT` supports action modes: XOR (default), PSET,
|
||||
PRESET, AND, OR.
|
||||
|
||||
```
|
||||
DIM S%(50)
|
||||
GET (0,0)-(15,15), S%
|
||||
PUT (100,50), S%, XOR
|
||||
```
|
||||
|
||||
### DEF SEG / PEEK / POKE
|
||||
|
||||
A virtual 8086 address space emulates the memory layout that GW-BASIC programs
|
||||
expected on a real IBM PC:
|
||||
|
||||
| Segment | Description |
|
||||
|---------|-------------|
|
||||
| `0040` | BIOS data area — video mode, cursor position, timer ticks (18.2 Hz) |
|
||||
| `B800` | CGA text buffer — character/attribute pairs (80×25 = 4000 bytes) |
|
||||
|
||||
```
|
||||
DEF SEG = &HB800
|
||||
POKE 0, 65 ' write 'A' to top-left screen cell
|
||||
PRINT PEEK(1) ' read the color attribute
|
||||
DEF SEG ' reset to default segment
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
The interpreter follows the original GW-BASIC's internal structure:
|
||||
@@ -174,6 +202,7 @@ Source text → Tokenizer (CRUNCH) → Token stream
|
||||
| File I/O | fileio.c | BIPTRG.ASM |
|
||||
| PRINT USING | print_using.c | BIPRTU.ASM |
|
||||
| Sound | sound.c | — |
|
||||
| Virtual memory | virmem.c | — |
|
||||
| Platform | hal_posix.c | OEM*.ASM |
|
||||
|
||||
Key design differences from the original:
|
||||
@@ -191,7 +220,7 @@ input, so they're not part of the automated test suite.
|
||||
|
||||
## Tests
|
||||
|
||||
58 test programs in `tests/programs/`, with golden-file regression testing
|
||||
61 test programs in `tests/programs/`, with golden-file regression testing
|
||||
and CI via GitHub Actions:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -48,15 +48,16 @@ HAL writes straight to stdout.
|
||||
| Program I/O (SAVE/LOAD) | `program_io.c` | BIMISC.ASM |
|
||||
| INPUT/LINE INPUT | `input.c` | BINTRP.ASM |
|
||||
| Sound engine | `sound.c` | — |
|
||||
| Virtual memory (PEEK/POKE) | `virmem.c` | — |
|
||||
| Platform abstraction | `hal_posix.c` | OEM*.ASM |
|
||||
|
||||
## Source Layout
|
||||
|
||||
```
|
||||
src/ — core interpreter (20 files)
|
||||
include/ — headers (12 files)
|
||||
src/ — core interpreter (21 files)
|
||||
include/ — headers (13 files)
|
||||
platform/ — HAL backends (1 file)
|
||||
tests/ — 58 automated test programs, 4 classic interactive programs, compat harness
|
||||
tests/ — 61 automated test programs, 4 classic interactive programs, compat harness
|
||||
docs/ — Sphinx documentation
|
||||
```
|
||||
|
||||
|
||||
+6
-4
@@ -14,10 +14,11 @@
|
||||
| 0.8.0 | `c68167c` | Dynamic TUI screen buffer, `--full` flag, LPRINT/LLIST with `--lpt` |
|
||||
| 0.9.0 | `2a8f98b` | EDIT statement, ON TIMER/ON KEY event trapping, F-key escape parser fixes |
|
||||
| 0.10.0 | | Binary tokenized SAVE/LOAD, INKEY$ extended key sequences, golden-file regression tests, classic BASIC programs |
|
||||
| 0.11.0 | | DEF SEG / PEEK / POKE virtual memory, GET/PUT graphics sprites, PRINT USING fixes |
|
||||
|
||||
## Tests
|
||||
|
||||
58 test programs in `tests/programs/`, plus 4 classic interactive programs in
|
||||
61 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*).
|
||||
|
||||
@@ -27,9 +28,10 @@ Run the full automated suite:
|
||||
bash tests/run_tests.sh
|
||||
```
|
||||
|
||||
Each test has a 5-second timeout. 55 tests have `.expected` golden files
|
||||
Each test has a 5-second timeout. 57 tests have `.expected` golden files
|
||||
for output regression detection. Three timing-dependent tests (datetime,
|
||||
on_timer, timer_stop) run without golden comparison.
|
||||
on_timer, timer_stop) and one sound test (play_scale) run without golden
|
||||
comparison.
|
||||
|
||||
### Compatibility Testing
|
||||
|
||||
@@ -46,6 +48,6 @@ bash tests/run_compat.sh
|
||||
## CI
|
||||
|
||||
GitHub Actions runs on every push to `main` and on pull requests. The workflow
|
||||
builds the project with PulseAudio support and runs all 58 test programs.
|
||||
builds the project with PulseAudio support and runs all 61 test programs.
|
||||
|
||||
See [`.github/workflows/ci.yml`](https://github.com/evvaletov/gw-basic-2026/blob/main/.github/workflows/ci.yml).
|
||||
|
||||
@@ -37,7 +37,7 @@ Running `./gwbasic` with no arguments launches the full-screen editor:
|
||||
|
||||
```
|
||||
$ ./gwbasic
|
||||
GW-BASIC 2026 0.10.0
|
||||
GW-BASIC 2026 0.11.0
|
||||
(C) Eremey Valetov 2026. MIT License.
|
||||
Based on Microsoft GW-BASIC assembly source.
|
||||
Ok
|
||||
|
||||
@@ -57,9 +57,10 @@ type suffixes (`%`, `!`, `#`)
|
||||
| File management | `KILL`, `NAME`, `FILES`, `MKDIR`, `RMDIR`, `CHDIR`, `SHELL` |
|
||||
| Date/time | `DATE$`, `TIME$`, `TIMER` |
|
||||
| Screen | `LOCATE`, `COLOR`, `WIDTH`, `SCREEN`, `KEY ON`/`OFF`/`LIST`, `KEY n,"string"` |
|
||||
| Graphics | `PSET`, `PRESET`, `LINE`, `CIRCLE`, `DRAW`, `PAINT` |
|
||||
| Graphics | `PSET`, `PRESET`, `LINE`, `CIRCLE`, `DRAW`, `PAINT`, `GET`, `PUT` |
|
||||
| Sound | `SOUND`, `BEEP`, `PLAY` (MML parser, PulseAudio backend) |
|
||||
| Misc | `POKE`, `KEY`, `TRON`/`TROFF`, `OPTION BASE`, `MID$` assignment |
|
||||
| Memory | `DEF SEG`, `PEEK`, `POKE` |
|
||||
| Misc | `KEY`, `TRON`/`TROFF`, `OPTION BASE`, `MID$` assignment |
|
||||
| System | `SYSTEM` |
|
||||
|
||||
## Program I/O (SAVE / LOAD)
|
||||
@@ -138,6 +139,25 @@ which works in terminals like xterm, mlterm, foot, and WezTerm.
|
||||
- `POINT (x,y)` — read pixel color
|
||||
- `COLOR fg, bg` — set foreground/background colors
|
||||
|
||||
### Sprite Capture and Blit (GET / PUT)
|
||||
|
||||
- `GET (x1,y1)-(x2,y2), array` — capture a screen rectangle into an integer array
|
||||
- `PUT (x,y), array [, action]` — blit a captured sprite back to the screen
|
||||
|
||||
The `action` parameter controls how the sprite combines with the existing screen:
|
||||
|
||||
| Action | Effect |
|
||||
|--------|--------|
|
||||
| `XOR` (default) | Exclusive OR — drawing twice erases the sprite |
|
||||
| `PSET` | Overwrite screen with sprite pixels |
|
||||
| `PRESET` | Overwrite screen with inverted sprite pixels |
|
||||
| `AND` | Bitwise AND of screen and sprite |
|
||||
| `OR` | Bitwise OR of screen and sprite |
|
||||
|
||||
Sprite data is stored in CGA-compatible packed format: word 0 is the width
|
||||
in bits, word 1 is the height, and remaining words contain packed pixel data
|
||||
matching the original GW-BASIC representation.
|
||||
|
||||
### Example
|
||||
|
||||
```
|
||||
@@ -147,6 +167,28 @@ CIRCLE (160,100), 80, 2
|
||||
PAINT (160,100), 3, 2
|
||||
```
|
||||
|
||||
## DEF SEG / PEEK / POKE
|
||||
|
||||
`DEF SEG`, `PEEK`, and `POKE` provide access to a virtual 8086 address space
|
||||
that emulates the memory layout programs expected on a real IBM PC:
|
||||
|
||||
```
|
||||
DEF SEG = &HB800 ' select CGA video buffer segment
|
||||
POKE 0, 65 ' write 'A' to top-left screen cell
|
||||
PRINT PEEK(1) ' read the color attribute
|
||||
DEF SEG ' reset to default segment
|
||||
```
|
||||
|
||||
### Emulated Memory Regions
|
||||
|
||||
| Segment | Address Range | Description |
|
||||
|---------|---------------|-------------|
|
||||
| `0040` | BIOS data area | Video mode (`0049`), column count (`004A`), cursor position (`0050-0051`), timer ticks (`006C-006F`), screen rows (`0084`) |
|
||||
| `B800` | CGA text buffer | Character/attribute pairs in text mode (even byte = char, odd byte = attr, 80 columns × 25 rows = 4000 bytes) |
|
||||
|
||||
All other segments read as 0 and writes are silently discarded. The timer
|
||||
tick counter at `0040:006C` tracks real time at the original 18.2 Hz rate.
|
||||
|
||||
## Sound
|
||||
|
||||
- `SOUND frequency, duration` — play a tone (frequency in Hz, duration in clock ticks)
|
||||
|
||||
+4
-11
@@ -10,28 +10,20 @@
|
||||
|
||||
## Planned Features
|
||||
|
||||
- **DEF SEG / PEEK / POKE emulation** — a virtual address space for the BIOS data
|
||||
area and CGA screen buffer (B800:0000), so programs that directly twiddle
|
||||
screen memory or read the keyboard buffer actually work. Not quite cycle-
|
||||
accurate, but enough to run most "tricks" from the 1980s magazines.
|
||||
- **Hardware I/O simulator** — an optional emulation layer for `OUT`, `INP`,
|
||||
`WAIT`, `MOTOR`, and friends. The idea is to provide a virtual PC peripheral
|
||||
bus so retro programs that talk to the speaker, joystick port, or parallel
|
||||
interface can do *something* useful instead of silently no-oping. Think of it
|
||||
as a tiny museum exhibit for vintage BASIC hardware hacking.
|
||||
- **BSAVE / BLOAD** — binary file save/load for screen buffers and data
|
||||
- **PRINT USING edge cases** — `**` asterisk fill, `**$` combined, thousands
|
||||
separator with `,`, and `^^^^` scientific notation corner cases to match
|
||||
the original output formatting exactly
|
||||
- **GET/PUT graphics** — sprite capture and blit for graphics mode; the
|
||||
framebuffer infrastructure is already in place, this is the missing piece
|
||||
for any BASIC program that does animation
|
||||
- **TUI color support** — map GW-BASIC COLOR attributes to ANSI 16-color output
|
||||
in the TUI screen buffer
|
||||
- **VIEW / WINDOW / PALETTE** — graphics viewport, coordinate mapping, and
|
||||
palette remapping
|
||||
- **MBF format support for binary LOAD** — convert Microsoft Binary Format
|
||||
float constants when loading files saved by the original GWBASIC.EXE
|
||||
- **Extended PEEK/POKE regions** — keyboard shift flags (0040:0017), CGA
|
||||
graphics mode framebuffer mapping
|
||||
|
||||
## IDE and Notebook Integration
|
||||
|
||||
@@ -49,7 +41,8 @@
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- `PEEK`/`POKE` are stubs (POKE parses and discards, PEEK returns 0)
|
||||
- Virtual memory emulation covers BIOS data area and CGA text buffer only;
|
||||
CGA graphics mode PEEK/POKE not yet mapped
|
||||
- Binary files use IEEE 754 floats, not MBF — files saved here are not
|
||||
byte-compatible with original GWBASIC.EXE binary format
|
||||
- String garbage collection not implemented (uses `malloc`/`free` instead)
|
||||
|
||||
@@ -31,4 +31,23 @@ int gfx_get_height(void);
|
||||
#define GFX_BOX 'B'
|
||||
#define GFX_BOXF 'F'
|
||||
|
||||
/* GET/PUT sprite support */
|
||||
#include <stdint.h>
|
||||
|
||||
/* PUT action modes */
|
||||
#define GFX_ACTION_XOR 0
|
||||
#define GFX_ACTION_PSET 1
|
||||
#define GFX_ACTION_PRESET 2
|
||||
#define GFX_ACTION_AND 3
|
||||
#define GFX_ACTION_OR 4
|
||||
|
||||
/* Capture screen rectangle into int16 array. Returns required array size. */
|
||||
int gfx_sprite_get(int x1, int y1, int x2, int y2, int16_t *buf, int bufsize);
|
||||
|
||||
/* Blit sprite from int16 array to screen. */
|
||||
void gfx_sprite_put(int x, int y, const int16_t *buf, int bufsize, int action);
|
||||
|
||||
/* Calculate required array element count for a sprite rectangle. */
|
||||
int gfx_sprite_size(int x1, int y1, int x2, int y2);
|
||||
|
||||
#endif
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
#include "gw_math.h"
|
||||
#include "strings.h"
|
||||
|
||||
#define GW_VERSION "0.10.0"
|
||||
#define GW_VERSION "0.11.0"
|
||||
#define GW_BANNER "GW-BASIC " GW_VERSION
|
||||
|
||||
/* Tokenizer */
|
||||
|
||||
@@ -80,6 +80,9 @@ typedef struct {
|
||||
/* Event trapping */
|
||||
timer_trap_t timer_trap;
|
||||
event_trap_t key_traps[10]; /* KEY(1)-KEY(10) */
|
||||
|
||||
/* DEF SEG segment (for PEEK/POKE/BSAVE/BLOAD) */
|
||||
uint16_t def_seg;
|
||||
} interp_state_t;
|
||||
|
||||
extern interp_state_t gw;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
#ifndef GW_VIRMEM_H
|
||||
#define GW_VIRMEM_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/*
|
||||
* Virtual 8086 address space for DEF SEG / PEEK / POKE.
|
||||
*
|
||||
* Real GW-BASIC runs in a 1MB address space where programs routinely
|
||||
* peek and poke at the BIOS data area and CGA video buffer. We emulate
|
||||
* just enough of that space to make classic tricks work:
|
||||
*
|
||||
* 0040:0000 BIOS data area (keyboard flags, timer ticks, cursor pos)
|
||||
* B800:0000 CGA text video buffer (char/attr pairs, 80x25 = 4000 bytes)
|
||||
*
|
||||
* Everything else reads as 0 and writes are silently discarded.
|
||||
*/
|
||||
|
||||
/* Read a byte from virtual memory at segment:offset */
|
||||
uint8_t virmem_peek(uint16_t segment, uint16_t offset);
|
||||
|
||||
/* Write a byte to virtual memory at segment:offset */
|
||||
void virmem_poke(uint16_t segment, uint16_t offset, uint8_t value);
|
||||
|
||||
#endif
|
||||
+15
-3
@@ -1,6 +1,7 @@
|
||||
#include "gwbasic.h"
|
||||
#include "tui.h"
|
||||
#include "graphics.h"
|
||||
#include "virmem.h"
|
||||
#include <ctype.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
@@ -690,8 +691,16 @@ static gw_value_t eval_function(uint8_t prefix, uint8_t func_tok)
|
||||
return v;
|
||||
}
|
||||
|
||||
case FUNC_PEEK: {
|
||||
gw_expect('(');
|
||||
uint16_t offset = gw_eval_uint16();
|
||||
gw_expect_rparen();
|
||||
v.type = VT_INT;
|
||||
v.ival = virmem_peek(gw.def_seg, offset);
|
||||
return v;
|
||||
}
|
||||
|
||||
case FUNC_INP:
|
||||
case FUNC_PEEK:
|
||||
case FUNC_PEN:
|
||||
case FUNC_STICK:
|
||||
case FUNC_STRIG:
|
||||
@@ -1052,10 +1061,13 @@ int16_t gw_eval_int(void)
|
||||
uint16_t gw_eval_uint16(void)
|
||||
{
|
||||
gw_value_t v = gw_eval_num();
|
||||
/* Integer values may be negative due to int16 representation of &H8000+ */
|
||||
if (v.type == VT_INT)
|
||||
return (uint16_t)v.ival;
|
||||
double d = gw_to_dbl(&v);
|
||||
if (d < 0 || d > 65535)
|
||||
if (d < -32768 || d > 65535)
|
||||
gw_error(ERR_FC);
|
||||
return (uint16_t)d;
|
||||
return (uint16_t)(int16_t)d;
|
||||
}
|
||||
|
||||
void gw_expect_rparen(void)
|
||||
|
||||
+111
@@ -461,3 +461,114 @@ void gfx_flush(void)
|
||||
gw_hal->write_raw(buf, pos);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
/*
|
||||
* GET/PUT sprite support.
|
||||
*
|
||||
* Sprites are stored in integer arrays using the CGA packed format:
|
||||
* Word 0: width in bits (pixel_width * bits_per_pixel)
|
||||
* Word 1: height in scan lines
|
||||
* Remaining: packed pixel data, row by row, padded to word boundary
|
||||
*
|
||||
* SCREEN 1: 2 bits/pixel (4 pixels/byte)
|
||||
* SCREEN 2: 1 bit/pixel (8 pixels/byte)
|
||||
*/
|
||||
|
||||
static int bits_per_pixel(void)
|
||||
{
|
||||
return (screen_mode == 2) ? 1 : 2;
|
||||
}
|
||||
|
||||
int gfx_sprite_size(int x1, int y1, int x2, int y2)
|
||||
{
|
||||
if (!framebuf) return 0;
|
||||
int w = abs(x2 - x1) + 1;
|
||||
int h = abs(y2 - y1) + 1;
|
||||
int bpp = bits_per_pixel();
|
||||
int bytes_per_row = (w * bpp + 7) / 8;
|
||||
if (bytes_per_row & 1) bytes_per_row++; /* pad to word boundary */
|
||||
int data_bytes = bytes_per_row * h;
|
||||
return 2 + (data_bytes + 1) / 2; /* 2 header words + data words */
|
||||
}
|
||||
|
||||
int gfx_sprite_get(int x1, int y1, int x2, int y2, int16_t *buf, int bufsize)
|
||||
{
|
||||
if (!framebuf) return 0;
|
||||
|
||||
/* Normalize coordinates */
|
||||
if (x1 > x2) { int t = x1; x1 = x2; x2 = t; }
|
||||
if (y1 > y2) { int t = y1; y1 = y2; y2 = t; }
|
||||
|
||||
int w = x2 - x1 + 1;
|
||||
int h = y2 - y1 + 1;
|
||||
int bpp = bits_per_pixel();
|
||||
int width_bits = w * bpp;
|
||||
int bytes_per_row = (width_bits + 7) / 8;
|
||||
if (bytes_per_row & 1) bytes_per_row++; /* word-align */
|
||||
int data_bytes = bytes_per_row * h;
|
||||
int required = 2 + (data_bytes + 1) / 2;
|
||||
|
||||
if (bufsize < required) return required;
|
||||
|
||||
buf[0] = (int16_t)width_bits;
|
||||
buf[1] = (int16_t)h;
|
||||
|
||||
/* Pack pixels into CGA format */
|
||||
uint8_t *out = (uint8_t *)&buf[2];
|
||||
memset(out, 0, data_bytes);
|
||||
|
||||
for (int row = 0; row < h; row++) {
|
||||
for (int col = 0; col < w; col++) {
|
||||
int px = get_pixel(x1 + col, y1 + row);
|
||||
int bit_offset = col * bpp;
|
||||
int byte_idx = bit_offset / 8;
|
||||
int bit_pos = 8 - bpp - (bit_offset % 8); /* MSB first */
|
||||
out[row * bytes_per_row + byte_idx] |=
|
||||
(px & ((1 << bpp) - 1)) << bit_pos;
|
||||
}
|
||||
}
|
||||
|
||||
return required;
|
||||
}
|
||||
|
||||
void gfx_sprite_put(int x, int y, const int16_t *buf, int bufsize, int action)
|
||||
{
|
||||
if (!framebuf || bufsize < 2) return;
|
||||
|
||||
int bpp = bits_per_pixel();
|
||||
int width_bits = (uint16_t)buf[0];
|
||||
int h = (uint16_t)buf[1];
|
||||
int w = width_bits / bpp;
|
||||
if (w <= 0 || h <= 0) return;
|
||||
|
||||
int bytes_per_row = (width_bits + 7) / 8;
|
||||
if (bytes_per_row & 1) bytes_per_row++;
|
||||
|
||||
const uint8_t *src = (const uint8_t *)&buf[2];
|
||||
int mask = (1 << bpp) - 1;
|
||||
|
||||
for (int row = 0; row < h; row++) {
|
||||
int sy = y + row;
|
||||
if (sy < 0 || sy >= fb_height) continue;
|
||||
for (int col = 0; col < w; col++) {
|
||||
int sx = x + col;
|
||||
if (sx < 0 || sx >= fb_width) continue;
|
||||
|
||||
int bit_offset = col * bpp;
|
||||
int byte_idx = bit_offset / 8;
|
||||
int bit_pos = 8 - bpp - (bit_offset % 8);
|
||||
int sprite_px = (src[row * bytes_per_row + byte_idx] >> bit_pos) & mask;
|
||||
|
||||
int old_px = get_pixel(sx, sy);
|
||||
int new_px;
|
||||
switch (action) {
|
||||
case GFX_ACTION_PSET: new_px = sprite_px; break;
|
||||
case GFX_ACTION_PRESET: new_px = (~sprite_px) & mask; break;
|
||||
case GFX_ACTION_AND: new_px = old_px & sprite_px; break;
|
||||
case GFX_ACTION_OR: new_px = old_px | sprite_px; break;
|
||||
default: new_px = old_px ^ sprite_px; break; /* XOR */
|
||||
}
|
||||
set_pixel(sx, sy, new_px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+140
-5
@@ -1,6 +1,7 @@
|
||||
#include "gwbasic.h"
|
||||
#include "tui.h"
|
||||
#include "graphics.h"
|
||||
#include "virmem.h"
|
||||
#include "sound.h"
|
||||
#include <ctype.h>
|
||||
#include <string.h>
|
||||
@@ -722,6 +723,113 @@ void gw_stmt_chain(void)
|
||||
}
|
||||
}
|
||||
|
||||
/* Graphics GET (x1,y1)-(x2,y2), array */
|
||||
static void stmt_gfx_get(void)
|
||||
{
|
||||
gw_expect('(');
|
||||
int x1 = gw_eval_int();
|
||||
gw_skip_spaces();
|
||||
gw_expect(',');
|
||||
int y1 = gw_eval_int();
|
||||
gw_expect_rparen();
|
||||
gw_skip_spaces();
|
||||
gw_expect(TOK_MINUS);
|
||||
gw_expect('(');
|
||||
int x2 = gw_eval_int();
|
||||
gw_skip_spaces();
|
||||
gw_expect(',');
|
||||
int y2 = gw_eval_int();
|
||||
gw_expect_rparen();
|
||||
gw_skip_spaces();
|
||||
gw_expect(',');
|
||||
|
||||
char name[2];
|
||||
gw_valtype_t type = gw_parse_varname(name);
|
||||
if (type != VT_INT)
|
||||
gw_error(ERR_TM);
|
||||
|
||||
int required = gfx_sprite_size(x1, y1, x2, y2);
|
||||
if (required <= 0) gw_error(ERR_FC);
|
||||
|
||||
/* Find or auto-dim the array; skip its subscript so parser is happy */
|
||||
gw_skip_spaces();
|
||||
array_entry_t *a = NULL;
|
||||
for (int i = 0; i < gw.array_count; i++) {
|
||||
if (gw.arrays[i].name[0] == name[0] && gw.arrays[i].name[1] == name[1]
|
||||
&& gw.arrays[i].type == VT_INT) {
|
||||
a = &gw.arrays[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!a) gw_error(ERR_FC);
|
||||
if (a->total_elements < required) gw_error(ERR_FC);
|
||||
|
||||
/* Build a flat int16 buffer from the array's ival fields */
|
||||
int16_t *buf = malloc(a->total_elements * sizeof(int16_t));
|
||||
if (!buf) gw_error(ERR_OM);
|
||||
|
||||
gfx_sprite_get(x1, y1, x2, y2, buf, a->total_elements);
|
||||
|
||||
/* Copy back to array */
|
||||
for (int i = 0; i < a->total_elements && i < required; i++)
|
||||
a->data[i].ival = buf[i];
|
||||
|
||||
free(buf);
|
||||
}
|
||||
|
||||
/* Graphics PUT (x,y), array [, action] */
|
||||
static void stmt_gfx_put(void)
|
||||
{
|
||||
gw_expect('(');
|
||||
int x = gw_eval_int();
|
||||
gw_skip_spaces();
|
||||
gw_expect(',');
|
||||
int y = gw_eval_int();
|
||||
gw_expect_rparen();
|
||||
gw_skip_spaces();
|
||||
gw_expect(',');
|
||||
|
||||
char name[2];
|
||||
gw_valtype_t type = gw_parse_varname(name);
|
||||
if (type != VT_INT)
|
||||
gw_error(ERR_TM);
|
||||
|
||||
/* Find the array */
|
||||
array_entry_t *a = NULL;
|
||||
for (int i = 0; i < gw.array_count; i++) {
|
||||
if (gw.arrays[i].name[0] == name[0] && gw.arrays[i].name[1] == name[1]
|
||||
&& gw.arrays[i].type == VT_INT) {
|
||||
a = &gw.arrays[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!a) gw_error(ERR_FC);
|
||||
|
||||
/* Parse optional action verb */
|
||||
int action = GFX_ACTION_XOR;
|
||||
gw_skip_spaces();
|
||||
if (gw_chrgot() == ',') {
|
||||
gw_chrget();
|
||||
gw_skip_spaces();
|
||||
uint8_t tok = gw_chrgot();
|
||||
if (tok == TOK_PSET) { action = GFX_ACTION_PSET; gw_chrget(); }
|
||||
else if (tok == TOK_PRESET) { action = GFX_ACTION_PRESET; gw_chrget(); }
|
||||
else if (tok == TOK_AND) { action = GFX_ACTION_AND; gw_chrget(); }
|
||||
else if (tok == TOK_OR) { action = GFX_ACTION_OR; gw_chrget(); }
|
||||
else if (tok == TOK_XOR) { action = GFX_ACTION_XOR; gw_chrget(); }
|
||||
else gw_error(ERR_SN);
|
||||
}
|
||||
|
||||
/* Build int16 buffer from array */
|
||||
int16_t *buf = malloc(a->total_elements * sizeof(int16_t));
|
||||
if (!buf) gw_error(ERR_OM);
|
||||
for (int i = 0; i < a->total_elements; i++)
|
||||
buf[i] = a->data[i].ival;
|
||||
|
||||
gfx_sprite_put(x, y, buf, a->total_elements, action);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
* Statement Dispatcher
|
||||
* ================================================================ */
|
||||
@@ -841,12 +949,22 @@ void gw_exec_stmt(void)
|
||||
}
|
||||
if (xstmt == XSTMT_PUT) {
|
||||
gw_chrget();
|
||||
gw_stmt_put();
|
||||
gw_skip_spaces();
|
||||
if (gfx_active() && gw_chrgot() == '(') {
|
||||
stmt_gfx_put();
|
||||
} else {
|
||||
gw_stmt_put();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (xstmt == XSTMT_GET) {
|
||||
gw_chrget();
|
||||
gw_stmt_get();
|
||||
gw_skip_spaces();
|
||||
if (gfx_active() && gw_chrgot() == '(') {
|
||||
stmt_gfx_get();
|
||||
} else {
|
||||
gw_stmt_get();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (xstmt == XSTMT_KILL) {
|
||||
@@ -2181,6 +2299,21 @@ void gw_exec_stmt(void)
|
||||
stmt_def_fn();
|
||||
return;
|
||||
}
|
||||
/* DEF SEG [= segment] */
|
||||
uint8_t ch = gw_chrgot();
|
||||
if ((ch == 'S' || ch == 's') &&
|
||||
(gw.text_ptr[1] == 'E' || gw.text_ptr[1] == 'e') &&
|
||||
(gw.text_ptr[2] == 'G' || gw.text_ptr[2] == 'g')) {
|
||||
gw.text_ptr += 3;
|
||||
gw_skip_spaces();
|
||||
if (gw_chrgot() == TOK_EQ) {
|
||||
gw_chrget();
|
||||
gw.def_seg = gw_eval_uint16();
|
||||
} else {
|
||||
gw.def_seg = 0; /* DEF SEG with no argument resets to default */
|
||||
}
|
||||
return;
|
||||
}
|
||||
gw_error(ERR_SN);
|
||||
}
|
||||
|
||||
@@ -2241,13 +2374,15 @@ void gw_exec_stmt(void)
|
||||
return;
|
||||
}
|
||||
|
||||
/* POKE - ignore for now */
|
||||
/* POKE address, value */
|
||||
if (tok == TOK_POKE) {
|
||||
gw_chrget();
|
||||
gw_eval_int(); /* address */
|
||||
uint16_t addr = gw_eval_uint16();
|
||||
gw_skip_spaces();
|
||||
gw_expect(',');
|
||||
gw_eval_int(); /* value */
|
||||
int val = gw_eval_int();
|
||||
if (val < 0 || val > 255) gw_error(ERR_FC);
|
||||
virmem_poke(gw.def_seg, addr, (uint8_t)val);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+7
-7
@@ -160,8 +160,9 @@ static void format_number(FILE *fp, const char *fmt, int fmtlen, double val)
|
||||
exp_val = (int)floor(log10(absval));
|
||||
double mant = absval / pow(10, exp_val);
|
||||
snprintf(mantissa, sizeof(mantissa), "%.*f", dec, mant);
|
||||
/* Check for rounding overflow (e.g., 9.95 -> 10.0 with 1 dec) */
|
||||
if (mantissa[0] != '0' && (mantissa[0] - '0') >= 10) {
|
||||
/* Check for rounding/precision overflow (e.g., 9.95 -> 10.0,
|
||||
or log10 imprecision giving mant >= 10) */
|
||||
if (mant >= 9.9995 || (mantissa[0] == '1' && mantissa[1] == '0')) {
|
||||
exp_val++;
|
||||
mant = absval / pow(10, exp_val);
|
||||
snprintf(mantissa, sizeof(mantissa), "%.*f", dec, mant);
|
||||
@@ -227,11 +228,13 @@ static void format_number(FILE *fp, const char *fmt, int fmtlen, double val)
|
||||
/* Calculate total field width for integer part */
|
||||
int int_field = total_digits;
|
||||
int total_field = int_field + (has_decimal ? 1 + dec : 0);
|
||||
/* Commas in the actual formatted number expand the field */
|
||||
int actual_commas = (int)strlen(int_with_commas) - (int)strlen(intpart);
|
||||
|
||||
/* Pad the number to fill the field */
|
||||
char result[80];
|
||||
int numlen = strlen(numstr);
|
||||
int padding = total_field - numlen;
|
||||
int padding = total_field + actual_commas - numlen;
|
||||
if (padding < 0) padding = 0;
|
||||
|
||||
int ri = 0;
|
||||
@@ -247,7 +250,7 @@ static void format_number(FILE *fp, const char *fmt, int fmtlen, double val)
|
||||
char fillch = asterisk_fill ? '*' : ' ';
|
||||
bool dollar_placed = false;
|
||||
|
||||
if (numlen > total_field) {
|
||||
if (numlen > total_field + actual_commas) {
|
||||
/* Overflow: print % followed by the number */
|
||||
pu_putch(fp, '%');
|
||||
if (!sign_placed && !trailing_plus && !trailing_minus) {
|
||||
@@ -263,9 +266,6 @@ static void format_number(FILE *fp, const char *fmt, int fmtlen, double val)
|
||||
|
||||
for (int j = 0; j < padding; j++) {
|
||||
if (dollar && !dollar_placed && j == padding - 1) {
|
||||
if (!sign_placed && negative && !trailing_plus && !trailing_minus) {
|
||||
result[ri++] = fillch == '*' ? '*' : ' ';
|
||||
}
|
||||
result[ri++] = '$';
|
||||
dollar_placed = true;
|
||||
} else {
|
||||
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
#include "virmem.h"
|
||||
#include "tui.h"
|
||||
#include "graphics.h"
|
||||
#include <time.h>
|
||||
|
||||
/*
|
||||
* BIOS data area emulation (segment 0x0040).
|
||||
*
|
||||
* Real addresses we care about:
|
||||
* 0040:0017 Keyboard shift flags byte 1
|
||||
* 0040:0018 Keyboard shift flags byte 2
|
||||
* 0040:0049 Current video mode
|
||||
* 0040:004A Number of screen columns (word)
|
||||
* 0040:0050 Cursor position page 0 (col, row)
|
||||
* 0040:006C Timer tick count (dword, ~18.2 Hz)
|
||||
* 0040:0084 Rows on screen minus 1
|
||||
*/
|
||||
static uint8_t bios_peek(uint16_t offset)
|
||||
{
|
||||
switch (offset) {
|
||||
case 0x17:
|
||||
return 0; /* no shift keys pressed */
|
||||
case 0x18:
|
||||
return 0;
|
||||
|
||||
case 0x49:
|
||||
/* Current video mode: 3 = 80x25 color text, 1 = 320x200, 2 = 640x200 */
|
||||
return gfx_active() ? (uint8_t)gfx_get_mode() : 3;
|
||||
|
||||
case 0x4A:
|
||||
return tui.active ? (uint8_t)tui.cols : 80;
|
||||
case 0x4B:
|
||||
return 0; /* high byte of column count */
|
||||
|
||||
case 0x50:
|
||||
return tui.active ? (uint8_t)tui.cursor_col : 0;
|
||||
case 0x51:
|
||||
return tui.active ? (uint8_t)tui.cursor_row : 0;
|
||||
|
||||
case 0x6C: case 0x6D: case 0x6E: case 0x6F: {
|
||||
/* Timer ticks since midnight (~18.2065 Hz) */
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
struct tm tm;
|
||||
localtime_r(&ts.tv_sec, &tm);
|
||||
long secs = tm.tm_hour * 3600L + tm.tm_min * 60L + tm.tm_sec;
|
||||
uint32_t ticks = (uint32_t)(secs * 18.2065 + ts.tv_nsec / 54925400.0);
|
||||
return (ticks >> ((offset - 0x6C) * 8)) & 0xFF;
|
||||
}
|
||||
|
||||
case 0x84:
|
||||
return tui.active ? (uint8_t)(tui.rows - 1) : 24;
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void bios_poke(uint16_t offset, uint8_t value)
|
||||
{
|
||||
(void)offset;
|
||||
(void)value;
|
||||
/* BIOS data area writes are silently ignored */
|
||||
}
|
||||
|
||||
/*
|
||||
* CGA text video buffer emulation (segment 0xB800).
|
||||
*
|
||||
* Layout: 80x25 = 4000 bytes, alternating character and attribute bytes.
|
||||
* offset = (row * 80 + col) * 2 → character
|
||||
* offset = (row * 80 + col) * 2 + 1 → attribute (fg | bg<<4)
|
||||
*/
|
||||
static uint8_t cga_text_peek(uint16_t offset)
|
||||
{
|
||||
if (!tui.active || !tui.screen) return 0;
|
||||
|
||||
int cell = offset / 2;
|
||||
int cols = tui.cols;
|
||||
int row = cell / cols;
|
||||
int col = cell % cols;
|
||||
|
||||
if (row >= tui.rows) return 0;
|
||||
|
||||
if (offset & 1)
|
||||
return TUI_CELL(row, col).attr;
|
||||
else
|
||||
return TUI_CELL(row, col).ch;
|
||||
}
|
||||
|
||||
static void cga_text_poke(uint16_t offset, uint8_t value)
|
||||
{
|
||||
if (!tui.active || !tui.screen) return;
|
||||
|
||||
int cell = offset / 2;
|
||||
int cols = tui.cols;
|
||||
int row = cell / cols;
|
||||
int col = cell % cols;
|
||||
|
||||
if (row >= tui.rows) return;
|
||||
|
||||
if (offset & 1)
|
||||
TUI_CELL(row, col).attr = value;
|
||||
else
|
||||
TUI_CELL(row, col).ch = value;
|
||||
}
|
||||
|
||||
uint8_t virmem_peek(uint16_t segment, uint16_t offset)
|
||||
{
|
||||
if (segment == 0x0040)
|
||||
return bios_peek(offset);
|
||||
|
||||
if (segment == 0xB800 && !gfx_active())
|
||||
return cga_text_peek(offset);
|
||||
|
||||
/* Unhandled regions return 0 */
|
||||
return 0;
|
||||
}
|
||||
|
||||
void virmem_poke(uint16_t segment, uint16_t offset, uint8_t value)
|
||||
{
|
||||
if (segment == 0x0040) {
|
||||
bios_poke(offset, value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (segment == 0xB800 && !gfx_active()) {
|
||||
cga_text_poke(offset, value);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Unhandled regions: silently discard */
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
Pq#0;2;0;0;0#1;2;0;0;66#0!320~-#0!10~!11N!299~$#1!10?!11o!299?-#0!10~!11?!299~$#1!10?!11~!299?-#0!10~!11w!299~$#1!10?!11F!299?-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320B-\WIDTH BITS= 22
|
||||
HEIGHT= 11
|
||||
[2J[HPq#0;2;0;0;0#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320B-\XOR PUT OK
|
||||
XOR ERASE OK
|
||||
PSET PUT OK
|
||||
[2J[HPq#0;2;0;0;0#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320B-\OR PUT OK
|
||||
DONE
|
||||
@@ -0,0 +1,9 @@
|
||||
PEEK(0)= 0
|
||||
VIDEO MODE= 3
|
||||
COLUMNS= 80
|
||||
TIMER TICKS OK
|
||||
B800 PEEK= 0
|
||||
RESET PEEK= 0
|
||||
SEG 0 PEEK= 0
|
||||
SEG FFFF PEEK= 0
|
||||
DONE
|
||||
@@ -0,0 +1,23 @@
|
||||
****1.50
|
||||
-***1.50
|
||||
12345.67
|
||||
***$1.50
|
||||
-**$1.50
|
||||
$1.50
|
||||
-$1.50
|
||||
1,234.56
|
||||
123,456.78
|
||||
$1,234.56
|
||||
1.23E+03
|
||||
1.00E-04
|
||||
-1.00E-04
|
||||
0.00E+00
|
||||
123.45
|
||||
123.45-
|
||||
123.45+
|
||||
123.45-
|
||||
+123.45
|
||||
-123.45
|
||||
%123.45
|
||||
0.00
|
||||
DONE
|
||||
@@ -0,0 +1,27 @@
|
||||
10 REM Test GET/PUT graphics sprites
|
||||
20 SCREEN 1
|
||||
30 DIM SP%(50)
|
||||
40 REM Draw a small box
|
||||
50 LINE (10,10)-(20,20), 1, BF
|
||||
60 REM Capture it
|
||||
70 GET (10,10)-(20,20), SP%
|
||||
80 REM Check header (width in bits, height)
|
||||
90 PRINT "WIDTH BITS="; SP%(0)
|
||||
100 PRINT "HEIGHT="; SP%(1)
|
||||
110 REM Clear screen and PUT it elsewhere with XOR
|
||||
120 CLS
|
||||
130 PUT (50,50), SP%, XOR
|
||||
140 REM Check a pixel was placed
|
||||
150 IF POINT(55,55) > 0 THEN PRINT "XOR PUT OK" ELSE PRINT "XOR PUT FAIL"
|
||||
160 REM PUT again with XOR to erase
|
||||
170 PUT (50,50), SP%, XOR
|
||||
180 IF POINT(55,55) = 0 THEN PRINT "XOR ERASE OK" ELSE PRINT "XOR ERASE FAIL"
|
||||
190 REM Test PSET mode
|
||||
200 PUT (50,50), SP%, PSET
|
||||
210 IF POINT(55,55) > 0 THEN PRINT "PSET PUT OK" ELSE PRINT "PSET PUT FAIL"
|
||||
220 REM Test OR mode
|
||||
230 CLS
|
||||
240 PUT (50,50), SP%, OR
|
||||
250 IF POINT(55,55) > 0 THEN PRINT "OR PUT OK" ELSE PRINT "OR PUT FAIL"
|
||||
260 SCREEN 0
|
||||
270 PRINT "DONE"
|
||||
@@ -0,0 +1,27 @@
|
||||
10 REM Test DEF SEG / PEEK / POKE
|
||||
20 REM --- Default segment: PEEK returns 0 ---
|
||||
30 PRINT "PEEK(0)="; PEEK(0)
|
||||
40 REM --- BIOS data area: video mode ---
|
||||
50 DEF SEG = &H40
|
||||
60 PRINT "VIDEO MODE="; PEEK(&H49)
|
||||
70 REM --- BIOS data area: columns ---
|
||||
80 PRINT "COLUMNS="; PEEK(&H4A)
|
||||
90 REM --- BIOS data area: timer ticks (low byte, just check it runs) ---
|
||||
100 T! = PEEK(&H6C)
|
||||
110 IF T! >= 0 THEN PRINT "TIMER TICKS OK" ELSE PRINT "TIMER TICKS FAIL"
|
||||
120 REM --- CGA text buffer: POKE a character (no TUI in piped mode) ---
|
||||
130 DEF SEG = &HB800
|
||||
140 POKE 0, 65
|
||||
150 REM In piped mode TUI is not active, so PEEK returns 0
|
||||
160 P = PEEK(0)
|
||||
170 PRINT "B800 PEEK="; P
|
||||
180 REM --- DEF SEG reset ---
|
||||
190 DEF SEG
|
||||
200 PRINT "RESET PEEK="; PEEK(0)
|
||||
210 REM --- Boundary values ---
|
||||
220 DEF SEG = 0
|
||||
230 PRINT "SEG 0 PEEK="; PEEK(0)
|
||||
240 DEF SEG = 65535
|
||||
250 PRINT "SEG FFFF PEEK="; PEEK(0)
|
||||
260 REM --- Done ---
|
||||
270 PRINT "DONE"
|
||||
@@ -0,0 +1,32 @@
|
||||
10 REM PRINT USING edge cases
|
||||
20 REM --- Asterisk fill ---
|
||||
30 PRINT USING "**###.##"; 1.5
|
||||
40 PRINT USING "**###.##"; -1.5
|
||||
50 PRINT USING "**###.##"; 12345.67
|
||||
60 REM --- Asterisk fill + dollar ---
|
||||
70 PRINT USING "**$##.##"; 1.5
|
||||
80 PRINT USING "**$##.##"; -1.5
|
||||
90 REM --- Dollar sign ---
|
||||
100 PRINT USING "$$###.##"; 1.5
|
||||
110 PRINT USING "$$###.##"; -1.5
|
||||
120 REM --- Thousands separator ---
|
||||
130 PRINT USING "###,###.##"; 1234.56
|
||||
140 PRINT USING "###,###.##"; 123456.78
|
||||
150 PRINT USING "$$#,###.##"; 1234.56
|
||||
160 REM --- Scientific notation ---
|
||||
170 PRINT USING "#.##^^^^"; 1234.5
|
||||
180 PRINT USING "#.##^^^^"; 0.0001
|
||||
190 PRINT USING "#.##^^^^"; -0.0001
|
||||
200 PRINT USING "#.##^^^^"; 0
|
||||
210 REM --- Trailing signs ---
|
||||
220 PRINT USING "###.##-"; 123.45
|
||||
230 PRINT USING "###.##-"; -123.45
|
||||
240 PRINT USING "###.##+"; 123.45
|
||||
250 PRINT USING "###.##+"; -123.45
|
||||
260 PRINT USING "+###.##"; 123.45
|
||||
270 PRINT USING "+###.##"; -123.45
|
||||
280 REM --- Overflow ---
|
||||
290 PRINT USING "##.##"; 123.45
|
||||
300 REM --- Zero ---
|
||||
310 PRINT USING "###.##"; 0
|
||||
320 PRINT "DONE"
|
||||
Reference in New Issue
Block a user