@volter/editor-model-play 0.5.192 → 0.5.194
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/package.json +2 -2
- package/src/model-play.ts +109 -22
- package/src/play-script.ts +102 -26
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/editor-model-play",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.194",
|
|
4
4
|
"author": "Volter AI, Inc.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"type": "module",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
]
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@volter/editor-sdk": "0.5.
|
|
31
|
+
"@volter/editor-sdk": "0.5.194"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"react": "^19.0.0",
|
package/src/model-play.ts
CHANGED
|
@@ -55,29 +55,82 @@ const generations = new Map<string, number>();
|
|
|
55
55
|
const restarted = new Set<string>();
|
|
56
56
|
|
|
57
57
|
/** Who last switched autoplay: the panel's toggle, a verb (CLI or `eval`), a person's input
|
|
58
|
-
* in the game,
|
|
59
|
-
* threw). */
|
|
60
|
-
export type ModelPlayAutoplayBy = 'panel' | 'cli' | 'takeover' | 'script';
|
|
58
|
+
* in the game, the running script no longer offering a bot (replaced, failed, or its bot
|
|
59
|
+
* threw), or the run's time limit. */
|
|
60
|
+
export type ModelPlayAutoplayBy = 'panel' | 'cli' | 'takeover' | 'script' | 'limit';
|
|
61
|
+
/**
|
|
62
|
+
* EVERY AUTOPLAY RUN HAS A LIMIT, in simulation seconds: a bot whose game never ends would
|
|
63
|
+
* otherwise drive forever, and an agent waiting on it would wait forever. Reaching it turns
|
|
64
|
+
* autoplay off, pauses the game and logs `autoplay-limit` (`play-script.ts`).
|
|
65
|
+
*/
|
|
66
|
+
export const MODEL_PLAY_AUTOPLAY_LIMIT_SECONDS = 300;
|
|
61
67
|
export interface ModelPlayAutoplay {
|
|
62
68
|
/** The bot drives: its keys are merged into the keys the script reads. */
|
|
63
69
|
readonly on: boolean;
|
|
64
70
|
/** The running script registered a bot with `play.autoplay`. */
|
|
65
71
|
readonly available: boolean;
|
|
72
|
+
/** The behaviours the running script's bot offers (`play.autoplay({ win, lose })`), in its
|
|
73
|
+
* order; a bare function is the one behaviour `play`. */
|
|
74
|
+
readonly behaviors: readonly string[];
|
|
75
|
+
/** The behaviour driving now, or armed for the next start. */
|
|
76
|
+
readonly behavior: string | null;
|
|
77
|
+
/** This run's limit and the simulation time it began at; null while autoplay is off. */
|
|
78
|
+
readonly limit: number | null;
|
|
79
|
+
readonly since: number | null;
|
|
66
80
|
readonly by: ModelPlayAutoplayBy | null;
|
|
67
81
|
/** Pressed while stopped: the next start turns autoplay on once its script offers a bot. */
|
|
68
82
|
readonly armed: boolean;
|
|
83
|
+
/** A person's key or pointer reached the game in this run. Until one does, nobody drives
|
|
84
|
+
* while autoplay is off — the game runs on no input. */
|
|
85
|
+
readonly person: boolean;
|
|
86
|
+
/** What the bot last said it is doing (`{ keys, state }` from its controller); kept after
|
|
87
|
+
* autoplay goes off, so a limit or takeover still shows where the bot was. */
|
|
88
|
+
readonly state: string | null;
|
|
89
|
+
/** Why the last arm was not taken at start (the bot offers no bot, or not the behaviour it asked
|
|
90
|
+
* for, or several with none named); null otherwise. Said in the panel and the play log. */
|
|
91
|
+
readonly refused: string | null;
|
|
69
92
|
}
|
|
70
|
-
|
|
93
|
+
/** What a switch-on asks for: the behaviour (required when the bot offers several) and the limit. */
|
|
94
|
+
export interface ModelPlayAutoplayRequest {
|
|
95
|
+
readonly behavior?: string | null;
|
|
96
|
+
readonly limit?: number | null;
|
|
97
|
+
}
|
|
98
|
+
const NO_BOT: ModelPlayAutoplay = { on: false, available: false, behaviors: [], behavior: null, limit: null, since: null, by: null, armed: false, person: false, state: null, refused: null };
|
|
71
99
|
const ARMED: ModelPlayAutoplay = { ...NO_BOT, armed: true };
|
|
72
100
|
/** Replaced, never mutated, as the clocks are. Absent is {@link NO_BOT}: a new run's state. */
|
|
73
101
|
const autoplays = new Map<string, ModelPlayAutoplay>();
|
|
74
102
|
|
|
75
|
-
/** A new run's autoplay: off, no bot yet. Play keeps an arm made while stopped
|
|
103
|
+
/** A new run's autoplay: off, no bot yet, nobody driving. Play keeps an arm made while stopped
|
|
104
|
+
* (with the behaviour and limit it asked for); Stop drops it. The behaviours the last run's bot
|
|
105
|
+
* offered are kept, not available: while stopped they are what the panel's picker offers for an
|
|
106
|
+
* arm, which is checked against the bot the next start offers. */
|
|
76
107
|
function resetAutoplay(documentId: string, keepArm: boolean): void {
|
|
77
|
-
|
|
108
|
+
const now = modelPlayAutoplay(documentId);
|
|
109
|
+
if (keepArm && now.armed) autoplays.set(documentId, { ...ARMED, behaviors: now.behaviors, behavior: now.behavior, limit: now.limit });
|
|
110
|
+
else if (now.behaviors.length) autoplays.set(documentId, { ...NO_BOT, behaviors: now.behaviors });
|
|
78
111
|
else autoplays.delete(documentId);
|
|
79
112
|
}
|
|
80
113
|
|
|
114
|
+
/** A limit as given: a positive number of simulation seconds, or the default. */
|
|
115
|
+
function autoplayLimit(limit: number | null | undefined): number {
|
|
116
|
+
if (limit === undefined || limit === null) return MODEL_PLAY_AUTOPLAY_LIMIT_SECONDS;
|
|
117
|
+
if (!Number.isFinite(limit) || limit <= 0) throw new Error(`An autoplay limit is a positive number of simulation seconds; got ${String(limit)}.`);
|
|
118
|
+
return limit;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The behaviour a switch-on drives: the one named, which the bot must offer; with none named,
|
|
122
|
+
* the bot's only behaviour. A bot with several needs one named, so a run always says what its
|
|
123
|
+
* bot was trying to do. */
|
|
124
|
+
function chosenBehavior(behaviors: readonly string[], named: string | null | undefined): string {
|
|
125
|
+
if (named) {
|
|
126
|
+
if (!behaviors.includes(named))
|
|
127
|
+
throw new Error(`This game's bot has no behaviour "${named}"; it offers ${behaviors.map(b => `"${b}"`).join(', ')}.`);
|
|
128
|
+
return named;
|
|
129
|
+
}
|
|
130
|
+
if (behaviors.length === 1) return behaviors[0]!;
|
|
131
|
+
throw new Error(`This game's bot offers several behaviours (${behaviors.join(', ')}); name the one to run: \`play autoplay on <behaviour>\`.`);
|
|
132
|
+
}
|
|
133
|
+
|
|
81
134
|
function publish(): void {
|
|
82
135
|
for (const listener of [...listeners]) listener();
|
|
83
136
|
}
|
|
@@ -250,9 +303,10 @@ function setAutoplay(documentId: string, next: Partial<ModelPlayAutoplay>): void
|
|
|
250
303
|
}
|
|
251
304
|
|
|
252
305
|
/** Switch the running script's bot on or off. On asks for a playing document whose script
|
|
253
|
-
* registered a bot
|
|
254
|
-
*
|
|
255
|
-
|
|
306
|
+
* registered a bot, names the behaviour to drive (required when the bot offers several) and
|
|
307
|
+
* starts the run's limit; off always succeeds, and also drops an arm not yet taken — a person
|
|
308
|
+
* who takes over while the game is still starting is driving, and the arm must not override them. */
|
|
309
|
+
export function setModelPlayAutoplay(documentId: string, on: boolean, by: ModelPlayAutoplayBy, request: ModelPlayAutoplayRequest = {}): void {
|
|
256
310
|
const now = modelPlayAutoplay(documentId);
|
|
257
311
|
if (on && !playing.has(documentId))
|
|
258
312
|
throw new Error(`Autoplay is available once the game is running, and nothing is playing in ${documentId}; \`play\` starts it, then \`play autoplay on\`.`);
|
|
@@ -261,29 +315,62 @@ export function setModelPlayAutoplay(documentId: string, on: boolean, by: ModelP
|
|
|
261
315
|
throw new Error(clock.running
|
|
262
316
|
? 'No autoplay: this game doesn’t provide a bot — its play script registers none with `play.autoplay(controller)`.'
|
|
263
317
|
: `Autoplay is available once the game is running, and it is not running yet${clock.failure ? ` (${clock.failure})` : ''}.`);
|
|
264
|
-
if (on
|
|
318
|
+
if (on) {
|
|
319
|
+
const behavior = chosenBehavior(now.behaviors, request.behavior);
|
|
320
|
+
const limit = autoplayLimit(request.limit);
|
|
321
|
+
// Switching on again (another behaviour, a new limit) is a new run of the bot.
|
|
322
|
+
setAutoplay(documentId, { on, by, behavior, limit, since: clock.time, armed: false, state: null, refused: null });
|
|
323
|
+
} else if (now.on || now.armed) setAutoplay(documentId, { on, by, armed: false, since: null });
|
|
265
324
|
}
|
|
266
325
|
|
|
267
326
|
/** Arm (or disarm) autoplay for the next start, while stopped: the Game panel's Autoplay button
|
|
268
|
-
* before Play.
|
|
269
|
-
|
|
327
|
+
* before Play. The behaviour and limit are checked when the start takes the arm, since the bot
|
|
328
|
+
* is offered only then. Disarming always succeeds. */
|
|
329
|
+
export function armModelPlayAutoplay(documentId: string, armed: boolean, request: ModelPlayAutoplayRequest = {}): void {
|
|
270
330
|
if (armed && playing.has(documentId))
|
|
271
331
|
throw new Error(`${documentId} is already playing; switch autoplay on instead of arming it.`);
|
|
272
|
-
if (
|
|
332
|
+
if (armed) setAutoplay(documentId, { armed, refused: null, behavior: request.behavior ?? null, limit: request.limit === undefined || request.limit === null ? null : autoplayLimit(request.limit) });
|
|
333
|
+
else if (modelPlayAutoplay(documentId).armed) setAutoplay(documentId, { armed, behavior: null, limit: null });
|
|
273
334
|
}
|
|
274
335
|
|
|
275
|
-
/** The runner's report that a script has run its first update, offering a bot or
|
|
276
|
-
* taken here: on with a bot
|
|
277
|
-
|
|
336
|
+
/** The runner's report that a script has run its first update, offering a bot's behaviours or
|
|
337
|
+
* none. An arm is taken here: on with a bot that has the behaviour it asked for (or only one),
|
|
338
|
+
* dropped otherwise. */
|
|
339
|
+
export function settleModelPlayAutoplay(documentId: string, behaviors: readonly string[]): void {
|
|
278
340
|
const now = modelPlayAutoplay(documentId);
|
|
279
341
|
if (!playing.has(documentId)) return;
|
|
280
|
-
if (!now.armed) { setModelPlayAutoplayAvailable(documentId,
|
|
281
|
-
|
|
342
|
+
if (!now.armed) { setModelPlayAutoplayAvailable(documentId, behaviors); return; }
|
|
343
|
+
let behavior: string | null = null;
|
|
344
|
+
let refused: string | null = null;
|
|
345
|
+
if (!behaviors.length) refused = 'the play script registers no bot with play.autoplay';
|
|
346
|
+
else {
|
|
347
|
+
try { behavior = chosenBehavior(behaviors, now.behavior); }
|
|
348
|
+
catch (error) { refused = error instanceof Error ? error.message : String(error); }
|
|
349
|
+
}
|
|
350
|
+
setAutoplay(documentId, behavior !== null
|
|
351
|
+
? { available: true, behaviors, armed: false, on: true, by: 'panel', behavior, limit: now.limit ?? MODEL_PLAY_AUTOPLAY_LIMIT_SECONDS, since: modelPlayClock(documentId).time, state: null, refused: null }
|
|
352
|
+
: { available: behaviors.length > 0, behaviors, armed: false, behavior: null, limit: null, by: 'script', refused });
|
|
282
353
|
}
|
|
283
354
|
|
|
284
|
-
/** The runner's report of
|
|
285
|
-
|
|
355
|
+
/** The runner's report of the behaviours the running script's bot offers (none: no bot). Losing
|
|
356
|
+
* the bot, or the behaviour driving, turns autoplay off. */
|
|
357
|
+
export function setModelPlayAutoplayAvailable(documentId: string, behaviors: readonly string[]): void {
|
|
286
358
|
const now = modelPlayAutoplay(documentId);
|
|
287
|
-
if (
|
|
288
|
-
|
|
359
|
+
if (!playing.has(documentId)) return;
|
|
360
|
+
const available = behaviors.length > 0;
|
|
361
|
+
const same = now.available === available && now.behaviors.length === behaviors.length && now.behaviors.every((b, i) => b === behaviors[i]);
|
|
362
|
+
if (same) return;
|
|
363
|
+
const lost = now.on && (now.behavior === null || !behaviors.includes(now.behavior));
|
|
364
|
+
setAutoplay(documentId, lost ? { available, behaviors, on: false, by: 'script', since: null } : { available, behaviors });
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** The runner's report of what the driving bot says it is doing; announced only when it changes. */
|
|
368
|
+
export function setModelPlayBotState(documentId: string, state: string | null): void {
|
|
369
|
+
if (playing.has(documentId) && modelPlayAutoplay(documentId).state !== state) setAutoplay(documentId, { state });
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** The runner's report that a person's key or pointer reached the game: from now on in this run,
|
|
373
|
+
* autoplay off means the person drives. */
|
|
374
|
+
export function noteModelPlayPerson(documentId: string): void {
|
|
375
|
+
if (playing.has(documentId) && !modelPlayAutoplay(documentId).person) setAutoplay(documentId, { person: true });
|
|
289
376
|
}
|
package/src/play-script.ts
CHANGED
|
@@ -74,10 +74,13 @@ import {
|
|
|
74
74
|
modelPlayAutoplay,
|
|
75
75
|
modelPlayClock,
|
|
76
76
|
modelPlayGeneration,
|
|
77
|
+
noteModelPlayPerson,
|
|
77
78
|
registerModelPlayStop,
|
|
78
79
|
setModelPlayAutoplay,
|
|
80
|
+
setModelPlayBotState,
|
|
79
81
|
setModelPlayFailure,
|
|
80
82
|
setModelPlayAutoplayAvailable,
|
|
83
|
+
setModelPlayPaused,
|
|
81
84
|
settleModelPlayAutoplay,
|
|
82
85
|
subscribeModelPlayClock,
|
|
83
86
|
takeModelPlayStep,
|
|
@@ -136,20 +139,30 @@ export interface ModelPlayContext {
|
|
|
136
139
|
* authored opacity. Its colour is untouched, and the copies are `tint`'s. */
|
|
137
140
|
setOpacity(object: THREE.Object3D | string, opacity: number | null): void;
|
|
138
141
|
/**
|
|
139
|
-
* Offer this game's bot
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
142
|
+
* Offer this game's bot, as named BEHAVIOURS: what the bot sets out to do, each its own
|
|
143
|
+
* controller. While the person (or an agent, `play autoplay on <behaviour>`) has autoplay on in
|
|
144
|
+
* the Game panel, the chosen behaviour's controller is called before each `update` and the keys
|
|
145
|
+
* it answers are held for that update, merged into `keys`. Offer the outcomes worth checking —
|
|
146
|
+
* one that plays to win and one that loses on purpose — so a run tests an ending by saying so,
|
|
147
|
+
* not by leaving the game alone. A bare function is the one behaviour `play`. Every run has a
|
|
148
|
+
* limit in simulation seconds (`play autoplay on <behaviour> --for <seconds>`, 300 unless
|
|
149
|
+
* given): reaching it turns autoplay off and pauses the game. Autoplay is off at every Play and
|
|
150
|
+
* Restart, and a person's key or pointer in the game turns it off. One bot per script:
|
|
151
|
+
* registering again replaces it, `null` withdraws it, and it goes with the script. Never bind
|
|
152
|
+
* autoplay to a game key, and never start it from the script.
|
|
145
153
|
*
|
|
146
|
-
* play.autoplay(
|
|
154
|
+
* play.autoplay({
|
|
155
|
+
* win: ({ dt }) => (car.speed < 20 ? ['ArrowUp'] : []),
|
|
156
|
+
* lose: () => ['ArrowLeft'], // drives off the track
|
|
157
|
+
* });
|
|
147
158
|
*/
|
|
148
|
-
autoplay(
|
|
159
|
+
autoplay(bot: ModelPlayAutoplayController | Readonly<Record<string, ModelPlayAutoplayController>> | null): void;
|
|
149
160
|
}
|
|
150
161
|
|
|
151
162
|
/** What the bot is handed before each `update` it drives. */
|
|
152
163
|
export interface ModelPlayAutoplayInput {
|
|
164
|
+
/** The behaviour driving (`play autoplay on <behaviour>`), for a controller shared by several. */
|
|
165
|
+
readonly behavior: string;
|
|
153
166
|
/** The `dt` the coming `update` is handed. */
|
|
154
167
|
readonly dt: number;
|
|
155
168
|
/** Simulation seconds and the update's number, as that update's log entries carry them. */
|
|
@@ -158,8 +171,27 @@ export interface ModelPlayAutoplayInput {
|
|
|
158
171
|
/** The keys the person holds, by `KeyboardEvent.code`; the bot's are merged with them. */
|
|
159
172
|
readonly keys: ReadonlySet<string>;
|
|
160
173
|
}
|
|
161
|
-
/**
|
|
162
|
-
|
|
174
|
+
/**
|
|
175
|
+
* A game's bot: the keys (`KeyboardEvent.code`) it holds for the coming update — or those keys
|
|
176
|
+
* with what it is doing now, `{ keys, state: 'heading to nest 2' }`. The state is the bot
|
|
177
|
+
* explaining itself: the Game panel shows it beside the driver, `play state` reports it, and
|
|
178
|
+
* each change is a `bot-state` entry in the play log. Say intentions and their reasons (a goal,
|
|
179
|
+
* a target, why it is waiting), not per-frame numbers.
|
|
180
|
+
*/
|
|
181
|
+
export type ModelPlayAutoplayController = (input: ModelPlayAutoplayInput) =>
|
|
182
|
+
Iterable<string> | { readonly keys?: Iterable<string> | null; readonly state?: string | null } | null | undefined;
|
|
183
|
+
|
|
184
|
+
/** The longest bot state kept: a line for the panel, not a report. */
|
|
185
|
+
const BOT_STATE_CHARACTERS = 160;
|
|
186
|
+
|
|
187
|
+
/** A controller's answer as its keys and its state (undefined: it said nothing about its state). */
|
|
188
|
+
function botAnswer(answer: ReturnType<ModelPlayAutoplayController>): { keys: Iterable<string>; state: string | null | undefined } {
|
|
189
|
+
if (answer === null || answer === undefined) return { keys: [], state: undefined };
|
|
190
|
+
if (typeof (answer as Iterable<string>)[Symbol.iterator] === 'function') return { keys: answer as Iterable<string>, state: undefined };
|
|
191
|
+
const { keys, state } = answer as { keys?: Iterable<string> | null; state?: string | null };
|
|
192
|
+
const said = state === undefined ? undefined : state === null ? null : String(state).replace(/\s+/g, ' ').trim().slice(0, BOT_STATE_CHARACTERS) || null;
|
|
193
|
+
return { keys: keys ?? [], state: said };
|
|
194
|
+
}
|
|
163
195
|
|
|
164
196
|
export interface ModelPlayGame {
|
|
165
197
|
/** Once per drawn frame, with the simulation seconds since the last call (at most a tenth).
|
|
@@ -174,10 +206,32 @@ export interface ModelPlayGame {
|
|
|
174
206
|
* logged once — per script, so a typo that survives a reload is said again for the new one. */
|
|
175
207
|
interface Script {
|
|
176
208
|
value: boolean;
|
|
177
|
-
bot
|
|
209
|
+
/** The bot's behaviours by name, in the order offered; null without a bot. */
|
|
210
|
+
bot: Readonly<Record<string, ModelPlayAutoplayController>> | null;
|
|
178
211
|
readonly unknown: Set<string>;
|
|
179
212
|
}
|
|
180
213
|
|
|
214
|
+
/** The behaviour names a script's bot offers, in its order. */
|
|
215
|
+
function behaviorsOf(script: Script | undefined): string[] {
|
|
216
|
+
return script?.bot ? Object.keys(script.bot) : [];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** `play.autoplay`'s argument as behaviours by name: a bare function is the one behaviour `play`. */
|
|
220
|
+
function botBehaviors(bot: unknown): Readonly<Record<string, ModelPlayAutoplayController>> | null {
|
|
221
|
+
if (bot === null) return null;
|
|
222
|
+
if (typeof bot === 'function') return { play: bot as ModelPlayAutoplayController };
|
|
223
|
+
if (typeof bot === 'object' && !Array.isArray(bot)) {
|
|
224
|
+
const entries = Object.entries(bot as Record<string, unknown>);
|
|
225
|
+
if (entries.length === 0) throw new Error('play.autoplay was given no behaviours: offer at least one, `{ win: (input) => keys }`.');
|
|
226
|
+
for (const [name, controller] of entries) {
|
|
227
|
+
if (!/^[A-Za-z][\w-]{0,39}$/.test(name)) throw new Error(`play.autoplay behaviour "${name}" is not a name: letters, digits, - and _, starting with a letter.`);
|
|
228
|
+
if (typeof controller !== 'function') throw new Error(`play.autoplay behaviour "${name}" is not a function (the controller).`);
|
|
229
|
+
}
|
|
230
|
+
return Object.freeze({ ...(bot as Record<string, ModelPlayAutoplayController>) });
|
|
231
|
+
}
|
|
232
|
+
throw new Error('play.autoplay takes a function (the bot), its behaviours by name (`{ win, lose }`), or null.');
|
|
233
|
+
}
|
|
234
|
+
|
|
181
235
|
/** The longest `dt` one update is handed; longer scaled frames are split (`frameUpdates`). */
|
|
182
236
|
const MAX_UPDATE_SECONDS = 0.1;
|
|
183
237
|
|
|
@@ -296,10 +350,11 @@ export function runPlayScript(options: {
|
|
|
296
350
|
if (now.speed !== seen.speed) run.append('play', 'speed', { speed: now.speed, from: seen.speed });
|
|
297
351
|
seen = now;
|
|
298
352
|
const bot = modelPlayAutoplay(options.documentId);
|
|
299
|
-
if (bot.on !== seenBot.on
|
|
353
|
+
if (bot.on !== seenBot.on || (bot.on && (bot.behavior !== seenBot.behavior || bot.since !== seenBot.since)))
|
|
354
|
+
run.append('play', bot.on ? 'autoplay-on' : 'autoplay-off', bot.on ? { by: bot.by, behavior: bot.behavior, limit: bot.limit } : { by: bot.by, behavior: seenBot.behavior });
|
|
300
355
|
// An arm dropped before it was taken: a takeover, or a script that offers no bot. (Stop's
|
|
301
356
|
// reset has no `by`, and its `play-stop` says enough.)
|
|
302
|
-
else if (seenBot.armed && !bot.armed && bot.by !== null) run.append('play', 'autoplay-off', { by: bot.by, armed: true });
|
|
357
|
+
else if (seenBot.armed && !bot.armed && bot.by !== null) run.append('play', 'autoplay-off', { by: bot.by, armed: true, ...(bot.refused ? { why: bot.refused } : {}) });
|
|
303
358
|
seenBot = bot;
|
|
304
359
|
});
|
|
305
360
|
options.container.style.opacity = '0';
|
|
@@ -326,10 +381,9 @@ export function runPlayScript(options: {
|
|
|
326
381
|
const target = alive.value ? objectOf(alive, object, 'setOpacity') : null;
|
|
327
382
|
if (target) materials.setOpacity(target, opacity);
|
|
328
383
|
},
|
|
329
|
-
autoplay(
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
if (alive.value) alive.bot = controller;
|
|
384
|
+
autoplay(bot) {
|
|
385
|
+
const behaviors = botBehaviors(bot);
|
|
386
|
+
if (alive.value) alive.bot = behaviors;
|
|
333
387
|
},
|
|
334
388
|
});
|
|
335
389
|
const scripts = new WeakMap<ModelPlayGame, Script>();
|
|
@@ -450,7 +504,7 @@ export function runPlayScript(options: {
|
|
|
450
504
|
else if (!transition.acceptingKeys()) keys.clear();
|
|
451
505
|
else for (const key of heldKeys) keys.add(key);
|
|
452
506
|
// The panel's toggle is enabled by the bot the running script offers, as of the last frame.
|
|
453
|
-
if (current()) setModelPlayAutoplayAvailable(options.documentId, game !== null
|
|
507
|
+
if (current()) setModelPlayAutoplayAvailable(options.documentId, game !== null ? behaviorsOf(scripts.get(game)) : []);
|
|
454
508
|
const updates = frameUpdates(options.documentId, deltaSeconds);
|
|
455
509
|
if (updates.length === 0) {
|
|
456
510
|
// PAUSED: no update, so nothing states the camera; hold the pose the last frame drew.
|
|
@@ -478,13 +532,20 @@ export function runPlayScript(options: {
|
|
|
478
532
|
/** Merge the bot's keys into `keys` for one update; true when it did, and the caller then
|
|
479
533
|
* restores the person's keys after that update, however it ends. */
|
|
480
534
|
const drive = (script: ModelPlayGame, dt: number): boolean => {
|
|
481
|
-
|
|
482
|
-
//
|
|
483
|
-
|
|
484
|
-
|
|
535
|
+
// Asked again per update: the bot's own failure, a takeover, its limit, or
|
|
536
|
+
// `play.autoplay(null)` ends it mid-frame.
|
|
537
|
+
const now = modelPlayAutoplay(options.documentId);
|
|
538
|
+
const behavior = driving && now.on ? now.behavior : null;
|
|
539
|
+
const bot = behavior !== null ? scripts.get(script)?.bot?.[behavior] : undefined;
|
|
540
|
+
if (!bot || behavior === null) return false;
|
|
485
541
|
restorePerson();
|
|
486
542
|
try {
|
|
487
|
-
|
|
543
|
+
const answer = botAnswer(bot({ behavior, dt, simT: clock.time + simulated, tick: clock.tick + ran, keys: person }));
|
|
544
|
+
for (const key of answer.keys) keys.add(String(key));
|
|
545
|
+
if (answer.state !== undefined && answer.state !== now.state) {
|
|
546
|
+
setModelPlayBotState(options.documentId, answer.state);
|
|
547
|
+
run.append('play', 'bot-state', { behavior, state: answer.state });
|
|
548
|
+
}
|
|
488
549
|
return true;
|
|
489
550
|
} catch (error) {
|
|
490
551
|
restorePerson();
|
|
@@ -524,7 +585,8 @@ export function runPlayScript(options: {
|
|
|
524
585
|
// NOW THE SCRIPT HAS SAID WHETHER IT OFFERS A BOT (its default export and first update
|
|
525
586
|
// are where `play.autoplay` is called): said before `running`, so no reader sees a running
|
|
526
587
|
// game with its bot not yet counted, and an arm made while stopped is taken or dropped.
|
|
527
|
-
const
|
|
588
|
+
const behaviors = behaviorsOf(scripts.get(next.game));
|
|
589
|
+
const offered = behaviors.length > 0;
|
|
528
590
|
if (!offered && offeredBot !== false)
|
|
529
591
|
run.append('play', 'autoplay-unavailable', { why: 'the play script registers no bot with play.autoplay(controller)' });
|
|
530
592
|
offeredBot = offered;
|
|
@@ -532,7 +594,7 @@ export function runPlayScript(options: {
|
|
|
532
594
|
// the stage was still preparing, before these listeners existed, and has only repeated
|
|
533
595
|
// since. The person is driving, so an arm waiting for this update is dropped.
|
|
534
596
|
if (heldKeys.size > 0 && modelPlayAutoplay(options.documentId).armed) takeover();
|
|
535
|
-
settleModelPlayAutoplay(options.documentId,
|
|
597
|
+
settleModelPlayAutoplay(options.documentId, behaviors);
|
|
536
598
|
setModelPlayFailure(options.documentId, null);
|
|
537
599
|
startedAt = Date.now();
|
|
538
600
|
endedAt = null;
|
|
@@ -551,6 +613,17 @@ export function runPlayScript(options: {
|
|
|
551
613
|
}
|
|
552
614
|
advanceModelPlayClock(options.documentId, simulated, ran);
|
|
553
615
|
ran = 0;
|
|
616
|
+
// THE RUN'S LIMIT: a bot still driving when its simulation seconds are spent is stopped and
|
|
617
|
+
// the game held, so a game that never ends cannot keep a bot (and whoever waits on it) going.
|
|
618
|
+
const bot = modelPlayAutoplay(options.documentId);
|
|
619
|
+
if (current() && bot.on && bot.limit !== null && bot.since !== null) {
|
|
620
|
+
const now = modelPlayClock(options.documentId).time;
|
|
621
|
+
if (now - bot.since >= bot.limit) {
|
|
622
|
+
run.append('play', 'autoplay-limit', { behavior: bot.behavior, limit: bot.limit, ran: now - bot.since, state: bot.state });
|
|
623
|
+
setModelPlayAutoplay(options.documentId, false, 'limit');
|
|
624
|
+
setModelPlayPaused(options.documentId, true);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
554
627
|
transition.frame(camera(), deltaSeconds);
|
|
555
628
|
options.container.style.opacity = String(transition.hudOpacity());
|
|
556
629
|
if (firstFrame) { firstFrame = false; options.ready(); }
|
|
@@ -571,9 +644,12 @@ export function runPlayScript(options: {
|
|
|
571
644
|
// by hand.
|
|
572
645
|
const surface = options.container.parentElement ?? options.container;
|
|
573
646
|
const takeover = (): void => {
|
|
647
|
+
if (!current()) return;
|
|
648
|
+
// Whether or not a bot was driving, the person is driving from here on in this run.
|
|
649
|
+
noteModelPlayPerson(options.documentId);
|
|
574
650
|
// An arm still waiting for the bot counts too: the person is driving before it could start.
|
|
575
651
|
const now = modelPlayAutoplay(options.documentId);
|
|
576
|
-
if (
|
|
652
|
+
if (now.on || now.armed) setModelPlayAutoplay(options.documentId, false, 'takeover');
|
|
577
653
|
};
|
|
578
654
|
const onKeyDown = (event: KeyboardEvent): void => {
|
|
579
655
|
if (!surfaceAcceptsKey(event)) return;
|