@8bitscript/pet 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 +21 -0
- package/package.json +244 -0
- package/src/blocks.8bs +288 -0
- package/src/geometry.8bs +25 -0
- package/src/geometry.pet.8032.8bs +15 -0
- package/src/index.8bs +48 -0
- package/src/input.8bs +135 -0
- package/src/keyboard.8bs +72 -0
- package/src/keys.8bs +120 -0
- package/src/keys.pet.8032.8bs +100 -0
- package/src/pointer.8bs +39 -0
- package/src/screen.8bs +94 -0
- package/src/text.8bs +165 -0
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,244 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@8bitscript/pet",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Commodore PET 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
|
+
"./keyboard": "./src/keyboard.8bs",
|
|
12
|
+
"./keys": "./src/keys.8bs",
|
|
13
|
+
"./blocks": "./src/blocks.8bs",
|
|
14
|
+
"./input": "./src/input.8bs",
|
|
15
|
+
"./pointer": "./src/pointer.8bs"
|
|
16
|
+
},
|
|
17
|
+
"hardware": {
|
|
18
|
+
"facts": {
|
|
19
|
+
"video.columns": 40,
|
|
20
|
+
"video.rows": 25,
|
|
21
|
+
"video.cellWidth": 8,
|
|
22
|
+
"video.cellHeight": 8,
|
|
23
|
+
"video.palette": 2,
|
|
24
|
+
"video.cellColors": 2,
|
|
25
|
+
"video.colorPerCell": false,
|
|
26
|
+
"video.glyphs": 0,
|
|
27
|
+
"video.blockWidth": 2,
|
|
28
|
+
"video.blockHeight": 2,
|
|
29
|
+
"video.bitmap": false,
|
|
30
|
+
"video.layers": 1,
|
|
31
|
+
"video.scroll": false,
|
|
32
|
+
"video.sprites": 0,
|
|
33
|
+
"video.spritesPerLine": 0,
|
|
34
|
+
"video.spriteWidth": 0,
|
|
35
|
+
"video.spriteHeight": 0,
|
|
36
|
+
"video.spriteColors": 0,
|
|
37
|
+
"video.frameRate": 60,
|
|
38
|
+
"audio.voices": 1,
|
|
39
|
+
"audio.noise": false,
|
|
40
|
+
"audio.envelope": false,
|
|
41
|
+
"audio.filter": false,
|
|
42
|
+
"audio.pcm": false,
|
|
43
|
+
"audio.volume": false,
|
|
44
|
+
"audio.entropy": false,
|
|
45
|
+
"input.keyboard": true,
|
|
46
|
+
"input.joysticks": 0,
|
|
47
|
+
"input.pads": 0,
|
|
48
|
+
"input.mouse": false,
|
|
49
|
+
"input.paddles": false,
|
|
50
|
+
"storage.save": true,
|
|
51
|
+
"memory.ram": 31743,
|
|
52
|
+
"memory.banked": false,
|
|
53
|
+
"memory.bankedKib": 0,
|
|
54
|
+
"storage.kib": 166
|
|
55
|
+
},
|
|
56
|
+
"options": {
|
|
57
|
+
"model": {
|
|
58
|
+
"label": "Model",
|
|
59
|
+
"default": "3032",
|
|
60
|
+
"values": {
|
|
61
|
+
"3008": {
|
|
62
|
+
"label": "3008: 40 columns, 8K",
|
|
63
|
+
"build": {
|
|
64
|
+
"defsym": {
|
|
65
|
+
"__ram_size": 8
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"run": {
|
|
69
|
+
"xpet": [
|
|
70
|
+
"-model",
|
|
71
|
+
"3008"
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
"facts": {
|
|
75
|
+
"video.columns": 40,
|
|
76
|
+
"memory.ram": 7167,
|
|
77
|
+
"video.frameRate": 60
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"3016": {
|
|
81
|
+
"label": "3016: 40 columns, 16K",
|
|
82
|
+
"build": {
|
|
83
|
+
"defsym": {
|
|
84
|
+
"__ram_size": 16
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"run": {
|
|
88
|
+
"xpet": [
|
|
89
|
+
"-model",
|
|
90
|
+
"3016"
|
|
91
|
+
]
|
|
92
|
+
},
|
|
93
|
+
"facts": {
|
|
94
|
+
"video.columns": 40,
|
|
95
|
+
"memory.ram": 15359,
|
|
96
|
+
"video.frameRate": 60
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"3032": {
|
|
100
|
+
"label": "3032: 40 columns, 32K, no CRTC, ~60 Hz",
|
|
101
|
+
"build": {
|
|
102
|
+
"defsym": {
|
|
103
|
+
"__ram_size": 32
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
"run": {
|
|
107
|
+
"xpet": [
|
|
108
|
+
"-model",
|
|
109
|
+
"3032"
|
|
110
|
+
]
|
|
111
|
+
},
|
|
112
|
+
"facts": {
|
|
113
|
+
"video.columns": 40,
|
|
114
|
+
"memory.ram": 31743,
|
|
115
|
+
"video.frameRate": 60
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
"4016": {
|
|
119
|
+
"label": "4016: 40 columns, 16K, CRTC, 50 Hz",
|
|
120
|
+
"build": {
|
|
121
|
+
"defsym": {
|
|
122
|
+
"__ram_size": 16
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
"run": {
|
|
126
|
+
"xpet": [
|
|
127
|
+
"-model",
|
|
128
|
+
"4016"
|
|
129
|
+
]
|
|
130
|
+
},
|
|
131
|
+
"facts": {
|
|
132
|
+
"video.columns": 40,
|
|
133
|
+
"memory.ram": 15359,
|
|
134
|
+
"video.frameRate": 50
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
"4032": {
|
|
138
|
+
"label": "4032: 40 columns, 32K, CRTC, 50 Hz",
|
|
139
|
+
"build": {
|
|
140
|
+
"defsym": {
|
|
141
|
+
"__ram_size": 32
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
"run": {
|
|
145
|
+
"xpet": [
|
|
146
|
+
"-model",
|
|
147
|
+
"4032"
|
|
148
|
+
]
|
|
149
|
+
},
|
|
150
|
+
"facts": {
|
|
151
|
+
"video.columns": 40,
|
|
152
|
+
"memory.ram": 31743,
|
|
153
|
+
"video.frameRate": 50
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
"8032": {
|
|
157
|
+
"label": "8032: 80 columns, 32K, business keyboard, 50 Hz",
|
|
158
|
+
"build": {
|
|
159
|
+
"defsym": {
|
|
160
|
+
"__ram_size": 32
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
"run": {
|
|
164
|
+
"xpet": [
|
|
165
|
+
"-model",
|
|
166
|
+
"8032"
|
|
167
|
+
]
|
|
168
|
+
},
|
|
169
|
+
"facts": {
|
|
170
|
+
"video.columns": 80,
|
|
171
|
+
"memory.ram": 31743,
|
|
172
|
+
"video.frameRate": 50
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
"drive": {
|
|
178
|
+
"label": "Disk drive",
|
|
179
|
+
"default": "4040",
|
|
180
|
+
"values": {
|
|
181
|
+
"4040": {
|
|
182
|
+
"label": "2040/4040 (170K disk)",
|
|
183
|
+
"facts": {
|
|
184
|
+
"storage.save": true,
|
|
185
|
+
"storage.kib": 166
|
|
186
|
+
},
|
|
187
|
+
"tag": null
|
|
188
|
+
},
|
|
189
|
+
"8050": {
|
|
190
|
+
"label": "8050 (520K disk)",
|
|
191
|
+
"facts": {
|
|
192
|
+
"storage.save": true,
|
|
193
|
+
"storage.kib": 508
|
|
194
|
+
},
|
|
195
|
+
"tag": null
|
|
196
|
+
},
|
|
197
|
+
"8250": {
|
|
198
|
+
"label": "8250 (1M disk)",
|
|
199
|
+
"facts": {
|
|
200
|
+
"storage.save": true,
|
|
201
|
+
"storage.kib": 1025
|
|
202
|
+
},
|
|
203
|
+
"tag": null
|
|
204
|
+
},
|
|
205
|
+
"none": {
|
|
206
|
+
"label": "No drive (tape or nothing)",
|
|
207
|
+
"facts": {
|
|
208
|
+
"storage.save": false,
|
|
209
|
+
"storage.kib": 0
|
|
210
|
+
},
|
|
211
|
+
"tag": null
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
"presets": {
|
|
217
|
+
"3008": {
|
|
218
|
+
"model": "3008"
|
|
219
|
+
},
|
|
220
|
+
"3016": {
|
|
221
|
+
"model": "3016"
|
|
222
|
+
},
|
|
223
|
+
"3032": {
|
|
224
|
+
"model": "3032"
|
|
225
|
+
},
|
|
226
|
+
"4016": {
|
|
227
|
+
"model": "4016"
|
|
228
|
+
},
|
|
229
|
+
"4032": {
|
|
230
|
+
"model": "4032"
|
|
231
|
+
},
|
|
232
|
+
"8032": {
|
|
233
|
+
"model": "8032"
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
"files": [
|
|
239
|
+
"src"
|
|
240
|
+
],
|
|
241
|
+
"publishConfig": {
|
|
242
|
+
"access": "public"
|
|
243
|
+
}
|
|
244
|
+
}
|
package/src/blocks.8bs
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// @8bitscript/pet/blocks — quadrant-block pseudo-pixels, and the big digit
|
|
2
|
+
// tiles built from them.
|
|
3
|
+
//
|
|
4
|
+
// The PET has no bitmap and no redefinable glyphs (`video.glyphs` is 0),
|
|
5
|
+
// but its ROM has sixteen "quadrant block" characters — the fact sheet's
|
|
6
|
+
// `video.blockWidth`/`blockHeight` of 2: eight are ROM glyphs at screen
|
|
7
|
+
// codes 96-127 (blank, a half, a quadrant alone, or two diagonal
|
|
8
|
+
// quadrants), and the other eight are their hardware-inverted complements
|
|
9
|
+
// at 224-255 — bit 7 flips every pixel, so a pattern this ROM does not draw
|
|
10
|
+
// directly is its opposite, reversed. Together the sixteen cover every
|
|
11
|
+
// possible 2x2 on/off pattern one cell can show, turning the 40x25 (or
|
|
12
|
+
// 80x25) text grid into an 80x50 (or 160x50) grid of pseudo-pixels — see
|
|
13
|
+
// `docs/systems.md`'s "Blocks" canvas rung. Verified against the actual
|
|
14
|
+
// ROM: `characters-2.901447-10.bin`'s screen codes 96-127 are byte-for-byte
|
|
15
|
+
// identical whether the PCR selects the graphics or text set (both are the
|
|
16
|
+
// shared "$60-$7F graphics" range packages/pet/AGENTS.md's character-set
|
|
17
|
+
// table describes), so unlike `text.8bs`, nothing here ever needs to touch
|
|
18
|
+
// `viaPeripheralControl` — a block or a digit tile draws the same regardless
|
|
19
|
+
// of which set is selected, and can sit on screen next to either kind of text.
|
|
20
|
+
//
|
|
21
|
+
// `blocks.quad(topLeft, topRight, bottomLeft, bottomRight)` is the whole
|
|
22
|
+
// primitive: four bits in, one screen code out — enough to draw any
|
|
23
|
+
// pattern a cell can show. `digits` is one thing built from it: a 2x3-cell
|
|
24
|
+
// (4x6 pseudo-pixel) tile per digit 0-9, laid out as `const` data so the
|
|
25
|
+
// font costs only the bytes it draws — `draw()` for one digit, `print()`
|
|
26
|
+
// for a zero-padded field the way `text.printNumber` lays one out, and
|
|
27
|
+
// `printCentered()` for a number that grows and shrinks (a game tile: "2",
|
|
28
|
+
// then "16", then "2048") centered in a box instead of padded to a fixed
|
|
29
|
+
// width. Every one of them takes `invert`: drawn over a solid reverse-video
|
|
30
|
+
// tile, an inverted digit reads as dark ink cut into the tile's colour
|
|
31
|
+
// rather than a fresh glyph punching a black rectangle through it.
|
|
32
|
+
//
|
|
33
|
+
// 2 cells is the font's size, not a preference: four side-by-side tiles on
|
|
34
|
+
// a 40-column screen leave 9 cells per tile at most (4*9 + 3 gaps = 39), and
|
|
35
|
+
// a 3-wide font's own widest number ("2048", 4 digits) alone needs 12 — a
|
|
36
|
+
// 2-wide font needs 8, which fits with a column to spare. It also makes
|
|
37
|
+
// every digit count center exactly: leftover space is (cellWidth − digits×2),
|
|
38
|
+
// always the same parity as cellWidth, so an even tile width splits evenly
|
|
39
|
+
// on both sides for 1, 2, 3 or 4 digits alike — a scoreboard-size font (3
|
|
40
|
+
// wide) cannot do that for every digit count no matter which tile width is
|
|
41
|
+
// picked, the parity always favours some counts over others.
|
|
42
|
+
//
|
|
43
|
+
// The pixel art above each row of the table is the font's real source —
|
|
44
|
+
// redraw it there, then re-derive that row's two codes from `quad()`'s
|
|
45
|
+
// table below (or a quick script over it) to change how a digit looks.
|
|
46
|
+
import { Video } from "./geometry.8bs";
|
|
47
|
+
|
|
48
|
+
const COLUMNS: utinyint = Video.COLUMNS; // cells per row, so cell = y * COLUMNS + x
|
|
49
|
+
|
|
50
|
+
// Screen code for each of the sixteen 2x2 patterns, indexed by
|
|
51
|
+
// (topLeft<<3 | topRight<<2 | bottomLeft<<1 | bottomRight). The first eight
|
|
52
|
+
// (indexes 0-8, one bit set at most plus the all-off and the TL+BR
|
|
53
|
+
// diagonal) are ROM glyphs; the rest are those same bits inverted, which is
|
|
54
|
+
// exactly what reverse video already does in hardware, so the table just
|
|
55
|
+
// names the +128 codes directly rather than setting a reverse flag.
|
|
56
|
+
const QUAD: array<utinyint, 16> = [
|
|
57
|
+
32, 108, 123, 98, // 0000 0001 0010 0011
|
|
58
|
+
124, 225, 255, 254, // 0100 0101 0110 0111
|
|
59
|
+
126, 127, 97, 252, // 1000 1001 1010 1011
|
|
60
|
+
226, 251, 236, 160, // 1100 1101 1110 1111
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
function patternIndex(topLeft: bool, topRight: bool, bottomLeft: bool, bottomRight: bool): utinyint {
|
|
64
|
+
let index: utinyint = 0;
|
|
65
|
+
if (topLeft) {
|
|
66
|
+
index = index + 8;
|
|
67
|
+
}
|
|
68
|
+
if (topRight) {
|
|
69
|
+
index = index + 4;
|
|
70
|
+
}
|
|
71
|
+
if (bottomLeft) {
|
|
72
|
+
index = index + 2;
|
|
73
|
+
}
|
|
74
|
+
if (bottomRight) {
|
|
75
|
+
index = index + 1;
|
|
76
|
+
}
|
|
77
|
+
return index;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function place(cell: usmallint, code: utinyint): void {
|
|
81
|
+
memory.write(0x8000 + cell, code);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export namespace blocks {
|
|
85
|
+
// The screen code that draws one cell's four pseudo-pixels: `true` lights
|
|
86
|
+
// a quadrant, `false` leaves it off. Anything a 2x2 block can show is one
|
|
87
|
+
// call away — a checkerboard, a diagonal, a custom glyph of your own.
|
|
88
|
+
function quad(topLeft: bool, topRight: bool, bottomLeft: bool, bottomRight: bool): utinyint {
|
|
89
|
+
return QUAD[patternIndex(topLeft, topRight, bottomLeft, bottomRight)];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Writes one cell's worth of pseudo-pixels at `cell`, so a caller who
|
|
93
|
+
// does not want to hand-roll `memory.write` can still work in blocks.
|
|
94
|
+
function put(cell: usmallint, topLeft: bool, topRight: bool, bottomLeft: bool, bottomRight: bool): void {
|
|
95
|
+
place(cell, QUAD[patternIndex(topLeft, topRight, bottomLeft, bottomRight)]);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---- digits ---------------------------------------------------------------
|
|
100
|
+
//
|
|
101
|
+
// Each digit is 2 cells wide, 3 tall — 4x6 pseudo-pixels, a classic 3x5
|
|
102
|
+
// dot-matrix numeral elongated by one row (its own row 1 repeated) to fill
|
|
103
|
+
// all 6 rows with real strokes, plus one blank pixel-column of margin (the
|
|
104
|
+
// right column) so adjacent digits do not touch. There is no blank *row*:
|
|
105
|
+
// earlier drafts padded the 5-row numeral with one, and that row landing
|
|
106
|
+
// only at the bottom made a tile's margin above a digit come out twice its
|
|
107
|
+
// margin below (see @8bitscript/pet's AGENTS.md and this file's own
|
|
108
|
+
// digits.HEIGHT comment — a whole spare cell can only sit on one side of a
|
|
109
|
+
// digit, so any asymmetry inside the font itself compounds whichever side
|
|
110
|
+
// that cell lands on). A digit with no internal blank row leaves margin
|
|
111
|
+
// entirely to whoever places it, which is what makes a symmetric split
|
|
112
|
+
// possible at all. Drawn left to right, top to bottom, 6 codes per digit,
|
|
113
|
+
// one array line per tile row of 2 cells. `#` below is a lit pseudo-pixel,
|
|
114
|
+
// `.` unlit; every 2x2 block of picture cells is one QUAD code in the line
|
|
115
|
+
// beneath it, in the same (topLeft, topRight, bottomLeft, bottomRight)
|
|
116
|
+
// order `patternIndex` reads.
|
|
117
|
+
//
|
|
118
|
+
// 0 1 2 3 4 5 6 7 8 9
|
|
119
|
+
// ###. .#.. ###. ###. #.#. ###. ###. ###. ###. ###.
|
|
120
|
+
// #.#. ##.. ..#. ..#. #.#. #... #... ..#. #.#. #.#.
|
|
121
|
+
// #.#. ##.. ..#. ..#. #.#. #... #... ..#. #.#. #.#.
|
|
122
|
+
// #.#. .#.. ###. ###. ###. ###. ###. ..#. ###. ###.
|
|
123
|
+
// #.#. .#.. #... ..#. ..#. ..#. #.#. ..#. #.#. ..#.
|
|
124
|
+
// ###. ###. ###. ###. ..#. ###. ###. ..#. ###. ###.
|
|
125
|
+
//
|
|
126
|
+
const DIGIT_TILES: array<utinyint, 60> = [
|
|
127
|
+
// 0
|
|
128
|
+
236, 97,
|
|
129
|
+
97, 97,
|
|
130
|
+
252, 97,
|
|
131
|
+
// 1
|
|
132
|
+
254, 32,
|
|
133
|
+
251, 32,
|
|
134
|
+
254, 123,
|
|
135
|
+
// 2
|
|
136
|
+
226, 97,
|
|
137
|
+
98, 97,
|
|
138
|
+
252, 123,
|
|
139
|
+
// 3
|
|
140
|
+
226, 97,
|
|
141
|
+
98, 97,
|
|
142
|
+
98, 97,
|
|
143
|
+
// 4
|
|
144
|
+
97, 97,
|
|
145
|
+
252, 97,
|
|
146
|
+
32, 97,
|
|
147
|
+
// 5
|
|
148
|
+
236, 126,
|
|
149
|
+
252, 123,
|
|
150
|
+
98, 97,
|
|
151
|
+
// 6
|
|
152
|
+
236, 126,
|
|
153
|
+
252, 123,
|
|
154
|
+
252, 97,
|
|
155
|
+
// 7
|
|
156
|
+
226, 97,
|
|
157
|
+
32, 97,
|
|
158
|
+
32, 97,
|
|
159
|
+
// 8
|
|
160
|
+
236, 97,
|
|
161
|
+
252, 97,
|
|
162
|
+
252, 97,
|
|
163
|
+
// 9
|
|
164
|
+
236, 97,
|
|
165
|
+
252, 97,
|
|
166
|
+
98, 97,
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
// Cell offset into DIGIT_TILES per digit (value * 6), read off instead of
|
|
170
|
+
// multiplied — the 6502 has no multiply instruction, and every value here
|
|
171
|
+
// is small and fixed.
|
|
172
|
+
const DIGIT_BASE: array<usmallint, 10> = [0, 6, 12, 18, 24, 30, 36, 42, 48, 54];
|
|
173
|
+
|
|
174
|
+
// Places above a field are still taken off, so a field narrower than its
|
|
175
|
+
// number shows the low digits — the same rule text.printNumber follows.
|
|
176
|
+
const DIGIT_PLACES: array<usmallint, 5> = [10000, 1000, 100, 10, 1];
|
|
177
|
+
|
|
178
|
+
// How many cells `count` digit tiles take, read off instead of multiplied
|
|
179
|
+
// by WIDTH (2) — the same reason DIGIT_BASE exists. Index 0 is unused (a
|
|
180
|
+
// number always has at least one digit) and kept so `count` indexes it
|
|
181
|
+
// directly.
|
|
182
|
+
const DIGIT_TILE_WIDTHS: array<utinyint, 6> = [0, 2, 4, 6, 8, 10];
|
|
183
|
+
|
|
184
|
+
export namespace digits {
|
|
185
|
+
const WIDTH: utinyint = 2; // cells per digit tile
|
|
186
|
+
const HEIGHT: utinyint = 3; // cells per digit tile
|
|
187
|
+
|
|
188
|
+
// How many decimal digits `value` has with no leading zeros — "16" is
|
|
189
|
+
// 2, "2048" is 4 — the width printCentered() lays out around, as
|
|
190
|
+
// opposed to print()'s fixed, zero-padded width.
|
|
191
|
+
function count(value: usmallint): utinyint {
|
|
192
|
+
let digitCount: utinyint = 1;
|
|
193
|
+
if (value >= 10) {
|
|
194
|
+
digitCount = 2;
|
|
195
|
+
}
|
|
196
|
+
if (value >= 100) {
|
|
197
|
+
digitCount = 3;
|
|
198
|
+
}
|
|
199
|
+
if (value >= 1000) {
|
|
200
|
+
digitCount = 4;
|
|
201
|
+
}
|
|
202
|
+
if (value >= 10000) {
|
|
203
|
+
digitCount = 5;
|
|
204
|
+
}
|
|
205
|
+
return digitCount;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Draws one digit (0-9) as a WIDTH x HEIGHT tile whose top-left cell is
|
|
209
|
+
// `cell`. `base` and `rowCell` walk forward by WIDTH and COLUMNS each
|
|
210
|
+
// row instead of multiplying row by either, for the same reason
|
|
211
|
+
// DIGIT_BASE exists.
|
|
212
|
+
//
|
|
213
|
+
// `invert` swaps every cell's on and off quadrants (screen code bit 7,
|
|
214
|
+
// the same bit reverse video always was) — the digit's ink and its gaps
|
|
215
|
+
// trade places. Drawn over a solid reverse-video tile (a game board
|
|
216
|
+
// square, say), an inverted digit reads as dark ink cut into the tile's
|
|
217
|
+
// colour instead of a fresh glyph punching a black rectangle through
|
|
218
|
+
// it. QUAD's sixteen codes were chosen to be closed under this exact
|
|
219
|
+
// flip (every pattern's complement is also in the table), so inverting
|
|
220
|
+
// costs one add-or-subtract of 128 per cell, no second font.
|
|
221
|
+
function draw(cell: usmallint, value: utinyint, invert: bool = false): void {
|
|
222
|
+
let base: usmallint = DIGIT_BASE[value];
|
|
223
|
+
let rowCell: usmallint = cell;
|
|
224
|
+
let row: utinyint = 0;
|
|
225
|
+
while (row < digits.HEIGHT) {
|
|
226
|
+
let col: utinyint = 0;
|
|
227
|
+
while (col < digits.WIDTH) {
|
|
228
|
+
let code: utinyint = DIGIT_TILES[base + col];
|
|
229
|
+
if (invert) {
|
|
230
|
+
if (code < 128) {
|
|
231
|
+
code = code + 128;
|
|
232
|
+
} else {
|
|
233
|
+
code = code - 128;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
place(rowCell + col, code);
|
|
237
|
+
col++;
|
|
238
|
+
}
|
|
239
|
+
base = base + digits.WIDTH;
|
|
240
|
+
rowCell = rowCell + COLUMNS;
|
|
241
|
+
row++;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Writes `value` as exactly `width` block-digit tiles, zero-padded and
|
|
246
|
+
// right-aligned, laid out left to right starting at `cell` — a scoreboard
|
|
247
|
+
// line built from digits instead of text.printNumber's characters. `at`
|
|
248
|
+
// advances by WIDTH cells per digit instead of multiplying width by
|
|
249
|
+
// WIDTH, the same reason draw() steps rather than multiplies.
|
|
250
|
+
function print(cell: usmallint, value: usmallint, width: utinyint, invert: bool = false): void {
|
|
251
|
+
let k: utinyint = width;
|
|
252
|
+
if (k < DIGIT_PLACES.length) {
|
|
253
|
+
k = DIGIT_PLACES.length;
|
|
254
|
+
}
|
|
255
|
+
let at: usmallint = cell;
|
|
256
|
+
while (k > 0) {
|
|
257
|
+
let digit: utinyint = 0;
|
|
258
|
+
if (k <= DIGIT_PLACES.length) {
|
|
259
|
+
while (value >= DIGIT_PLACES[DIGIT_PLACES.length - k]) {
|
|
260
|
+
value = value - DIGIT_PLACES[DIGIT_PLACES.length - k];
|
|
261
|
+
digit++;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (k <= width) {
|
|
265
|
+
digits.draw(at, digit, invert);
|
|
266
|
+
at = at + digits.WIDTH;
|
|
267
|
+
}
|
|
268
|
+
k--;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Writes `value` with no leading zeros, centered in a `widthCells`-wide
|
|
273
|
+
// box whose left edge is `left` — a number that grows from one digit to
|
|
274
|
+
// several (a 2048 tile: "2", then "16", then "2048") without print()'s
|
|
275
|
+
// fixed field or its zero-padding. Odd leftover space rounds to the
|
|
276
|
+
// left, the same way a centered thing usually does when it cannot split
|
|
277
|
+
// evenly. If the digits are wider than the box, they simply start at
|
|
278
|
+
// `left` and run past its right edge rather than negative into `pad`.
|
|
279
|
+
function printCentered(left: usmallint, widthCells: utinyint, value: usmallint, invert: bool = false): void {
|
|
280
|
+
let digitCount: utinyint = digits.count(value);
|
|
281
|
+
let used: utinyint = DIGIT_TILE_WIDTHS[digitCount];
|
|
282
|
+
let pad: utinyint = 0;
|
|
283
|
+
if (widthCells > used) {
|
|
284
|
+
pad = (widthCells - used) >> 1;
|
|
285
|
+
}
|
|
286
|
+
digits.print(left + pad, value, digitCount, invert);
|
|
287
|
+
}
|
|
288
|
+
}
|
package/src/geometry.8bs
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// @8bitscript/pet — the screen's geometry, for the 40-column PETs.
|
|
2
|
+
//
|
|
3
|
+
// The one thing about the PET's display that changes between models is how
|
|
4
|
+
// wide it is: 40 columns on the 2001/3xxx/4xxx machines, 80 on the 8xxx
|
|
5
|
+
// business machines. Everything else this package does with the screen —
|
|
6
|
+
// screen RAM at $8000, one byte per cell, 25 rows, no colour — is the same
|
|
7
|
+
// on every PET ever made, so it is written once, in screen.8bs and text.8bs,
|
|
8
|
+
// against these three numbers. This file is the 40-column version, and
|
|
9
|
+
// what every PET profile reads except the 8032, which has its own
|
|
10
|
+
// `geometry.pet.8032.8bs` beside it: a build for that profile resolves the
|
|
11
|
+
// import of `./geometry.8bs` to that file instead (docs/packages.md,
|
|
12
|
+
// "System-specific files"), and text.COLUMNS, text.CELL_COUNT, and
|
|
13
|
+
// screen.blank()'s extent follow, with no second copy of the surface.
|
|
14
|
+
//
|
|
15
|
+
// Why a file, not a runtime probe: `cell = y * text.COLUMNS + x` is compile-
|
|
16
|
+
// time arithmetic in every program that uses it, and a 40-column constant on
|
|
17
|
+
// an 80-column screen does not draw narrow — row 1 lands in the middle of row
|
|
18
|
+
// 0. The width is a property of the build, chosen once, like Studio's tier.
|
|
19
|
+
// See packages/pet/AGENTS.md ("Models are profiles").
|
|
20
|
+
|
|
21
|
+
export namespace Video {
|
|
22
|
+
const COLUMNS: utinyint = 40;
|
|
23
|
+
const ROWS: utinyint = 25;
|
|
24
|
+
const CELL_COUNT: usmallint = 1000; // COLUMNS * ROWS: exactly the 1K of screen RAM at $8000
|
|
25
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// @8bitscript/pet — the screen's geometry, for the 80-column 8032.
|
|
2
|
+
//
|
|
3
|
+
// The 8032 profile's version of geometry.8bs (see that file for why the
|
|
4
|
+
// geometry is a file of its own): a build with `--profile 8032` resolves
|
|
5
|
+
// every import of `./geometry.8bs` in this package to this one. The 8032's
|
|
6
|
+
// CRTC reads two bytes of screen RAM per character clock, so the screen is
|
|
7
|
+
// 80 x 25 = 2000 cells, still at $8000 ($8000-$87CF); nothing else about
|
|
8
|
+
// the display differs from the 40-column machines. Verified on screen under
|
|
9
|
+
// `xpet -model 8032` — see packages/pet/AGENTS.md.
|
|
10
|
+
|
|
11
|
+
export namespace Video {
|
|
12
|
+
const COLUMNS: utinyint = 80;
|
|
13
|
+
const ROWS: utinyint = 25;
|
|
14
|
+
const CELL_COUNT: usmallint = 2000; // COLUMNS * ROWS: $8000-$87CF
|
|
15
|
+
}
|
package/src/index.8bs
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// @8bitscript/pet — Commodore PET 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 { viaPeripheralControl } from "@8bitscript/pet";
|
|
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.
|
|
14
|
+
//
|
|
15
|
+
// The PET is a monochrome machine — green or white phosphor, whatever the
|
|
16
|
+
// monitor is, with no colour hardware at all — so there is no colour
|
|
17
|
+
// register here to export; see screen.8bs for what that means for the
|
|
18
|
+
// portable surface. What it does have is a character set to select.
|
|
19
|
+
|
|
20
|
+
// $E84C, the VIA's peripheral control register (VIA at $E840 in
|
|
21
|
+
// llvm-mos-sdk's pet.h, `pcr` at offset 12 in its _6522.h). Bit 1 chooses
|
|
22
|
+
// the character set: $0C for graphics (upper case), $0E for text (lower
|
|
23
|
+
// case) — the POKE 59468,12 / POKE 59468,14 every PET manual gives.
|
|
24
|
+
@address(0xE84C)
|
|
25
|
+
export let viaPeripheralControl: volatile<u8>;
|
|
26
|
+
|
|
27
|
+
// PIA1 at $E810 (pet.h's `pia1`, a 6520): the keyboard is here, not on the
|
|
28
|
+
// VIA. PA0-3 select one of the ten matrix rows through a 4-to-10 decoder
|
|
29
|
+
// (write 0-9; 10-15 select nothing), PA4-7 are inputs (cassette switches,
|
|
30
|
+
// IEEE EOI in, the diagnostic sense line) that the ROM's data-direction
|
|
31
|
+
// setup leaves as inputs, so a bare row number is what to store. PB0-7 read
|
|
32
|
+
// the eight columns of the selected row, ACTIVE LOW: a pressed key is a 0
|
|
33
|
+
// bit. ./keyboard.8bs turns the pair into a per-frame snapshot.
|
|
34
|
+
//
|
|
35
|
+
// One thing to know before reading $E812 anywhere else: on a 6520, reading
|
|
36
|
+
// the port B data register also clears the CB1 interrupt flag in CRB
|
|
37
|
+
// ($E813 bit 7), and that flag is the vertical-retrace edge the frame
|
|
38
|
+
// runtime waits for (FRAME_SYNC.pet in packages/backend-6502 polls it and
|
|
39
|
+
// acknowledges it by reading this register). A read here between a retrace
|
|
40
|
+
// and the next waitFrame() eats that frame. Scan once, right after
|
|
41
|
+
// waitFrame() returns. $E811 (CRA / DDRA) and $E813 are not exported: CA2
|
|
42
|
+
// is IEEE EOI out on every board after the first, and the retrace flag is
|
|
43
|
+
// the runtime's.
|
|
44
|
+
@address(0xE810)
|
|
45
|
+
export let pia1PortA: volatile<u8>;
|
|
46
|
+
|
|
47
|
+
@address(0xE812)
|
|
48
|
+
export let pia1PortB: volatile<u8>;
|