@8bitscript/c64 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/src/input.8bs ADDED
@@ -0,0 +1,269 @@
1
+ // @8bitscript/c64/input — the C64 behind @8bitscript/input.
2
+ //
3
+ // Named by "8bitscript".exports["./input"] in this package's package.json,
4
+ // and by "8bitscript".entry.c64 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 C64. Nothing here is C64-specific to a program:
10
+ // it is the same ten calls on all nine machines, answered from whatever
11
+ // this one has. The machine-level layers underneath — ./keyboard.8bs,
12
+ // ./keys.8bs, ./joystick.8bs, ./mouse.8bs — stay available to a program
13
+ // that wants the matrix itself.
14
+ //
15
+ // ---- what a direction means here ------------------------------------------
16
+ //
17
+ // A menu asks "did the user just press right", not "is right held down",
18
+ // so every direction and button here is **edge-triggered**: true on the
19
+ // one frame the press begins, false while it is held. That is what makes
20
+ // a highlight step one item per press instead of racing across the bar,
21
+ // and it is why `poll()` has to be called exactly once a frame — twice a
22
+ // frame and every press is seen once and then swallowed.
23
+ //
24
+ // Three things can move the highlight on this machine, and all three are
25
+ // or'd together, because a program should not care which the user reached
26
+ // for:
27
+ //
28
+ // - **The cursor keys.** The C64 has two, not four: `CURSOR_RIGHT` and
29
+ // `CURSOR_DOWN`, with SHIFT reversing each. So left is SHIFT plus the
30
+ // right key, which is what the machine actually has and what this file
31
+ // reports rather than pretending there are four keys.
32
+ // - **A joystick in port 2**, which is where every C64 game asked for
33
+ // one. Port 1 shares wires with the keyboard's rows, so a stick there
34
+ // types; port 2 is the one that does not fight the matrix.
35
+ // - **A 1351 mouse in port 1**, if this build was fitted with one — see
36
+ // below.
37
+ //
38
+ // RETURN or the joystick's fire button confirms; RUN/STOP cancels.
39
+ //
40
+ // ---- the mouse is a compile-time decision and a run-time answer -----------
41
+ //
42
+ // `#fact(input.mouse)` is false on a stock C64 and true for a build made
43
+ // with `--hardware port1=mouse1351`. It is a `run` fact, so what it means
44
+ // is "this build may use a mouse" — and that is exactly the two-stage
45
+ // shape this machine needs:
46
+ //
47
+ // - **Compiling.** `HAS_MOUSE` is a const, so `if (HAS_MOUSE)` is
48
+ // `if (false)` on a stock build and the pointer code below is never
49
+ // reached, never referenced, and never linked. A program that did not
50
+ // ask for a mouse pays nothing for one — not a byte of code, not a
51
+ // byte of the accumulator.
52
+ // - **Running.** A 1351 says nothing about itself except through the
53
+ // lines its movement arrives on, so even a build that asked for one
54
+ // has to find out whether one is plugged in. `pointer()` answers that,
55
+ // from ./mouse.8bs's probe, every frame.
56
+ //
57
+ // **The port assignment is this file's choice, not a fact.**
58
+ // `input.mouse` is one flag and does not say which port the mouse is in.
59
+ // This file reads the mouse in **port 1** and the joystick in **port 2**,
60
+ // which is the arrangement C64 software settled on — a stick in the port
61
+ // that does not fight the keyboard, a pointer in the one that does. A
62
+ // program that needs it the other way round wants ./mouse.8bs directly.
63
+ //
64
+ // **The pointer is reported in cells, not pixels**, because that is the
65
+ // unit @8bitscript/text works in and the unit a component hit-tests
66
+ // against: `pointerCell()` is a flat cell index, the same number
67
+ // `text.print()` takes. A 1351 count is about a pixel, so the accumulator
68
+ // is limited to the screen in pixels and shifted down by the cell size.
69
+ //
70
+ // **Y is inverted in ./mouse.8bs**, not here. A 1351's Y counter runs
71
+ // opposite the screen; `poll()` there swaps last and now on that axis so
72
+ // `mouse.y()` is already in screen rows (0 at the top). This file only
73
+ // shifts that number to a cell. Verified under x64sc (Studio, 2026-09-07):
74
+ // applying Y with the same sign as X moved the arrow up when the mouse
75
+ // moved down; inverting the delta, not `LIMIT_Y - y`, kept the rest
76
+ // position in the top-left corner.
77
+ import { Video } from "./geometry.8bs";
78
+ import { keyboard } from "./keyboard.8bs";
79
+ import { Key } from "./keys.8bs";
80
+ import { joystick, Joystick } from "./joystick.8bs";
81
+ import { mouse, Mouse } from "./mouse.8bs";
82
+
83
+ // Was this build fitted with a mouse? Folded while compiling, so the whole
84
+ // pointer half of this file is either present or absent, never tested.
85
+ const HAS_MOUSE: bool = #fact(input.mouse);
86
+
87
+ // The pointer's travel, in mouse counts — a count is about a pixel, and a
88
+ // cell is 8 of them on this machine. The shift is written as a shift
89
+ // rather than a divide by CELL_PIXELS because a 6502 has neither, and the
90
+ // one the compiler can do without a helper routine is this one.
91
+ const CELL_SHIFT: utinyint = 3; // 8 pixels a cell
92
+ const LIMIT_X: usmallint = 319; // Video.COLUMNS * 8 - 1
93
+ const LIMIT_Y: usmallint = 199; // Video.ROWS * 8 - 1
94
+
95
+ // One bit per thing a program can ask about, so a frame's input is one
96
+ // byte and an edge is one AND.
97
+ namespace Edge {
98
+ const LEFT: utinyint = 1;
99
+ const RIGHT: utinyint = 2;
100
+ const UP: utinyint = 4;
101
+ const DOWN: utinyint = 8;
102
+ const CONFIRM: utinyint = 16;
103
+ const CANCEL: utinyint = 32;
104
+ const BUTTON: utinyint = 64;
105
+ }
106
+
107
+ // What is held now, what was held at the last poll, and what began this
108
+ // frame — `now & ~last`, written with the XOR the language has.
109
+ let held: utinyint = 0;
110
+ let before: utinyint = 0;
111
+ let began: utinyint = 0;
112
+
113
+ // Where the pointer is, in cells, and whether one is really there.
114
+ let atCell: usmallint = 0;
115
+ let havePointer: bool = false;
116
+
117
+ export namespace input {
118
+
119
+ // Once, at start-up, before the first poll() — the input equivalent of
120
+ // screen.blank(). On this machine it gives the pointer its ceiling and
121
+ // points the SID at the mouse's port so the first poll() has something
122
+ // settled to read. On a build with no mouse it compiles to nothing at
123
+ // all, and on a machine with no pointer it is an empty function that
124
+ // costs a program one call it can write once and forget.
125
+ function begin(): void {
126
+ if (HAS_MOUSE) {
127
+ mouse.setLimits(LIMIT_X, LIMIT_Y);
128
+ mouse.select(Mouse.PORT_1);
129
+ }
130
+ }
131
+
132
+ // Everything this machine offers, gathered once. Call it once a frame,
133
+ // right after waitFrame(), and never twice.
134
+ //
135
+ // The order is forced by the hardware and is the whole reason this
136
+ // function exists rather than three calls in a program:
137
+ //
138
+ // 1. **Read the pots first.** They answer for the port `select()`
139
+ // pointed the SID at during the *previous* poll — the converter
140
+ // needs about half a millisecond to settle, and a frame is thirty
141
+ // times that, so last frame's select is exactly what makes this
142
+ // frame's reading mean something.
143
+ // 2. **Then the keyboard**, which drives all eight column lines and
144
+ // leaves port A at $FF.
145
+ // 3. **Then the joysticks**, which need port A at $FF to read as
146
+ // joysticks — which is the state the keyboard scan just left.
147
+ // 4. **Then point the SID back at the mouse's port**, for the read
148
+ // at the top of the next frame.
149
+ function poll(): void {
150
+ if (HAS_MOUSE) {
151
+ mouse.poll();
152
+ }
153
+ keyboard.scan();
154
+ joystick.scan();
155
+
156
+ before = held;
157
+ held = 0;
158
+
159
+ // The two cursor keys and SHIFT, which is what "four directions"
160
+ // is on this keyboard.
161
+ let shifted: bool = keyboard.pressed(Key.SHIFT_LEFT) || keyboard.pressed(Key.SHIFT_RIGHT);
162
+ if (keyboard.pressed(Key.CURSOR_RIGHT)) {
163
+ if (shifted) {
164
+ held = held | Edge.LEFT;
165
+ } else {
166
+ held = held | Edge.RIGHT;
167
+ }
168
+ }
169
+ if (keyboard.pressed(Key.CURSOR_DOWN)) {
170
+ if (shifted) {
171
+ held = held | Edge.UP;
172
+ } else {
173
+ held = held | Edge.DOWN;
174
+ }
175
+ }
176
+ if (keyboard.pressed(Key.RETURN)) {
177
+ held = held | Edge.CONFIRM;
178
+ }
179
+ if (keyboard.pressed(Key.STOP)) {
180
+ held = held | Edge.CANCEL;
181
+ }
182
+
183
+ // The stick in port 2, or'd on top: same directions, same confirm.
184
+ let stick: utinyint = joystick.bits(Joystick.PORT_2);
185
+ if ((stick & Joystick.LEFT) != 0) {
186
+ held = held | Edge.LEFT;
187
+ }
188
+ if ((stick & Joystick.RIGHT) != 0) {
189
+ held = held | Edge.RIGHT;
190
+ }
191
+ if ((stick & Joystick.UP) != 0) {
192
+ held = held | Edge.UP;
193
+ }
194
+ if ((stick & Joystick.DOWN) != 0) {
195
+ held = held | Edge.DOWN;
196
+ }
197
+ if ((stick & Joystick.FIRE) != 0) {
198
+ held = held | Edge.CONFIRM;
199
+ }
200
+
201
+ if (HAS_MOUSE) {
202
+ havePointer = mouse.present();
203
+ if (havePointer) {
204
+ // Counts to cells. Y is already screen-space: ./mouse.8bs
205
+ // inverts the 1351's counter so 0 is the top row.
206
+ let column: usmallint = mouse.x() >> CELL_SHIFT;
207
+ let row: usmallint = mouse.y() >> CELL_SHIFT;
208
+ atCell = row * Video.COLUMNS + column;
209
+ }
210
+ // The mouse's buttons ride the port's ordinary joystick lines,
211
+ // so they are already in the snapshot taken above — read from
212
+ // there rather than from the live port, which by now has a
213
+ // keyboard column driven on it.
214
+ if ((joystick.bits(Joystick.PORT_1) & Joystick.FIRE) != 0) {
215
+ held = held | Edge.BUTTON;
216
+ }
217
+ // Point the SID at the mouse's port for the next frame's read.
218
+ // Last, because joystick.scan() puts port A back to $FF.
219
+ mouse.select(Mouse.PORT_1);
220
+ }
221
+
222
+ began = held & (before ^ 0xFF);
223
+ }
224
+
225
+ // The four directions, each true only on the frame its press began.
226
+ function left(): bool {
227
+ return (began & Edge.LEFT) != 0;
228
+ }
229
+
230
+ function right(): bool {
231
+ return (began & Edge.RIGHT) != 0;
232
+ }
233
+
234
+ function up(): bool {
235
+ return (began & Edge.UP) != 0;
236
+ }
237
+
238
+ function down(): bool {
239
+ return (began & Edge.DOWN) != 0;
240
+ }
241
+
242
+ // RETURN, or the stick's fire button.
243
+ function confirm(): bool {
244
+ return (began & Edge.CONFIRM) != 0;
245
+ }
246
+
247
+ // RUN/STOP.
248
+ function cancel(): bool {
249
+ return (began & Edge.CANCEL) != 0;
250
+ }
251
+
252
+ // Is a pointer really there? False on every stock build, where the
253
+ // question folds away entirely; on a build fitted with a mouse, this
254
+ // is ./mouse.8bs's probe answering every frame.
255
+ function pointer(): bool {
256
+ return havePointer;
257
+ }
258
+
259
+ // Where it is, as a flat cell index — the same unit text.print() takes.
260
+ // Meaningless unless pointer() is true.
261
+ function pointerCell(): usmallint {
262
+ return atCell;
263
+ }
264
+
265
+ // The pointer's main button, on the frame its press began.
266
+ function pointerButton(): bool {
267
+ return (began & Edge.BUTTON) != 0;
268
+ }
269
+ }
@@ -0,0 +1,78 @@
1
+ // @8bitscript/c64/joystick — the two control ports, one snapshot a frame.
2
+ //
3
+ // Named by "8bitscript".exports["./joystick"] in this package's package.json:
4
+ //
5
+ // import { joystick, Joystick } from "@8bitscript/c64/joystick";
6
+ //
7
+ // joystick.scan();
8
+ // if (joystick.left(Joystick.PORT_2)) { ... }
9
+ //
10
+ // Hardware-level, C64-only surface, like ./keyboard.8bs: the layer a
11
+ // portable input capability will sit on. A digital joystick is five
12
+ // switches to ground — up, down, left, right, fire — on CIA1: port 2 on
13
+ // port A ($DC00) bits 0-4, port 1 on port B ($DC01) bits 0-4, a 0 bit
14
+ // where a switch is closed. `scan()` reads both once a frame into a
15
+ // snapshot with the bits inverted (1 = pushed) and everything else answers
16
+ // from it.
17
+ //
18
+ // Port A has to be $FF for the read to mean the joystick: its bits are
19
+ // the keyboard's column selects, and a selected column's keys would show
20
+ // up in port B as joystick 1. `keyboard.scan()` leaves it so; `scan()`
21
+ // writes it again anyway, so a program with no keyboard is right too. The
22
+ // order once a frame is: waitFrame(), keyboard.scan(), joystick.scan().
23
+ //
24
+ // Port 2 is where a game reads its player, and where every C64 game asked
25
+ // for the stick: port 1 shares wires with the keyboard's rows, so a stick
26
+ // there "types" (the KERNAL's scan sees it — irrelevant here, interrupts
27
+ // are off) and, the other way round, keys held down read as joystick 1
28
+ // pushes. Port 2 shares the column lines, which a scan only drives for the
29
+ // length of `keyboard.scan()`. Paddles and the 1351 mouse are the SID's
30
+ // two A/D lines (`paddleX`/`paddleY` in ./index.8bs), not this file.
31
+ import { cia1PortA, cia1PortB } from "./index.8bs";
32
+
33
+ // The last scan: index 0 port 1, index 1 port 2; bits as in `Joystick`.
34
+ let state: array<utinyint, 2>;
35
+
36
+ export namespace Joystick {
37
+ const PORT_1: utinyint = 0;
38
+ const PORT_2: utinyint = 1;
39
+ const UP: utinyint = 1;
40
+ const DOWN: utinyint = 2;
41
+ const LEFT: utinyint = 4;
42
+ const RIGHT: utinyint = 8;
43
+ const FIRE: utinyint = 16;
44
+ }
45
+
46
+ export namespace joystick {
47
+ // Read both ports into the snapshot. Once a frame, after keyboard.scan().
48
+ function scan(): void {
49
+ cia1PortA = 0xFF;
50
+ state[0] = (cia1PortB ^ 0xFF) & 0x1F;
51
+ state[1] = (cia1PortA ^ 0xFF) & 0x1F;
52
+ }
53
+
54
+ // The five switches of one port as `Joystick.*` bits, 1 = pushed.
55
+ function bits(port: utinyint): utinyint {
56
+ return state[port];
57
+ }
58
+
59
+ function up(port: utinyint): bool {
60
+ return (state[port] & Joystick.UP) != 0;
61
+ }
62
+
63
+ function down(port: utinyint): bool {
64
+ return (state[port] & Joystick.DOWN) != 0;
65
+ }
66
+
67
+ function left(port: utinyint): bool {
68
+ return (state[port] & Joystick.LEFT) != 0;
69
+ }
70
+
71
+ function right(port: utinyint): bool {
72
+ return (state[port] & Joystick.RIGHT) != 0;
73
+ }
74
+
75
+ function fire(port: utinyint): bool {
76
+ return (state[port] & Joystick.FIRE) != 0;
77
+ }
78
+ }
@@ -0,0 +1,84 @@
1
+ // @8bitscript/c64/keyboard — the C64's keyboard matrix, one snapshot a frame.
2
+ //
3
+ // Named by "8bitscript".exports["./keyboard"] in this package's package.json:
4
+ //
5
+ // import { keyboard } from "@8bitscript/c64/keyboard";
6
+ // import { Key } from "@8bitscript/c64/keys";
7
+ //
8
+ // This is hardware-level, C64-only surface — the layer the portable input
9
+ // capability is built on: `@8bitscript/c64/input` reads this file and
10
+ // @8bitscript/input resolves to that, the way ./text.8bs is the layer
11
+ // under @8bitscript/text. Importing it makes a program C64-specific.
12
+ // It keeps no key buffer and decodes no PETSCII; it answers "is this key
13
+ // down now", which is what a game loop asks.
14
+ //
15
+ // ---- how the matrix works -----------------------------------------------
16
+ //
17
+ // Eight columns by eight rows on CIA1, no diodes. Writing a byte with one
18
+ // 0 bit to port A ($DC00) selects that column; port B ($DC01) then reads
19
+ // the column's eight keys, a 0 bit where a key is down. The KERNAL's own
20
+ // scan (SCNKEY, from its 60 Hz timer IRQ) drives the same two ports, and
21
+ // would land its column select between this scan's write and read — which
22
+ // is one reason the frame runtime starts a C64 program with `sei`
23
+ // (FRAME_SYNC.c64 in packages/backend-6502): from the first waitFrame()
24
+ // on, only this program touches CIA1. `scan()` writes the two direction
25
+ // registers itself (A out, B in — the KERNAL's setting, made sure of), so
26
+ // it does not depend on what ran before.
27
+ //
28
+ // `scan()` reads all eight columns into `state`, once a frame, right after
29
+ // waitFrame() returns; every question after that reads the snapshot.
30
+ // Sixteen port accesses, well under 200 cycles of a 17000-cycle NTSC frame.
31
+ // It leaves port A at $FF — no column selected — which is the state
32
+ // @8bitscript/c64/joystick needs to read the ports as joysticks; call
33
+ // `keyboard.scan()` first and `joystick.scan()` second, both once a frame.
34
+ //
35
+ // The joystick lines are wired in parallel with the matrix: a stick in
36
+ // port 1 pulls rows (a pushed direction reads as a key in every column),
37
+ // a stick in port 2 pulls columns (an extra column selected, its keys
38
+ // showing up under another's). A program that uses the keyboard and a
39
+ // joystick at once lives with that, as every C64 game did — port 2 for
40
+ // the stick, and keys that a stick cannot fake (a game that pauses on
41
+ // RUN/STOP should expect it while the port-1 stick is pushed up: row 7).
42
+ // Ghosting: three keys down can read as four (any three corners of a
43
+ // rectangle in the matrix light the fourth). Two keys are always safe.
44
+ import { cia1PortA, cia1PortB, cia1DirectionA, cia1DirectionB } from "./index.8bs";
45
+
46
+ // The last scan, one byte per column, a 1 bit where a key is down (the
47
+ // port's active-low rows inverted once, here, so nothing else has to
48
+ // think about it). All zero until the first scan.
49
+ let state: array<utinyint, 8>;
50
+
51
+ // The column-select byte for each column, and the row masks — indexed
52
+ // rather than shifted, as `1 << n` with a variable n is a loop on the 6502.
53
+ const COLUMN_SELECT: array<utinyint, 8> = [254, 253, 251, 247, 239, 223, 191, 127];
54
+ const ROW_BIT: array<utinyint, 8> = [1, 2, 4, 8, 16, 32, 64, 128];
55
+
56
+ export namespace keyboard {
57
+ const COLUMNS: utinyint = 8;
58
+ const ROWS: utinyint = 8;
59
+
60
+ // Read all eight columns into the snapshot. Once a frame, right after
61
+ // waitFrame(), before joystick.scan().
62
+ function scan(): void {
63
+ cia1DirectionA = 0xFF;
64
+ cia1DirectionB = 0x00;
65
+ for (let column: utinyint = 0; column < 8; column++) {
66
+ cia1PortA = COLUMN_SELECT[column];
67
+ state[column] = cia1PortB ^ 0xFF;
68
+ }
69
+ cia1PortA = 0xFF;
70
+ }
71
+
72
+ // Is this key down in the last scan? `key` is a `Key.*` value from
73
+ // @8bitscript/c64/keys — `column * 8 + row` in the matrix.
74
+ function pressed(key: utinyint): bool {
75
+ return (state[key >> 3] & ROW_BIT[key & 7]) != 0;
76
+ }
77
+
78
+ // The last scan's eight rows of one column, a 1 bit per key down:
79
+ // for code that wants a whole column at once (an "any key" test over
80
+ // all eight, a ghosting check).
81
+ function column(index: utinyint): utinyint {
82
+ return state[index];
83
+ }
84
+ }
package/src/keys.8bs ADDED
@@ -0,0 +1,103 @@
1
+ // @8bitscript/c64/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/c64/keyboard";
6
+ // import { Key } from "@8bitscript/c64/keys";
7
+ //
8
+ // if (keyboard.pressed(Key.SPACE)) { ... }
9
+ //
10
+ // Each value is `column * 8 + row` in CIA1's matrix (see ./keyboard.8bs):
11
+ // the column is the bit of port A ($DC00) driven low to select it, the row
12
+ // the bit of port B ($DC01) that reads low. Every C64 ever made has this
13
+ // one matrix — the C64C, SX-64, and the rest moved keys on the case, not
14
+ // on the wires — so unlike the PET there is one table and no profile twin.
15
+ // Two keys are not in it: RESTORE is wired to the CPU's NMI line, and
16
+ // SHIFT LOCK is the left SHIFT key's own wire, held down.
17
+ //
18
+ // A key that also exists on the PET has the same name here — letters,
19
+ // `DIGIT_n`, SPACE, RETURN, STOP, HOME, DELETE, the cursor keys, both
20
+ // shifts, the arrows and the shared punctuation — so a program that
21
+ // sticks to those names reads the same on both machines' keyboard layers.
22
+ //
23
+ // Source: the matrix in the Programmer's Reference Guide and every C64
24
+ // reference since, checked here key for key against VICE's own positional
25
+ // keyboard map for the C64, /opt/homebrew/share/vice/C64/gtk3_pos.vkm
26
+ // (each line there is `keysym column row shiftflag`, in the same
27
+ // numbering as this file); packages/compiler/test/c64-keys.test.mjs repeats
28
+ // that check when the file is present.
29
+
30
+ export namespace Key {
31
+ // column 0
32
+ const DELETE: utinyint = 0; // INST/DEL
33
+ const RETURN: utinyint = 1;
34
+ const CURSOR_RIGHT: utinyint = 2; // shifted: cursor left
35
+ const F7: utinyint = 3; // shifted: F8
36
+ const F1: utinyint = 4; // shifted: F2
37
+ const F3: utinyint = 5; // shifted: F4
38
+ const F5: utinyint = 6; // shifted: F6
39
+ const CURSOR_DOWN: utinyint = 7; // shifted: cursor up
40
+ // column 1
41
+ const DIGIT_3: utinyint = 8;
42
+ const W: utinyint = 9;
43
+ const A: utinyint = 10;
44
+ const DIGIT_4: utinyint = 11;
45
+ const Z: utinyint = 12;
46
+ const S: utinyint = 13;
47
+ const E: utinyint = 14;
48
+ const SHIFT_LEFT: utinyint = 15; // also SHIFT LOCK
49
+ // column 2
50
+ const DIGIT_5: utinyint = 16;
51
+ const R: utinyint = 17;
52
+ const D: utinyint = 18;
53
+ const DIGIT_6: utinyint = 19;
54
+ const C: utinyint = 20;
55
+ const F: utinyint = 21;
56
+ const T: utinyint = 22;
57
+ const X: utinyint = 23;
58
+ // column 3
59
+ const DIGIT_7: utinyint = 24;
60
+ const Y: utinyint = 25;
61
+ const G: utinyint = 26;
62
+ const DIGIT_8: utinyint = 27;
63
+ const B: utinyint = 28;
64
+ const H: utinyint = 29;
65
+ const U: utinyint = 30;
66
+ const V: utinyint = 31;
67
+ // column 4
68
+ const DIGIT_9: utinyint = 32;
69
+ const I: utinyint = 33;
70
+ const J: utinyint = 34;
71
+ const DIGIT_0: utinyint = 35;
72
+ const M: utinyint = 36;
73
+ const K: utinyint = 37;
74
+ const O: utinyint = 38;
75
+ const N: utinyint = 39;
76
+ // column 5
77
+ const PLUS: utinyint = 40;
78
+ const P: utinyint = 41;
79
+ const L: utinyint = 42;
80
+ const MINUS: utinyint = 43;
81
+ const PERIOD: utinyint = 44;
82
+ const COLON: utinyint = 45;
83
+ const AT: utinyint = 46;
84
+ const COMMA: utinyint = 47;
85
+ // column 6
86
+ const POUND: utinyint = 48; // £
87
+ const ASTERISK: utinyint = 49;
88
+ const SEMICOLON: utinyint = 50;
89
+ const HOME: utinyint = 51; // CLR/HOME
90
+ const SHIFT_RIGHT: utinyint = 52;
91
+ const EQUALS: utinyint = 53;
92
+ const UP_ARROW: utinyint = 54;
93
+ const SLASH: utinyint = 55;
94
+ // column 7
95
+ const DIGIT_1: utinyint = 56;
96
+ const LEFT_ARROW: utinyint = 57;
97
+ const CONTROL: utinyint = 58;
98
+ const DIGIT_2: utinyint = 59;
99
+ const SPACE: utinyint = 60;
100
+ const COMMODORE: utinyint = 61; // the C= key
101
+ const Q: utinyint = 62;
102
+ const STOP: utinyint = 63; // RUN/STOP
103
+ }