@moonbase-machines/player 0.10.0 → 0.12.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 +23 -8
- package/bin/moonbase-play.mjs +18 -7
- package/disk/disk.js +5 -7
- package/dist/moonbase-player.js +16 -16
- package/dist/moonbase-player.js.map +4 -4
- package/engines/original.js +10 -8
- package/engines/scenes.js +29 -31
- package/index.js +2 -0
- package/package.json +6 -6
- package/play.js +59 -50
- package/provider.js +27 -0
- package/remake/README.md +138 -0
- package/settings.js +82 -0
- package/test/play.test.js +76 -2
package/settings.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// A person's settings: what the player, its engines and a site's pages let a person choose (the sound, the printer,
|
|
2
|
+
// the old style text, a page's own choices), each declared where it acts, in one shape, and kept through the provider
|
|
3
|
+
// (provider.js) on a disk of the owner's, settings/<owner>, where the person's files go. The store is typed against
|
|
4
|
+
// the declarations: only a declared setting can be read or kept, every value checked against its type. Nothing here
|
|
5
|
+
// knows of an address: a site that lets its address set a setting for a visit reads it itself and gives the value.
|
|
6
|
+
//
|
|
7
|
+
// A declaration: { name, type, default, owner, words, labels?, title? }
|
|
8
|
+
// name its name, in the store's file (and for a site, in whatever sets it for a visit)
|
|
9
|
+
// type 'boolean', or an array of its fixed values, an enum
|
|
10
|
+
// default its value where nothing gives one
|
|
11
|
+
// owner whose it is: 'person' (the only owner yet; a parent's or teacher's later is another layer here, with
|
|
12
|
+
// nothing changed where the setting is declared)
|
|
13
|
+
// words what its control calls it; labels, an enum's values' words in order; title, a note on the control
|
|
14
|
+
//
|
|
15
|
+
// check(type, value) whether a value is of the type
|
|
16
|
+
// describe(type) the type in words, for saying what was refused
|
|
17
|
+
//
|
|
18
|
+
// const settings = settingsStore({ disks, visit, fixed, hidden })
|
|
19
|
+
// disks { owner: disk }, the owner's settings disk (settings/<owner>), opened by the provider; its one file,
|
|
20
|
+
// `settings`, the values by name
|
|
21
|
+
// visit def => a value for this visit, or undefined: the site's, from wherever it lets a visit set one (its
|
|
22
|
+
// address, say); checked here, a value not of the type refused and said once; never kept
|
|
23
|
+
// fixed { name: value }, what the page fixes, over everything; hidden, [name], what it does not offer
|
|
24
|
+
// settings.get(def) its value: the page's fixed one, else the visit's, else the kept one where it is of
|
|
25
|
+
// the type (one that is not falls back, said), else its default
|
|
26
|
+
// settings.set(def, value) kept, for the next visit; a promise, kept when stored (a value not of the type throws)
|
|
27
|
+
// settings.offered(def) whether its control is shown: not where the page hides it or fixes it
|
|
28
|
+
import { textBytes, bytesText } from './disk/disk.js';
|
|
29
|
+
|
|
30
|
+
const FILE = 'settings';
|
|
31
|
+
|
|
32
|
+
export function check(type, value) {
|
|
33
|
+
if (Array.isArray(type)) return type.includes(value);
|
|
34
|
+
if (type === 'boolean') return typeof value === 'boolean';
|
|
35
|
+
throw new Error('settings: no such type: ' + JSON.stringify(type));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const describe = type => Array.isArray(type) ? 'one of ' + type.map(v => v === '' ? 'nothing' : v).join(', ') : 'on or off';
|
|
39
|
+
|
|
40
|
+
export function settingsStore({ disks, visit = () => undefined, fixed = {}, hidden = [] }) {
|
|
41
|
+
const said = new Set();
|
|
42
|
+
const say = (key, what) => { if (!said.has(key)) { said.add(key); console.warn(what); } };
|
|
43
|
+
const layers = Object.fromEntries(Object.entries(disks).map(([owner, disk]) => {
|
|
44
|
+
let kept = null;
|
|
45
|
+
try { kept = JSON.parse(bytesText(disk.read(FILE)) ?? '{}'); } catch {}
|
|
46
|
+
return [owner, { disk, kept: kept && typeof kept === 'object' && !Array.isArray(kept) ? kept : {} }];
|
|
47
|
+
}));
|
|
48
|
+
// (the declaration itself checked as it is first met: its owner has a store here, its default is of its type)
|
|
49
|
+
const seen = new Set();
|
|
50
|
+
const layer = def => {
|
|
51
|
+
const l = layers[def.owner];
|
|
52
|
+
if (!l) throw new Error(`settings: no store for the owner ${def.owner} (${def.name})`);
|
|
53
|
+
if (!seen.has(def.name)) { seen.add(def.name); if (!check(def.type, def.default)) throw new Error(`settings: ${def.name}'s default ${JSON.stringify(def.default)} is not ${describe(def.type)}`); }
|
|
54
|
+
return l;
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
get(def) {
|
|
58
|
+
const l = layer(def);
|
|
59
|
+
if (def.name in fixed) {
|
|
60
|
+
if (!check(def.type, fixed[def.name])) throw new Error(`settings: ${def.name} fixed at ${JSON.stringify(fixed[def.name])}, not ${describe(def.type)}`);
|
|
61
|
+
return fixed[def.name];
|
|
62
|
+
}
|
|
63
|
+
const v = visit(def);
|
|
64
|
+
if (v !== undefined) {
|
|
65
|
+
if (check(def.type, v)) return v;
|
|
66
|
+
say('visit ' + def.name, `${def.name}: ${JSON.stringify(v)} for this visit refused: not ${describe(def.type)}`);
|
|
67
|
+
}
|
|
68
|
+
if (def.name in l.kept) {
|
|
69
|
+
if (check(def.type, l.kept[def.name])) return l.kept[def.name];
|
|
70
|
+
say('kept ' + def.name, `${def.name}: the kept ${JSON.stringify(l.kept[def.name])} is not ${describe(def.type)}; its default used`);
|
|
71
|
+
}
|
|
72
|
+
return def.default;
|
|
73
|
+
},
|
|
74
|
+
set(def, value) {
|
|
75
|
+
const l = layer(def);
|
|
76
|
+
if (!check(def.type, value)) throw new Error(`settings: ${def.name} cannot be ${JSON.stringify(value)}: not ${describe(def.type)}`);
|
|
77
|
+
l.kept[def.name] = value;
|
|
78
|
+
return l.disk.write(FILE, textBytes(JSON.stringify(l.kept)));
|
|
79
|
+
},
|
|
80
|
+
offered: def => !hidden.includes(def.name) && !(def.name in fixed),
|
|
81
|
+
};
|
|
82
|
+
}
|
package/test/play.test.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// The player's parts that every way to play shares (play.js): the keys as each machine takes them (keys.js), the
|
|
2
|
-
// saves on the player's disk (saves.js), a write the disk's backend cannot store (disk/disk.js);
|
|
3
|
-
// the
|
|
2
|
+
// saves on the player's disk (saves.js), a write the disk's backend cannot store (disk/disk.js); the settings store
|
|
3
|
+
// typed against its declarations (settings.js) and the provider giving a page its disks and settings on one backend
|
|
4
|
+
// (provider.js); the package knowing nothing of an address and keeping nothing in the browser itself; and no
|
|
5
|
+
// title named in the packages' code, only in their comments, as examples.
|
|
4
6
|
//
|
|
5
7
|
// node --test test/*.test.js
|
|
6
8
|
import { test } from 'node:test';
|
|
@@ -11,6 +13,8 @@ import { code, named } from '@moonbase-machines/player/keys.js';
|
|
|
11
13
|
import { savesFor } from '@moonbase-machines/player/saves.js';
|
|
12
14
|
import { openDisk } from '@moonbase-machines/player/disk/disk.js';
|
|
13
15
|
import { memory } from '@moonbase-machines/player/disk/memory.js';
|
|
16
|
+
import { settingsStore, check } from '@moonbase-machines/player/settings.js';
|
|
17
|
+
import { provider, settingsDisk } from '@moonbase-machines/player/provider.js';
|
|
14
18
|
import { Drive } from '@moonbase-machines/c64/drive.js';
|
|
15
19
|
import { make } from '@moonbase-machines/c64/d64.js';
|
|
16
20
|
|
|
@@ -59,6 +63,76 @@ test('a write the backend cannot store is said, and the writes after it are stor
|
|
|
59
63
|
assert.deepEqual([...(await openDisk('title/two', { backend })).read('GOOD')], [2]);
|
|
60
64
|
});
|
|
61
65
|
|
|
66
|
+
// (what the console is told, for the tests of what is refused)
|
|
67
|
+
const warned = async f => { const said = [], warn = console.warn; console.warn = m => said.push(m); try { await f(); } finally { console.warn = warn; } return said; };
|
|
68
|
+
|
|
69
|
+
test('a setting\'s type: on or off, or one of fixed values', () => {
|
|
70
|
+
assert.ok(check('boolean', false) && !check('boolean', 'off') && check(['a', 'b'], 'a') && !check(['a', 'b'], 'c'));
|
|
71
|
+
assert.throws(() => check('text', 'x'), /no such type/);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const SOUND = { name: 'sound', type: 'boolean', default: true, owner: 'person', words: 'Sound' };
|
|
75
|
+
const MINI = { name: 'mini', type: ['', 'live', 'pictures'], default: '', owner: 'person', words: 'Mini view', labels: ['Off', 'Live', 'Pictures'] };
|
|
76
|
+
const OLD = { name: 'old-style', type: 'boolean', default: false, owner: 'person', words: 'Old style text' };
|
|
77
|
+
const FIXED = { name: 'fixed', type: 'boolean', default: false, owner: 'person', words: 'Bugs fixed' };
|
|
78
|
+
|
|
79
|
+
test('a setting by its declaration: the page\'s fixed value, else the visit\'s, else the kept one, else its default; offered unless hidden or fixed', async () => {
|
|
80
|
+
const backend = memory(), person = () => openDisk(settingsDisk('person'), { backend });
|
|
81
|
+
const first = settingsStore({ disks: { person: await person() } });
|
|
82
|
+
assert.equal(first.get(SOUND), true, 'its default');
|
|
83
|
+
await first.set(SOUND, false); await first.set(MINI, 'pictures');
|
|
84
|
+
const visit = def => ({ 'old-style': true, mini: 'live', sound: false })[def.name];
|
|
85
|
+
const again = settingsStore({ disks: { person: await person() }, visit, fixed: { fixed: true }, hidden: ['mini'] });
|
|
86
|
+
assert.equal(again.get(SOUND), false, 'the visit\'s, and kept');
|
|
87
|
+
assert.equal(again.get(MINI), 'live', 'the visit\'s over the kept');
|
|
88
|
+
assert.equal(again.get(OLD), true, 'the visit\'s on');
|
|
89
|
+
assert.equal(again.get(FIXED), true, 'the page\'s fixed value over all');
|
|
90
|
+
assert.deepEqual([SOUND, MINI, FIXED].map(again.offered), [true, false, false]);
|
|
91
|
+
assert.deepEqual(JSON.parse(new TextDecoder().decode((await person()).read('settings'))), { sound: false, mini: 'pictures' }, 'the visit\'s values never kept');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('a setting\'s value is checked: a visit\'s value not of the type refused and said once, a kept value not of the type its default, a set of the wrong type thrown', async () => {
|
|
95
|
+
const backend = memory(), disk = await openDisk(settingsDisk('person'), { backend });
|
|
96
|
+
disk.write('settings', new TextEncoder().encode(JSON.stringify({ sound: 'loud', mini: 'live' })));
|
|
97
|
+
const store = settingsStore({ disks: { person: disk }, visit: def => ({ mini: 'huge', 'old-style': 'maybe' })[def.name] });
|
|
98
|
+
const said = await warned(() => { assert.equal(store.get(SOUND), true, 'the kept value refused: its default'); assert.equal(store.get(MINI), 'live', 'huge refused: the kept'); store.get(MINI); assert.equal(store.get(OLD), false); });
|
|
99
|
+
assert.deepEqual(said, ['sound: the kept "loud" is not on or off; its default used', 'mini: "huge" for this visit refused: not one of nothing, live, pictures', 'old-style: "maybe" for this visit refused: not on or off']);
|
|
100
|
+
assert.throws(() => store.set(SOUND, 'off'), /sound cannot be "off": not on or off/);
|
|
101
|
+
assert.throws(() => store.set(MINI, 'big'), /not one of nothing, live, pictures/);
|
|
102
|
+
assert.throws(() => store.get({ ...SOUND, owner: 'teacher' }), /no store for the owner teacher/);
|
|
103
|
+
assert.throws(() => store.get({ name: 'bad', type: 'boolean', default: 'on', owner: 'person' }), /default "on" is not on or off/);
|
|
104
|
+
assert.throws(() => settingsStore({ disks: { person: disk }, fixed: { sound: 'off' } }).get(SOUND), /fixed at "off"/);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('the provider: every disk on the one backend it is given, and the settings kept there too', async () => {
|
|
108
|
+
const backend = memory();
|
|
109
|
+
const { disk, settings } = await provider({ backend, visit: def => def.name === 'sound' ? false : undefined, hidden: ['vectors'] });
|
|
110
|
+
const title = await disk('title/one');
|
|
111
|
+
await title.write('SAVED', [1]);
|
|
112
|
+
await settings.set(MINI, 'live');
|
|
113
|
+
assert.deepEqual((await backend.load('title/one')).map(f => f.name), ['SAVED']);
|
|
114
|
+
assert.deepEqual((await backend.load('settings/person')).map(f => f.name), ['settings']);
|
|
115
|
+
assert.equal(settings.get(SOUND), false);
|
|
116
|
+
assert.equal(settings.offered({ name: 'vectors', type: 'boolean', default: false, owner: 'person' }), false);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// the package's own code knows nothing of an address and keeps nothing in the browser: the page gives it everything
|
|
120
|
+
// (bin/moonbase-play.mjs is a page of its own, the one place an address is read, by that page)
|
|
121
|
+
test('the player, its engines, its settings and its provider know no address and no browser storage; the player opens no disk', () => {
|
|
122
|
+
const own = ['play.js', 'settings.js', 'provider.js', 'engines/original.js', 'engines/scenes.js', 'saves.js', 'keys.js', 'parts.js', 'disk/disk.js', 'disk/dialogs.js'];
|
|
123
|
+
const found = [];
|
|
124
|
+
for (const f of own) {
|
|
125
|
+
const lines = readFileSync(join(packages, 'player', f), 'utf8').replace(/\/\*[\s\S]*?\*\//g, c => c.replace(/[^\n]/g, '')).split('\n');
|
|
126
|
+
lines.forEach((l, i) => {
|
|
127
|
+
const c = l.replace(/(^|\s)\/\/.*$/, '');
|
|
128
|
+
if (/location\.(search|hash|href)|URLSearchParams|localStorage|sessionStorage/.test(c)) found.push(`${f}:${i + 1}`);
|
|
129
|
+
if (/openDisk/.test(c) && !['provider.js', 'disk/disk.js'].includes(f)) found.push(`${f}:${i + 1} opens a disk`);
|
|
130
|
+
if (/\blocal\(/.test(c) && f !== 'provider.js') found.push(`${f}:${i + 1} chooses a backend`);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
assert.deepEqual(found, []);
|
|
134
|
+
});
|
|
135
|
+
|
|
62
136
|
// the titles, by name and by address; the packages' code, their comments taken out
|
|
63
137
|
const TITLES = /pieces[ -]of[ -]eight|detect-a-pet|goldfields|zoopak|nature[ -]park|animal[ -]rescue|aztlan|crayon|po8/i;
|
|
64
138
|
const packages = new URL('../../', import.meta.url).pathname;
|