@moonbase-machines/player 0.11.0 → 0.13.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/README.md +10 -4
- package/audio.js +11 -3
- package/data.js +16 -1
- package/dist/moonbase-player.js +20 -14
- package/dist/moonbase-player.js.map +4 -4
- package/engines/original.js +35 -24
- package/engines/scenes.js +5 -1
- package/keys.js +18 -0
- package/package.json +6 -6
- package/play.js +11 -2
- package/remake/README.md +138 -0
- package/test/play.test.js +20 -1
package/engines/original.js
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
// { choice ('commodore' or 'moonbase-petscii', @moonbase-machines/c64/start.js), load: async name => a ROM's bytes };
|
|
10
10
|
// and `studies`, files of its own for a Commodore 64 game's disk, a name to its bytes in base64, in the drive, not kept.
|
|
11
11
|
import { Speaker } from '../audio.js';
|
|
12
|
+
import { typing as typingOf } from '../keys.js';
|
|
13
|
+
import { pace } from '@moonbase-machines/mos6502/pace.js';
|
|
12
14
|
|
|
13
15
|
// the setting the Apple's engine acts on (play.js `setting`): its pictures drawn as vectors over the screen
|
|
14
16
|
const VECTORS = { name: 'vectors', type: 'boolean', default: false, owner: 'person', words: 'Draw the pictures as vectors' };
|
|
@@ -68,8 +70,10 @@ const rgba = new Uint8ClampedArray(560 * 384 * 4);
|
|
|
68
70
|
const image = new ImageData(rgba, 560, 384);
|
|
69
71
|
const cols = new Uint8Array(140 * 192);
|
|
70
72
|
let mode = '';
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
// the Apple's frames, 60 a second against the clock (@moonbase-machines/mos6502/pace.js), the screen painted each
|
|
74
|
+
// animation frame
|
|
75
|
+
const frame = () => { if (!m.halted) m.runCycles(17030); }; // a frame's worth of the Apple's clock
|
|
76
|
+
function after() {
|
|
73
77
|
const now = `text ${m.sw.text} page2 ${m.sw.page2} hires ${m.sw.hires} dhires ${m.sw.dhires}`;
|
|
74
78
|
if (now !== mode) { console.log('screen:', now, 'at pc $' + m.cpu.pc.toString(16), 'cycles', m.cpu.cycles); mode = now; }
|
|
75
79
|
speaker.feed(sound ? m.clicks : [], m.cpu.cycles); m.clicks.length = 0;
|
|
@@ -79,13 +83,12 @@ function frame() {
|
|
|
79
83
|
ctx.putImageData(image, 0, 0);
|
|
80
84
|
layer.style.visibility = vectors.value && !text ? 'visible' : 'hidden';
|
|
81
85
|
status.textContent = m.halted ? 'The game has stopped: ' + m.halted + '.' : (m.idle > 2000 ? 'Waiting for a key.' : 'Running.');
|
|
82
|
-
requestAnimationFrame(frame);
|
|
83
86
|
}
|
|
84
87
|
canvas.addEventListener('pointerdown', () => canvas.focus());
|
|
85
88
|
// (the keys, the Apple's codes, given by the player: play.js, keys.js)
|
|
86
89
|
let sound = true;
|
|
87
90
|
return {
|
|
88
|
-
run() { canvas.focus(); frame
|
|
91
|
+
run() { canvas.focus(); pace(frame, 60, { after }); },
|
|
89
92
|
press(k) { console.log('key', k, 'at pc $' + m.cpu.pc.toString(16)); m.press(k); },
|
|
90
93
|
start() { speaker.start(); },
|
|
91
94
|
sound(on) { sound = on; },
|
|
@@ -96,7 +99,9 @@ return {
|
|
|
96
99
|
// A C64 game: on moonbase's own ROMs (@moonbase-machines/c64/rom.js), as the site has it everywhere, or on
|
|
97
100
|
// Commodore's where the page gives them (`roms`) and asks for them; the machine autostarted
|
|
98
101
|
// with LOAD"name",8,1, and RUN for a BASIC program, typed into the KERNAL's buffer, the frame on a canvas of
|
|
99
|
-
// 384 by 272 doubled, the keys as the matrix
|
|
102
|
+
// 384 by 272 doubled, the keys as the matrix, the arrows its cursor keys, and a gamepad the joystick. A demonstration
|
|
103
|
+
// (play.js `demonstrate`, the game's data's steps) is typed into the same buffer the same way, after the start, the game
|
|
104
|
+
// taking each key when it asks for one; the person's first key or push of the stick drops what is left of it.
|
|
100
105
|
async function c64(g, { canvas, layer, speaker }) {
|
|
101
106
|
const { game, saves, roms: makers, studies, status, printer } = g;
|
|
102
107
|
const [{ C64 }, { keyboard, painter }, { SID }, { c64Roms, typed: startTyped }, { add }] = await Promise.all([
|
|
@@ -115,36 +120,42 @@ async function c64(g, { canvas, layer, speaker }) {
|
|
|
115
120
|
for (const [file, data] of Object.entries(studies ?? {})) m.drive.saved.set(file, raw(data));
|
|
116
121
|
layer.style.display = 'none';
|
|
117
122
|
const draw = painter(canvas);
|
|
118
|
-
let typing = null, typed = 0, sound = true;
|
|
123
|
+
let typing = null, typed = 0, sound = true, demo = null;
|
|
124
|
+
// (the person at the keys or the stick: what is left of a demonstration is theirs to stop)
|
|
125
|
+
const theirs = () => { demo = null; };
|
|
119
126
|
// the keys as the 64's matrix (@moonbase-machines/c64/browser.js), given them by the player (play.js)
|
|
120
127
|
const listeners = { keydown: [], keyup: [] };
|
|
121
128
|
const keys = keyboard(m, { log: true, listen: (type, f) => listeners[type].push(f) });
|
|
122
129
|
// once BASIC is ready, the start typed in; the KERNAL's buffer takes ten characters at a time.
|
|
123
|
-
// The C64 runs 50 frames a second
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (speaker.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
speaker.node.port.postMessage(wave, [wave.buffer]);
|
|
134
|
-
}
|
|
135
|
-
m.sidWrites.length = 0;
|
|
130
|
+
// The C64 runs 50 frames a second against the clock (@moonbase-machines/mos6502/pace.js: a hitch caught up, a pause
|
|
131
|
+
// dropped), the SID's frame rendered with each, and the screen painted each animation frame
|
|
132
|
+
let frames = 0, sid = null;
|
|
133
|
+
const frame = n => {
|
|
134
|
+
frames = n; m.runFrame(); keys.frame();
|
|
135
|
+
if (speaker.node && sound) {
|
|
136
|
+
if (!sid) sid = new SID(speaker.rate);
|
|
137
|
+
const n = Math.round(19656 / 985248 * speaker.rate), first = m.cpu.cycles - 19656;
|
|
138
|
+
const wave = sid.render(n, m.sidWrites.map(([c, r, v]) => [Math.floor((c - first) / 19656 * n), r, v]));
|
|
139
|
+
speaker.node.port.postMessage(wave, [wave.buffer]);
|
|
136
140
|
}
|
|
137
|
-
|
|
141
|
+
m.sidWrites.length = 0;
|
|
142
|
+
};
|
|
143
|
+
function after() {
|
|
144
|
+
// (LOAD, and RUN for a BASIC program: @moonbase-machines/c64/start.js; then a demonstration, if one is asked for)
|
|
138
145
|
if (frames >= 120 && typing === null && !typed) { typing = startTyped(m, game); typed = 1; }
|
|
139
|
-
if (
|
|
146
|
+
if (demo !== null && m.joystick) theirs();
|
|
147
|
+
if (!m.ram[0xc6]) {
|
|
148
|
+
if (typing !== null) { typing = m.type(typing); if (!typing.length) typing = null; }
|
|
149
|
+
else if (demo !== null && typed) { demo = m.type(demo); if (!demo.length) demo = null; }
|
|
150
|
+
}
|
|
140
151
|
draw(m.vic.frame);
|
|
141
152
|
status.textContent = m.halted ? 'The game has stopped: ' + m.halted + '.' : 'Running.';
|
|
142
|
-
requestAnimationFrame(frame);
|
|
143
153
|
}
|
|
144
154
|
return {
|
|
145
|
-
run() { canvas.focus(); frame
|
|
155
|
+
run() { canvas.focus(); pace(frame, 50, { after }); },
|
|
146
156
|
press(k) { m.type(String.fromCharCode(k)); }, // (into the KERNAL's buffer, as typed)
|
|
147
|
-
key(e) { for (const f of listeners[e.type]) f(e); },
|
|
157
|
+
key(e) { if (e.type === 'keydown' && !e.repeat) theirs(); for (const f of listeners[e.type]) f(e); },
|
|
158
|
+
demonstrate(steps) { demo = typingOf(steps, 'c64'); },
|
|
148
159
|
start() { speaker.start(); },
|
|
149
160
|
sound(on) { sound = on; },
|
|
150
161
|
engine: m,
|
package/engines/scenes.js
CHANGED
|
@@ -157,7 +157,11 @@ export async function play(g) {
|
|
|
157
157
|
}
|
|
158
158
|
if (speaker?.node && sounding) {
|
|
159
159
|
sid ??= new SID(speaker.rate); from ??= performance.now();
|
|
160
|
-
|
|
160
|
+
// (a quarter of a second at most: after a pause, a tab in the background, the sound goes on from now, not from
|
|
161
|
+
// where it stopped, and no wave the length of the pause is made)
|
|
162
|
+
let n = Math.floor((performance.now() - from) / 1000 * speaker.rate) - sent;
|
|
163
|
+
const most = Math.floor(speaker.rate / 4);
|
|
164
|
+
if (n > most) { sent += n - most; n = most; }
|
|
161
165
|
if (n > 0) { const wave = sid.render(n, engine.sid.map(([, r, v]) => [0, r, v])); sent += n; speaker.node.port.postMessage(wave, [wave.buffer]); }
|
|
162
166
|
}
|
|
163
167
|
engine.sid.length = 0;
|
package/keys.js
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
// code(event, kind) the key's code for the machine of that kind ('prodos', 'dos33' or 'c64'; none for a title
|
|
6
6
|
// of its own engine), or undefined for a key it has none for
|
|
7
7
|
// named(name, kind) a key by its name in a route or ?keys= (space, return, left...), as the machine's code
|
|
8
|
+
// typing(steps, kind) a demonstration's steps (data.js `demonstrations`) as the one string the machine is to be
|
|
9
|
+
// given, as typed: each step a key by its name, { key: 'f4' }, or text, { type: '1 PISTONS' };
|
|
10
|
+
// a step of any other shape, or a key the machine has no name for, refused
|
|
8
11
|
|
|
9
12
|
// the Apple II's keyboard: the arrows as its control keys, DELETE, ESC, TAB
|
|
10
13
|
const APPLE = { ArrowLeft: 8, ArrowRight: 21, ArrowDown: 10, ArrowUp: 11, Enter: 13, Backspace: 127, Delete: 127, Escape: 27, Tab: 9, ' ': 32 };
|
|
@@ -13,6 +16,9 @@ const C64 = { ArrowLeft: 157, ArrowRight: 29, ArrowDown: 17, ArrowUp: 145, Enter
|
|
|
13
16
|
// the names a route's keys and ?keys= use, as the Apple's codes, and those a Commodore 64 has in their place
|
|
14
17
|
const NAMES = { space: 32, return: 13, escape: 27, delete: 127, left: 8, right: 21, down: 10, up: 11, 'control-e': 5, 'control-t': 20, none: -1 };
|
|
15
18
|
const PETSCII = { 8: 157, 21: 29, 10: 17, 11: 145, 127: 20, 27: 3 };
|
|
19
|
+
// and the keys a Commodore 64 alone has, by name: its function keys, CLR and HOME, INST and DEL, RUN/STOP, £ and ←, in
|
|
20
|
+
// PETSCII as the KERNAL's GETIN gives them
|
|
21
|
+
const C64_NAMES = { f1: 133, f2: 137, f3: 134, f4: 138, f5: 135, f6: 139, f7: 136, f8: 140, home: 19, clr: 147, inst: 148, del: 20, stop: 3, pound: 92, arrow: 95 };
|
|
16
22
|
|
|
17
23
|
export function code(e, kind) {
|
|
18
24
|
if (!kind || e.metaKey || e.altKey) return undefined;
|
|
@@ -23,6 +29,18 @@ export function code(e, kind) {
|
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
export function named(name, kind) {
|
|
32
|
+
if (kind === 'c64' && name in C64_NAMES) return C64_NAMES[name];
|
|
26
33
|
const k = NAMES[name] ?? name.charCodeAt(0);
|
|
27
34
|
return kind === 'c64' ? PETSCII[k] ?? k : k;
|
|
28
35
|
}
|
|
36
|
+
|
|
37
|
+
export function typing(steps, kind) {
|
|
38
|
+
if (!Array.isArray(steps) || !steps.length) throw new Error('a demonstration is a list of steps, each { key } or { type }');
|
|
39
|
+
return steps.map(s => {
|
|
40
|
+
const [what, ...more] = Object.keys(s ?? {});
|
|
41
|
+
if (more.length || !['key', 'type'].includes(what) || typeof s[what] !== 'string' || !s[what]) throw new Error('a step is { key: name } or { type: text }: ' + JSON.stringify(s));
|
|
42
|
+
if (what === 'type') return kind === 'c64' ? s.type.toUpperCase() : s.type; // (the 64's letters its capitals, as the keyboard gives them)
|
|
43
|
+
if (s.key === 'none' || !(s.key in NAMES || (kind === 'c64' && s.key in C64_NAMES))) throw new Error(`no key named ${s.key} for a ${kind} game`);
|
|
44
|
+
return String.fromCharCode(named(s.key, kind));
|
|
45
|
+
}).join('');
|
|
46
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moonbase-machines/player",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "The player for Mark Wharton's titles of 1985 to 1995: the Original, a game's own disk on Moonbase's machines, and the Remake, its scenes on an engine; the player's own disk of saves in the browser.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
"moonbase-play": "./bin/moonbase-play.mjs"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@moonbase-machines/apple2e": "0.
|
|
18
|
-
"@moonbase-machines/c64": "0.
|
|
19
|
-
"@moonbase-machines/charset": "0.
|
|
20
|
-
"@moonbase-machines/mos6502": "0.
|
|
21
|
-
"@moonbase-machines/vector": "0.
|
|
17
|
+
"@moonbase-machines/apple2e": "0.13.0",
|
|
18
|
+
"@moonbase-machines/c64": "0.13.0",
|
|
19
|
+
"@moonbase-machines/charset": "0.13.0",
|
|
20
|
+
"@moonbase-machines/mos6502": "0.13.0",
|
|
21
|
+
"@moonbase-machines/vector": "0.13.0"
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
24
|
"test": "node --test test/*.test.js",
|
package/play.js
CHANGED
|
@@ -33,19 +33,22 @@
|
|
|
33
33
|
// roms the makers' ROMs this machine has (engines/original.js; a Commodore 64 remake's { chargen })
|
|
34
34
|
// studies files of its own for a Commodore 64 game's disk, in the drive, not kept
|
|
35
35
|
// downloadDisk a Commodore 64 game's disk offered to download, with what is saved on it
|
|
36
|
+
// and, from the game's data (data.js `demonstrations`), where the engine can type them: a button for each
|
|
37
|
+
// demonstration, its name, typing its steps into the game as the keys would be; the person's first key ends it.
|
|
36
38
|
//
|
|
37
39
|
// An engine (engines/original.js, engines/scenes.js, or the title's own, its title.js `engine` a module in its folder)
|
|
38
40
|
// exports `play(game)`: given `game`, the player's own for it (below), it declares the settings it acts on
|
|
39
41
|
// (`game.setting(declaration)`, settings.js, each shown only so), makes what it draws on the stage and returns
|
|
40
42
|
// its handle, or nothing where it cannot start (and has said why in the status):
|
|
41
43
|
// { run(), press(k)?, key(e, k)?, keys(list)?, route(route)?, start()?, sound(on)?, quick(on)?,
|
|
42
|
-
// engine?, diskImage()? }
|
|
44
|
+
// demonstrate(steps)?, engine?, diskImage()? }
|
|
43
45
|
// run() starts it; press(k) a key, the machine's code (none for a title of its own engine with no machine's keys,
|
|
44
46
|
// which reads the keyboard itself); key(e, k), for an engine that reads the keyboard itself, each
|
|
45
47
|
// keydown and keyup with its code (undefined where the machine has none); keys(list), the page's `keys` at the start;
|
|
46
48
|
// route, the page's route; start(), at the player's first gesture (sound may start only then); sound(on), the
|
|
47
49
|
// player's speaker on or off (the Sound setting; a game's own sound setting stays the game's); quick(on) the game at
|
|
48
|
-
// full speed; engine, what `watch` gives a page;
|
|
50
|
+
// full speed; demonstrate(steps), a demonstration's steps typed into the game; engine, what `watch` gives a page;
|
|
51
|
+
// diskImage(), the disk to download.
|
|
49
52
|
import { titleOf, loadTitle, read } from './remake/load.js';
|
|
50
53
|
import { savesFor } from './saves.js';
|
|
51
54
|
import { code, named } from './keys.js';
|
|
@@ -162,6 +165,12 @@ export async function play(options) {
|
|
|
162
165
|
a.click(); URL.revokeObjectURL(a.href);
|
|
163
166
|
});
|
|
164
167
|
}
|
|
168
|
+
// the game's demonstrations (data.js), where the engine types them: a button each, named as the data names it
|
|
169
|
+
if (handle.demonstrate) for (const d of game?.demonstrations ?? []) {
|
|
170
|
+
const button = Object.assign(document.createElement('button'), { type: 'button', textContent: d.name });
|
|
171
|
+
button.addEventListener('click', () => { handle.start?.(); handle.demonstrate(d.steps); });
|
|
172
|
+
$('disk').before(button);
|
|
173
|
+
}
|
|
165
174
|
// a page that follows the game: its engine, a key pressed as the player presses one, the game at full speed
|
|
166
175
|
options.watch?.({ engine: handle.engine, press: k => { handle.start?.(); handle.press?.(k); }, quick: on => handle.quick?.(on) });
|
|
167
176
|
// a route the page gives, played first; then the game; then the page's keys, to go straight to a scene
|
package/remake/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# The remake engine
|
|
2
|
+
|
|
3
|
+
A remake is not a game written again in JavaScript. It is the title's own code, lifted from the original into scene
|
|
4
|
+
scripts, compiled to JavaScript and run on this engine. The engine stands in for the machine and for the routines the
|
|
5
|
+
title called. The logic is the original's, statement for statement, so the remake does what the original did, and the
|
|
6
|
+
pilots can hold the two side by side, event by event.
|
|
7
|
+
|
|
8
|
+
A title of its own engine is the exception: its `title.js` names a module in its folder as its `engine`, and that
|
|
9
|
+
module plays the game itself (below).
|
|
10
|
+
|
|
11
|
+
## A title's folder
|
|
12
|
+
|
|
13
|
+
The page gives the player the title's folder (`../README.md` says what is in it). For a remake on this engine it holds:
|
|
14
|
+
|
|
15
|
+
- `title.js`: the engine, `'card'` or `'vector'`; the scripts and the one it starts with; the files that load as
|
|
16
|
+
scripts (`fileScripts`); the memory at the start; the entries other scripts jump into; and the `map`, the routines
|
|
17
|
+
the scenes call, by address, each with the name of its step.
|
|
18
|
+
- `<name>.scene`: the scripts, as lifted and as edited after. They are the source.
|
|
19
|
+
- `compiled/<name>.js`: each scene compiled by `moonbase-compile`, which the remake runs. These are always compiled
|
|
20
|
+
again from the scene and never edited.
|
|
21
|
+
- the title's own modules, where it has them: its problems (`modules`) and its view's drawing (`view`).
|
|
22
|
+
|
|
23
|
+
## A scene script
|
|
24
|
+
|
|
25
|
+
A scene is a step or a statement to a line (`script.js`), each line with the address in the original it came from:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
L0813: a label
|
|
29
|
+
0813 if a != 127 goto L0825 a branch on a compare, or on a flag: if zero goto L0825
|
|
30
|
+
0817 call L08BF a call into the script's own code
|
|
31
|
+
081C char 32 a step: one of the title's routines, with its inputs
|
|
32
|
+
0825 x = 0 a statement on the registers a, x and y, the flags, the stack and memory
|
|
33
|
+
data $08D7 00 00 80 80 bytes of the original's tables, at their addresses
|
|
34
|
+
; a comment
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
A statement is the original's own instruction, written so it can be read. The engine runs it on three registers,
|
|
38
|
+
four flags, a stack and 64K of memory, as the 6502 did. A step is a call into one of the title's routines, which the
|
|
39
|
+
title's `map` names by address (`0x0800: 'char'`). The engine does the routine's work in JavaScript and emits an
|
|
40
|
+
event. Everything a scene does is an event: the view draws the events, and a pilot reads them as the transcript to
|
|
41
|
+
hold against the original's.
|
|
42
|
+
|
|
43
|
+
### Code that rewrites itself
|
|
44
|
+
|
|
45
|
+
A scene is the code as text, lifted once, so it can't follow code that changes its own instructions. The 6502 does
|
|
46
|
+
follow it, because it reads each instruction's bytes from memory as it runs. A routine that keeps a pointer in the
|
|
47
|
+
address bytes of its own `LDA` and moves the pointer by incrementing them would be lifted as a read of one fixed
|
|
48
|
+
address. Microsoft's CHRGET in 6502 BASIC is the best-known case. Such a line needs either a step, the routine
|
|
49
|
+
answered in JavaScript, or a hand edit to read through the pointer instead (`a = [$007A]`, the byte at the address
|
|
50
|
+
held at `$7A` and `$7B`), with the reason beside it.
|
|
51
|
+
|
|
52
|
+
## Compiled to JavaScript
|
|
53
|
+
|
|
54
|
+
`moonbase-compile <title folder>` writes each scene as an ES module in `compiled/` (`compile.js`). The module's
|
|
55
|
+
default export is `{ labels, data, steps, run }`:
|
|
56
|
+
|
|
57
|
+
- `labels`, each label's step;
|
|
58
|
+
- `data`, the bytes the script puts in memory;
|
|
59
|
+
- `steps`, each step's address, its line in the scene and its text;
|
|
60
|
+
- `run`, a function for each block of the script, by the step it starts at.
|
|
61
|
+
|
|
62
|
+
A run of statements that only computes is one function, a loop where it branches back to its own start, each line
|
|
63
|
+
with the scene's line beside it as a comment. Anything that can stop the run stays a step of its own, so the title
|
|
64
|
+
waits where it waited: a routine's step, and a read or write that may reach the I/O page. A jump, a call or a return
|
|
65
|
+
ends a function.
|
|
66
|
+
|
|
67
|
+
The player loads the compiled modules with `import()` (`load.js` `loadTitle`). There is no JSON in between. The JSON
|
|
68
|
+
beside the title, `<name>.json`, is the game's data: its disks' files, its kind and where it saves. `loadTitle` with
|
|
69
|
+
`scenes` reads the `.scene` files instead and runs them line by line, which gives the same results more slowly.
|
|
70
|
+
|
|
71
|
+
## The two engines
|
|
72
|
+
|
|
73
|
+
The scripts are the same for every title. What differs is the routines their steps call, and there are two sets.
|
|
74
|
+
|
|
75
|
+
- **Card** (`card.js`, `cards.js`, `card-view.js`, `c64-view.js`): a title on the Apple IIe or the Commodore 64. A
|
|
76
|
+
toolkit's own code is lifted into scripts too, beside the scenes. What cannot be a script is JavaScript here: the
|
|
77
|
+
routines that take their data inline after the call (fill, copy, print), the Apple's ROM routines and ProDOS's
|
|
78
|
+
calls (`@moonbase-machines/apple2e`), the Commodore 64's KERNAL, I/O page, drive, printer and BASIC
|
|
79
|
+
(`@moonbase-machines/c64`), the keyboard and the soft switches. The screen is the machine's own, written byte for
|
|
80
|
+
byte as the original wrote it.
|
|
81
|
+
- **Vector** (`vector.js`, `layout.js`, `raster.js`, `problems.js`, `view.js`): a title on the vector engine. Its
|
|
82
|
+
routines (a text, a picture, a box, a wait, a key) are written again here by the original's rules for keys, random
|
|
83
|
+
numbers and waits. The view draws the pictures from their shapes as SVG (`@moonbase-machines/vector`), at any size.
|
|
84
|
+
A title's problems are its own module, rewritten in JavaScript as generators, run with the helpers in `problems.js`.
|
|
85
|
+
|
|
86
|
+
`load.js` makes the engine from the title and its scripts (`makeEngine`), with the routines of the title's `engine`.
|
|
87
|
+
`../engines/scenes.js` plays it on the page.
|
|
88
|
+
|
|
89
|
+
## The original's time
|
|
90
|
+
|
|
91
|
+
A title's code sits in the remake's memory where it came from, so the engine counts the time the original took
|
|
92
|
+
(`Engine.time`). Each line costs its instruction's cycles, read from the opcode at its address, and a branch taken
|
|
93
|
+
costs one more. A click of the speaker is kept at its cycle and played as the emulator plays it (`../audio.js`), so
|
|
94
|
+
the remake runs at the original's speed and sounds as it did.
|
|
95
|
+
|
|
96
|
+
## Bugs fixed
|
|
97
|
+
|
|
98
|
+
The remake plays the game as it was, bugs and all, because the pilots hold it against the original. A title can
|
|
99
|
+
also have a version with its bugs fixed, as a separate script beside the faithful one. Its `title.js` names each
|
|
100
|
+
script that version replaces:
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
fixed: { game: 'game-fixed' }
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The player's setting **Bugs fixed** (`play.js`, `fixed`) is offered only for a title that has one. With it on,
|
|
107
|
+
`loadTitle` runs `compiled/game-fixed.js` in place of `compiled/game.js`, and the page starts again when the
|
|
108
|
+
setting changes. Only compiled scripts are replaced: with `scenes`, `loadTitle` runs the faithful scenes.
|
|
109
|
+
|
|
110
|
+
Because every statement is the original's, with its address, a fix is an edit to a known line, not a guess at what
|
|
111
|
+
the game meant. Make the fixed script from the faithful one by edits that each check the line they change is the
|
|
112
|
+
one they expect. A changed line keeps its address, and a line that is new has none. That keeps the fixed version
|
|
113
|
+
the same game with only its bugs changed. When the faithful script is lifted again, the edits are made again, and
|
|
114
|
+
one whose line has changed stops instead of editing the wrong line.
|
|
115
|
+
|
|
116
|
+
Corrupted text can be fixed the same way where the text is in the code: a string a call takes inline after it, or a
|
|
117
|
+
scene's `data` line. Text the game reads from its disk at run time is the game's data, `<name>.json`, not a
|
|
118
|
+
script, and a fixed script does not change it.
|
|
119
|
+
|
|
120
|
+
## A title of its own engine
|
|
121
|
+
|
|
122
|
+
When `title.js` names a module as its `engine`, the title has no scripts. `loadTitle` imports that module as
|
|
123
|
+
`title.own`, and the player hands it the game (`../play.js`). That engine is the original program's own, read from
|
|
124
|
+
its code and written again routine for routine, playing the game's data as the original read it.
|
|
125
|
+
|
|
126
|
+
## The files
|
|
127
|
+
|
|
128
|
+
| File | What it is |
|
|
129
|
+
|---|---|
|
|
130
|
+
| `engine.js` | runs the scripts: the statements on the registers, flags, stack and memory, the steps through the routines, every step an event |
|
|
131
|
+
| `script.js` | reads a scene script, a step to a line |
|
|
132
|
+
| `compile.js` | compiles a scene script to JavaScript (`moonbase-compile`) |
|
|
133
|
+
| `load.js` | loads a title's folder, its compiled scripts or its scenes, and makes its engine |
|
|
134
|
+
| `events.js` | what each step is as an event, in the same words for the original and the remake |
|
|
135
|
+
| `lift.js` | the lifter: a file's 6502 walked from its entries into a scene script; the card engine uses it to lift code a title's scenes miss, as it runs |
|
|
136
|
+
| `card.js`, `cards.js` | the card engine's routines, and what it knows of every title it plays |
|
|
137
|
+
| `vector.js`, `layout.js`, `raster.js`, `problems.js` | the vector engine's routines; its text laid out as the engine laid it out; the original's screen dot for dot, for the scenes that read it back; its problems' helpers |
|
|
138
|
+
| `view.js`, `card-view.js`, `c64-view.js` | the views: a vector title's as SVG, an Apple's hi-res page, a Commodore 64's screen |
|
package/test/play.test.js
CHANGED
|
@@ -9,7 +9,8 @@ import { test } from 'node:test';
|
|
|
9
9
|
import assert from 'node:assert/strict';
|
|
10
10
|
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
|
-
import { code, named } from '@moonbase-machines/player/keys.js';
|
|
12
|
+
import { code, named, typing } from '@moonbase-machines/player/keys.js';
|
|
13
|
+
import { gameData } from '@moonbase-machines/player/data.js';
|
|
13
14
|
import { savesFor } from '@moonbase-machines/player/saves.js';
|
|
14
15
|
import { openDisk } from '@moonbase-machines/player/disk/disk.js';
|
|
15
16
|
import { memory } from '@moonbase-machines/player/disk/memory.js';
|
|
@@ -34,6 +35,24 @@ test('a key is the Apple\'s code or the Commodore 64\'s, by the machine\'s kind'
|
|
|
34
35
|
assert.deepEqual(['left', 'delete'].map(n => named(n, 'c64')), [157, 20]);
|
|
35
36
|
});
|
|
36
37
|
|
|
38
|
+
test('a demonstration\'s steps as typed: the 64\'s own keys by name, its text in capitals; a step of another shape refused', () => {
|
|
39
|
+
assert.deepEqual(['f1', 'f4', 'f8', 'return', 'stop', 'clr', 'pound'].map(n => named(n, 'c64')), [133, 138, 140, 13, 3, 147, 92]);
|
|
40
|
+
assert.equal(typing([{ key: 'return' }, { key: 'f4' }, { type: '1 Pistons' }, { key: 'return' }, { key: 'f8' }], 'c64'), '\r\x8a1 PISTONS\r\x8c');
|
|
41
|
+
assert.equal(typing([{ type: 'run' }, { key: 'return' }], 'prodos'), 'run\r', 'an Apple\'s text as typed');
|
|
42
|
+
for (const [bad, kind] of [[[], 'c64'], [[{}], 'c64'], [[{ key: 'f4', type: 'x' }], 'c64'], [[{ key: 'none' }], 'c64'], [[{ type: '' }], 'c64'], [[{ press: 'a' }], 'c64'], [[{ key: 'f4' }], 'prodos'], ['f4', 'c64']])
|
|
43
|
+
assert.throws(() => typing(bad, kind), JSON.stringify(bad));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('a game\'s data carries its demonstrations, each step checked; a game of another kind refused one', () => {
|
|
47
|
+
const disk = make({ name: 'DISK', id: 'MB', files: [] }), steps = [{ key: 'return' }, { key: 'f8' }];
|
|
48
|
+
const c64 = more => ({ title: 'GAME', kind: 'c64', disks: [disk], start: 'GAME', ...more });
|
|
49
|
+
assert.deepEqual(gameData('game', c64({ demonstrations: [{ name: 'Frames', steps }] })).data.demonstrations, [{ name: 'Frames', steps }]);
|
|
50
|
+
assert.equal(gameData('game', c64({})).data.demonstrations, undefined);
|
|
51
|
+
assert.throws(() => gameData('game', c64({ demonstrations: [{ name: 'Frames', steps: [{ key: 'f9' }] }] })), /no key named f9/);
|
|
52
|
+
assert.throws(() => gameData('game', c64({ demonstrations: [{ steps }] })), /has a name/);
|
|
53
|
+
assert.throws(() => gameData('game', { title: 'GAME', kind: 'dos33', disks: [], demonstrations: [{ name: 'Frames', steps }] }), /C64 game only/);
|
|
54
|
+
});
|
|
55
|
+
|
|
37
56
|
test('the saves: a game\'s volume given what is kept, and a change to its files kept', async () => {
|
|
38
57
|
const disk = await openDisk('title/one', { backend: memory() });
|
|
39
58
|
disk.write('KEPT', [1, 2], { type: 'BIN', aux: 0x2000 });
|