@datamoc/mw_games 0.37.0 → 0.38.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/CHANGELOG.md +24 -0
- package/README.md +26 -0
- package/api-surface.json +150 -0
- package/dist/mw_games.global.js +4 -4
- package/dist/mwl/presentation.d.ts +70 -0
- package/dist/mwl/presentation.d.ts.map +1 -0
- package/dist/mwl/presentation.js +412 -0
- package/dist/mwl/presentation.js.map +1 -0
- package/dist/two-d/presentation/MwlPresentation.d.ts +98 -0
- package/dist/two-d/presentation/MwlPresentation.d.ts.map +1 -0
- package/dist/two-d/presentation/MwlPresentation.js +704 -0
- package/dist/two-d/presentation/MwlPresentation.js.map +1 -0
- package/dist/two-d/presentation/index.d.ts +3 -0
- package/dist/two-d/presentation/index.d.ts.map +1 -0
- package/dist/two-d/presentation/index.js +2 -0
- package/dist/two-d/presentation/index.js.map +1 -0
- package/dist/two-d/ui/Bar.d.ts +3 -1
- package/dist/two-d/ui/Bar.d.ts.map +1 -1
- package/dist/two-d/ui/Bar.js +5 -0
- package/dist/two-d/ui/Bar.js.map +1 -1
- package/dist/two-d/ui/Button.d.ts +12 -0
- package/dist/two-d/ui/Button.d.ts.map +1 -1
- package/dist/two-d/ui/Button.js +16 -4
- package/dist/two-d/ui/Button.js.map +1 -1
- package/dist/two-d/ui/Window.d.ts +10 -0
- package/dist/two-d/ui/Window.d.ts.map +1 -1
- package/dist/two-d/ui/Window.js +17 -6
- package/dist/two-d/ui/Window.js.map +1 -1
- package/dist/two-d/ui/index.d.ts +2 -0
- package/dist/two-d/ui/index.d.ts.map +1 -1
- package/dist/two-d/ui/index.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +16 -1
- package/sbom.cdx.json +6 -6
- package/tools/verify-scenarios.d.mts +15 -0
- package/tools/verify-scenarios.mjs +129 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface ScenarioPlanEntry {
|
|
2
|
+
id: string;
|
|
3
|
+
enter: string;
|
|
4
|
+
/** Steps that play all sides and advance dialogue for one game-owned turn. */
|
|
5
|
+
turn: string[];
|
|
6
|
+
/** A step that throws when game-owned map, units, outcome or campaign checks fail. */
|
|
7
|
+
verify: string;
|
|
8
|
+
leave?: string;
|
|
9
|
+
}
|
|
10
|
+
export function validateScenarioPlan(plan: unknown): ScenarioPlanEntry[];
|
|
11
|
+
export function verifyScenarios(options: { file: string; turns?: number; timeoutMs?: number; only?: string }): Promise<{
|
|
12
|
+
ok: boolean;
|
|
13
|
+
scenarios: { id: string; turns: number; ok: boolean }[];
|
|
14
|
+
failures: string[];
|
|
15
|
+
}>;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
|
+
import { launchChrome } from './find-chrome.mjs';
|
|
6
|
+
|
|
7
|
+
/** Validate the serialisable, game-owned plan before running any scene steps. */
|
|
8
|
+
export function validateScenarioPlan(plan) {
|
|
9
|
+
if (!Array.isArray(plan) || plan.length === 0) throw new Error('scenario plan must be a nonempty array');
|
|
10
|
+
const ids = new Set();
|
|
11
|
+
for (const scenario of plan) {
|
|
12
|
+
if (!scenario || typeof scenario.id !== 'string' || !scenario.id || ids.has(scenario.id))
|
|
13
|
+
throw new Error('scenario IDs must be nonempty and unique');
|
|
14
|
+
ids.add(scenario.id);
|
|
15
|
+
for (const field of ['enter', 'verify'])
|
|
16
|
+
if (typeof scenario[field] !== 'string' || !scenario[field])
|
|
17
|
+
throw new Error(`${scenario.id}: ${field} needs a scene control name`);
|
|
18
|
+
if (
|
|
19
|
+
!Array.isArray(scenario.turn) ||
|
|
20
|
+
scenario.turn.length === 0 ||
|
|
21
|
+
scenario.turn.some((step) => typeof step !== 'string' || !step)
|
|
22
|
+
)
|
|
23
|
+
throw new Error(`${scenario.id}: turn needs a nonempty list of scene control names`);
|
|
24
|
+
if (scenario.leave !== undefined && (typeof scenario.leave !== 'string' || !scenario.leave))
|
|
25
|
+
throw new Error(`${scenario.id}: leave needs a scene control name`);
|
|
26
|
+
}
|
|
27
|
+
return plan;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Opens one local page and drives every scenario through its registered SceneControls. */
|
|
31
|
+
export async function verifyScenarios({ file, turns = 3, timeoutMs = 120000, only = '' }) {
|
|
32
|
+
if (!Number.isSafeInteger(turns) || turns < 1) throw new Error('turns must be a positive integer');
|
|
33
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1) throw new Error('timeoutMs must be a positive integer');
|
|
34
|
+
const browser = await launchChrome({ argsEnv: 'MWG_VISUAL_CHROME_ARGS' });
|
|
35
|
+
const failures = [];
|
|
36
|
+
const scenarios = [];
|
|
37
|
+
let expired = false;
|
|
38
|
+
const bounded = async (operation) => {
|
|
39
|
+
let timer;
|
|
40
|
+
try {
|
|
41
|
+
return await Promise.race([
|
|
42
|
+
operation,
|
|
43
|
+
new Promise((_, reject) => {
|
|
44
|
+
timer = setTimeout(() => {
|
|
45
|
+
expired = true;
|
|
46
|
+
reject(new Error(`scene step exceeded ${timeoutMs} ms`));
|
|
47
|
+
}, timeoutMs);
|
|
48
|
+
}),
|
|
49
|
+
]);
|
|
50
|
+
} finally {
|
|
51
|
+
clearTimeout(timer);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
try {
|
|
55
|
+
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
|
|
56
|
+
let current = 'startup';
|
|
57
|
+
page.on('pageerror', (error) => failures.push(`${current}: ${error.message}`));
|
|
58
|
+
page.on('console', (message) => {
|
|
59
|
+
if (message.type() === 'error') failures.push(`${current}: ${message.text()}`);
|
|
60
|
+
});
|
|
61
|
+
await page.goto(pathToFileURL(resolve(file)).href, { waitUntil: 'load', timeout: timeoutMs });
|
|
62
|
+
await page.waitForFunction(() => Boolean(window.__MWG_SCENARIOS__), undefined, { timeout: timeoutMs });
|
|
63
|
+
const plan = validateScenarioPlan(await page.evaluate(() => window.__MWG_SCENARIOS__.scenarios));
|
|
64
|
+
const selected = plan.filter((scenario) => scenario.id.startsWith(only));
|
|
65
|
+
if (!selected.length) throw new Error(`no scenarios match ${JSON.stringify(only)}`);
|
|
66
|
+
// Refuse a missing step before entering the first scenario.
|
|
67
|
+
await page.evaluate((plan) => {
|
|
68
|
+
const controls = window.__MWG_SCENARIOS__.controls;
|
|
69
|
+
for (const scenario of plan)
|
|
70
|
+
for (const step of [scenario.enter, ...scenario.turn, scenario.verify, scenario.leave].filter(Boolean))
|
|
71
|
+
if (!controls.has(step)) throw new Error(`scene control step is not registered: ${step}`);
|
|
72
|
+
}, selected);
|
|
73
|
+
for (const scenario of selected) {
|
|
74
|
+
current = scenario.id;
|
|
75
|
+
const before = failures.length;
|
|
76
|
+
let played = 0;
|
|
77
|
+
const run = (step) => bounded(page.evaluate((name) => window.__MWG_SCENARIOS__.controls.run(name), step));
|
|
78
|
+
try {
|
|
79
|
+
await run(scenario.enter);
|
|
80
|
+
for (; played < turns; played++) {
|
|
81
|
+
for (const step of scenario.turn) await run(step);
|
|
82
|
+
await run(scenario.verify);
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
failures.push(`${current}: ${error.message}`);
|
|
86
|
+
} finally {
|
|
87
|
+
if (scenario.leave && !expired) {
|
|
88
|
+
try {
|
|
89
|
+
await run(scenario.leave);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
failures.push(`${current}: cleanup: ${error.message}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
scenarios.push({ id: scenario.id, turns: played, ok: failures.length === before });
|
|
96
|
+
// A timed-out promise is still live in the page. Do not overlap another scenario with it.
|
|
97
|
+
if (expired) break;
|
|
98
|
+
}
|
|
99
|
+
return { ok: failures.length === 0, scenarios, failures };
|
|
100
|
+
} finally {
|
|
101
|
+
await browser.close();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
106
|
+
const [file, ...args] = process.argv.slice(2);
|
|
107
|
+
const options = { file };
|
|
108
|
+
try {
|
|
109
|
+
if (!file)
|
|
110
|
+
throw new Error('usage: mwg-scenarios <built index.html> [--turns N] [--timeout-ms N] [--only prefix]');
|
|
111
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
112
|
+
const name = { '--turns': 'turns', '--timeout-ms': 'timeoutMs', '--only': 'only' }[args[i]];
|
|
113
|
+
if (!name || args[i + 1] === undefined) throw new Error(`invalid option: ${args[i]}`);
|
|
114
|
+
options[name] = name === 'only' ? args[i + 1] : Number(args[i + 1]);
|
|
115
|
+
}
|
|
116
|
+
verifyScenarios(options)
|
|
117
|
+
.then((report) => {
|
|
118
|
+
console.log(JSON.stringify(report, null, 2));
|
|
119
|
+
process.exitCode = report.ok ? 0 : 1;
|
|
120
|
+
})
|
|
121
|
+
.catch((error) => {
|
|
122
|
+
console.error(error.message);
|
|
123
|
+
process.exitCode = 1;
|
|
124
|
+
});
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error(error.message);
|
|
127
|
+
process.exitCode = 1;
|
|
128
|
+
}
|
|
129
|
+
}
|