@table-games-engine/core 0.0.0-stage → 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 +202 -0
- package/README.md +68 -2
- package/dist/engine/context.d.ts +29 -0
- package/dist/engine/drive.d.ts +28 -0
- package/dist/engine/frames.d.ts +23 -0
- package/dist/engine/lifecycle.d.ts +44 -0
- package/dist/engine/operations.d.ts +45 -0
- package/dist/engine/priority.d.ts +17 -0
- package/dist/engine/reducer.d.ts +18 -0
- package/dist/engine/resolve.d.ts +56 -0
- package/dist/engine/rng.d.ts +25 -0
- package/dist/engine/setup-match.d.ts +17 -0
- package/dist/engine/stack.d.ts +33 -0
- package/dist/engine/transition.d.ts +22 -0
- package/dist/engine/triggers.d.ts +45 -0
- package/dist/engine/validate.d.ts +22 -0
- package/dist/engine/views.d.ts +83 -0
- package/dist/engine/window.d.ts +15 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +1411 -0
- package/dist/types/adapter.d.ts +96 -0
- package/dist/types/definition.d.ts +483 -0
- package/dist/types/engine.d.ts +549 -0
- package/dist/types/index.d.ts +23 -0
- package/dist/types/plugins.d.ts +497 -0
- package/dist/types/primitives.d.ts +109 -0
- package/dist/types/stack.d.ts +445 -0
- package/dist/types/state.d.ts +438 -0
- package/dist/types.d.ts +1 -0
- package/package.json +27 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
package/README.md
CHANGED
|
@@ -1,3 +1,69 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @table-games-engine/core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Pure, in-process implementation of the action-stack game engine described in
|
|
4
|
+
[`docs/normative/`](../../docs/normative/).
|
|
5
|
+
|
|
6
|
+
## Run
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install
|
|
10
|
+
npm test --workspace=@table-games-engine/core
|
|
11
|
+
npx tsc -p packages/core --noEmit
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## API entry point
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import {
|
|
18
|
+
createEngine,
|
|
19
|
+
computePlayerView,
|
|
20
|
+
} from '@table-games-engine/core';
|
|
21
|
+
|
|
22
|
+
const engine = createEngine(manifest, initialState, handlers);
|
|
23
|
+
|
|
24
|
+
const result = engine.applyAction(playerInput);
|
|
25
|
+
if (result.ok) {
|
|
26
|
+
engine.resolveTopFrame();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const state = engine.getState();
|
|
30
|
+
const viewForP1 = computePlayerView(state, manifest, p1Id);
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
See [`__tests__/mini-duel.test.ts`](./__tests__/mini-duel.test.ts) for a full
|
|
34
|
+
end-to-end example.
|
|
35
|
+
|
|
36
|
+
## Determinism guarantees
|
|
37
|
+
|
|
38
|
+
Same `(initialState, manifest, handlers, inputs[])` always produces the same
|
|
39
|
+
final state. The engine never calls `Math.random` or `Date.now`; all
|
|
40
|
+
randomness flows through `MatchState.rng` (xorshift32 PRNG). All IDs
|
|
41
|
+
(transition, frame, trigger) derive from `state.version` plus caller-supplied
|
|
42
|
+
input (clientId, event id) — no hidden module-level state.
|
|
43
|
+
|
|
44
|
+
## Module map
|
|
45
|
+
|
|
46
|
+
| File | Purpose |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `src/types/` | Type definitions (the public API contract) |
|
|
49
|
+
| `src/engine/rng.ts` | Seeded xorshift32 PRNG |
|
|
50
|
+
| `src/engine/reducer.ts` | Pure `applyMutation` over 8 mutation domains |
|
|
51
|
+
| `src/engine/transition.ts` | Compose mutations + bump version + log events |
|
|
52
|
+
| `src/engine/stack.ts` | Stack helpers (peek, find, depth) |
|
|
53
|
+
| `src/engine/priority.ts` | Priority pass-order operations |
|
|
54
|
+
| `src/engine/window.ts` | Compute legal `ActionWindow` from phase + stack |
|
|
55
|
+
| `src/engine/context.ts` | `GameEngineContext` — the API game logic sees |
|
|
56
|
+
| `src/engine/validate.ts` | Validation pipeline (priority → window → speed) |
|
|
57
|
+
| `src/engine/frames.ts` | `StackFrame` builder (deterministic ids) |
|
|
58
|
+
| `src/engine/resolve.ts` | Top-of-stack resolver, dispatch to handlers |
|
|
59
|
+
| `src/engine/triggers.ts` | Scan triggers, APNAP-order |
|
|
60
|
+
| `src/engine/lifecycle.ts` | Advance turn / phase, win check |
|
|
61
|
+
| `src/engine/operations.ts` | `createEngine` facade |
|
|
62
|
+
| `src/engine/views.ts` | Per-player view filtering |
|
|
63
|
+
|
|
64
|
+
## Status
|
|
65
|
+
|
|
66
|
+
M1 — all engine modules implemented and unit-tested. 95 tests across 15 files.
|
|
67
|
+
|
|
68
|
+
Out of scope: networking, persistence, client SDK, reference games. See
|
|
69
|
+
[`docs/ROADMAP.md`](../../docs/ROADMAP.md).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview GameEngineContext factory.
|
|
3
|
+
*
|
|
4
|
+
* Builds a read-only context that game logic uses to query state and consume
|
|
5
|
+
* deterministic randomness. The context is the ONLY API surface available to
|
|
6
|
+
* game logic; no Math.random, no Date.now, no I/O.
|
|
7
|
+
*
|
|
8
|
+
* The context is stateless w.r.t. RNG: every random* call seeds from
|
|
9
|
+
* state.rng.state and returns a new state for the caller to thread via an
|
|
10
|
+
* RngMutation. The engine — not the context — advances state.rng.
|
|
11
|
+
*
|
|
12
|
+
* Default behaviors for `evaluateCondition`, `validateTargets`, and
|
|
13
|
+
* `prepareEffect` are intentionally no-ops here. Manifests register concrete
|
|
14
|
+
* evaluators / executors via the EngineOperations facade in Task 14, which
|
|
15
|
+
* wraps the base context.
|
|
16
|
+
*/
|
|
17
|
+
import type { GameEngineContext } from '../types/engine';
|
|
18
|
+
import type { GameManifest } from '../types/definition';
|
|
19
|
+
import type { MatchState } from '../types/state';
|
|
20
|
+
/**
|
|
21
|
+
* Build a read-only GameEngineContext bound to (manifest, state).
|
|
22
|
+
*
|
|
23
|
+
* The returned context:
|
|
24
|
+
* - Holds a per-context counter for `generateId`. Two contexts built from the
|
|
25
|
+
* same state will produce the same ID sequence given the same call order.
|
|
26
|
+
* - Treats RNG as stateless: each `random*` call seeds from `state.rng.state`.
|
|
27
|
+
* - Returns immutable views of state collections.
|
|
28
|
+
*/
|
|
29
|
+
export declare function createContext(manifest: GameManifest, state: MatchState): GameEngineContext;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The single "player submits a move" execution path.
|
|
3
|
+
*
|
|
4
|
+
* Used by the Nakama match handler and by game-kit simulate, so CI runs
|
|
5
|
+
* exactly what the server runs. Pure except for throwing on game-logic
|
|
6
|
+
* bugs (unresolvable frame, state-based actions that never settle).
|
|
7
|
+
*/
|
|
8
|
+
import type { ActionHandler, ActionInput, ActionValidationResult, GameManifest, ResolutionFailure, ResolutionResult, GamePlugin, MatchState, StateBasedActionChecker, StateTransition, WinConditionChecker } from '../types';
|
|
9
|
+
export interface GameRules {
|
|
10
|
+
readonly manifest: GameManifest;
|
|
11
|
+
readonly handlers: Readonly<Record<string, ActionHandler>>;
|
|
12
|
+
readonly stateBasedActions?: StateBasedActionChecker;
|
|
13
|
+
readonly winChecker?: WinConditionChecker;
|
|
14
|
+
}
|
|
15
|
+
export type DriveResult = {
|
|
16
|
+
readonly ok: true;
|
|
17
|
+
readonly state: MatchState;
|
|
18
|
+
readonly transitions: ReadonlyArray<StateTransition>;
|
|
19
|
+
} | {
|
|
20
|
+
readonly ok: false;
|
|
21
|
+
readonly error: ActionValidationResult;
|
|
22
|
+
};
|
|
23
|
+
/** Handler result that rejects the move: the server refuses the input and nothing changes. */
|
|
24
|
+
export declare function reject(reason?: ResolutionFailure): ResolutionResult;
|
|
25
|
+
/** Successful handler result. */
|
|
26
|
+
export declare function resolved(mutations: ResolutionResult['mutations'], events?: ResolutionResult['events']): ResolutionResult;
|
|
27
|
+
export declare function rulesFromPlugin(plugin: GamePlugin): GameRules;
|
|
28
|
+
export declare function driveAction(rules: GameRules, state: MatchState, input: ActionInput): DriveResult;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Stack frame builders.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions that turn a validated `ActionInput` into a concrete
|
|
5
|
+
* `StackFrame`. Frame IDs are derived from `state.version` and the input's
|
|
6
|
+
* `clientId` (an idempotency key carried by every `ActionInput`). Same input
|
|
7
|
+
* applied to the same state always produces the same id — required for
|
|
8
|
+
* replay and event-log comparison.
|
|
9
|
+
*/
|
|
10
|
+
import type { ActionFrame, ActionInputAction, StackFrame } from '../types/stack';
|
|
11
|
+
import type { ActionDef } from '../types/definition';
|
|
12
|
+
import type { LogicalTimestamp, PlayerId } from '../types/primitives';
|
|
13
|
+
export interface BuildActionFrameArgs {
|
|
14
|
+
readonly actor: PlayerId;
|
|
15
|
+
readonly def: ActionDef;
|
|
16
|
+
readonly data: ActionInputAction;
|
|
17
|
+
readonly tick: LogicalTimestamp;
|
|
18
|
+
readonly version: number;
|
|
19
|
+
readonly clientId: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function buildActionFrame(args: BuildActionFrameArgs): ActionFrame;
|
|
22
|
+
/** Re-export of the discriminated union for callers that want it. */
|
|
23
|
+
export type AnyFrame = StackFrame;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Lifecycle ops: advance turn, advance phase, win check.
|
|
3
|
+
*
|
|
4
|
+
* This is the glue layer between the resolution loop and game-defined
|
|
5
|
+
* predicates from the manifest. Each function is pure: it takes a state
|
|
6
|
+
* (and possibly the manifest) and returns either a new state or a
|
|
7
|
+
* `GameEndCheck` verdict — never mutating its inputs.
|
|
8
|
+
*
|
|
9
|
+
* State shape is left untouched. All structural changes go through
|
|
10
|
+
* `applyMutation` from `./reducer.ts` so we share the same exhaustive
|
|
11
|
+
* mutation machinery the rest of the engine uses.
|
|
12
|
+
*/
|
|
13
|
+
import type { MatchState } from '../types/state';
|
|
14
|
+
import type { GameManifest } from '../types/definition';
|
|
15
|
+
import type { GameEndCheck } from '../types/engine';
|
|
16
|
+
/**
|
|
17
|
+
* Rotate the active player by one position in turnOrder and bump turnNumber.
|
|
18
|
+
*
|
|
19
|
+
* Delegates to the reducer's `turn:advance` op so wrap-around, index
|
|
20
|
+
* adjustment, and turnNumber accounting all live in one place.
|
|
21
|
+
*/
|
|
22
|
+
export declare function advanceTurn(state: MatchState): MatchState;
|
|
23
|
+
/**
|
|
24
|
+
* Move to the first declared transition target leaving the current phase.
|
|
25
|
+
*
|
|
26
|
+
* Transitions live on the `PhaseGraph` (manifest.phases.transitions), NOT
|
|
27
|
+
* on individual `PhaseDef` objects. We pick the highest-priority transition
|
|
28
|
+
* whose `from` matches the current phase. If no transition exists, the
|
|
29
|
+
* state is returned unchanged.
|
|
30
|
+
*
|
|
31
|
+
* Conditional transition selection (e.g. via `triggerCondition`) is the
|
|
32
|
+
* responsibility of the EngineOperations facade in Task 13/14; this layer
|
|
33
|
+
* implements the universal "first declared next phase" default.
|
|
34
|
+
*/
|
|
35
|
+
export declare function advancePhase(state: MatchState, manifest: GameManifest): MatchState;
|
|
36
|
+
/**
|
|
37
|
+
* Universal default win check: <=1 non-eliminated player.
|
|
38
|
+
*
|
|
39
|
+
* Game-specific manifests can override via a custom `WinConditionChecker`
|
|
40
|
+
* wired through the EngineOperations facade. This default handles the
|
|
41
|
+
* elimination case (`status === 'eliminated'`) and produces a `GameResult`
|
|
42
|
+
* with rankings ordered so the surviving player ranks 1st.
|
|
43
|
+
*/
|
|
44
|
+
export declare function checkWin(state: MatchState): GameEndCheck;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview EngineOperations facade.
|
|
3
|
+
*
|
|
4
|
+
* `createEngine(manifest, initialState, handlers)` returns an `Engine` that
|
|
5
|
+
* implements {@link EngineOperations} plus a `getState()` accessor. Every
|
|
6
|
+
* public method:
|
|
7
|
+
* - validates inputs against the current state (read-only),
|
|
8
|
+
* - composes a {@link StateTransition} via `applyTransition`,
|
|
9
|
+
* - replaces the engine's internal state reference with the new state,
|
|
10
|
+
* - returns the transition (or an error) wrapped in a {@link Result}.
|
|
11
|
+
*
|
|
12
|
+
* The engine itself is the only stateful piece in the runtime; every
|
|
13
|
+
* operation it performs is a pure transformation of the previous state,
|
|
14
|
+
* mediated by the reducer-driven `applyTransition` helper.
|
|
15
|
+
*
|
|
16
|
+
* Design notes:
|
|
17
|
+
* - The action window in `state.priority.window` is derived data: it
|
|
18
|
+
* depends on the current phase and stack. Before validating each
|
|
19
|
+
* player input, we recompute the window via `computeWindow` and
|
|
20
|
+
* replace it in-place on the snapshot fed to `validateAction`. This
|
|
21
|
+
* replacement is NOT a transition — it never bumps `version`.
|
|
22
|
+
* - Pass / concede / timeout inputs do NOT push a frame onto the stack.
|
|
23
|
+
* Validation produces a sentinel "noop" frame (id starts with
|
|
24
|
+
* `pass-noop` / `concede-noop` / `timeout-noop`); we detect those
|
|
25
|
+
* sentinels and emit minimal mutations instead.
|
|
26
|
+
* - Lifecycle helpers (`advancePhase`, `advanceTurn`) compute the next
|
|
27
|
+
* state shape; we then synthesise the equivalent typed mutation and
|
|
28
|
+
* run it through `applyTransition` so the version/event bookkeeping
|
|
29
|
+
* stays consistent.
|
|
30
|
+
*/
|
|
31
|
+
import type { EngineOperations, ActionHandler } from '../types/engine';
|
|
32
|
+
import type { GameManifest } from '../types/definition';
|
|
33
|
+
import type { MatchState } from '../types/state';
|
|
34
|
+
/**
|
|
35
|
+
* Engine instance: implements `EngineOperations` plus `getState()`.
|
|
36
|
+
*
|
|
37
|
+
* The instance holds the latest `MatchState` internally; tests and host
|
|
38
|
+
* runtimes (Nakama adapter, mini-duel fixture) inspect it via
|
|
39
|
+
* `getState()` between operations.
|
|
40
|
+
*/
|
|
41
|
+
export interface Engine extends EngineOperations {
|
|
42
|
+
/** Current internal state snapshot. */
|
|
43
|
+
getState(): MatchState;
|
|
44
|
+
}
|
|
45
|
+
export declare function createEngine(manifest: GameManifest, initialState: MatchState, handlers: Record<string, ActionHandler>): Engine;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PriorityState } from '../types/stack';
|
|
2
|
+
import type { PlayerId, ImmutableList } from '../types/primitives';
|
|
3
|
+
/**
|
|
4
|
+
* Pass order = active player, then turn order in clockwise rotation.
|
|
5
|
+
* Mirrors `runtime-action-processing.md` § 6.
|
|
6
|
+
*/
|
|
7
|
+
export declare function buildPassOrder(turnOrder: ImmutableList<PlayerId>, activePlayer: PlayerId): PlayerId[];
|
|
8
|
+
export declare function advancePriority(p: PriorityState): PriorityState;
|
|
9
|
+
export declare function markPassed(p: PriorityState, player: PlayerId): PriorityState;
|
|
10
|
+
export declare function allPassed(p: PriorityState, activePlayers: ImmutableList<PlayerId>): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* After any stack change (push or pop), priority resets:
|
|
13
|
+
* - holder = active player
|
|
14
|
+
* - passed = []
|
|
15
|
+
* - consecutivePasses = 0
|
|
16
|
+
*/
|
|
17
|
+
export declare function resetPriorityOnStackChange(p: PriorityState, activePlayer: PlayerId): PriorityState;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Pure state mutation reducer.
|
|
3
|
+
*
|
|
4
|
+
* `applyMutation(state, mutation) -> newState` is the ONE place state mutates.
|
|
5
|
+
* It is a pure function: never mutates input, always returns a new state via
|
|
6
|
+
* spread. The switch over `mutation.domain` is exhaustive: a `never`-typed
|
|
7
|
+
* default catches any forgotten case at compile time.
|
|
8
|
+
*/
|
|
9
|
+
import type { MatchState } from '../types/state';
|
|
10
|
+
import type { StateMutation } from '../types/engine';
|
|
11
|
+
/**
|
|
12
|
+
* Apply a single mutation, returning a new MatchState.
|
|
13
|
+
* Pure function; never mutates input.
|
|
14
|
+
*
|
|
15
|
+
* Unknown operation types are caught at compile time via the `never`-typed
|
|
16
|
+
* exhaustive default. The runtime throw guards against malformed JSON.
|
|
17
|
+
*/
|
|
18
|
+
export declare function applyMutation(state: MatchState, mutation: StateMutation): MatchState;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Top-of-stack resolver.
|
|
3
|
+
*
|
|
4
|
+
* `resolveTopFrame` is the pure, read-only step that runs when the engine is
|
|
5
|
+
* ready to resolve the next stack frame:
|
|
6
|
+
*
|
|
7
|
+
* 1. Peek the top frame (if the stack is empty, return `stack-empty`).
|
|
8
|
+
* 2. If the frame's targets are no longer valid, the frame fizzles.
|
|
9
|
+
* 3. Look up the registered `ActionHandler` for the frame:
|
|
10
|
+
* - `action`/`reaction` frames dispatch by `actionTypeId`.
|
|
11
|
+
* - `trigger` frames dispatch by `triggerId`.
|
|
12
|
+
* - `effect` frames dispatch by `effectType`.
|
|
13
|
+
* 4. Build a read-only context and invoke the handler.
|
|
14
|
+
* 5. Return the handler's `ResolutionResult` wrapped in a `ResolveOutcome`.
|
|
15
|
+
*
|
|
16
|
+
* The resolver does NOT mutate state. It is the EngineOperations facade
|
|
17
|
+
* (Task 13) that takes the outcome and applies it via `applyTransition`.
|
|
18
|
+
*/
|
|
19
|
+
import type { MatchState } from '../types/state';
|
|
20
|
+
import type { GameManifest } from '../types/definition';
|
|
21
|
+
import type { ActionHandler, ResolutionResult } from '../types/engine';
|
|
22
|
+
import type { FrameId } from '../types/primitives';
|
|
23
|
+
/**
|
|
24
|
+
* Outcome of attempting to resolve the top stack frame.
|
|
25
|
+
*
|
|
26
|
+
* Discriminated union — callers should switch on `kind`:
|
|
27
|
+
* - `stack-empty` : nothing to resolve.
|
|
28
|
+
* - `no-handler` : a frame is on the stack but the manifest registered
|
|
29
|
+
* no handler for its dispatch key. The facade should
|
|
30
|
+
* treat this as a system error / no-op.
|
|
31
|
+
* - `fizzled` : the frame's targets became invalid; the frame should
|
|
32
|
+
* be popped without effect.
|
|
33
|
+
* - `resolved` : the handler ran and returned a `ResolutionResult`.
|
|
34
|
+
*/
|
|
35
|
+
export type ResolveOutcome = {
|
|
36
|
+
kind: 'stack-empty';
|
|
37
|
+
} | {
|
|
38
|
+
kind: 'no-handler';
|
|
39
|
+
actionTypeId: string;
|
|
40
|
+
} | {
|
|
41
|
+
kind: 'fizzled';
|
|
42
|
+
frameId: FrameId;
|
|
43
|
+
reason: 'targets-illegal';
|
|
44
|
+
} | {
|
|
45
|
+
kind: 'resolved';
|
|
46
|
+
frameId: FrameId;
|
|
47
|
+
result: ResolutionResult;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Pure resolver: pops nothing, mutates nothing.
|
|
51
|
+
*
|
|
52
|
+
* The caller (EngineOperations facade) is responsible for translating the
|
|
53
|
+
* returned outcome into a `StateTransition` (popping the frame, applying
|
|
54
|
+
* mutations, appending events, queueing fired triggers, pushing child frames).
|
|
55
|
+
*/
|
|
56
|
+
export declare function resolveTopFrame(state: MatchState, manifest: GameManifest, handlers: Record<string, ActionHandler>): ResolveOutcome;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ImmutableList } from '../types/primitives';
|
|
2
|
+
import type { RngState } from '../types/state';
|
|
3
|
+
import type { RandomResult } from '../types/engine';
|
|
4
|
+
/**
|
|
5
|
+
* Initialize an RngState from a string seed.
|
|
6
|
+
* Same string -> same state.
|
|
7
|
+
*/
|
|
8
|
+
export declare function seedRng(seed: string): RngState;
|
|
9
|
+
/**
|
|
10
|
+
* Advance state once, return value in [0, 1) and new state.
|
|
11
|
+
*/
|
|
12
|
+
export declare function nextRandom(state: ImmutableList<number>): RandomResult<number>;
|
|
13
|
+
/**
|
|
14
|
+
* Random integer in [min, max] inclusive.
|
|
15
|
+
*/
|
|
16
|
+
export declare function randomInt(state: ImmutableList<number>, min: number, max: number): RandomResult<number>;
|
|
17
|
+
/**
|
|
18
|
+
* Pick a random element from array. Empty array -> undefined, no state advance.
|
|
19
|
+
*/
|
|
20
|
+
export declare function randomPick<T>(state: ImmutableList<number>, array: ReadonlyArray<T>): RandomResult<T | undefined>;
|
|
21
|
+
/**
|
|
22
|
+
* Fisher-Yates shuffle using deterministic RNG.
|
|
23
|
+
* Returns a NEW array; does not mutate input.
|
|
24
|
+
*/
|
|
25
|
+
export declare function randomShuffle<T>(state: ImmutableList<number>, array: ReadonlyArray<T>): RandomResult<ReadonlyArray<T>>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Plugin → initial MatchState.
|
|
3
|
+
*
|
|
4
|
+
* Generalised from the TCG Nakama registration: setup functions receive a
|
|
5
|
+
* real GameEngineContext (seeded RNG, manifest) over a schema-valid empty
|
|
6
|
+
* state, and may build on `ctx.state`.
|
|
7
|
+
*/
|
|
8
|
+
import type { GameConfig } from '../types/definition';
|
|
9
|
+
import type { GamePlugin } from '../types/plugins';
|
|
10
|
+
import type { MatchState } from '../types/state';
|
|
11
|
+
import type { PlayerId } from '../types/primitives';
|
|
12
|
+
export interface SetupMatchArgs {
|
|
13
|
+
readonly playerIds: ReadonlyArray<PlayerId>;
|
|
14
|
+
readonly config: GameConfig;
|
|
15
|
+
readonly seed: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function setupMatch(plugin: GamePlugin, args: SetupMatchArgs): MatchState;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Pure helper functions over the action stack.
|
|
3
|
+
*
|
|
4
|
+
* These helpers are read-only inspectors of the {@link ActionStack}.
|
|
5
|
+
* They never mutate input.
|
|
6
|
+
*
|
|
7
|
+
* NOTE: `topFrame` and `isStackEmpty` already live in
|
|
8
|
+
* `../types/stack.ts` and are re-exported from the package root.
|
|
9
|
+
* Do not duplicate them here.
|
|
10
|
+
*/
|
|
11
|
+
import type { ActionStack, StackFrame } from '../types/stack';
|
|
12
|
+
import type { FrameId } from '../types/primitives';
|
|
13
|
+
/**
|
|
14
|
+
* Return the frame currently at the top of the stack, or undefined if empty.
|
|
15
|
+
*
|
|
16
|
+
* Equivalent to `topFrame` but provided here as the canonical engine helper.
|
|
17
|
+
*/
|
|
18
|
+
export declare function peek(stack: ActionStack): StackFrame | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Find a frame by its FrameId. Returns undefined if no such frame exists.
|
|
21
|
+
*/
|
|
22
|
+
export declare function findFrame(stack: ActionStack, id: FrameId): StackFrame | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Return the frame immediately above the given frame in the stack
|
|
25
|
+
* (the next frame closer to the top), or undefined if the given
|
|
26
|
+
* frame is at the top or not in the stack.
|
|
27
|
+
*/
|
|
28
|
+
export declare function frameAfter(stack: ActionStack, id: FrameId): StackFrame | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Return the depth (index from the bottom of the stack, 0-indexed) of
|
|
31
|
+
* the frame with the given id, or -1 if not present.
|
|
32
|
+
*/
|
|
33
|
+
export declare function frameDepth(stack: ActionStack, id: FrameId): number;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Transition applier.
|
|
3
|
+
*
|
|
4
|
+
* `applyTransition` composes a sequence of mutations on top of `MatchState`,
|
|
5
|
+
* appends events to `recentEvents`, bumps `version` by 1 and `tick` by 1, and
|
|
6
|
+
* returns both the new state and a `StateTransition` record describing the
|
|
7
|
+
* change. Pure: never mutates the input state.
|
|
8
|
+
*/
|
|
9
|
+
import type { StateMutation, StateTransition, TransitionType, TransitionCause } from '../types/engine';
|
|
10
|
+
import type { MatchState, GameEvent } from '../types/state';
|
|
11
|
+
import type { StateVersion } from '../types/primitives';
|
|
12
|
+
/** Increment a branded StateVersion. */
|
|
13
|
+
export declare function inc(v: StateVersion): StateVersion;
|
|
14
|
+
export interface ApplyTransitionResult {
|
|
15
|
+
readonly newState: MatchState;
|
|
16
|
+
readonly transition: StateTransition;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Apply mutations + events to state, producing a new state and a logged transition.
|
|
20
|
+
* Increments version by 1 and tick by 1. Pure: input state is not mutated.
|
|
21
|
+
*/
|
|
22
|
+
export declare function applyTransition(state: MatchState, mutations: ReadonlyArray<StateMutation>, events: ReadonlyArray<GameEvent>, cause: TransitionCause, type?: TransitionType): ApplyTransitionResult;
|