@8bitscript/vic20 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 8BitScript contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json ADDED
@@ -0,0 +1,256 @@
1
+ {
2
+ "name": "@8bitscript/vic20",
3
+ "version": "0.1.0",
4
+ "description": "VIC-20 target support for 8BitScript: the hardware underneath the portable APIs.",
5
+ "license": "MIT",
6
+ "8bitscript": {
7
+ "entry": "./src/index.8bs",
8
+ "exports": {
9
+ "./screen": "./src/screen.8bs",
10
+ "./text": "./src/text.8bs",
11
+ "./input": "./src/input.8bs",
12
+ "./pointer": "./src/pointer.8bs",
13
+ "./keyboard": "./src/keyboard.8bs",
14
+ "./keys": "./src/keys.8bs"
15
+ },
16
+ "hardware": {
17
+ "facts": {
18
+ "video.columns": 22,
19
+ "video.rows": 23,
20
+ "video.cellWidth": 8,
21
+ "video.cellHeight": 8,
22
+ "video.palette": 16,
23
+ "video.cellColors": 2,
24
+ "video.colorPerCell": true,
25
+ "video.glyphs": 256,
26
+ "video.blockWidth": 2,
27
+ "video.blockHeight": 2,
28
+ "video.bitmap": false,
29
+ "video.layers": 1,
30
+ "video.scroll": false,
31
+ "video.sprites": 0,
32
+ "video.spritesPerLine": 0,
33
+ "video.spriteWidth": 0,
34
+ "video.spriteHeight": 0,
35
+ "video.spriteColors": 0,
36
+ "video.frameRate": 60,
37
+ "audio.voices": 4,
38
+ "audio.noise": true,
39
+ "audio.envelope": false,
40
+ "audio.filter": false,
41
+ "audio.pcm": false,
42
+ "audio.volume": false,
43
+ "audio.entropy": false,
44
+ "input.keyboard": true,
45
+ "input.joysticks": 1,
46
+ "input.pads": 0,
47
+ "input.mouse": false,
48
+ "input.paddles": false,
49
+ "storage.save": true,
50
+ "memory.ram": 3583,
51
+ "memory.banked": false,
52
+ "memory.bankedKib": 0,
53
+ "storage.kib": 164
54
+ },
55
+ "options": {
56
+ "ram": {
57
+ "label": "RAM expansion",
58
+ "default": "none",
59
+ "values": {
60
+ "none": {
61
+ "label": "Unexpanded (3583 bytes for the program)",
62
+ "build": {
63
+ "defsym": {
64
+ "__memory_expansion": 0
65
+ }
66
+ },
67
+ "run": {
68
+ "xvic": [
69
+ "-memory",
70
+ "none"
71
+ ]
72
+ },
73
+ "facts": {
74
+ "memory.ram": 3583
75
+ }
76
+ },
77
+ "3k": {
78
+ "label": "3K (VIC-1210): $0400-$0FFF",
79
+ "tag": "3k",
80
+ "build": {
81
+ "defsym": {
82
+ "__memory_expansion": 3
83
+ }
84
+ },
85
+ "run": {
86
+ "xvic": [
87
+ "-memory",
88
+ "3k"
89
+ ]
90
+ },
91
+ "facts": {
92
+ "memory.ram": 6655
93
+ }
94
+ },
95
+ "8k": {
96
+ "label": "8K: screen moves to $1000",
97
+ "tag": "expanded",
98
+ "build": {
99
+ "defsym": {
100
+ "__memory_expansion": 8
101
+ }
102
+ },
103
+ "run": {
104
+ "xvic": [
105
+ "-memory",
106
+ "8k"
107
+ ]
108
+ },
109
+ "facts": {
110
+ "memory.ram": 11775
111
+ }
112
+ },
113
+ "16k": {
114
+ "label": "16K: screen moves to $1000",
115
+ "tag": "expanded",
116
+ "build": {
117
+ "defsym": {
118
+ "__memory_expansion": 16
119
+ }
120
+ },
121
+ "run": {
122
+ "xvic": [
123
+ "-memory",
124
+ "16k"
125
+ ]
126
+ },
127
+ "facts": {
128
+ "memory.ram": 19967
129
+ }
130
+ },
131
+ "24k": {
132
+ "label": "24K: screen moves to $1000",
133
+ "tag": "expanded",
134
+ "build": {
135
+ "defsym": {
136
+ "__memory_expansion": 24
137
+ }
138
+ },
139
+ "run": {
140
+ "xvic": [
141
+ "-memory",
142
+ "24k"
143
+ ]
144
+ },
145
+ "facts": {
146
+ "memory.ram": 28159
147
+ }
148
+ }
149
+ }
150
+ },
151
+ "port1": {
152
+ "label": "Control port 1",
153
+ "default": "joystick",
154
+ "values": {
155
+ "none": {
156
+ "label": "Nothing plugged in",
157
+ "run": {
158
+ "xvic": [
159
+ "-controlport1device",
160
+ "0"
161
+ ]
162
+ }
163
+ },
164
+ "joystick": {
165
+ "label": "Joystick",
166
+ "run": {
167
+ "xvic": [
168
+ "-controlport1device",
169
+ "1"
170
+ ]
171
+ }
172
+ },
173
+ "paddles": {
174
+ "label": "Paddles",
175
+ "run": {
176
+ "xvic": [
177
+ "-controlport1device",
178
+ "2"
179
+ ]
180
+ },
181
+ "facts": {
182
+ "input.paddles": true
183
+ }
184
+ },
185
+ "mouse1351": {
186
+ "label": "Commodore 1351 mouse",
187
+ "run": {
188
+ "xvic": [
189
+ "-controlport1device",
190
+ "3",
191
+ "-mouse"
192
+ ]
193
+ },
194
+ "facts": {
195
+ "input.mouse": true
196
+ }
197
+ }
198
+ }
199
+ },
200
+ "drive": {
201
+ "label": "Disk drive",
202
+ "default": "1541",
203
+ "values": {
204
+ "1541": {
205
+ "label": "1541 (170K disk)",
206
+ "facts": {
207
+ "storage.save": true,
208
+ "storage.kib": 164
209
+ },
210
+ "tag": null
211
+ },
212
+ "1581": {
213
+ "label": "1581 (800K disk)",
214
+ "facts": {
215
+ "storage.save": true,
216
+ "storage.kib": 783
217
+ },
218
+ "tag": null
219
+ },
220
+ "none": {
221
+ "label": "No drive (tape or nothing)",
222
+ "facts": {
223
+ "storage.save": false,
224
+ "storage.kib": 0
225
+ },
226
+ "tag": null
227
+ }
228
+ }
229
+ }
230
+ },
231
+ "presets": {
232
+ "unexpanded": {
233
+ "ram": "none"
234
+ },
235
+ "3k": {
236
+ "ram": "3k"
237
+ },
238
+ "8k": {
239
+ "ram": "8k"
240
+ },
241
+ "16k": {
242
+ "ram": "16k"
243
+ },
244
+ "24k": {
245
+ "ram": "24k"
246
+ }
247
+ }
248
+ }
249
+ },
250
+ "files": [
251
+ "src"
252
+ ],
253
+ "publishConfig": {
254
+ "access": "public"
255
+ }
256
+ }
@@ -0,0 +1,36 @@
1
+ // @8bitscript/vic20 — where the screen is, for the unexpanded and 3K VIC-20.
2
+ //
3
+ // The VIC-20's screen moves with its memory. Unexpanded (and with a 3K
4
+ // expansion, which fills $0400-$0FFF and leaves everything above alone)
5
+ // the KERNAL puts the 22x23 screen matrix at $1E00 with its colour nybbles
6
+ // at $9600, and BASIC starts at $1001. With 8K or more the KERNAL moves the
7
+ // screen down to $1000 and colour RAM to $9400, so that $1200 upward is one
8
+ // contiguous run of BASIC RAM — which is why the SDK loads an 8K+ program at
9
+ // $1201 (mos-platform/vic20/lib/link.ld). This file is the unexpanded/3K
10
+ // geometry; `geometry.vic20.expanded.8bs` beside it is the 8K-and-up one,
11
+ // read by a build whose hardware carries the `expanded` tag — the tag the
12
+ // package's catalog gives the 8k, 16k and 24k values of its `ram` option
13
+ // (package.json, "8bitscript".hardware), one word for the one thing they
14
+ // share. text.8bs and screen.8bs are written once, against these numbers
15
+ // (docs/packages.md, "System-specific files").
16
+ //
17
+ // Two of these are the VIC's, not the KERNAL's, and the package writes one
18
+ // of them itself: MEMORY_POINTER_UPPERCASE is what text.8bs stores in $9005
19
+ // before every run of text — bits 4-7 the screen base, bits 0-3 the
20
+ // upper-case character ROM at $8000 — $F0 for a screen at $1E00, $C0 for
21
+ // one at $1000 (POKE 36869,240 and POKE 36869,192 in the Programmer's
22
+ // Reference Guide). The screen's address bit 9 and the colour RAM base
23
+ // both come from $9002 bit 7, which the package leaves as the KERNAL set
24
+ // it at boot from the RAM it found. So the profile has to match the
25
+ // machine: a `--profile 8k` build on an unexpanded VIC-20 would write $C0
26
+ // to a VIC whose $9002 still says $1E00, and draw nowhere useful. `8bs run`
27
+ // launches xvic with the matching `-memory`, so the emulator always agrees.
28
+
29
+ export namespace Video {
30
+ const SCREEN: usmallint = 0x1E00; // screen matrix: one screen code per cell
31
+ const COLOR: usmallint = 0x9600; // colour RAM: one nybble per cell, same cell index
32
+ const MEMORY_POINTER_UPPERCASE: utinyint = 0xF0; // $9005: this screen, upper-case ROM
33
+ const COLUMNS: utinyint = 22;
34
+ const ROWS: utinyint = 23;
35
+ const CELL_COUNT: usmallint = 506; // COLUMNS * ROWS
36
+ }
@@ -0,0 +1,20 @@
1
+ // @8bitscript/vic20 — where the screen is, with 8K or more of expansion.
2
+ //
3
+ // The `expanded` version of geometry.8bs (see that file for the whole
4
+ // story): with 8K or more the KERNAL moves the screen matrix to $1000 and
5
+ // colour RAM to $9400 so BASIC RAM runs unbroken from $1200, and the SDK
6
+ // loads the program at $1201. $9005's screen nybble becomes $C0 (POKE
7
+ // 36869,192). One file serves the 8k, 16k and 24k values of the `ram`
8
+ // option because the catalog (package.json, "8bitscript".hardware) gives
9
+ // all three the same `expanded` tag: where the screen is depends on
10
+ // whether there is 8K or more, not on how much more, and a tag names
11
+ // exactly the thing a file differs on.
12
+
13
+ export namespace Video {
14
+ const SCREEN: usmallint = 0x1000; // screen matrix: one screen code per cell
15
+ const COLOR: usmallint = 0x9400; // colour RAM: one nybble per cell, same cell index
16
+ const MEMORY_POINTER_UPPERCASE: utinyint = 0xC0; // $9005: this screen, upper-case ROM
17
+ const COLUMNS: utinyint = 22;
18
+ const ROWS: utinyint = 23;
19
+ const CELL_COUNT: usmallint = 506; // COLUMNS * ROWS
20
+ }
package/src/index.8bs ADDED
@@ -0,0 +1,57 @@
1
+ // @8bitscript/vic20 — VIC-20 target support: the hardware underneath.
2
+ //
3
+ // The module entry point named by the "8bitscript".entry field in this
4
+ // package's package.json, resolved and linked for:
5
+ //
6
+ // import { vicColor, memoryPointer } from "@8bitscript/vic20";
7
+ //
8
+ // The surface is the hardware, one register at a time. What a program
9
+ // usually wants sits one layer up, in this package's own implementations of
10
+ // the portable capability packages — `./src/screen.8bs` behind
11
+ // @8bitscript/screen and `./src/text.8bs` behind @8bitscript/text, named by
12
+ // the "8bitscript".exports map in package.json — which import these
13
+ // registers and are built on nothing else. A project that imports THIS
14
+ // package has declared itself VIC-20 specific.
15
+
16
+ // $900F, the VIC chip's screen/colour register. Bits verified against the
17
+ // VIC-20's own hardware map, not inferred from a BASIC POKE example:
18
+ //
19
+ // bits 4-7 background colour (16 values)
20
+ // bit 3 normal/inverted video (1 = normal)
21
+ // bits 0-2 border colour (8 values — the VIC only gives border 3 bits)
22
+ @address(0x900F)
23
+ export let vicColor: volatile<u8>;
24
+
25
+ // $9005, the VIC's memory pointer (`addr` in llvm-mos-sdk's _vic.h): bits
26
+ // 4-7 the screen base, bits 0-3 the character set base. $F0 is screen $1E00
27
+ // with the upper-case/graphics character ROM at $8000 — the power-on value
28
+ // (POKE 36869,240 in the Programmer's Reference Guide), where $F2 is the
29
+ // lower-case ROM at $8800.
30
+ @address(0x9005)
31
+ export let memoryPointer: volatile<u8>;
32
+
33
+ // ---- the VIAs ($9110, $9120): the control port and the keyboard -------------
34
+ //
35
+ // The VIC-20 splits one joystick across two chips, which is the single
36
+ // most surprising thing about input on this machine. VIA1's port A carries
37
+ // up, down, left and fire; **right is VIA2 port B bit 7** — a line that is
38
+ // otherwise a keyboard *column output*. So reading it means turning that
39
+ // one bit around to an input and back, which is what
40
+ // `@8bitscript/vic20/input` does and why it cannot simply read a port.
41
+ //
42
+ // The keyboard is VIA2: port B selects columns (outputs, DDRB $FF at boot)
43
+ // and port A reads the rows of the selected column (inputs, DDRA $00).
44
+ //
45
+ // Verified against VICE's `vic20via1.c` and `vic20via2.c` — see
46
+ // ../AGENTS.md, which records the port bits and the boot-time direction
47
+ // registers this package relies on.
48
+ @address(0x9111)
49
+ export let via1PortA: volatile<u8>;
50
+ @address(0x9120)
51
+ export let via2PortB: volatile<u8>;
52
+ @address(0x9121)
53
+ export let via2PortA: volatile<u8>;
54
+ @address(0x9122)
55
+ export let via2DirectionB: volatile<u8>;
56
+ @address(0x9123)
57
+ export let via2DirectionA: volatile<u8>;
package/src/input.8bs ADDED
@@ -0,0 +1,179 @@
1
+ // @8bitscript/vic20/input — the VIC-20 behind @8bitscript/input.
2
+ //
3
+ // Named by "8bitscript".exports["./input"] in this package's package.json,
4
+ // and by "8bitscript".entry.vic20 in @8bitscript/input's, so a portable
5
+ // program writes
6
+ //
7
+ // import { input } from "@8bitscript/input";
8
+ //
9
+ // and gets this file on a VIC-20. The machine-level layers underneath —
10
+ // ./keyboard.8bs, ./keys.8bs — stay available to a program that wants the
11
+ // matrix itself.
12
+ //
13
+ // ---- one joystick split across two chips ---------------------------------
14
+ //
15
+ // This is the machine where reading a joystick is not reading a port. Up,
16
+ // down, left and fire are VIA1 port A ($9111) bits 2, 3, 4 and 5 — a 0 bit
17
+ // where a switch is closed, as everywhere. **Right is VIA2 port B bit 7**
18
+ // ($9120), which is otherwise a keyboard *column output*. So reading right
19
+ // means turning that one bit around to an input, reading it, and putting
20
+ // it back an output before anything else touches the keyboard lines — and
21
+ // it has to happen *after* `keyboard.scan()` below, which is the thing
22
+ // actually driving those seven other columns each frame; read the joystick
23
+ // first and scan() would find every column selected but the last, and read
24
+ // "right" as though nothing else had touched the port.
25
+ //
26
+ // Verified against VICE's `vic20via1.c` and `vic20via2.c`; ../AGENTS.md
27
+ // records the bits and the boot-time direction registers.
28
+ //
29
+ // ---- the keyboard, and the cursor keys' two directions --------------------
30
+ //
31
+ // Like the C64, this keyboard has two cursor keys, not four:
32
+ // `CURSOR_LEFT_RIGHT` and `CURSOR_UP_DOWN`, with SHIFT reversing each — so
33
+ // left is SHIFT plus the right key, and up is SHIFT plus the down key,
34
+ // which is what the machine actually has rather than four keys it does
35
+ // not. `Key`'s table (./keys.8bs) is checked key for key against VICE's
36
+ // own positional keyboard map (packages/compiler/test/vic20-keys.test.mjs)
37
+ // rather than recalled from memory, per the root AGENTS.md's rule to
38
+ // verify hardware facts before writing them down.
39
+ //
40
+ // **A mouse.** `--hardware port1=mouse1351` sets `input.mouse` on this
41
+ // machine, but a 1351 here would read through the VIC's own analogue lines
42
+ // at $9008/$9009, not a SID's — a different driver from the C64's, and one
43
+ // nobody has written or measured. `pointer()` is false rather than
44
+ // hopeful.
45
+ //
46
+ // ---- edges, not levels ---------------------------------------------------
47
+ //
48
+ // Every answer is edge-triggered: true on the one frame the press begins.
49
+ // `poll()` once a frame, right after waitFrame().
50
+ import { via1PortA, via2PortB, via2DirectionB } from "./index.8bs";
51
+ import { keyboard } from "./keyboard.8bs";
52
+ import { Key } from "./keys.8bs";
53
+
54
+ // VIA1 port A's four switches, and the odd one out on VIA2.
55
+ const JOY_UP: utinyint = 4;
56
+ const JOY_DOWN: utinyint = 8;
57
+ const JOY_LEFT: utinyint = 16;
58
+ const JOY_FIRE: utinyint = 32;
59
+ const JOY_RIGHT: utinyint = 128; // VIA2 port B bit 7
60
+
61
+ namespace Edge {
62
+ const LEFT: utinyint = 1;
63
+ const RIGHT: utinyint = 2;
64
+ const UP: utinyint = 4;
65
+ const DOWN: utinyint = 8;
66
+ const CONFIRM: utinyint = 16;
67
+ const CANCEL: utinyint = 32;
68
+ }
69
+
70
+ let held: utinyint = 0;
71
+ let before: utinyint = 0;
72
+ let began: utinyint = 0;
73
+
74
+ export namespace input {
75
+
76
+ // Nothing to set up: no pointer to give a ceiling to.
77
+ function begin(): void {
78
+ }
79
+
80
+ function poll(): void {
81
+ // First, so the other seven keyboard columns are already driven
82
+ // for this frame before the joystick-right read below borrows
83
+ // bit 7 of the same port.
84
+ keyboard.scan();
85
+
86
+ // The four on VIA1, inverted once so a 1 bit means pushed.
87
+ let stick: utinyint = via1PortA ^ 0xFF;
88
+
89
+ // Right, on a line that is normally a keyboard column output.
90
+ // Turn bit 7 around, read it, put it back — leaving the other
91
+ // seven columns exactly as they were, because the KERNAL is not
92
+ // the only thing that drives them.
93
+ via2DirectionB = via2DirectionB & 0x7F;
94
+ let rightPushed: bool = (via2PortB & JOY_RIGHT) == 0;
95
+ via2DirectionB = via2DirectionB | 0x80;
96
+
97
+ before = held;
98
+ held = 0;
99
+
100
+ if ((stick & JOY_LEFT) != 0) {
101
+ held = held | Edge.LEFT;
102
+ }
103
+ if (rightPushed) {
104
+ held = held | Edge.RIGHT;
105
+ }
106
+ if ((stick & JOY_UP) != 0) {
107
+ held = held | Edge.UP;
108
+ }
109
+ if ((stick & JOY_DOWN) != 0) {
110
+ held = held | Edge.DOWN;
111
+ }
112
+ if ((stick & JOY_FIRE) != 0) {
113
+ held = held | Edge.CONFIRM;
114
+ }
115
+
116
+ // The two cursor keys and SHIFT, which is what "four directions"
117
+ // is on this keyboard — or'd on top of the joystick, same as the
118
+ // C64: a program should not care which the user reached for.
119
+ let shifted: bool = keyboard.pressed(Key.SHIFT_LEFT) || keyboard.pressed(Key.SHIFT_RIGHT);
120
+ if (keyboard.pressed(Key.CURSOR_LEFT_RIGHT)) {
121
+ if (shifted) {
122
+ held = held | Edge.LEFT;
123
+ } else {
124
+ held = held | Edge.RIGHT;
125
+ }
126
+ }
127
+ if (keyboard.pressed(Key.CURSOR_UP_DOWN)) {
128
+ if (shifted) {
129
+ held = held | Edge.UP;
130
+ } else {
131
+ held = held | Edge.DOWN;
132
+ }
133
+ }
134
+ if (keyboard.pressed(Key.RETURN)) {
135
+ held = held | Edge.CONFIRM;
136
+ }
137
+ if (keyboard.pressed(Key.RUN_STOP)) {
138
+ held = held | Edge.CANCEL;
139
+ }
140
+
141
+ began = held & (before ^ 0xFF);
142
+ }
143
+
144
+ function left(): bool {
145
+ return (began & Edge.LEFT) != 0;
146
+ }
147
+
148
+ function right(): bool {
149
+ return (began & Edge.RIGHT) != 0;
150
+ }
151
+
152
+ function up(): bool {
153
+ return (began & Edge.UP) != 0;
154
+ }
155
+
156
+ function down(): bool {
157
+ return (began & Edge.DOWN) != 0;
158
+ }
159
+
160
+ function confirm(): bool {
161
+ return (began & Edge.CONFIRM) != 0;
162
+ }
163
+
164
+ function cancel(): bool {
165
+ return (began & Edge.CANCEL) != 0;
166
+ }
167
+
168
+ function pointer(): bool {
169
+ return false;
170
+ }
171
+
172
+ function pointerCell(): usmallint {
173
+ return 0;
174
+ }
175
+
176
+ function pointerButton(): bool {
177
+ return false;
178
+ }
179
+ }
@@ -0,0 +1,80 @@
1
+ // @8bitscript/vic20/keyboard — the VIC-20's keyboard matrix, one snapshot a
2
+ // frame.
3
+ //
4
+ // Named by "8bitscript".exports["./keyboard"] in this package's package.json:
5
+ //
6
+ // import { keyboard } from "@8bitscript/vic20/keyboard";
7
+ // import { Key } from "@8bitscript/vic20/keys";
8
+ //
9
+ // This is hardware-level, VIC-20-only surface — the layer the portable
10
+ // input capability is built on: `@8bitscript/vic20/input` reads this file
11
+ // and @8bitscript/input resolves to that, the way ./text.8bs is the layer
12
+ // under @8bitscript/text. Importing it makes a program VIC-20-specific.
13
+ // It keeps no key buffer and decodes no PETSCII; it answers "is this key
14
+ // down now", which is what a game loop asks.
15
+ //
16
+ // ---- how the matrix works -----------------------------------------------
17
+ //
18
+ // Eight columns by eight rows, on VIA2 alone — ../index.8bs and
19
+ // ../AGENTS.md (verified against VICE's `vic20via2.c`): port B selects a
20
+ // column (DDRB $FF at boot, all eight lines outputs), port A reads that
21
+ // column's eight keys (DDRA $00, inputs), a 0 bit where a key is down.
22
+ // `scan()` writes both direction registers itself, so it does not depend
23
+ // on what ran before, and drives every column exactly once — the KERNAL's
24
+ // own IRQ scan (packages/vic20/AGENTS.md: a free-running ~60 Hz timer, not
25
+ // locked to the frame) would otherwise land its own column select between
26
+ // this scan's write and read, which is one reason the frame runtime
27
+ // starts a VIC-20 program with interrupts held off.
28
+ //
29
+ // `scan()` reads all eight columns into `state`, once a frame, right after
30
+ // waitFrame() returns; every question after that reads the snapshot. It
31
+ // leaves port B at $FF — no column selected — which is the state
32
+ // @8bitscript/vic20/input's joystick-right read needs: VIA2 port B bit 7
33
+ // doubles as that line, and turning it around to an input reads correctly
34
+ // only once the keyboard scan is no longer driving it low for some other
35
+ // column. Call `keyboard.scan()` first and read the joystick second, both
36
+ // once a frame.
37
+ //
38
+ // Ghosting is the same hazard it is on every unshifted matrix: three keys
39
+ // down can read as four. Two keys are always safe.
40
+ import { via2PortA, via2PortB, via2DirectionA, via2DirectionB } from "./index.8bs";
41
+
42
+ // The last scan, one byte per column, a 1 bit where a key is down (the
43
+ // port's active-low rows inverted once, here, so nothing else has to
44
+ // think about it). All zero until the first scan.
45
+ let state: array<utinyint, 8>;
46
+
47
+ // The column-select byte for each column, and the row masks — indexed
48
+ // rather than shifted, as `1 << n` with a variable n is a loop on the 6502.
49
+ const COLUMN_SELECT: array<utinyint, 8> = [254, 253, 251, 247, 239, 223, 191, 127];
50
+ const ROW_BIT: array<utinyint, 8> = [1, 2, 4, 8, 16, 32, 64, 128];
51
+
52
+ export namespace keyboard {
53
+ const COLUMNS: utinyint = 8;
54
+ const ROWS: utinyint = 8;
55
+
56
+ // Read all eight columns into the snapshot. Once a frame, right after
57
+ // waitFrame(), before reading the joystick's right line.
58
+ function scan(): void {
59
+ via2DirectionB = 0xFF;
60
+ via2DirectionA = 0x00;
61
+ for (let select: utinyint = 0; select < 8; select++) {
62
+ via2PortB = COLUMN_SELECT[select];
63
+ state[select] = via2PortA ^ 0xFF;
64
+ }
65
+ via2PortB = 0xFF;
66
+ }
67
+
68
+ // Is this key down in the last scan? `key` is a `Key.*` value from
69
+ // @8bitscript/vic20/keys — `select * 8 + row` in the matrix.
70
+ function pressed(key: utinyint): bool {
71
+ return (state[key >> 3] & ROW_BIT[key & 7]) != 0;
72
+ }
73
+
74
+ // The last scan's eight rows of one column, a 1 bit per key down:
75
+ // for code that wants a whole column at once (an "any key" test over
76
+ // all eight, a ghosting check).
77
+ function column(index: utinyint): utinyint {
78
+ return state[index];
79
+ }
80
+ }
package/src/keys.8bs ADDED
@@ -0,0 +1,139 @@
1
+ // @8bitscript/vic20/keys — the keyboard matrix, key by key.
2
+ //
3
+ // Named by "8bitscript".exports["./keys"] in this package's package.json, for
4
+ //
5
+ // import { keyboard } from "@8bitscript/vic20/keyboard";
6
+ // import { Key } from "@8bitscript/vic20/keys";
7
+ //
8
+ // if (keyboard.pressed(Key.SPACE)) { ... }
9
+ //
10
+ // Each value is `select * 8 + row` in VIA2's matrix (see ./keyboard.8bs):
11
+ // `select` is which of VIA2 port B's eight column-select lines was driven
12
+ // low to reach this key, `row` the bit of VIA2 port A that reads low —
13
+ // ../AGENTS.md, "Keyboard: VIA2 port B selects columns (output), port A
14
+ // reads rows against the selected columns" (verified there against
15
+ // vic20via2.c `read_pra`: port B is the column mask, `rev_keyarr[column]`
16
+ // the row bits). One matrix on every VIC-20 ever made, expanded or not.
17
+ //
18
+ // Source: the matrix diagram in VICE's own vic20/gtk3_pos.vkm (reproduced
19
+ // below), checked here key for key against that file,
20
+ // /opt/homebrew/share/vice/VIC20/gtk3_pos.vkm. Each line there is
21
+ // `keysym row column shiftflag`: VICE's first number is the **row** (VIA2
22
+ // port A bit), its second the **column** (VIA2 port B bit). `keyboard.scan()`
23
+ // indexes `state[column]` with row bits, so a Key is `column * 8 + row`,
24
+ // not `row * 8 + column`. The C64 table can use VICE's pair in file order
25
+ // because that machine's gtk3_pos.vkm writes the *column* first; copying
26
+ // that convention here looked up every key in the transposed cell
27
+ // (2048 on xvic, 2026-09-07: host Right lit port-B column 2 / port-A bit 7
28
+ // — Key 23 — while this table named that position 58).
29
+ // packages/compiler/test/vic20-keys.test.mjs repeats the check when the
30
+ // keymap is present.
31
+ //
32
+ // +------+------+------+------+------+------+------+------+
33
+ // |sel. 0|sel. 1|sel. 2|sel. 3|sel. 4|sel. 5|sel. 6|sel. 7|
34
+ // +-----+------+------+------+------+------+------+------+------+
35
+ // |row 0| 1 ! |A_LEFT| CTRL | R/S |SPACE | C= | Q | 2 " |
36
+ // +-----+------+------+------+------+------+------+------+------+
37
+ // |row 1| 3 # | W | A |S_LEFT| Z | S | E | 4 $ |
38
+ // +-----+------+------+------+------+------+------+------+------+
39
+ // |row 2| 5 % | R | D | X | C | F | T | 6 & |
40
+ // +-----+------+------+------+------+------+------+------+------+
41
+ // |row 3| 7 ' | Y | G | V | B | H | U | 8 ( |
42
+ // +-----+------+------+------+------+------+------+------+------+
43
+ // |row 4| 9 ) | I | J | N | M | K | O | 0 |
44
+ // +-----+------+------+------+------+------+------+------+------+
45
+ // |row 5| + | P | L | , < | . > | : [ | @ | - |
46
+ // +-----+------+------+------+------+------+------+------+------+
47
+ // |row 6|POUND | * | ; ] | / ? |S_RGHT| = |A_UP | HOME |
48
+ // +-----+------+------+------+------+------+------+------+------+
49
+ // |row 7| DEL |RETURN|C_L/R |C_U/D | F1 | F3 | F5 | F7 |
50
+ // +-----+------+------+------+------+------+------+------+------+
51
+ //
52
+ // Two keys are not in it: RESTORE is wired to the CPU's NMI line (VIA1's
53
+ // CA1, ../AGENTS.md), and SHIFT LOCK is the left SHIFT key's own wire,
54
+ // held down (VICE's `Caps_Lock` maps to the same position as `Shift_L`).
55
+ //
56
+ // A key that also exists on the C64 or PET has the same name here —
57
+ // letters, `DIGIT_n`, SPACE, RETURN, HOME, DELETE, both shifts, the arrows
58
+ // and the shared punctuation — so a program that sticks to those names
59
+ // reads the same across the Commodores it runs on. `CURSOR_LEFT_RIGHT` and
60
+ // `CURSOR_UP_DOWN` are named for what this keyboard actually has: one key
61
+ // per pair, not four, with SHIFT reversing each — the same shape as the
62
+ // C64's `CURSOR_RIGHT`/`CURSOR_DOWN`, under different names because they
63
+ // are not the same two directions unshifted (this machine's un-shifted
64
+ // pair is right and down, matching the C64's).
65
+
66
+ export namespace Key {
67
+ // select 0 — VIA2 port B bit 0
68
+ const DIGIT_1: utinyint = 0;
69
+ const DIGIT_3: utinyint = 1;
70
+ const DIGIT_5: utinyint = 2;
71
+ const DIGIT_7: utinyint = 3;
72
+ const DIGIT_9: utinyint = 4;
73
+ const PLUS: utinyint = 5;
74
+ const POUND: utinyint = 6; // £
75
+ const DELETE: utinyint = 7; // INST/DEL
76
+ // select 1
77
+ const LEFT_ARROW: utinyint = 8; // the "<-" key left of DIGIT_1, not a cursor key
78
+ const W: utinyint = 9;
79
+ const R: utinyint = 10;
80
+ const Y: utinyint = 11;
81
+ const I: utinyint = 12;
82
+ const P: utinyint = 13;
83
+ const ASTERISK: utinyint = 14;
84
+ const RETURN: utinyint = 15;
85
+ // select 2
86
+ const CTRL: utinyint = 16;
87
+ const A: utinyint = 17;
88
+ const D: utinyint = 18;
89
+ const G: utinyint = 19;
90
+ const J: utinyint = 20;
91
+ const L: utinyint = 21;
92
+ const SEMICOLON: utinyint = 22;
93
+ const CURSOR_LEFT_RIGHT: utinyint = 23; // unshifted: right; shifted: left
94
+ // select 3
95
+ const RUN_STOP: utinyint = 24;
96
+ const SHIFT_LEFT: utinyint = 25; // also SHIFT LOCK
97
+ const X: utinyint = 26;
98
+ const V: utinyint = 27;
99
+ const N: utinyint = 28;
100
+ const COMMA: utinyint = 29;
101
+ const SLASH: utinyint = 30;
102
+ const CURSOR_UP_DOWN: utinyint = 31; // unshifted: down; shifted: up
103
+ // select 4
104
+ const SPACE: utinyint = 32;
105
+ const Z: utinyint = 33;
106
+ const C: utinyint = 34;
107
+ const B: utinyint = 35;
108
+ const M: utinyint = 36;
109
+ const PERIOD: utinyint = 37;
110
+ const SHIFT_RIGHT: utinyint = 38;
111
+ const F1: utinyint = 39; // shifted: F2
112
+ // select 5
113
+ const COMMODORE: utinyint = 40; // the C= key
114
+ const S: utinyint = 41;
115
+ const F: utinyint = 42;
116
+ const H: utinyint = 43;
117
+ const K: utinyint = 44;
118
+ const COLON: utinyint = 45;
119
+ const EQUALS: utinyint = 46;
120
+ const F3: utinyint = 47; // shifted: F4
121
+ // select 6
122
+ const Q: utinyint = 48;
123
+ const E: utinyint = 49;
124
+ const T: utinyint = 50;
125
+ const U: utinyint = 51;
126
+ const O: utinyint = 52;
127
+ const AT: utinyint = 53;
128
+ const UP_ARROW: utinyint = 54;
129
+ const F5: utinyint = 55; // shifted: F6
130
+ // select 7
131
+ const DIGIT_2: utinyint = 56;
132
+ const DIGIT_4: utinyint = 57;
133
+ const DIGIT_6: utinyint = 58;
134
+ const DIGIT_8: utinyint = 59;
135
+ const DIGIT_0: utinyint = 60;
136
+ const MINUS: utinyint = 61;
137
+ const HOME: utinyint = 62; // CLR/HOME
138
+ const F7: utinyint = 63; // shifted: F8
139
+ }
@@ -0,0 +1,40 @@
1
+ // @8bitscript/vic20/pointer — the VIC-20 behind @8bitscript/pointer.
2
+ //
3
+ // Every call here does nothing and `DRAWS` is false, and on this machine
4
+ // that is the hardware's answer: **the VIC has no sprites.** There are no
5
+ // movable objects to put an arrow in, so a cursor here would have to be a
6
+ // character cell — which means putting back the cell it covered, and
7
+ // @8bitscript/text can write a cell but not read one, so it cannot. A
8
+ // redefined glyph moved a cell at a time is the shape such a cursor would
9
+ // take, and it would be a different thing from the C64's pixel-precise
10
+ // arrow rather than a poorer copy of it.
11
+ //
12
+ // The other half is missing too: @8bitscript/vic20/input reports no
13
+ // pointer. The catalog offers `port1: mouse1351` and xvic accepts it, but
14
+ // this repository has no 1351 driver for the VIC-20 and the hardware note
15
+ // marks the 1351 on this machine *to verify* (see
16
+ // packages/vic20/AGENTS.md). So there is nothing to draw and nowhere to
17
+ // draw it from, in that order.
18
+ //
19
+ // It still has to exist: Studio and every other portable program links
20
+ // @8bitscript/pointer, so a missing entry is not a program without a
21
+ // cursor, it is a program that does not build for the VIC-20 — the machine
22
+ // with the least room to spare, which is exactly why this file is
23
+ // constants. `DRAWS` is a const and every function is empty, so a
24
+ // program's `if (pointer.DRAWS)` folds away and LLVM deletes the calls.
25
+ export namespace pointer {
26
+
27
+ const DRAWS: bool = false;
28
+
29
+ function begin(): void {
30
+ }
31
+
32
+ function setColor(color: utinyint): void {
33
+ }
34
+
35
+ function update(): void {
36
+ }
37
+
38
+ function hide(): void {
39
+ }
40
+ }
package/src/screen.8bs ADDED
@@ -0,0 +1,101 @@
1
+ // @8bitscript/vic20/screen — the VIC-20's implementation of @8bitscript/screen.
2
+ //
3
+ // Named by "8bitscript".exports["./screen"] in this package's package.json,
4
+ // and what
5
+ //
6
+ // import { screen, BorderColor, BackgroundColor } from "@8bitscript/screen";
7
+ //
8
+ // resolves to when the build is for the VIC-20: @8bitscript/screen's entry is
9
+ // keyed by machine and delegates here. Built on the register @8bitscript/vic20
10
+ // itself exports, one layer up from it. Every machine's screen.8bs exports
11
+ // this same surface, which is what lets a program import it from
12
+ // @8bitscript/screen and never name the hardware.
13
+ import { vicColor } from "./index.8bs";
14
+ import { Video } from "./geometry.8bs";
15
+
16
+ // ---- the screen: border and background --------------------------------------
17
+ //
18
+ // One packed register on the VIC: border in bits 0-2 (the VIC has eight
19
+ // border colours, so the value is masked to them), background in bits 4-7,
20
+ // bit 3 held at 1 for normal video. Both colours in one call because they
21
+ // are one write here — and because on the NES and the X16 the two are
22
+ // genuinely one operation (see those packages), so the portable surface is
23
+ // the one every machine can honour.
24
+ export namespace screen {
25
+ function setColors(border: u8, background: u8): void {
26
+ vicColor = (8 | (border & 7)) | (background << 4);
27
+ }
28
+
29
+ // `blank(border, background)`: every cell blank and both colours set —
30
+ // black when left off, or `BorderColor.KEEP` / `BackgroundColor.KEEP`
31
+ // to leave one as it is. `setBorder` and `setBackground` are one
32
+ // colour each, `setColors` the pair; `text.setColor` owns what the
33
+ // next print looks like.
34
+ function blank(border: utinyint = BorderColor.BLACK, background: utinyint = BackgroundColor.BLACK): void {
35
+ if (border != BorderColor.KEEP) {
36
+ screen.setBorder(border);
37
+ }
38
+ if (background != BackgroundColor.KEEP) {
39
+ screen.setBackground(background);
40
+ }
41
+ for (let cell: usmallint = 0; cell < Video.CELL_COUNT; cell++) {
42
+ memory.write(Video.SCREEN + cell, 32); // 32: the space screen code
43
+ }
44
+ }
45
+
46
+ function setBackground(background: u8): void {
47
+ vicColor = (vicColor & 0x0F) | (background << 4);
48
+ }
49
+
50
+ function setBorder(border: u8): void {
51
+ vicColor = (vicColor & 0xF0) | 8 | (border & 7);
52
+ }
53
+ }
54
+
55
+ // ---- colour names ----------------------------------------------------------
56
+ //
57
+ // The border field only has 3 bits, so it can only reach the first 8 of the
58
+ // VIC's 16 colours — `BorderColor` and `BackgroundColor` are two namespaces,
59
+ // not one shared `Color`, because the hardware itself draws that line, not
60
+ // an API design preference. Values and names match the VIC-20's own colour
61
+ // numbering (POKE 36879 in the Programmer's Reference Guide). Those first
62
+ // eight names — Black, White, Red, Cyan, Purple, Green, Blue, Yellow — are
63
+ // the ones every machine's screen.8bs exports in both namespaces, each with
64
+ // its own hardware's value, which is what lets a program say
65
+ // `screen.setColors(BorderColor.BLUE, BackgroundColor.BLACK)` on any target.
66
+
67
+ export namespace BorderColor {
68
+ const BLACK: utinyint = 0;
69
+ const WHITE: utinyint = 1;
70
+ const RED: utinyint = 2;
71
+ const CYAN: utinyint = 3;
72
+ const PURPLE: utinyint = 4;
73
+ const GREEN: utinyint = 5;
74
+ const BLUE: utinyint = 6;
75
+ const YELLOW: utinyint = 7;
76
+ // Not a colour: `blank(KEEP, ...)` leaves this one as it is. 255 is
77
+ // a value no register here takes.
78
+ const KEEP: utinyint = 255;
79
+ }
80
+
81
+ export namespace BackgroundColor {
82
+ const BLACK: utinyint = 0;
83
+ const WHITE: utinyint = 1;
84
+ const RED: utinyint = 2;
85
+ const CYAN: utinyint = 3;
86
+ const PURPLE: utinyint = 4;
87
+ const GREEN: utinyint = 5;
88
+ const BLUE: utinyint = 6;
89
+ const YELLOW: utinyint = 7;
90
+ const ORANGE: utinyint = 8;
91
+ const LIGHT_ORANGE: utinyint = 9;
92
+ const PINK_RED: utinyint = 10;
93
+ const LIGHT_CYAN: utinyint = 11;
94
+ const LIGHT_PURPLE: utinyint = 12;
95
+ const LIGHT_GREEN: utinyint = 13;
96
+ const LIGHT_BLUE: utinyint = 14;
97
+ const LIGHT_YELLOW: utinyint = 15;
98
+ // Not a colour: `blank(KEEP, ...)` leaves this one as it is. 255 is
99
+ // a value no register here takes.
100
+ const KEEP: utinyint = 255;
101
+ }
package/src/text.8bs ADDED
@@ -0,0 +1,169 @@
1
+ // @8bitscript/vic20/text — the VIC-20's implementation of @8bitscript/text.
2
+ //
3
+ // Named by "8bitscript".exports["./text"] in this package's package.json,
4
+ // and what
5
+ //
6
+ // import { text } from "@8bitscript/text";
7
+ //
8
+ // resolves to when the build is for the VIC-20. Built on the registers
9
+ // @8bitscript/vic20 exports. Every machine's text.8bs exports this same
10
+ // namespace — the same names, the same shapes, ASCII codes, and a cell 0
11
+ // at the top-left corner inside the border — so a program that imports it
12
+ // from @8bitscript/text draws the same thing on every target.
13
+ import { memoryPointer } from "./index.8bs";
14
+ import { Video } from "./geometry.8bs";
15
+
16
+ // The screen is addressed by flat cell index — a position is
17
+ // `y * text.COLUMNS + x` — and these are the plainest things a text API
18
+ // can be built from: poke one cell's character code, poke one cell's colour, and how many
19
+ // cells the whole screen has. Whether a caller uses these to clear the
20
+ // screen, lay out a label, or nothing at all is entirely up to the caller —
21
+ // this package draws nothing on its own.
22
+ //
23
+ // Where the 22x23 screen and its colour nybbles are depends on the RAM the
24
+ // machine has — $1E00/$9600 unexpanded or with 3K, $1000/$9400 with 8K or
25
+ // more — and that is the build's `--profile` to say (VIC20_PROFILES in
26
+ // packages/backend-6502). It reaches this file as ./geometry.8bs's `Video`
27
+ // namespace, whose 8k/16k/24k twins the profile selects; nothing here
28
+ // names an address.
29
+ //
30
+ // ---- character codes: ASCII in, screen codes out ----------------------------
31
+ //
32
+ // `text.putChar` takes ASCII on every machine — space (32), '0'-'9'
33
+ // (48-57), 'A'-'Z' (65-90) and the punctuation `! , - . : ?` — upper case
34
+ // only: that is the portable set, the characters every target's character
35
+ // set has (the NES ships its own font, and that is what it has). The VIC
36
+ // wants screen codes: in every Commodore character ROM codes 32-63 are
37
+ // those same ASCII values, and 'A'-'Z' are 1-26, so asciiToScreenCode()
38
+ // moves 64-95 down by 64 and leaves 32-63 alone.
39
+ //
40
+ // Whether 1-26 then LOOK like 'A'-'Z' or 'a'-'z' is the character set's
41
+ // choice — the ROM's boot state, whatever ran before, a user's SHIFT+C= —
42
+ // so this package owns the choice rather than inheriting one (LLVM-MOS's
43
+ // libc used to flip the machine to lower-case before main() with a hidden
44
+ // CHR$(14); packages/backend-6502's commodoreCharsetGuard() keeps that out
45
+ // of the link). putChar()
46
+ // selects the upper-case set, every call, through the VIC's memory pointer
47
+ // (see @8bitscript/vic20's `memoryPointer`): one register write — of the
48
+ // value that also names this profile's screen base, since $9005 holds
49
+ // both — and "TICK" reads as TICK here the way it does on the NES, the
50
+ // Atari, and the X16, none of which have a lower-case set to fall into.
51
+
52
+ function asciiToScreenCode(code: utinyint): utinyint {
53
+ if (code >= 64 && code < 96) {
54
+ return code - 64;
55
+ }
56
+ return code;
57
+ }
58
+
59
+ // ---- print: strings and number fields ------------------------------------
60
+ //
61
+ // `text.print(cell, s)` writes a string's characters into consecutive
62
+ // cells from `cell`, and `text.printNumber(cell, value, width)` writes
63
+ // `value` as exactly `width` decimal digits, zero-padded and right-aligned,
64
+ // so a field on a HUD never shifts columns. Both draw in the current colour —
65
+ // white until `text.setColor(TextColor.CYAN)` changes it, and that one
66
+ // call then colours everything printed after it, on the machines that have
67
+ // per-cell colour. They are also the two
68
+ // functions the compiler's template layout targets: `text.print(0,
69
+ // \`TICK ${ticks:1}\`)` is laid out at compile time into these same calls
70
+ // (see packages/compiler/src/ir).
71
+ //
72
+ // Under them: `place()` puts one character at one cell in the current
73
+ // colour, and is all a run of text costs per character, with `prepare()` selecting the
74
+ // upper-case set once for the run.
75
+ // `text.putChar` and `text.putColor` stay the one-cell pokes a caller can
76
+ // build anything from.
77
+ let currentColor: utinyint = 1; // white, until text.setColor() says otherwise
78
+ let currentReverse: bool = false; // until text.setReverse() says otherwise
79
+
80
+ // Reverse video is a real ROM copy at codes 128-255.
81
+ function toScreen(code: utinyint): utinyint {
82
+ let screen: utinyint = asciiToScreenCode(code);
83
+ if (currentReverse) {
84
+ screen = screen + 128;
85
+ }
86
+ return screen;
87
+ }
88
+
89
+ // What a run of text needs once, before its first character.
90
+ function prepare(): void {
91
+ memoryPointer = Video.MEMORY_POINTER_UPPERCASE;
92
+ }
93
+
94
+ function place(cell: usmallint, code: utinyint): void {
95
+ memory.write(Video.SCREEN + cell, toScreen(code));
96
+ memory.write(Video.COLOR + cell, currentColor & 0x07); // bit 3 would make the cell multicolour
97
+ }
98
+
99
+ // ---- digits -------------------------------------------------------------
100
+ //
101
+ // A number is written one place at a time, high to low, by subtracting the
102
+ // place value until it no longer fits: the 6502 has no divide instruction,
103
+ // and `value / 10` would link a 250-byte routine to do it. Places above the
104
+ // field are still taken off, so a field narrower than its number shows the
105
+ // low digits; places the number does not reach print as zeros.
106
+ const DIGIT_PLACES: array<usmallint, 5> = [10000, 1000, 100, 10, 1];
107
+
108
+ export namespace text {
109
+ const CELL_COUNT: usmallint = Video.CELL_COUNT; // 22 columns x 23 rows
110
+ const COLUMNS: utinyint = Video.COLUMNS; // cells per row, so cell = y * text.COLUMNS + x
111
+
112
+ function putChar(cell: usmallint, code: utinyint): void {
113
+ prepare();
114
+ memory.write(Video.SCREEN + cell, toScreen(code));
115
+ }
116
+
117
+ function putColor(cell: usmallint, color: utinyint): void {
118
+ memory.write(Video.COLOR + cell, color & 0x07);
119
+ }
120
+
121
+ function setColor(color: utinyint): void {
122
+ currentColor = color;
123
+ }
124
+
125
+ function setReverse(on: bool): void {
126
+ currentReverse = on;
127
+ }
128
+
129
+ function print(cell: usmallint, s: string): void {
130
+ prepare();
131
+ for (let i: utinyint = 0; i < s.length; i++) {
132
+ place(cell + i, s[i]);
133
+ }
134
+ }
135
+
136
+ function printNumber(cell: usmallint, value: usmallint, width: utinyint): void {
137
+ prepare();
138
+ let k: utinyint = width;
139
+ if (k < DIGIT_PLACES.length) {
140
+ k = DIGIT_PLACES.length;
141
+ }
142
+ while (k > 0) {
143
+ let digit: utinyint = 48; // '0'
144
+ if (k <= DIGIT_PLACES.length) {
145
+ while (value >= DIGIT_PLACES[DIGIT_PLACES.length - k]) {
146
+ value = value - DIGIT_PLACES[DIGIT_PLACES.length - k];
147
+ digit++;
148
+ }
149
+ }
150
+ if (k <= width) {
151
+ place(cell + width - k, digit);
152
+ }
153
+ k--;
154
+ }
155
+ }
156
+ }
157
+
158
+ // The colours `text.setColor()` takes: the eight names every machine
159
+ // shares, with this machine's colour-RAM values.
160
+ export namespace TextColor {
161
+ const BLACK: utinyint = 0;
162
+ const WHITE: utinyint = 1;
163
+ const RED: utinyint = 2;
164
+ const CYAN: utinyint = 3;
165
+ const PURPLE: utinyint = 4;
166
+ const GREEN: utinyint = 5;
167
+ const BLUE: utinyint = 6;
168
+ const YELLOW: utinyint = 7;
169
+ }