@moonbase-machines/player 0.12.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.
@@ -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
- function frame() {
72
- if (!m.halted) m.runCycles(17030); // a frame's worth of the Apple's clock, at 60 a second
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, the arrows its cursor keys, and a gamepad the joystick.
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, so frames are run to keep up with the clock, two at most.
124
- let frames = 0, sid = null, started = performance.now();
125
- function frame() {
126
- const due = Math.min(2, Math.floor((performance.now() - started) / 20) - frames);
127
- for (let i = 0; i < due; i++) {
128
- m.runFrame(); frames++; keys.frame();
129
- if (speaker.node && sound) {
130
- if (!sid) sid = new SID(speaker.rate);
131
- const n = Math.round(19656 / 985248 * speaker.rate), first = m.cpu.cycles - 19656;
132
- const wave = sid.render(n, m.sidWrites.map(([c, r, v]) => [Math.floor((c - first) / 19656 * n), r, v]));
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
- // (LOAD, and RUN for a BASIC program: @moonbase-machines/c64/start.js)
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 (typing !== null && !m.ram[0xc6]) { typing = m.type(typing); if (!typing.length) typing = null; }
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
- const n = Math.floor((performance.now() - from) / 1000 * speaker.rate) - sent;
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.12.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.12.0",
18
- "@moonbase-machines/c64": "0.12.0",
19
- "@moonbase-machines/charset": "0.12.0",
20
- "@moonbase-machines/mos6502": "0.12.0",
21
- "@moonbase-machines/vector": "0.12.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; diskImage(), the disk to download.
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/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 });