@seriousgaming/sdk 0.0.0-stage → 1.0.0-alpha.2
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/LICENSE +202 -0
- package/NOTICE +1 -0
- package/README.md +33 -2
- package/lib/build/callbacks.d.ts +998 -0
- package/lib/build/callbacks.js +32 -0
- package/lib/build/context-merge.d.ts +59 -0
- package/lib/build/context-merge.js +118 -0
- package/lib/build/document.d.ts +36 -0
- package/lib/build/document.js +8 -0
- package/lib/build/encoding.d.ts +26 -0
- package/lib/build/encoding.js +78 -0
- package/lib/build/endpoints.d.ts +421 -0
- package/lib/build/endpoints.js +46 -0
- package/lib/build/hooks.d.ts +74 -0
- package/lib/build/hooks.js +1 -0
- package/lib/build/index.d.ts +18 -0
- package/lib/build/index.js +16 -0
- package/lib/build/llm.d.ts +166 -0
- package/lib/build/llm.js +17 -0
- package/lib/build/resource-schema.d.ts +1048 -0
- package/lib/build/resource-schema.js +7 -0
- package/lib/build/scheduling.d.ts +112 -0
- package/lib/build/scheduling.js +217 -0
- package/lib/build/seed.d.ts +51 -0
- package/lib/build/seed.js +89 -0
- package/lib/build/signatures.d.ts +372 -0
- package/lib/build/signatures.js +1 -0
- package/lib/build/speech.d.ts +102 -0
- package/lib/build/speech.js +11 -0
- package/lib/build/tick-validation.d.ts +23 -0
- package/lib/build/tick-validation.js +96 -0
- package/lib/build/timer.d.ts +74 -0
- package/lib/build/timer.js +1 -0
- package/lib/build/validators.d.ts +116 -0
- package/lib/build/validators.js +1 -0
- package/lib/commands.d.ts +357 -0
- package/lib/commands.js +14 -0
- package/lib/e2e.d.ts +238 -0
- package/lib/e2e.js +248 -0
- package/lib/projection.d.ts +266 -0
- package/lib/projection.js +23 -0
- package/lib/testing/index.d.ts +13 -0
- package/lib/testing/index.js +13 -0
- package/lib/testing/logic-args.d.ts +177 -0
- package/lib/testing/logic-args.js +229 -0
- package/lib/testing/stub-services.d.ts +41 -0
- package/lib/testing/stub-services.js +176 -0
- package/lib/ui/CrossTeamDataContext.d.ts +36 -0
- package/lib/ui/CrossTeamDataContext.js +32 -0
- package/lib/ui/PresentationContext.d.ts +72 -0
- package/lib/ui/PresentationContext.js +13 -0
- package/lib/ui/PresentationWrapper.d.ts +12 -0
- package/lib/ui/PresentationWrapper.js +78 -0
- package/lib/ui/SessionUserInterfaceCallbacksContext.d.ts +3 -0
- package/lib/ui/SessionUserInterfaceCallbacksContext.js +7 -0
- package/lib/ui/SessionUserInterfaceDataContext.d.ts +103 -0
- package/lib/ui/SessionUserInterfaceDataContext.js +17 -0
- package/lib/ui/SessionUserInterfaceHookFailuresContext.d.ts +44 -0
- package/lib/ui/SessionUserInterfaceHookFailuresContext.js +15 -0
- package/lib/ui/SessionUserInterfaceHostOverlaysContext.d.ts +23 -0
- package/lib/ui/SessionUserInterfaceHostOverlaysContext.js +23 -0
- package/lib/ui/SessionUserInterfaceLocaleContext.d.ts +11 -0
- package/lib/ui/SessionUserInterfaceLocaleContext.js +13 -0
- package/lib/ui/SessionUserInterfaceMetaContext.d.ts +39 -0
- package/lib/ui/SessionUserInterfaceMetaContext.js +13 -0
- package/lib/ui/SessionUserInterfaceParticipantsContext.d.ts +12 -0
- package/lib/ui/SessionUserInterfaceParticipantsContext.js +16 -0
- package/lib/ui/SessionUserInterfaceTeamConfigurationContext.d.ts +53 -0
- package/lib/ui/SessionUserInterfaceTeamConfigurationContext.js +23 -0
- package/lib/ui/SessionUserInterfaceTeamLinksContext.d.ts +56 -0
- package/lib/ui/SessionUserInterfaceTeamLinksContext.js +12 -0
- package/lib/ui/SessionUserInterfaceTeamsContext.d.ts +13 -0
- package/lib/ui/SessionUserInterfaceTeamsContext.js +13 -0
- package/lib/ui/SessionUserInterfaceThemeContext.d.ts +9 -0
- package/lib/ui/SessionUserInterfaceThemeContext.js +7 -0
- package/lib/ui/SessionUserInterfaceThemePreferenceContext.d.ts +3 -0
- package/lib/ui/SessionUserInterfaceThemePreferenceContext.js +7 -0
- package/lib/ui/SessionUserInterfaceWalkthroughLinkContext.d.ts +42 -0
- package/lib/ui/SessionUserInterfaceWalkthroughLinkContext.js +26 -0
- package/lib/ui/SessionUserInterfaceWrapper.d.ts +8 -0
- package/lib/ui/SessionUserInterfaceWrapper.js +321 -0
- package/lib/ui/SettingsEditorCallbacksContext.d.ts +3 -0
- package/lib/ui/SettingsEditorCallbacksContext.js +7 -0
- package/lib/ui/SettingsEditorContext.d.ts +33 -0
- package/lib/ui/SettingsEditorContext.js +16 -0
- package/lib/ui/SettingsEditorLocaleContext.d.ts +13 -0
- package/lib/ui/SettingsEditorLocaleContext.js +15 -0
- package/lib/ui/SettingsEditorThemeContext.d.ts +9 -0
- package/lib/ui/SettingsEditorThemeContext.js +7 -0
- package/lib/ui/SettingsEditorThemePreferenceContext.d.ts +3 -0
- package/lib/ui/SettingsEditorThemePreferenceContext.js +7 -0
- package/lib/ui/SettingsEditorWrapper.d.ts +13 -0
- package/lib/ui/SettingsEditorWrapper.js +74 -0
- package/lib/ui/UserInterfaceCallbacksContext.d.ts +3 -0
- package/lib/ui/UserInterfaceCallbacksContext.js +7 -0
- package/lib/ui/UserInterfaceDataContext.d.ts +78 -0
- package/lib/ui/UserInterfaceDataContext.js +11 -0
- package/lib/ui/UserInterfaceLocaleContext.d.ts +13 -0
- package/lib/ui/UserInterfaceLocaleContext.js +15 -0
- package/lib/ui/UserInterfaceOwnTeamContext.d.ts +25 -0
- package/lib/ui/UserInterfaceOwnTeamContext.js +26 -0
- package/lib/ui/UserInterfaceOwnTeamJoinLinkContext.d.ts +52 -0
- package/lib/ui/UserInterfaceOwnTeamJoinLinkContext.js +13 -0
- package/lib/ui/UserInterfaceParticipantContext.d.ts +16 -0
- package/lib/ui/UserInterfaceParticipantContext.js +24 -0
- package/lib/ui/UserInterfaceParticipantsContext.d.ts +12 -0
- package/lib/ui/UserInterfaceParticipantsContext.js +13 -0
- package/lib/ui/UserInterfaceRunKindContext.d.ts +31 -0
- package/lib/ui/UserInterfaceRunKindContext.js +31 -0
- package/lib/ui/UserInterfaceTeamsContext.d.ts +12 -0
- package/lib/ui/UserInterfaceTeamsContext.js +13 -0
- package/lib/ui/UserInterfaceThemeContext.d.ts +9 -0
- package/lib/ui/UserInterfaceThemeContext.js +7 -0
- package/lib/ui/UserInterfaceThemePreferenceContext.d.ts +3 -0
- package/lib/ui/UserInterfaceThemePreferenceContext.js +7 -0
- package/lib/ui/UserInterfaceTimerContext.d.ts +10 -0
- package/lib/ui/UserInterfaceTimerContext.js +12 -0
- package/lib/ui/UserInterfaceValidationContext.d.ts +25 -0
- package/lib/ui/UserInterfaceValidationContext.js +14 -0
- package/lib/ui/UserInterfaceViewerRoleContext.d.ts +14 -0
- package/lib/ui/UserInterfaceViewerRoleContext.js +17 -0
- package/lib/ui/UserInterfaceWrapper.d.ts +8 -0
- package/lib/ui/UserInterfaceWrapper.js +219 -0
- package/lib/ui/expectedParentOrigin.d.ts +10 -0
- package/lib/ui/expectedParentOrigin.js +32 -0
- package/lib/ui/index.d.ts +50 -0
- package/lib/ui/index.js +52 -0
- package/lib/ui/internal/SurfaceGate.d.ts +19 -0
- package/lib/ui/internal/SurfaceGate.js +17 -0
- package/lib/ui/internal/SurfaceScope.d.ts +17 -0
- package/lib/ui/internal/SurfaceScope.js +16 -0
- package/lib/ui/internal/createContexts.d.ts +50 -0
- package/lib/ui/internal/createContexts.js +32 -0
- package/lib/ui/internal/document-attributes.d.ts +9 -0
- package/lib/ui/internal/document-attributes.js +87 -0
- package/lib/ui/internal/surface-timer.d.ts +3 -0
- package/lib/ui/internal/surface-timer.js +11 -0
- package/lib/ui/internal/useLatestRef.d.ts +7 -0
- package/lib/ui/internal/useLatestRef.js +11 -0
- package/lib/ui/internal/useOptimisticMerge.d.ts +19 -0
- package/lib/ui/internal/useOptimisticMerge.js +28 -0
- package/lib/ui/internal/useSurfaceControls.d.ts +52 -0
- package/lib/ui/internal/useSurfaceControls.js +19 -0
- package/lib/ui/internal/useTypedPastState.d.ts +17 -0
- package/lib/ui/internal/useTypedPastState.js +17 -0
- package/lib/ui/mutation-commit.d.ts +32 -0
- package/lib/ui/mutation-commit.js +1 -0
- package/lib/ui/mutation-outcome.d.ts +66 -0
- package/lib/ui/mutation-outcome.js +49 -0
- package/lib/ui/request-id.d.ts +18 -0
- package/lib/ui/request-id.js +44 -0
- package/lib/ui/testing.d.ts +365 -0
- package/lib/ui/testing.js +572 -0
- package/lib/ui/tick-snapshot.d.ts +28 -0
- package/lib/ui/tick-snapshot.js +34 -0
- package/lib/ui/ui-protocol.d.ts +799 -0
- package/lib/ui/ui-protocol.js +50 -0
- package/lib/ui/useAsset.d.ts +80 -0
- package/lib/ui/useAsset.js +139 -0
- package/lib/ui/useEndpointCallbacks.d.ts +31 -0
- package/lib/ui/useEndpointCallbacks.js +68 -0
- package/lib/ui/useHostChannel.d.ts +90 -0
- package/lib/ui/useHostChannel.js +338 -0
- package/lib/ui/useMutationReporter.d.ts +100 -0
- package/lib/ui/useMutationReporter.js +97 -0
- package/lib/ui/usePastState.d.ts +39 -0
- package/lib/ui/usePastState.js +63 -0
- package/lib/ui/useViewerStorage.d.ts +54 -0
- package/lib/ui/useViewerStorage.js +141 -0
- package/package.json +89 -3
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { TickControllerResult } from './signatures.js';
|
|
2
|
+
/**
|
|
3
|
+
* Options accepted by all schedule-computation helpers.
|
|
4
|
+
*/
|
|
5
|
+
export interface ScheduleOptions {
|
|
6
|
+
/**
|
|
7
|
+
* IANA timezone name (e.g. `'Australia/Sydney'`, `'America/New_York'`).
|
|
8
|
+
* Defaults to `'UTC'`. DST transitions are handled automatically via
|
|
9
|
+
* `Intl.DateTimeFormat`.
|
|
10
|
+
*/
|
|
11
|
+
timezone?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Override for "now" in ms since epoch — useful for deterministic tests.
|
|
14
|
+
* Defaults to `Date.now()`.
|
|
15
|
+
*/
|
|
16
|
+
now?: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Compute the next occurrence of a daily time.
|
|
20
|
+
*
|
|
21
|
+
* @param hours Hour of the day (0–23)
|
|
22
|
+
* @param minutes Minute of the hour (0–59)
|
|
23
|
+
* @param options Optional timezone and `now` override
|
|
24
|
+
* @returns Unix timestamp in milliseconds of the next occurrence
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* // Next midnight UTC
|
|
29
|
+
* nextDailyAt(0, 0);
|
|
30
|
+
*
|
|
31
|
+
* // Next midnight in Sydney (handles DST)
|
|
32
|
+
* nextDailyAt(0, 0, { timezone: 'Australia/Sydney' });
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare function nextDailyAt(hours: number, minutes: number, options?: ScheduleOptions): number;
|
|
36
|
+
/**
|
|
37
|
+
* Compute the next occurrence of a weekly time.
|
|
38
|
+
*
|
|
39
|
+
* @param dayOfWeek 0 = Sunday, 1 = Monday, …, 6 = Saturday
|
|
40
|
+
* @param hours Hour of the day (0–23)
|
|
41
|
+
* @param minutes Minute of the hour (0–59)
|
|
42
|
+
* @param options Optional timezone and `now` override
|
|
43
|
+
* @returns Unix timestamp in milliseconds of the next occurrence
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* // Next Monday at 09:00 UTC
|
|
48
|
+
* nextWeeklyAt(1, 9, 0);
|
|
49
|
+
*
|
|
50
|
+
* // Next Friday at 17:00 in New York
|
|
51
|
+
* nextWeeklyAt(5, 17, 0, { timezone: 'America/New_York' });
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export declare function nextWeeklyAt(dayOfWeek: number, hours: number, minutes: number, options?: ScheduleOptions): number;
|
|
55
|
+
/**
|
|
56
|
+
* Pick the soonest future time from a list of daily times.
|
|
57
|
+
*
|
|
58
|
+
* @param times Array of `{ hours, minutes }` pairs
|
|
59
|
+
* @param options Optional timezone and `now` override
|
|
60
|
+
* @returns Unix timestamp in milliseconds of the soonest next occurrence
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```ts
|
|
64
|
+
* // Next occurrence of 09:00 or 17:00 UTC
|
|
65
|
+
* nextDailyFromSchedule([
|
|
66
|
+
* { hours: 9, minutes: 0 },
|
|
67
|
+
* { hours: 17, minutes: 0 },
|
|
68
|
+
* ]);
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export declare function nextDailyFromSchedule(times: Array<{
|
|
72
|
+
hours: number;
|
|
73
|
+
minutes: number;
|
|
74
|
+
}>, options?: ScheduleOptions): number;
|
|
75
|
+
/**
|
|
76
|
+
* Convenience builder that creates a `TickControllerResult` for scheduled
|
|
77
|
+
* (absolute-time) ticking.
|
|
78
|
+
*
|
|
79
|
+
* `intervalMs` is auto-computed as milliseconds-until-`nextTickAt`, so a
|
|
80
|
+
* surface drawing a progress bar has the window's full length to draw against.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```ts
|
|
84
|
+
* return scheduledTick(nextDailyAt(0, 0, { timezone: settings.engine.Timezone }));
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export declare function scheduledTick(nextTickAt: number, options?: {
|
|
88
|
+
complete?: boolean;
|
|
89
|
+
}): TickControllerResult;
|
|
90
|
+
/**
|
|
91
|
+
* Convenience builder that creates a `TickControllerResult` for interval-based
|
|
92
|
+
* (cadence) ticking — the sanctioned way to return an interval result.
|
|
93
|
+
*
|
|
94
|
+
* `intervalMs` is in **milliseconds** and must be a finite, positive, whole
|
|
95
|
+
* number within the §11.8 effective interval bounds (a 30-second floor, 30000,
|
|
96
|
+
* and a 7-day ceiling, 604800000). Out-of-range, fractional, `NaN`,
|
|
97
|
+
* `Infinity`, zero, and negative values are rejected with a clear message, so
|
|
98
|
+
* a bad cadence (or a value written in seconds) is caught at authoring time
|
|
99
|
+
* rather than at runtime.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```ts
|
|
103
|
+
* // Tick every 30 seconds
|
|
104
|
+
* return intervalTick(30_000);
|
|
105
|
+
*
|
|
106
|
+
* // Tick once a minute, and mark the game complete
|
|
107
|
+
* return intervalTick(60_000, { complete: true });
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
export declare function intervalTick(intervalMs: number, options?: {
|
|
111
|
+
complete?: boolean;
|
|
112
|
+
}): TickControllerResult;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { validateTickControllerResult } from './tick-validation.js';
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// Internal helpers
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
/**
|
|
6
|
+
* Return the wall-clock components (year, month, day, hours, minutes) in
|
|
7
|
+
* the given IANA timezone. Uses `Intl.DateTimeFormat` so no third-party
|
|
8
|
+
* timezone library is needed.
|
|
9
|
+
*/
|
|
10
|
+
function wallClock(epochMs, timezone) {
|
|
11
|
+
const fmt = new Intl.DateTimeFormat('en-US', {
|
|
12
|
+
timeZone: timezone,
|
|
13
|
+
year: 'numeric',
|
|
14
|
+
month: '2-digit',
|
|
15
|
+
day: '2-digit',
|
|
16
|
+
hour: '2-digit',
|
|
17
|
+
minute: '2-digit',
|
|
18
|
+
weekday: 'short',
|
|
19
|
+
hour12: false,
|
|
20
|
+
});
|
|
21
|
+
const parts = Object.fromEntries(fmt.formatToParts(new Date(epochMs)).map((p) => [p.type, p.value]));
|
|
22
|
+
const weekdayMap = {
|
|
23
|
+
Sun: 0,
|
|
24
|
+
Mon: 1,
|
|
25
|
+
Tue: 2,
|
|
26
|
+
Wed: 3,
|
|
27
|
+
Thu: 4,
|
|
28
|
+
Fri: 5,
|
|
29
|
+
Sat: 6,
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
year: Number(parts.year),
|
|
33
|
+
month: Number(parts.month),
|
|
34
|
+
day: Number(parts.day),
|
|
35
|
+
hours: Number(parts.hour),
|
|
36
|
+
minutes: Number(parts.minute),
|
|
37
|
+
weekday: weekdayMap[parts.weekday] ?? 0,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Build a UTC epoch-ms timestamp for a specific wall-clock date/time in the
|
|
42
|
+
* given timezone. Works by creating a candidate UTC date, checking what
|
|
43
|
+
* wall-clock time that maps to, and compensating for the difference.
|
|
44
|
+
*/
|
|
45
|
+
function toEpoch(year, month, day, hours, minutes, timezone) {
|
|
46
|
+
// Treat the target date/time as if it were UTC
|
|
47
|
+
const naiveUtc = Date.UTC(year, month - 1, day, hours, minutes, 0, 0);
|
|
48
|
+
// What wall-clock reading does naiveUtc produce in the target timezone?
|
|
49
|
+
const wall = wallClock(naiveUtc, timezone);
|
|
50
|
+
// Build a "naive UTC" for that wall-clock reading so we can compute the
|
|
51
|
+
// full offset including any day difference.
|
|
52
|
+
const wallNaiveUtc = Date.UTC(wall.year, wall.month - 1, wall.day, wall.hours, wall.minutes, 0, 0);
|
|
53
|
+
// The offset is the difference: positive means the timezone is ahead of UTC.
|
|
54
|
+
// Subtract it to get the true UTC instant for the target wall-clock time.
|
|
55
|
+
return naiveUtc - (wallNaiveUtc - naiveUtc);
|
|
56
|
+
}
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Public API
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
/**
|
|
61
|
+
* Compute the next occurrence of a daily time.
|
|
62
|
+
*
|
|
63
|
+
* @param hours Hour of the day (0–23)
|
|
64
|
+
* @param minutes Minute of the hour (0–59)
|
|
65
|
+
* @param options Optional timezone and `now` override
|
|
66
|
+
* @returns Unix timestamp in milliseconds of the next occurrence
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* // Next midnight UTC
|
|
71
|
+
* nextDailyAt(0, 0);
|
|
72
|
+
*
|
|
73
|
+
* // Next midnight in Sydney (handles DST)
|
|
74
|
+
* nextDailyAt(0, 0, { timezone: 'Australia/Sydney' });
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export function nextDailyAt(hours, minutes, options) {
|
|
78
|
+
const tz = options?.timezone ?? 'UTC';
|
|
79
|
+
const now = options?.now ?? Date.now();
|
|
80
|
+
const wall = wallClock(now, tz);
|
|
81
|
+
// Try today first
|
|
82
|
+
let target = toEpoch(wall.year, wall.month, wall.day, hours, minutes, tz);
|
|
83
|
+
// If that's not in the future, use tomorrow
|
|
84
|
+
if (target <= now) {
|
|
85
|
+
const tomorrow = new Date(Date.UTC(wall.year, wall.month - 1, wall.day + 1));
|
|
86
|
+
const tw = wallClock(tomorrow.getTime(), tz);
|
|
87
|
+
target = toEpoch(tw.year, tw.month, tw.day, hours, minutes, tz);
|
|
88
|
+
}
|
|
89
|
+
return target;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Compute the next occurrence of a weekly time.
|
|
93
|
+
*
|
|
94
|
+
* @param dayOfWeek 0 = Sunday, 1 = Monday, …, 6 = Saturday
|
|
95
|
+
* @param hours Hour of the day (0–23)
|
|
96
|
+
* @param minutes Minute of the hour (0–59)
|
|
97
|
+
* @param options Optional timezone and `now` override
|
|
98
|
+
* @returns Unix timestamp in milliseconds of the next occurrence
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```ts
|
|
102
|
+
* // Next Monday at 09:00 UTC
|
|
103
|
+
* nextWeeklyAt(1, 9, 0);
|
|
104
|
+
*
|
|
105
|
+
* // Next Friday at 17:00 in New York
|
|
106
|
+
* nextWeeklyAt(5, 17, 0, { timezone: 'America/New_York' });
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export function nextWeeklyAt(dayOfWeek, hours, minutes, options) {
|
|
110
|
+
const tz = options?.timezone ?? 'UTC';
|
|
111
|
+
const now = options?.now ?? Date.now();
|
|
112
|
+
const wall = wallClock(now, tz);
|
|
113
|
+
// Days until the target day-of-week
|
|
114
|
+
let daysAhead = (dayOfWeek - wall.weekday + 7) % 7;
|
|
115
|
+
// If it's the same weekday, check if the time has passed
|
|
116
|
+
if (daysAhead === 0) {
|
|
117
|
+
const todayTarget = toEpoch(wall.year, wall.month, wall.day, hours, minutes, tz);
|
|
118
|
+
if (todayTarget > now) {
|
|
119
|
+
return todayTarget;
|
|
120
|
+
}
|
|
121
|
+
daysAhead = 7;
|
|
122
|
+
}
|
|
123
|
+
const targetDate = new Date(Date.UTC(wall.year, wall.month - 1, wall.day + daysAhead));
|
|
124
|
+
const tw = wallClock(targetDate.getTime(), tz);
|
|
125
|
+
return toEpoch(tw.year, tw.month, tw.day, hours, minutes, tz);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Pick the soonest future time from a list of daily times.
|
|
129
|
+
*
|
|
130
|
+
* @param times Array of `{ hours, minutes }` pairs
|
|
131
|
+
* @param options Optional timezone and `now` override
|
|
132
|
+
* @returns Unix timestamp in milliseconds of the soonest next occurrence
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```ts
|
|
136
|
+
* // Next occurrence of 09:00 or 17:00 UTC
|
|
137
|
+
* nextDailyFromSchedule([
|
|
138
|
+
* { hours: 9, minutes: 0 },
|
|
139
|
+
* { hours: 17, minutes: 0 },
|
|
140
|
+
* ]);
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
export function nextDailyFromSchedule(times, options) {
|
|
144
|
+
if (times.length === 0) {
|
|
145
|
+
throw new Error('nextDailyFromSchedule requires at least one time entry');
|
|
146
|
+
}
|
|
147
|
+
let earliest = Infinity;
|
|
148
|
+
for (const { hours, minutes } of times) {
|
|
149
|
+
const t = nextDailyAt(hours, minutes, options);
|
|
150
|
+
if (t < earliest)
|
|
151
|
+
earliest = t;
|
|
152
|
+
}
|
|
153
|
+
return earliest;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Convenience builder that creates a `TickControllerResult` for scheduled
|
|
157
|
+
* (absolute-time) ticking.
|
|
158
|
+
*
|
|
159
|
+
* `intervalMs` is auto-computed as milliseconds-until-`nextTickAt`, so a
|
|
160
|
+
* surface drawing a progress bar has the window's full length to draw against.
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* ```ts
|
|
164
|
+
* return scheduledTick(nextDailyAt(0, 0, { timezone: settings.engine.Timezone }));
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
export function scheduledTick(nextTickAt, options) {
|
|
168
|
+
const now = Date.now();
|
|
169
|
+
const intervalMs = Math.max(0, Math.ceil(nextTickAt - now));
|
|
170
|
+
const result = {
|
|
171
|
+
enabled: true,
|
|
172
|
+
...(intervalMs >= 1 ? { intervalMs } : {}),
|
|
173
|
+
nextTickAt,
|
|
174
|
+
...(options?.complete !== undefined ? { complete: options.complete } : {}),
|
|
175
|
+
};
|
|
176
|
+
// Enforce the §11.8 bounds on the effective cadence (nextTickAt − now), not
|
|
177
|
+
// just on the intervalMs display hint, so a sub-floor or over-ceiling
|
|
178
|
+
// schedule is rejected at authoring time rather than silently honoured at
|
|
179
|
+
// runtime.
|
|
180
|
+
const error = validateTickControllerResult(result, { now });
|
|
181
|
+
if (error) {
|
|
182
|
+
throw new Error(error);
|
|
183
|
+
}
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Convenience builder that creates a `TickControllerResult` for interval-based
|
|
188
|
+
* (cadence) ticking — the sanctioned way to return an interval result.
|
|
189
|
+
*
|
|
190
|
+
* `intervalMs` is in **milliseconds** and must be a finite, positive, whole
|
|
191
|
+
* number within the §11.8 effective interval bounds (a 30-second floor, 30000,
|
|
192
|
+
* and a 7-day ceiling, 604800000). Out-of-range, fractional, `NaN`,
|
|
193
|
+
* `Infinity`, zero, and negative values are rejected with a clear message, so
|
|
194
|
+
* a bad cadence (or a value written in seconds) is caught at authoring time
|
|
195
|
+
* rather than at runtime.
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* ```ts
|
|
199
|
+
* // Tick every 30 seconds
|
|
200
|
+
* return intervalTick(30_000);
|
|
201
|
+
*
|
|
202
|
+
* // Tick once a minute, and mark the game complete
|
|
203
|
+
* return intervalTick(60_000, { complete: true });
|
|
204
|
+
* ```
|
|
205
|
+
*/
|
|
206
|
+
export function intervalTick(intervalMs, options) {
|
|
207
|
+
const result = {
|
|
208
|
+
enabled: true,
|
|
209
|
+
intervalMs,
|
|
210
|
+
...(options?.complete !== undefined ? { complete: options.complete } : {}),
|
|
211
|
+
};
|
|
212
|
+
const error = validateTickControllerResult(result);
|
|
213
|
+
if (error) {
|
|
214
|
+
throw new Error(error);
|
|
215
|
+
}
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic randomness for engine logic.
|
|
3
|
+
*
|
|
4
|
+
* Two constraints shape these. First, engine functions may run more than once
|
|
5
|
+
* for one logical tick, so a live `Math.random()` produces a value that
|
|
6
|
+
* disagrees between runs and only survives if it happened to be in the last
|
|
7
|
+
* one. Second, logic bundles run against the ECMAScript intrinsics alone:
|
|
8
|
+
* there is no `crypto`, no `TextEncoder`, and nothing else to hash with. So the
|
|
9
|
+
* hash is written out here, in the SDK, rather than assumed to exist.
|
|
10
|
+
*
|
|
11
|
+
* The pairing is deliberate. `deriveSeed` turns 'this session, this team, this
|
|
12
|
+
* purpose' into a stable seed; `seededRandom` turns a seed into numbers. A seed
|
|
13
|
+
* on its own does not give you randomness, which is why shipping only
|
|
14
|
+
* `meta.seed` would have left every author writing this file.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Derive a stable seed from a session seed and whatever identifies the thing
|
|
18
|
+
* being seeded.
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* const seed = deriveSeed(meta.seed, meta.index, 'opening-hand');
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* The same parts always give the same result, in this process and in any other,
|
|
25
|
+
* so a re-run of a tick derives the same value and two teams derive different
|
|
26
|
+
* ones. Parts are joined with a separator that cannot appear in a stringified
|
|
27
|
+
* number, so `deriveSeed(s, 1, 23)` and `deriveSeed(s, 12, 3)` cannot collide.
|
|
28
|
+
*
|
|
29
|
+
* This is a mixing function, not a cryptographic hash. It is for dealing hands
|
|
30
|
+
* and placing customers; it is not for anything an adversary should be unable
|
|
31
|
+
* to predict.
|
|
32
|
+
*/
|
|
33
|
+
export declare function deriveSeed(...parts: (string | number)[]): string;
|
|
34
|
+
/**
|
|
35
|
+
* A seeded pseudo-random generator: call it for the next value in `[0, 1)`,
|
|
36
|
+
* like `Math.random()`.
|
|
37
|
+
*
|
|
38
|
+
* ```ts
|
|
39
|
+
* const random = seededRandom(deriveSeed(meta.seed, meta.index));
|
|
40
|
+
* const price = 10 + Math.floor(random() * 5);
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* The sequence depends only on the seed, so the same seed always replays the
|
|
44
|
+
* same numbers. Draw a fresh generator per purpose rather than threading one
|
|
45
|
+
* through a tick: two independent sequences are easier to reason about than one
|
|
46
|
+
* whose values depend on how many times something else was called first.
|
|
47
|
+
*
|
|
48
|
+
* mulberry32: one 32-bit state word, good statistical quality for simulation,
|
|
49
|
+
* and short enough to read. Not for anything security-sensitive.
|
|
50
|
+
*/
|
|
51
|
+
export declare function seededRandom(seed: string): () => number;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic randomness for engine logic.
|
|
3
|
+
*
|
|
4
|
+
* Two constraints shape these. First, engine functions may run more than once
|
|
5
|
+
* for one logical tick, so a live `Math.random()` produces a value that
|
|
6
|
+
* disagrees between runs and only survives if it happened to be in the last
|
|
7
|
+
* one. Second, logic bundles run against the ECMAScript intrinsics alone:
|
|
8
|
+
* there is no `crypto`, no `TextEncoder`, and nothing else to hash with. So the
|
|
9
|
+
* hash is written out here, in the SDK, rather than assumed to exist.
|
|
10
|
+
*
|
|
11
|
+
* The pairing is deliberate. `deriveSeed` turns 'this session, this team, this
|
|
12
|
+
* purpose' into a stable seed; `seededRandom` turns a seed into numbers. A seed
|
|
13
|
+
* on its own does not give you randomness, which is why shipping only
|
|
14
|
+
* `meta.seed` would have left every author writing this file.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Derive a stable seed from a session seed and whatever identifies the thing
|
|
18
|
+
* being seeded.
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* const seed = deriveSeed(meta.seed, meta.index, 'opening-hand');
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* The same parts always give the same result, in this process and in any other,
|
|
25
|
+
* so a re-run of a tick derives the same value and two teams derive different
|
|
26
|
+
* ones. Parts are joined with a separator that cannot appear in a stringified
|
|
27
|
+
* number, so `deriveSeed(s, 1, 23)` and `deriveSeed(s, 12, 3)` cannot collide.
|
|
28
|
+
*
|
|
29
|
+
* This is a mixing function, not a cryptographic hash. It is for dealing hands
|
|
30
|
+
* and placing customers; it is not for anything an adversary should be unable
|
|
31
|
+
* to predict.
|
|
32
|
+
*/
|
|
33
|
+
export function deriveSeed(...parts) {
|
|
34
|
+
const joined = parts.map((part) => String(part)).join('\u001f');
|
|
35
|
+
// Two FNV-1a passes from different offset bases, concatenated. A single
|
|
36
|
+
// 32-bit value starts colliding noticeably once a game derives tens of
|
|
37
|
+
// thousands of seeds (the birthday bound is near 2^16); widening it pushes
|
|
38
|
+
// that past anything a session reaches. The passes share a multiplier, so
|
|
39
|
+
// this is wider rather than twice as strong, which is all it needs to be.
|
|
40
|
+
const low = fnv1a(joined, 0x811c9dc5);
|
|
41
|
+
const high = fnv1a(joined, 0x01000193);
|
|
42
|
+
return toHex(high) + toHex(low);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A seeded pseudo-random generator: call it for the next value in `[0, 1)`,
|
|
46
|
+
* like `Math.random()`.
|
|
47
|
+
*
|
|
48
|
+
* ```ts
|
|
49
|
+
* const random = seededRandom(deriveSeed(meta.seed, meta.index));
|
|
50
|
+
* const price = 10 + Math.floor(random() * 5);
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* The sequence depends only on the seed, so the same seed always replays the
|
|
54
|
+
* same numbers. Draw a fresh generator per purpose rather than threading one
|
|
55
|
+
* through a tick: two independent sequences are easier to reason about than one
|
|
56
|
+
* whose values depend on how many times something else was called first.
|
|
57
|
+
*
|
|
58
|
+
* mulberry32: one 32-bit state word, good statistical quality for simulation,
|
|
59
|
+
* and short enough to read. Not for anything security-sensitive.
|
|
60
|
+
*/
|
|
61
|
+
export function seededRandom(seed) {
|
|
62
|
+
let state = fnv1a(seed, 0x811c9dc5);
|
|
63
|
+
return () => {
|
|
64
|
+
state = (state + 0x6d2b79f5) >>> 0;
|
|
65
|
+
let mixed = state;
|
|
66
|
+
mixed = Math.imul(mixed ^ (mixed >>> 15), mixed | 1);
|
|
67
|
+
mixed ^= mixed + Math.imul(mixed ^ (mixed >>> 7), mixed | 61);
|
|
68
|
+
return ((mixed ^ (mixed >>> 14)) >>> 0) / 4294967296;
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* FNV-1a over the string's UTF-16 code units.
|
|
73
|
+
*
|
|
74
|
+
* Code units rather than UTF-8 bytes because there is no encoder to reach for
|
|
75
|
+
* in the logic environment, and the choice does not matter: the requirement is
|
|
76
|
+
* that the same string always mixes to the same number, not that it matches
|
|
77
|
+
* any other implementation of FNV.
|
|
78
|
+
*/
|
|
79
|
+
function fnv1a(value, offsetBasis) {
|
|
80
|
+
let hash = offsetBasis >>> 0;
|
|
81
|
+
for (let i = 0; i < value.length; i++) {
|
|
82
|
+
hash ^= value.charCodeAt(i);
|
|
83
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
84
|
+
}
|
|
85
|
+
return hash >>> 0;
|
|
86
|
+
}
|
|
87
|
+
function toHex(value) {
|
|
88
|
+
return (value >>> 0).toString(16).padStart(8, '0');
|
|
89
|
+
}
|