Implement DEF SEG/PEEK/POKE, GET/PUT sprites, fix PRINT USING, update to v0.11.0

This commit is contained in:
Eremey Valetov
2026-03-01 13:07:28 -05:00
parent e7f35c21ff
commit 0743757029
22 changed files with 670 additions and 40 deletions
+1
View File
@@ -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
+32 -3
View File
@@ -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
+4 -3
View File
@@ -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
View File
@@ -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).
+1 -1
View File
@@ -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
+44 -2
View File
@@ -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
View File
@@ -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)
+19
View File
@@ -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
View File
@@ -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 */
+3
View File
@@ -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;
+25
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 */
}
+7
View File
@@ -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
Pq#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
Pq#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
+9
View File
@@ -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
+23
View File
@@ -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
+27
View File
@@ -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"
+27
View File
@@ -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"
+32
View File
@@ -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"