@flayerlabs/gamemode-spec 0.1.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/LICENSE +9 -0
- package/README.md +78 -0
- package/dist/embed.d.ts +145 -0
- package/dist/embed.d.ts.map +1 -0
- package/dist/embed.js +332 -0
- package/dist/embed.js.map +1 -0
- package/dist/index.d.ts +124 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/live.d.ts +139 -0
- package/dist/live.d.ts.map +1 -0
- package/dist/live.js +30 -0
- package/dist/live.js.map +1 -0
- package/dist/round.d.ts +58 -0
- package/dist/round.d.ts.map +1 -0
- package/dist/round.js +123 -0
- package/dist/round.js.map +1 -0
- package/dist/schedule.d.ts +18 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/schedule.js +44 -0
- package/dist/schedule.js.map +1 -0
- package/package.json +59 -0
- package/src/embed.ts +450 -0
- package/src/index.ts +133 -0
- package/src/live.ts +173 -0
- package/src/round.ts +141 -0
- package/src/schedule.ts +58 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contract between a game and the platform.
|
|
3
|
+
*
|
|
4
|
+
* Nothing in this file knows what any game is about. If a concept here can be named after a
|
|
5
|
+
* particular game — a score curve, a question, a projectile — it is in the wrong file.
|
|
6
|
+
*
|
|
7
|
+
* The whole platform rests on one property: a game's rules are a PURE FUNCTION of state and a
|
|
8
|
+
* command. Everything else — durability, replay, reconnect, anti-cheat forensics, running the
|
|
9
|
+
* same rules in a browser and on a server — is a consequence of that, not an addition to it.
|
|
10
|
+
*/
|
|
11
|
+
/** A player, opaque to the platform. In practice a lowercased wallet address. */
|
|
12
|
+
export type PlayerId = string;
|
|
13
|
+
/** When a round's window opens and closes. Absolute epoch milliseconds. */
|
|
14
|
+
export interface RoundWindow {
|
|
15
|
+
opensAt: number;
|
|
16
|
+
closesAt: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* What the platform asks a game to decide on.
|
|
20
|
+
*
|
|
21
|
+
* Every command carries `at` — the authoritative time it happened. A game reads time from here
|
|
22
|
+
* and nowhere else. There is no clock in scope and no ambient randomness: `seed` is supplied so
|
|
23
|
+
* anything a game wants to randomise is derived, and therefore reproducible.
|
|
24
|
+
*/
|
|
25
|
+
export type Command<Action> = {
|
|
26
|
+
kind: 'join';
|
|
27
|
+
player: PlayerId;
|
|
28
|
+
seed: number;
|
|
29
|
+
at: number;
|
|
30
|
+
} | {
|
|
31
|
+
kind: 'leave';
|
|
32
|
+
player: PlayerId;
|
|
33
|
+
at: number;
|
|
34
|
+
} | {
|
|
35
|
+
kind: 'action';
|
|
36
|
+
player: PlayerId;
|
|
37
|
+
action: Action;
|
|
38
|
+
at: number;
|
|
39
|
+
}
|
|
40
|
+
/** The platform woke the round at a time the game asked for via {@link GameModule.nextWakeAt}. */
|
|
41
|
+
| {
|
|
42
|
+
kind: 'wake';
|
|
43
|
+
at: number;
|
|
44
|
+
};
|
|
45
|
+
/** Points awarded to a player. The platform converts these to spending allowance; it does not
|
|
46
|
+
* know or care how they were earned. */
|
|
47
|
+
export interface Award {
|
|
48
|
+
player: PlayerId;
|
|
49
|
+
points: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* What a game decides in response to a command.
|
|
53
|
+
*
|
|
54
|
+
* Awards are separate from events on purpose. A game may accept an action now and pay for it
|
|
55
|
+
* later — a quiz must, or a player learns they answered correctly by watching their own balance
|
|
56
|
+
* before the reveal, and the reveal stops meaning anything.
|
|
57
|
+
*/
|
|
58
|
+
export interface Decision<Event> {
|
|
59
|
+
events: readonly Event[];
|
|
60
|
+
awards?: readonly Award[];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* A refused command. `code` is stable and machine-readable — alerting and months of history key
|
|
64
|
+
* on it, so it is never renamed, only added to. Player-facing copy lives in the client.
|
|
65
|
+
*
|
|
66
|
+
* Never put a threshold in a code. A refusal a cheater can calibrate against is an oracle.
|
|
67
|
+
*/
|
|
68
|
+
export interface Refusal {
|
|
69
|
+
refuse: string;
|
|
70
|
+
}
|
|
71
|
+
export declare function isRefusal<E>(result: Decision<E> | Refusal): result is Refusal;
|
|
72
|
+
/** The most a single player can earn in one round. The platform checks this against the pool's
|
|
73
|
+
* on-chain per-wallet cap before a round is allowed to start. */
|
|
74
|
+
export interface RewardBounds {
|
|
75
|
+
maxPointsPerPlayer: number;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* A game.
|
|
79
|
+
*
|
|
80
|
+
* `decide` and `evolve` must be pure: same inputs, same outputs, forever. No clock, no
|
|
81
|
+
* randomness, no I/O, no module-level mutable state. This is enforced by lint rather than by
|
|
82
|
+
* review, because it is the property everything else depends on.
|
|
83
|
+
*/
|
|
84
|
+
export interface GameModule<Config, State, Event, Action, PublicView, PlayerView> {
|
|
85
|
+
/** Stable identifier, unique across games. */
|
|
86
|
+
readonly id: string;
|
|
87
|
+
/** Reject anything that is not a well-formed action, including anything oversized. Returning
|
|
88
|
+
* null refuses the command before it reaches {@link decide}. */
|
|
89
|
+
parseAction(input: unknown): Action | null;
|
|
90
|
+
/** Build the starting state. Deterministic in `seed`. */
|
|
91
|
+
initRound(config: Config, seed: number, window: RoundWindow): State;
|
|
92
|
+
/** The trust boundary. Pure. The only thing that may award points. */
|
|
93
|
+
decide(state: State, command: Command<Action>): Decision<Event> | Refusal;
|
|
94
|
+
/** Apply one event. Pure. */
|
|
95
|
+
evolve(state: State, event: Event): State;
|
|
96
|
+
/**
|
|
97
|
+
* What everyone sees, including spectators and anyone reading the socket.
|
|
98
|
+
*
|
|
99
|
+
* A value that must stay hidden must not appear in this type. Do not return it and filter
|
|
100
|
+
* downstream — the type is the enforcement, which is what makes a leak unrenderable rather
|
|
101
|
+
* than merely unlikely.
|
|
102
|
+
*/
|
|
103
|
+
publicView(state: State): PublicView;
|
|
104
|
+
/** What one player sees. Anything private belongs here. */
|
|
105
|
+
playerView(state: State, player: PlayerId): PlayerView;
|
|
106
|
+
/**
|
|
107
|
+
* When the game next needs waking, or null if it is waiting only on players. Absolute epoch
|
|
108
|
+
* milliseconds.
|
|
109
|
+
*
|
|
110
|
+
* Each wake must ask for a time strictly LATER than the one being served. A phase that ends at
|
|
111
|
+
* the instant it begins has no duration, and asking to be woken at the time it already is would
|
|
112
|
+
* spin forever. If two things must happen back to back, they are two phases with real durations —
|
|
113
|
+
* which is usually what the product wanted anyway.
|
|
114
|
+
*/
|
|
115
|
+
nextWakeAt(state: State): number | null;
|
|
116
|
+
/** Declared up front so the platform can refuse an incoherent economy before anyone plays. */
|
|
117
|
+
rewardBounds(config: Config): RewardBounds;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Authoring entry point. Exists for type inference — writing a `GameModule` literal directly
|
|
121
|
+
* means naming six type arguments by hand.
|
|
122
|
+
*/
|
|
123
|
+
export declare function defineGame<Config, State, Event, Action, PublicView, PlayerView>(game: GameModule<Config, State, Event, Action, PublicView, PlayerView>): GameModule<Config, State, Event, Action, PublicView, PlayerView>;
|
|
124
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contract between a game and the platform.
|
|
3
|
+
*
|
|
4
|
+
* Nothing in this file knows what any game is about. If a concept here can be named after a
|
|
5
|
+
* particular game — a score curve, a question, a projectile — it is in the wrong file.
|
|
6
|
+
*
|
|
7
|
+
* The whole platform rests on one property: a game's rules are a PURE FUNCTION of state and a
|
|
8
|
+
* command. Everything else — durability, replay, reconnect, anti-cheat forensics, running the
|
|
9
|
+
* same rules in a browser and on a server — is a consequence of that, not an addition to it.
|
|
10
|
+
*/
|
|
11
|
+
export function isRefusal(result) {
|
|
12
|
+
return 'refuse' in result;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Authoring entry point. Exists for type inference — writing a `GameModule` literal directly
|
|
16
|
+
* means naming six type arguments by hand.
|
|
17
|
+
*/
|
|
18
|
+
export function defineGame(game) {
|
|
19
|
+
return game;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAsDH,MAAM,UAAU,SAAS,CAAI,MAA6B;IACxD,OAAO,QAAQ,IAAI,MAAM,CAAC;AAC5B,CAAC;AA2DD;;;GAGG;AACH,MAAM,UAAU,UAAU,CACxB,IAAsE;IAEtE,OAAO,IAAI,CAAC;AACd,CAAC"}
|
package/dist/live.d.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { PlayerId } from './index.js';
|
|
2
|
+
/** Static launch facts that are safe for every game and spectator to render. */
|
|
3
|
+
export interface LaunchContext {
|
|
4
|
+
readonly roundId: string;
|
|
5
|
+
readonly poolId: string;
|
|
6
|
+
readonly coinAddress: string | null;
|
|
7
|
+
readonly name: string;
|
|
8
|
+
readonly symbol: string;
|
|
9
|
+
readonly imageUrl: string | null;
|
|
10
|
+
readonly opensAt: number;
|
|
11
|
+
readonly closesAt: number;
|
|
12
|
+
/** The chart/standings settlement tail may outlive gameplay and claim issuance. */
|
|
13
|
+
readonly booksCloseAt: number;
|
|
14
|
+
}
|
|
15
|
+
/** Check an untrusted launch response before using it to choose a room or trusted service. */
|
|
16
|
+
export declare function isLaunchContext(input: unknown): input is LaunchContext;
|
|
17
|
+
export type ConnectionState = 'connecting' | 'connected' | 'reconnecting' | 'superseded' | 'ended';
|
|
18
|
+
/**
|
|
19
|
+
* Who is currently connected. Telemetry, and deliberately nothing more.
|
|
20
|
+
*
|
|
21
|
+
* This carries no readiness, because the platform has no opinion about what "ready" means. A game
|
|
22
|
+
* that gates its lobby on players being ready defines a ready Action of its own: it then travels
|
|
23
|
+
* the same authenticated, ordered, persisted path as every other action and arrives in `decide()`,
|
|
24
|
+
* where it can actually change the round.
|
|
25
|
+
*
|
|
26
|
+
* Readiness modelled here instead was a rule the rules module could not see. It could not be
|
|
27
|
+
* replayed, it could not decide anything, and it imposed one lobby policy on every kind of game —
|
|
28
|
+
* a live quiz, a shooter and a turn-based game do not agree about when to start, and should not
|
|
29
|
+
* have to.
|
|
30
|
+
*/
|
|
31
|
+
export interface PresenceState {
|
|
32
|
+
connectedCount: number;
|
|
33
|
+
}
|
|
34
|
+
export interface PlayerIdentity {
|
|
35
|
+
player: PlayerId;
|
|
36
|
+
displayName: string | null;
|
|
37
|
+
avatarUrl: string | null;
|
|
38
|
+
}
|
|
39
|
+
export interface Reaction {
|
|
40
|
+
player: PlayerId;
|
|
41
|
+
id: string;
|
|
42
|
+
}
|
|
43
|
+
export interface MarketPrice {
|
|
44
|
+
at: number;
|
|
45
|
+
/** Display price of one whole coin in ETH. Market data never participates in scoring. */
|
|
46
|
+
priceEth: number;
|
|
47
|
+
}
|
|
48
|
+
export interface MarketTrade {
|
|
49
|
+
/** Stable across reconnects, normally transaction hash plus log index. */
|
|
50
|
+
id: string;
|
|
51
|
+
at: number;
|
|
52
|
+
player: PlayerId | null;
|
|
53
|
+
side: 'buy' | 'sell';
|
|
54
|
+
spendWei: bigint;
|
|
55
|
+
priceEth: number | null;
|
|
56
|
+
transactionHash?: string;
|
|
57
|
+
}
|
|
58
|
+
export interface MarketState {
|
|
59
|
+
status: 'unavailable' | 'loading' | 'live' | 'stale';
|
|
60
|
+
prices: readonly MarketPrice[];
|
|
61
|
+
trades: readonly MarketTrade[];
|
|
62
|
+
marketCapUsd: number | null;
|
|
63
|
+
}
|
|
64
|
+
export interface WireMarketTrade extends Omit<MarketTrade, 'spendWei'> {
|
|
65
|
+
spendWei: string;
|
|
66
|
+
}
|
|
67
|
+
export interface WireMarketState extends Omit<MarketState, 'trades'> {
|
|
68
|
+
trades: readonly WireMarketTrade[];
|
|
69
|
+
}
|
|
70
|
+
/** Authoritative ledger state. A signed but unsettled authorisation remains held. */
|
|
71
|
+
export interface EconomyBalance {
|
|
72
|
+
/** Immutable entitlement rate for this round. This is wei, not a live dollar quote. */
|
|
73
|
+
weiPerPoint: bigint;
|
|
74
|
+
earnedWei: bigint;
|
|
75
|
+
heldWei: bigint;
|
|
76
|
+
spentWei: bigint;
|
|
77
|
+
availableWei: bigint;
|
|
78
|
+
/**
|
|
79
|
+
* When the earliest outstanding hold expires, in epoch ms, or null when nothing is held.
|
|
80
|
+
*
|
|
81
|
+
* A player who opens the buy dialog and then declines keeps that allowance held until this
|
|
82
|
+
* passes, which is correct — the authorisation is still live and could still be spent. But
|
|
83
|
+
* without the deadline on the balance, a game cannot explain the gap between what a player
|
|
84
|
+
* earned and what they can spend, so it either tracks the deadline itself or shows a number
|
|
85
|
+
* that is wrong for five minutes.
|
|
86
|
+
*/
|
|
87
|
+
holdExpiresAt: number | null;
|
|
88
|
+
}
|
|
89
|
+
/** Stable player-facing reasons a platform buy did not happen. */
|
|
90
|
+
export type BuyFailure = 'nothing-to-spend' | 'declined' | 'not-enough-for-fees' | 'window-closed' | 'try-again';
|
|
91
|
+
/**
|
|
92
|
+
* JSON-safe form used on the wire.
|
|
93
|
+
*
|
|
94
|
+
* The wei fields cross as decimal strings because JSON has no bigint. `holdExpiresAt` is an
|
|
95
|
+
* ordinary millisecond number and stays one.
|
|
96
|
+
*/
|
|
97
|
+
export type WireEconomyBalance = {
|
|
98
|
+
[Key in keyof Omit<EconomyBalance, 'holdExpiresAt'>]: string;
|
|
99
|
+
} & {
|
|
100
|
+
holdExpiresAt: number | null;
|
|
101
|
+
};
|
|
102
|
+
/** Keep cosmetic reaction identifiers bounded and safe to use as asset keys. */
|
|
103
|
+
export declare function isReactionId(id: string): boolean;
|
|
104
|
+
export declare const LIVE_PROTOCOL_VERSION: 1;
|
|
105
|
+
export type LiveServerFrame<PublicView, PlayerView> = {
|
|
106
|
+
v: typeof LIVE_PROTOCOL_VERSION;
|
|
107
|
+
type: 'snapshot';
|
|
108
|
+
sequence: number;
|
|
109
|
+
now: number;
|
|
110
|
+
view: PublicView;
|
|
111
|
+
you: PlayerView | null;
|
|
112
|
+
economy: WireEconomyBalance;
|
|
113
|
+
launch: LaunchContext;
|
|
114
|
+
presence: PresenceState;
|
|
115
|
+
} | {
|
|
116
|
+
v: typeof LIVE_PROTOCOL_VERSION;
|
|
117
|
+
type: 'reaction';
|
|
118
|
+
reaction: Reaction;
|
|
119
|
+
} | {
|
|
120
|
+
v: typeof LIVE_PROTOCOL_VERSION;
|
|
121
|
+
type: 'market';
|
|
122
|
+
market: WireMarketState;
|
|
123
|
+
} | {
|
|
124
|
+
v: typeof LIVE_PROTOCOL_VERSION;
|
|
125
|
+
type: 'presence';
|
|
126
|
+
presence: PresenceState;
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Everything a client may say over the socket.
|
|
130
|
+
*
|
|
131
|
+
* Reactions only. Actions go over HTTP so they are ordered and persisted with the round, and
|
|
132
|
+
* readiness is an ordinary action for whichever games want one.
|
|
133
|
+
*/
|
|
134
|
+
export type LiveClientFrame = {
|
|
135
|
+
v: typeof LIVE_PROTOCOL_VERSION;
|
|
136
|
+
type: 'reaction';
|
|
137
|
+
id: string;
|
|
138
|
+
};
|
|
139
|
+
//# sourceMappingURL=live.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"live.d.ts","sourceRoot":"","sources":["../src/live.ts"],"names":[],"mappings":"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"}
|
package/dist/live.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const ADDRESS = /^0x[0-9a-fA-F]{40}$/;
|
|
2
|
+
/** Check an untrusted launch response before using it to choose a room or trusted service. */
|
|
3
|
+
export function isLaunchContext(input) {
|
|
4
|
+
if (typeof input !== 'object' || input === null)
|
|
5
|
+
return false;
|
|
6
|
+
const value = input;
|
|
7
|
+
return (typeof value.roundId === 'string' &&
|
|
8
|
+
value.roundId.length > 0 &&
|
|
9
|
+
value.roundId.length <= 128 &&
|
|
10
|
+
typeof value.poolId === 'string' &&
|
|
11
|
+
value.poolId.length > 0 &&
|
|
12
|
+
value.poolId.length <= 128 &&
|
|
13
|
+
(value.coinAddress === null || (typeof value.coinAddress === 'string' && ADDRESS.test(value.coinAddress))) &&
|
|
14
|
+
typeof value.name === 'string' &&
|
|
15
|
+
value.name.length <= 256 &&
|
|
16
|
+
typeof value.symbol === 'string' &&
|
|
17
|
+
value.symbol.length <= 64 &&
|
|
18
|
+
(value.imageUrl === null || (typeof value.imageUrl === 'string' && value.imageUrl.length <= 2_048)) &&
|
|
19
|
+
Number.isSafeInteger(value.opensAt) &&
|
|
20
|
+
Number.isSafeInteger(value.closesAt) &&
|
|
21
|
+
Number.isSafeInteger(value.booksCloseAt) &&
|
|
22
|
+
value.opensAt < value.closesAt &&
|
|
23
|
+
value.closesAt <= value.booksCloseAt);
|
|
24
|
+
}
|
|
25
|
+
/** Keep cosmetic reaction identifiers bounded and safe to use as asset keys. */
|
|
26
|
+
export function isReactionId(id) {
|
|
27
|
+
return /^[A-Za-z0-9._:-]{1,64}$/.test(id);
|
|
28
|
+
}
|
|
29
|
+
export const LIVE_PROTOCOL_VERSION = 1;
|
|
30
|
+
//# sourceMappingURL=live.js.map
|
package/dist/live.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"live.js","sourceRoot":"","sources":["../src/live.ts"],"names":[],"mappings":"AAgBA,MAAM,OAAO,GAAG,qBAAqB,CAAC;AAEtC,8FAA8F;AAC9F,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC9D,MAAM,KAAK,GAAG,KAAgC,CAAC;IAC/C,OAAO,CACL,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ;QACjC,KAAK,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC;QACxB,KAAK,CAAC,OAAO,CAAC,MAAM,IAAI,GAAG;QAC3B,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ;QAChC,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC;QACvB,KAAK,CAAC,MAAM,CAAC,MAAM,IAAI,GAAG;QAC1B,CAAC,KAAK,CAAC,WAAW,KAAK,IAAI,IAAI,CAAC,OAAO,KAAK,CAAC,WAAW,KAAK,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC;QAC1G,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ;QAC9B,KAAK,CAAC,IAAI,CAAC,MAAM,IAAI,GAAG;QACxB,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ;QAChC,KAAK,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE;QACzB,CAAC,KAAK,CAAC,QAAQ,KAAK,IAAI,IAAI,CAAC,OAAO,KAAK,CAAC,QAAQ,KAAK,QAAQ,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,IAAI,KAAK,CAAC,CAAC;QACnG,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,OAAO,CAAC;QACnC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,QAAQ,CAAC;QACpC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,YAAY,CAAC;QACvC,KAAK,CAAC,OAAkB,GAAI,KAAK,CAAC,QAAmB;QACrD,KAAK,CAAC,QAAmB,IAAK,KAAK,CAAC,YAAuB,CAC7D,CAAC;AACJ,CAAC;AAsGD,gFAAgF;AAChF,MAAM,UAAU,YAAY,CAAC,EAAU;IACrC,OAAO,yBAAyB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AAC5C,CAAC;AAED,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAU,CAAC"}
|
package/dist/round.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Award, Command, Decision, GameModule, PlayerId, Refusal, RoundWindow } from './index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Drives a game module through commands. Pure, in-memory, no I/O.
|
|
4
|
+
*
|
|
5
|
+
* This is the only implementation of "how to run a round". The server persists around it and the
|
|
6
|
+
* browser mock runs it directly, so a game cannot behave one way offline and another way live —
|
|
7
|
+
* there is no second copy to drift. The reference games this platform replaces had exactly that
|
|
8
|
+
* bug: two copies of the same scoring rules, disagreeing about a constant, with the mechanism
|
|
9
|
+
* meant to pin them not covering the part that moved.
|
|
10
|
+
*
|
|
11
|
+
* Ordering is the platform's job, not the game's: commands are applied one at a time, in the
|
|
12
|
+
* order they arrive here.
|
|
13
|
+
*/
|
|
14
|
+
export declare class Round<Config, State, Event, Action, PublicView, PlayerView> {
|
|
15
|
+
private readonly game;
|
|
16
|
+
private state;
|
|
17
|
+
readonly window: RoundWindow;
|
|
18
|
+
private readonly onAward?;
|
|
19
|
+
private readonly points;
|
|
20
|
+
/**
|
|
21
|
+
* @param onAward Called for every award as it happens, including ones produced by a wake that
|
|
22
|
+
* `send` ran on the way to a command. A durable caller needs this: it has to write the state and
|
|
23
|
+
* the money in one transaction, and it cannot see inside a wake any other way.
|
|
24
|
+
*/
|
|
25
|
+
private constructor();
|
|
26
|
+
/** Begin a new round. */
|
|
27
|
+
static start<Config, State, Event, Action, PublicView, PlayerView>(game: GameModule<Config, State, Event, Action, PublicView, PlayerView>, config: Config, seed: number, window: RoundWindow, onAward?: (award: Award) => void): Round<Config, State, Event, Action, PublicView, PlayerView>;
|
|
28
|
+
/**
|
|
29
|
+
* Pick a round back up from a stored state.
|
|
30
|
+
*
|
|
31
|
+
* Nothing is replayed and nothing recomputed: the state is the game's own and the platform never
|
|
32
|
+
* inspects it, so resuming is a straight substitution. That is what makes a restart cheap.
|
|
33
|
+
*/
|
|
34
|
+
static resume<Config, State, Event, Action, PublicView, PlayerView>(game: GameModule<Config, State, Event, Action, PublicView, PlayerView>, state: State, window: RoundWindow, onAward?: (award: Award) => void): Round<Config, State, Event, Action, PublicView, PlayerView>;
|
|
35
|
+
/**
|
|
36
|
+
* Apply one command. Fires any wakes that fall due at or before it first, so a game never sees
|
|
37
|
+
* an action from after a deadline it asked to be woken for.
|
|
38
|
+
*/
|
|
39
|
+
send(command: Command<Action>): Decision<Event> | Refusal;
|
|
40
|
+
/**
|
|
41
|
+
* Fire every wake due at or before `time`, and report how many fired.
|
|
42
|
+
*
|
|
43
|
+
* The count is what a durable caller needs: zero means nothing changed and there is nothing to
|
|
44
|
+
* write. Never send a wake through {@link send} to achieve this — `send` runs due wakes on the
|
|
45
|
+
* way to the command, so a synthetic wake command fires the same deadline twice and collapses
|
|
46
|
+
* whatever phase sat between them.
|
|
47
|
+
*/
|
|
48
|
+
advanceTo(time: number): number;
|
|
49
|
+
private runWakesUpTo;
|
|
50
|
+
private apply;
|
|
51
|
+
private credit;
|
|
52
|
+
pointsFor(player: PlayerId): number;
|
|
53
|
+
publicView(): PublicView;
|
|
54
|
+
playerView(player: PlayerId): PlayerView;
|
|
55
|
+
/** For replay assertions. Structural equality of this is what determinism means. */
|
|
56
|
+
snapshot(): State;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=round.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"round.d.ts","sourceRoot":"","sources":["../src/round.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAGvG;;;;;;;;;;;GAWG;AACH,qBAAa,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU;IASnE,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,KAAK;IACb,QAAQ,CAAC,MAAM,EAAE,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;IAX3B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA+B;IAEtD;;;;OAIG;IACH,OAAO;IAOP,yBAAyB;IACzB,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAC/D,IAAI,EAAE,UAAU,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,CAAC,EACtE,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,WAAW,EACnB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAC/B,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,CAAC;IAI9D;;;;;OAKG;IACH,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAChE,IAAI,EAAE,UAAU,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,CAAC,EACtE,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,WAAW,EACnB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAC/B,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,CAAC;IAI9D;;;OAGG;IACH,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,OAAO;IAKzD;;;;;;;OAOG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAI/B,OAAO,CAAC,YAAY;IA0BpB,OAAO,CAAC,KAAK;IAab,OAAO,CAAC,MAAM;IAQd,SAAS,CAAC,MAAM,EAAE,QAAQ,GAAG,MAAM;IAInC,UAAU,IAAI,UAAU;IAIxB,UAAU,CAAC,MAAM,EAAE,QAAQ,GAAG,UAAU;IAIxC,oFAAoF;IACpF,QAAQ,IAAI,KAAK;CAGlB"}
|
package/dist/round.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { isRefusal } from './index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Drives a game module through commands. Pure, in-memory, no I/O.
|
|
4
|
+
*
|
|
5
|
+
* This is the only implementation of "how to run a round". The server persists around it and the
|
|
6
|
+
* browser mock runs it directly, so a game cannot behave one way offline and another way live —
|
|
7
|
+
* there is no second copy to drift. The reference games this platform replaces had exactly that
|
|
8
|
+
* bug: two copies of the same scoring rules, disagreeing about a constant, with the mechanism
|
|
9
|
+
* meant to pin them not covering the part that moved.
|
|
10
|
+
*
|
|
11
|
+
* Ordering is the platform's job, not the game's: commands are applied one at a time, in the
|
|
12
|
+
* order they arrive here.
|
|
13
|
+
*/
|
|
14
|
+
export class Round {
|
|
15
|
+
game;
|
|
16
|
+
state;
|
|
17
|
+
window;
|
|
18
|
+
onAward;
|
|
19
|
+
points = new Map();
|
|
20
|
+
/**
|
|
21
|
+
* @param onAward Called for every award as it happens, including ones produced by a wake that
|
|
22
|
+
* `send` ran on the way to a command. A durable caller needs this: it has to write the state and
|
|
23
|
+
* the money in one transaction, and it cannot see inside a wake any other way.
|
|
24
|
+
*/
|
|
25
|
+
constructor(game, state, window, onAward) {
|
|
26
|
+
this.game = game;
|
|
27
|
+
this.state = state;
|
|
28
|
+
this.window = window;
|
|
29
|
+
this.onAward = onAward;
|
|
30
|
+
}
|
|
31
|
+
/** Begin a new round. */
|
|
32
|
+
static start(game, config, seed, window, onAward) {
|
|
33
|
+
return new Round(game, game.initRound(config, seed, window), window, onAward);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Pick a round back up from a stored state.
|
|
37
|
+
*
|
|
38
|
+
* Nothing is replayed and nothing recomputed: the state is the game's own and the platform never
|
|
39
|
+
* inspects it, so resuming is a straight substitution. That is what makes a restart cheap.
|
|
40
|
+
*/
|
|
41
|
+
static resume(game, state, window, onAward) {
|
|
42
|
+
return new Round(game, state, window, onAward);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Apply one command. Fires any wakes that fall due at or before it first, so a game never sees
|
|
46
|
+
* an action from after a deadline it asked to be woken for.
|
|
47
|
+
*/
|
|
48
|
+
send(command) {
|
|
49
|
+
this.runWakesUpTo(command.at);
|
|
50
|
+
return this.apply(command);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Fire every wake due at or before `time`, and report how many fired.
|
|
54
|
+
*
|
|
55
|
+
* The count is what a durable caller needs: zero means nothing changed and there is nothing to
|
|
56
|
+
* write. Never send a wake through {@link send} to achieve this — `send` runs due wakes on the
|
|
57
|
+
* way to the command, so a synthetic wake command fires the same deadline twice and collapses
|
|
58
|
+
* whatever phase sat between them.
|
|
59
|
+
*/
|
|
60
|
+
advanceTo(time) {
|
|
61
|
+
return this.runWakesUpTo(time);
|
|
62
|
+
}
|
|
63
|
+
runWakesUpTo(time) {
|
|
64
|
+
// A wake may schedule the next one. Bounded by each wake having to move strictly forward.
|
|
65
|
+
let previous = -Infinity;
|
|
66
|
+
let fired = 0;
|
|
67
|
+
for (;;) {
|
|
68
|
+
const at = this.game.nextWakeAt(this.state);
|
|
69
|
+
if (at === null)
|
|
70
|
+
return fired;
|
|
71
|
+
if (!Number.isFinite(at)) {
|
|
72
|
+
throw new Error(`${this.game.id}: nextWakeAt returned a non-finite time`);
|
|
73
|
+
}
|
|
74
|
+
if (!Number.isSafeInteger(at)) {
|
|
75
|
+
throw new Error(`${this.game.id}: nextWakeAt returned a non-safe-integer time`);
|
|
76
|
+
}
|
|
77
|
+
if (at > this.window.closesAt) {
|
|
78
|
+
throw new Error(`${this.game.id}: nextWakeAt ${at} is after the round closes at ${this.window.closesAt}`);
|
|
79
|
+
}
|
|
80
|
+
if (at > time)
|
|
81
|
+
return fired;
|
|
82
|
+
if (at <= previous) {
|
|
83
|
+
throw new Error(`${this.game.id}: nextWakeAt did not advance past ${at}`);
|
|
84
|
+
}
|
|
85
|
+
previous = at;
|
|
86
|
+
this.apply({ kind: 'wake', at });
|
|
87
|
+
fired += 1;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
apply(command) {
|
|
91
|
+
const result = this.game.decide(this.state, command);
|
|
92
|
+
if (isRefusal(result))
|
|
93
|
+
return result;
|
|
94
|
+
for (const event of result.events) {
|
|
95
|
+
this.state = this.game.evolve(this.state, event);
|
|
96
|
+
}
|
|
97
|
+
for (const award of result.awards ?? []) {
|
|
98
|
+
this.credit(award);
|
|
99
|
+
}
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
credit(award) {
|
|
103
|
+
if (!Number.isFinite(award.points) || award.points < 0) {
|
|
104
|
+
throw new Error(`${this.game.id}: award of ${award.points} points to ${award.player}`);
|
|
105
|
+
}
|
|
106
|
+
this.points.set(award.player, (this.points.get(award.player) ?? 0) + award.points);
|
|
107
|
+
this.onAward?.(award);
|
|
108
|
+
}
|
|
109
|
+
pointsFor(player) {
|
|
110
|
+
return this.points.get(player) ?? 0;
|
|
111
|
+
}
|
|
112
|
+
publicView() {
|
|
113
|
+
return this.game.publicView(this.state);
|
|
114
|
+
}
|
|
115
|
+
playerView(player) {
|
|
116
|
+
return this.game.playerView(this.state, player);
|
|
117
|
+
}
|
|
118
|
+
/** For replay assertions. Structural equality of this is what determinism means. */
|
|
119
|
+
snapshot() {
|
|
120
|
+
return this.state;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=round.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"round.js","sourceRoot":"","sources":["../src/round.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { RoundWindow } from './index.js';
|
|
2
|
+
export interface ScheduleStep<Value> {
|
|
3
|
+
value: Value;
|
|
4
|
+
durationMs: number;
|
|
5
|
+
}
|
|
6
|
+
export interface ScheduledStep<Value> {
|
|
7
|
+
value: Value;
|
|
8
|
+
startsAt: number;
|
|
9
|
+
endsAt: number;
|
|
10
|
+
}
|
|
11
|
+
export declare class ScheduleWindowError extends RangeError {
|
|
12
|
+
readonly requiredMs: number;
|
|
13
|
+
readonly availableMs: number;
|
|
14
|
+
constructor(requiredMs: number, availableMs: number);
|
|
15
|
+
}
|
|
16
|
+
/** Turn relative authored durations into the absolute timestamps a round can replay forever. */
|
|
17
|
+
export declare function scheduleWithin<Value>(window: RoundWindow, steps: readonly ScheduleStep<Value>[]): ScheduledStep<Value>[];
|
|
18
|
+
//# sourceMappingURL=schedule.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schedule.d.ts","sourceRoot":"","sources":["../src/schedule.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,MAAM,WAAW,YAAY,CAAC,KAAK;IACjC,KAAK,EAAE,KAAK,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,aAAa,CAAC,KAAK;IAClC,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,mBAAoB,SAAQ,UAAU;IACrC,QAAQ,CAAC,UAAU,EAAE,MAAM;IAAE,QAAQ,CAAC,WAAW,EAAE,MAAM;gBAAhD,UAAU,EAAE,MAAM,EAAW,WAAW,EAAE,MAAM;CAItE;AAED,gGAAgG;AAChG,wBAAgB,cAAc,CAAC,KAAK,EAClC,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,SAAS,YAAY,CAAC,KAAK,CAAC,EAAE,GACpC,aAAa,CAAC,KAAK,CAAC,EAAE,CAiCxB"}
|
package/dist/schedule.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export class ScheduleWindowError extends RangeError {
|
|
2
|
+
requiredMs;
|
|
3
|
+
availableMs;
|
|
4
|
+
constructor(requiredMs, availableMs) {
|
|
5
|
+
super(`schedule needs ${requiredMs}ms but the round window has ${availableMs}ms`);
|
|
6
|
+
this.requiredMs = requiredMs;
|
|
7
|
+
this.availableMs = availableMs;
|
|
8
|
+
this.name = 'ScheduleWindowError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/** Turn relative authored durations into the absolute timestamps a round can replay forever. */
|
|
12
|
+
export function scheduleWithin(window, steps) {
|
|
13
|
+
if (!Number.isSafeInteger(window.opensAt) ||
|
|
14
|
+
!Number.isSafeInteger(window.closesAt) ||
|
|
15
|
+
window.closesAt <= window.opensAt) {
|
|
16
|
+
throw new RangeError('schedule needs safe-integer timestamps whose close is after its open');
|
|
17
|
+
}
|
|
18
|
+
const availableMs = window.closesAt - window.opensAt;
|
|
19
|
+
if (!Number.isSafeInteger(availableMs)) {
|
|
20
|
+
throw new RangeError('schedule round-window duration is outside the safe-integer range');
|
|
21
|
+
}
|
|
22
|
+
let cursor = window.opensAt;
|
|
23
|
+
const scheduled = steps.map((step, index) => {
|
|
24
|
+
if (!Number.isSafeInteger(step.durationMs) || step.durationMs <= 0) {
|
|
25
|
+
throw new RangeError(`schedule phase ${index} must have a positive safe-integer duration`);
|
|
26
|
+
}
|
|
27
|
+
const endsAt = cursor + step.durationMs;
|
|
28
|
+
if (!Number.isSafeInteger(endsAt)) {
|
|
29
|
+
throw new RangeError(`schedule phase ${index} ends outside the safe-integer timestamp range`);
|
|
30
|
+
}
|
|
31
|
+
const scheduledStep = { value: step.value, startsAt: cursor, endsAt };
|
|
32
|
+
cursor = scheduledStep.endsAt;
|
|
33
|
+
return scheduledStep;
|
|
34
|
+
});
|
|
35
|
+
if (cursor > window.closesAt) {
|
|
36
|
+
const requiredMs = cursor - window.opensAt;
|
|
37
|
+
if (!Number.isSafeInteger(requiredMs)) {
|
|
38
|
+
throw new RangeError('schedule duration is outside the safe-integer range');
|
|
39
|
+
}
|
|
40
|
+
throw new ScheduleWindowError(requiredMs, availableMs);
|
|
41
|
+
}
|
|
42
|
+
return scheduled;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=schedule.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schedule.js","sourceRoot":"","sources":["../src/schedule.ts"],"names":[],"mappings":"AAaA,MAAM,OAAO,mBAAoB,SAAQ,UAAU;IAC5B;IAA6B;IAAlD,YAAqB,UAAkB,EAAW,WAAmB;QACnE,KAAK,CAAC,kBAAkB,UAAU,+BAA+B,WAAW,IAAI,CAAC,CAAC;QAD/D,eAAU,GAAV,UAAU,CAAQ;QAAW,gBAAW,GAAX,WAAW,CAAQ;QAEnE,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;IACpC,CAAC;CACF;AAED,gGAAgG;AAChG,MAAM,UAAU,cAAc,CAC5B,MAAmB,EACnB,KAAqC;IAErC,IACE,CAAC,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,OAAO,CAAC;QACrC,CAAC,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,QAAQ,CAAC;QACtC,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,OAAO,EACjC,CAAC;QACD,MAAM,IAAI,UAAU,CAAC,sEAAsE,CAAC,CAAC;IAC/F,CAAC;IACD,MAAM,WAAW,GAAG,MAAM,CAAC,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC;IACrD,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,WAAW,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,UAAU,CAAC,kEAAkE,CAAC,CAAC;IAC3F,CAAC;IACD,IAAI,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC;IAC5B,MAAM,SAAS,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;QAC1C,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC,UAAU,IAAI,CAAC,EAAE,CAAC;YACnE,MAAM,IAAI,UAAU,CAAC,kBAAkB,KAAK,6CAA6C,CAAC,CAAC;QAC7F,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC;QACxC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC;YAClC,MAAM,IAAI,UAAU,CAAC,kBAAkB,KAAK,gDAAgD,CAAC,CAAC;QAChG,CAAC;QACD,MAAM,aAAa,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;QACtE,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC;QAC9B,OAAO,aAAa,CAAC;IACvB,CAAC,CAAC,CAAC;IACH,IAAI,MAAM,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAC;QAC7B,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC;QAC3C,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,UAAU,CAAC,EAAE,CAAC;YACtC,MAAM,IAAI,UAAU,CAAC,qDAAqD,CAAC,CAAC;QAC9E,CAAC;QACD,MAAM,IAAI,mBAAmB,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;IACzD,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
|