@drmxrcy/tcg-core 0.0.0-202602060542
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/README.md +882 -0
- package/package.json +58 -0
- package/src/__tests__/alpha-clash-engine-definition.test.ts +319 -0
- package/src/__tests__/createMockAlphaClashGame.ts +462 -0
- package/src/__tests__/createMockGrandArchiveGame.ts +373 -0
- package/src/__tests__/createMockGundamGame.ts +379 -0
- package/src/__tests__/createMockLorcanaGame.ts +328 -0
- package/src/__tests__/createMockOnePieceGame.ts +429 -0
- package/src/__tests__/createMockRiftboundGame.ts +462 -0
- package/src/__tests__/grand-archive-engine-definition.test.ts +118 -0
- package/src/__tests__/gundam-engine-definition.test.ts +110 -0
- package/src/__tests__/integration-complete-game.test.ts +508 -0
- package/src/__tests__/integration-network-sync.test.ts +469 -0
- package/src/__tests__/lorcana-engine-definition.test.ts +100 -0
- package/src/__tests__/move-enumeration.test.ts +725 -0
- package/src/__tests__/multiplayer-engine.test.ts +555 -0
- package/src/__tests__/one-piece-engine-definition.test.ts +114 -0
- package/src/__tests__/riftbound-engine-definition.test.ts +124 -0
- package/src/actions/action-definition.test.ts +201 -0
- package/src/actions/action-definition.ts +122 -0
- package/src/actions/action-timing.test.ts +490 -0
- package/src/actions/action-timing.ts +257 -0
- package/src/cards/card-definition.test.ts +268 -0
- package/src/cards/card-definition.ts +27 -0
- package/src/cards/card-instance.test.ts +422 -0
- package/src/cards/card-instance.ts +49 -0
- package/src/cards/computed-properties.test.ts +530 -0
- package/src/cards/computed-properties.ts +84 -0
- package/src/cards/conditional-modifiers.test.ts +390 -0
- package/src/cards/modifiers.test.ts +286 -0
- package/src/cards/modifiers.ts +51 -0
- package/src/engine/MULTIPLAYER.md +425 -0
- package/src/engine/__tests__/rule-engine-flow.test.ts +348 -0
- package/src/engine/__tests__/rule-engine-history.test.ts +535 -0
- package/src/engine/__tests__/rule-engine-moves.test.ts +488 -0
- package/src/engine/__tests__/rule-engine.test.ts +366 -0
- package/src/engine/index.ts +14 -0
- package/src/engine/multiplayer-engine.example.ts +571 -0
- package/src/engine/multiplayer-engine.ts +409 -0
- package/src/engine/rule-engine.test.ts +286 -0
- package/src/engine/rule-engine.ts +1539 -0
- package/src/engine/tracker-system.ts +172 -0
- package/src/examples/__tests__/coin-flip-game.test.ts +641 -0
- package/src/filtering/card-filter.test.ts +230 -0
- package/src/filtering/card-filter.ts +91 -0
- package/src/filtering/card-query.test.ts +901 -0
- package/src/filtering/card-query.ts +273 -0
- package/src/filtering/filter-matching.test.ts +944 -0
- package/src/filtering/filter-matching.ts +315 -0
- package/src/flow/SERIALIZATION.md +428 -0
- package/src/flow/__tests__/flow-definition.test.ts +427 -0
- package/src/flow/__tests__/flow-manager.test.ts +756 -0
- package/src/flow/__tests__/flow-serialization.test.ts +565 -0
- package/src/flow/flow-definition.ts +453 -0
- package/src/flow/flow-manager.ts +1044 -0
- package/src/flow/index.ts +35 -0
- package/src/game-definition/__tests__/game-definition-validation.test.ts +359 -0
- package/src/game-definition/__tests__/game-definition.test.ts +291 -0
- package/src/game-definition/__tests__/move-definitions.test.ts +328 -0
- package/src/game-definition/game-definition.ts +261 -0
- package/src/game-definition/index.ts +28 -0
- package/src/game-definition/move-definitions.ts +188 -0
- package/src/game-definition/validation.ts +183 -0
- package/src/history/history-manager.test.ts +497 -0
- package/src/history/history-manager.ts +312 -0
- package/src/history/history-operations.ts +122 -0
- package/src/history/index.ts +9 -0
- package/src/history/types.ts +255 -0
- package/src/index.ts +32 -0
- package/src/logging/index.ts +27 -0
- package/src/logging/log-formatter.ts +187 -0
- package/src/logging/logger.ts +276 -0
- package/src/logging/types.ts +148 -0
- package/src/moves/create-move.test.ts +331 -0
- package/src/moves/create-move.ts +64 -0
- package/src/moves/move-enumeration.ts +228 -0
- package/src/moves/move-executor.test.ts +431 -0
- package/src/moves/move-executor.ts +195 -0
- package/src/moves/move-system.test.ts +380 -0
- package/src/moves/move-system.ts +463 -0
- package/src/moves/standard-moves.ts +231 -0
- package/src/operations/card-operations.test.ts +236 -0
- package/src/operations/card-operations.ts +116 -0
- package/src/operations/card-registry-impl.test.ts +251 -0
- package/src/operations/card-registry-impl.ts +70 -0
- package/src/operations/card-registry.test.ts +234 -0
- package/src/operations/card-registry.ts +106 -0
- package/src/operations/counter-operations.ts +152 -0
- package/src/operations/game-operations.test.ts +280 -0
- package/src/operations/game-operations.ts +140 -0
- package/src/operations/index.ts +24 -0
- package/src/operations/operations-impl.test.ts +354 -0
- package/src/operations/operations-impl.ts +468 -0
- package/src/operations/zone-operations.test.ts +295 -0
- package/src/operations/zone-operations.ts +223 -0
- package/src/rng/seeded-rng.test.ts +339 -0
- package/src/rng/seeded-rng.ts +123 -0
- package/src/targeting/index.ts +48 -0
- package/src/targeting/target-definition.test.ts +273 -0
- package/src/targeting/target-definition.ts +37 -0
- package/src/targeting/target-dsl.ts +279 -0
- package/src/targeting/target-resolver.ts +486 -0
- package/src/targeting/target-validation.test.ts +994 -0
- package/src/targeting/target-validation.ts +286 -0
- package/src/telemetry/events.ts +202 -0
- package/src/telemetry/index.ts +21 -0
- package/src/telemetry/telemetry-manager.ts +127 -0
- package/src/telemetry/types.ts +68 -0
- package/src/testing/__tests__/testing-utilities-integration.test.ts +161 -0
- package/src/testing/index.ts +88 -0
- package/src/testing/test-assertions.test.ts +341 -0
- package/src/testing/test-assertions.ts +256 -0
- package/src/testing/test-card-factory.test.ts +228 -0
- package/src/testing/test-card-factory.ts +111 -0
- package/src/testing/test-context-factory.ts +187 -0
- package/src/testing/test-end-assertions.test.ts +262 -0
- package/src/testing/test-end-assertions.ts +95 -0
- package/src/testing/test-engine-builder.test.ts +389 -0
- package/src/testing/test-engine-builder.ts +46 -0
- package/src/testing/test-flow-assertions.test.ts +284 -0
- package/src/testing/test-flow-assertions.ts +115 -0
- package/src/testing/test-player-builder.test.ts +132 -0
- package/src/testing/test-player-builder.ts +46 -0
- package/src/testing/test-replay-assertions.test.ts +356 -0
- package/src/testing/test-replay-assertions.ts +164 -0
- package/src/testing/test-rng-helpers.test.ts +260 -0
- package/src/testing/test-rng-helpers.ts +190 -0
- package/src/testing/test-state-builder.test.ts +373 -0
- package/src/testing/test-state-builder.ts +99 -0
- package/src/testing/test-zone-factory.test.ts +295 -0
- package/src/testing/test-zone-factory.ts +224 -0
- package/src/types/branded-utils.ts +54 -0
- package/src/types/branded.test.ts +175 -0
- package/src/types/branded.ts +33 -0
- package/src/types/index.ts +8 -0
- package/src/types/state.test.ts +198 -0
- package/src/types/state.ts +154 -0
- package/src/validation/card-type-guards.test.ts +242 -0
- package/src/validation/card-type-guards.ts +179 -0
- package/src/validation/index.ts +40 -0
- package/src/validation/schema-builders.test.ts +403 -0
- package/src/validation/schema-builders.ts +345 -0
- package/src/validation/type-guard-builder.test.ts +216 -0
- package/src/validation/type-guard-builder.ts +109 -0
- package/src/validation/validator-builder.test.ts +375 -0
- package/src/validation/validator-builder.ts +273 -0
- package/src/zones/index.ts +28 -0
- package/src/zones/zone-factory.test.ts +183 -0
- package/src/zones/zone-factory.ts +44 -0
- package/src/zones/zone-operations.test.ts +800 -0
- package/src/zones/zone-operations.ts +306 -0
- package/src/zones/zone-state-helpers.test.ts +337 -0
- package/src/zones/zone-state-helpers.ts +128 -0
- package/src/zones/zone-visibility.test.ts +156 -0
- package/src/zones/zone-visibility.ts +36 -0
- package/src/zones/zone.test.ts +186 -0
- package/src/zones/zone.ts +66 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { SeededRNG } from "./seeded-rng";
|
|
3
|
+
|
|
4
|
+
describe("SeededRNG", () => {
|
|
5
|
+
describe("Interface", () => {
|
|
6
|
+
it("should create RNG instance with default seed", () => {
|
|
7
|
+
const rng = new SeededRNG();
|
|
8
|
+
|
|
9
|
+
expect(rng).toBeDefined();
|
|
10
|
+
expect(typeof rng.getSeed()).toBe("string");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("should create RNG instance with provided seed", () => {
|
|
14
|
+
const seed = "test-seed-123";
|
|
15
|
+
const rng = new SeededRNG(seed);
|
|
16
|
+
|
|
17
|
+
expect(rng.getSeed()).toBe(seed);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("should have all required methods", () => {
|
|
21
|
+
const rng = new SeededRNG();
|
|
22
|
+
|
|
23
|
+
expect(typeof rng.getSeed).toBe("function");
|
|
24
|
+
expect(typeof rng.setSeed).toBe("function");
|
|
25
|
+
expect(typeof rng.random).toBe("function");
|
|
26
|
+
expect(typeof rng.randomInt).toBe("function");
|
|
27
|
+
expect(typeof rng.pick).toBe("function");
|
|
28
|
+
expect(typeof rng.shuffle).toBe("function");
|
|
29
|
+
expect(typeof rng.rollDice).toBe("function");
|
|
30
|
+
expect(typeof rng.flipCoin).toBe("function");
|
|
31
|
+
expect(typeof rng.createChild).toBe("function");
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("Seed Management", () => {
|
|
36
|
+
it("should get current seed", () => {
|
|
37
|
+
const seed = "my-seed";
|
|
38
|
+
const rng = new SeededRNG(seed);
|
|
39
|
+
|
|
40
|
+
expect(rng.getSeed()).toBe(seed);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should set new seed", () => {
|
|
44
|
+
const rng = new SeededRNG("initial-seed");
|
|
45
|
+
const newSeed = "new-seed";
|
|
46
|
+
|
|
47
|
+
rng.setSeed(newSeed);
|
|
48
|
+
|
|
49
|
+
expect(rng.getSeed()).toBe(newSeed);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should reset generator when seed changes", () => {
|
|
53
|
+
const rng = new SeededRNG("seed1");
|
|
54
|
+
const value1 = rng.random();
|
|
55
|
+
|
|
56
|
+
rng.setSeed("seed1");
|
|
57
|
+
const value2 = rng.random();
|
|
58
|
+
|
|
59
|
+
expect(value1).toBe(value2);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("Random Number Generation", () => {
|
|
64
|
+
it("should generate random float between 0 and 1", () => {
|
|
65
|
+
const rng = new SeededRNG("test");
|
|
66
|
+
|
|
67
|
+
for (let i = 0; i < 100; i++) {
|
|
68
|
+
const value = rng.random();
|
|
69
|
+
expect(value).toBeGreaterThanOrEqual(0);
|
|
70
|
+
expect(value).toBeLessThan(1);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("should generate random integer in range [min, max]", () => {
|
|
75
|
+
const rng = new SeededRNG("test");
|
|
76
|
+
|
|
77
|
+
for (let i = 0; i < 100; i++) {
|
|
78
|
+
const value = rng.randomInt(1, 10);
|
|
79
|
+
expect(value).toBeGreaterThanOrEqual(1);
|
|
80
|
+
expect(value).toBeLessThanOrEqual(10);
|
|
81
|
+
expect(Number.isInteger(value)).toBe(true);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should generate random integer with single argument [0, max]", () => {
|
|
86
|
+
const rng = new SeededRNG("test");
|
|
87
|
+
|
|
88
|
+
for (let i = 0; i < 100; i++) {
|
|
89
|
+
const value = rng.randomInt(5);
|
|
90
|
+
expect(value).toBeGreaterThanOrEqual(0);
|
|
91
|
+
expect(value).toBeLessThanOrEqual(5);
|
|
92
|
+
expect(Number.isInteger(value)).toBe(true);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("should handle min === max in randomInt", () => {
|
|
97
|
+
const rng = new SeededRNG("test");
|
|
98
|
+
const value = rng.randomInt(5, 5);
|
|
99
|
+
|
|
100
|
+
expect(value).toBe(5);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("Array Operations", () => {
|
|
105
|
+
it("should pick random element from array", () => {
|
|
106
|
+
const rng = new SeededRNG("test");
|
|
107
|
+
const array = ["a", "b", "c", "d", "e"];
|
|
108
|
+
|
|
109
|
+
const picked = rng.pick(array);
|
|
110
|
+
|
|
111
|
+
expect(array).toContain(picked);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("should not mutate original array when picking", () => {
|
|
115
|
+
const rng = new SeededRNG("test");
|
|
116
|
+
const array = ["a", "b", "c"];
|
|
117
|
+
const originalArray = [...array];
|
|
118
|
+
|
|
119
|
+
rng.pick(array);
|
|
120
|
+
|
|
121
|
+
expect(array).toEqual(originalArray);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("should shuffle array", () => {
|
|
125
|
+
const rng = new SeededRNG("test");
|
|
126
|
+
const array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
|
127
|
+
|
|
128
|
+
const shuffled = rng.shuffle(array);
|
|
129
|
+
|
|
130
|
+
expect(shuffled).toHaveLength(array.length);
|
|
131
|
+
expect(shuffled.every((item) => array.includes(item))).toBe(true);
|
|
132
|
+
expect(array.every((item) => shuffled.includes(item))).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("should not mutate original array when shuffling", () => {
|
|
136
|
+
const rng = new SeededRNG("test");
|
|
137
|
+
const array = [1, 2, 3, 4, 5];
|
|
138
|
+
const originalArray = [...array];
|
|
139
|
+
|
|
140
|
+
rng.shuffle(array);
|
|
141
|
+
|
|
142
|
+
expect(array).toEqual(originalArray);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("should handle single element array", () => {
|
|
146
|
+
const rng = new SeededRNG("test");
|
|
147
|
+
const array = [42];
|
|
148
|
+
|
|
149
|
+
const picked = rng.pick(array);
|
|
150
|
+
const shuffled = rng.shuffle(array);
|
|
151
|
+
|
|
152
|
+
expect(picked).toBe(42);
|
|
153
|
+
expect(shuffled).toEqual([42]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("should handle empty array for pick", () => {
|
|
157
|
+
const rng = new SeededRNG("test");
|
|
158
|
+
const array: number[] = [];
|
|
159
|
+
|
|
160
|
+
const picked = rng.pick(array);
|
|
161
|
+
|
|
162
|
+
expect(picked).toBeUndefined();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("should handle empty array for shuffle", () => {
|
|
166
|
+
const rng = new SeededRNG("test");
|
|
167
|
+
const array: number[] = [];
|
|
168
|
+
|
|
169
|
+
const shuffled = rng.shuffle(array);
|
|
170
|
+
|
|
171
|
+
expect(shuffled).toEqual([]);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe("Dice and Coin", () => {
|
|
176
|
+
it("should roll dice with specified sides", () => {
|
|
177
|
+
const rng = new SeededRNG("test");
|
|
178
|
+
|
|
179
|
+
for (let i = 0; i < 100; i++) {
|
|
180
|
+
const roll = rng.rollDice(6);
|
|
181
|
+
expect(roll).toBeGreaterThanOrEqual(1);
|
|
182
|
+
expect(roll).toBeLessThanOrEqual(6);
|
|
183
|
+
expect(Number.isInteger(roll)).toBe(true);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("should roll multiple dice", () => {
|
|
188
|
+
const rng = new SeededRNG("test");
|
|
189
|
+
|
|
190
|
+
for (let i = 0; i < 50; i++) {
|
|
191
|
+
const rolls = rng.rollDice(6, 3);
|
|
192
|
+
expect(Array.isArray(rolls)).toBe(true);
|
|
193
|
+
expect(rolls).toHaveLength(3);
|
|
194
|
+
if (Array.isArray(rolls)) {
|
|
195
|
+
for (const roll of rolls) {
|
|
196
|
+
expect(roll).toBeGreaterThanOrEqual(1);
|
|
197
|
+
expect(roll).toBeLessThanOrEqual(6);
|
|
198
|
+
expect(Number.isInteger(roll)).toBe(true);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("should flip coin returning boolean", () => {
|
|
205
|
+
const rng = new SeededRNG("test");
|
|
206
|
+
let heads = 0;
|
|
207
|
+
let tails = 0;
|
|
208
|
+
|
|
209
|
+
for (let i = 0; i < 100; i++) {
|
|
210
|
+
const flip = rng.flipCoin();
|
|
211
|
+
expect(typeof flip).toBe("boolean");
|
|
212
|
+
if (flip) heads++;
|
|
213
|
+
else tails++;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Both outcomes should occur (probability check)
|
|
217
|
+
expect(heads).toBeGreaterThan(0);
|
|
218
|
+
expect(tails).toBeGreaterThan(0);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("should flip coin with bias", () => {
|
|
222
|
+
const rng = new SeededRNG("test");
|
|
223
|
+
let heads = 0;
|
|
224
|
+
|
|
225
|
+
// 90% bias towards heads
|
|
226
|
+
for (let i = 0; i < 1000; i++) {
|
|
227
|
+
if (rng.flipCoin(0.9)) heads++;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Should be roughly 900 heads out of 1000
|
|
231
|
+
expect(heads).toBeGreaterThan(850);
|
|
232
|
+
expect(heads).toBeLessThan(950);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe("Child RNG Creation", () => {
|
|
237
|
+
it("should create child RNG with derived seed", () => {
|
|
238
|
+
const parent = new SeededRNG("parent-seed");
|
|
239
|
+
const child = parent.createChild("operation-1");
|
|
240
|
+
|
|
241
|
+
expect(child).toBeInstanceOf(SeededRNG);
|
|
242
|
+
expect(child.getSeed()).not.toBe(parent.getSeed());
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("should create independent child RNG", () => {
|
|
246
|
+
const parent = new SeededRNG("parent-seed");
|
|
247
|
+
const child = parent.createChild("child");
|
|
248
|
+
|
|
249
|
+
// Generate some values in child
|
|
250
|
+
child.random();
|
|
251
|
+
child.random();
|
|
252
|
+
|
|
253
|
+
// Parent should not be affected
|
|
254
|
+
parent.setSeed("parent-seed");
|
|
255
|
+
const parentValue = parent.random();
|
|
256
|
+
|
|
257
|
+
parent.setSeed("parent-seed");
|
|
258
|
+
const resetValue = parent.random();
|
|
259
|
+
|
|
260
|
+
expect(parentValue).toBe(resetValue);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("should create deterministic child RNGs", () => {
|
|
264
|
+
const parent1 = new SeededRNG("seed");
|
|
265
|
+
const child1 = parent1.createChild("operation");
|
|
266
|
+
|
|
267
|
+
const parent2 = new SeededRNG("seed");
|
|
268
|
+
const child2 = parent2.createChild("operation");
|
|
269
|
+
|
|
270
|
+
expect(child1.random()).toBe(child2.random());
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe("Deterministic Behavior", () => {
|
|
275
|
+
it("should produce same sequence with same seed", () => {
|
|
276
|
+
const rng1 = new SeededRNG("deterministic-seed");
|
|
277
|
+
const sequence1 = Array.from({ length: 10 }, () => rng1.random());
|
|
278
|
+
|
|
279
|
+
const rng2 = new SeededRNG("deterministic-seed");
|
|
280
|
+
const sequence2 = Array.from({ length: 10 }, () => rng2.random());
|
|
281
|
+
|
|
282
|
+
expect(sequence1).toEqual(sequence2);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("should produce same shuffle with same seed", () => {
|
|
286
|
+
const array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
|
287
|
+
|
|
288
|
+
const rng1 = new SeededRNG("shuffle-seed");
|
|
289
|
+
const shuffled1 = rng1.shuffle(array);
|
|
290
|
+
|
|
291
|
+
const rng2 = new SeededRNG("shuffle-seed");
|
|
292
|
+
const shuffled2 = rng2.shuffle(array);
|
|
293
|
+
|
|
294
|
+
expect(shuffled1).toEqual(shuffled2);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("should produce same dice rolls with same seed", () => {
|
|
298
|
+
const rng1 = new SeededRNG("dice-seed");
|
|
299
|
+
const rolls1 = Array.from({ length: 10 }, () => rng1.rollDice(20));
|
|
300
|
+
|
|
301
|
+
const rng2 = new SeededRNG("dice-seed");
|
|
302
|
+
const rolls2 = Array.from({ length: 10 }, () => rng2.rollDice(20));
|
|
303
|
+
|
|
304
|
+
expect(rolls1).toEqual(rolls2);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("should produce different sequences with different seeds", () => {
|
|
308
|
+
const rng1 = new SeededRNG("seed1");
|
|
309
|
+
const sequence1 = Array.from({ length: 10 }, () => rng1.random());
|
|
310
|
+
|
|
311
|
+
const rng2 = new SeededRNG("seed2");
|
|
312
|
+
const sequence2 = Array.from({ length: 10 }, () => rng2.random());
|
|
313
|
+
|
|
314
|
+
expect(sequence1).not.toEqual(sequence2);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("should maintain determinism across all operations", () => {
|
|
318
|
+
const rng1 = new SeededRNG("complex-seed");
|
|
319
|
+
const results1 = {
|
|
320
|
+
random: rng1.random(),
|
|
321
|
+
int: rng1.randomInt(1, 100),
|
|
322
|
+
pick: rng1.pick(["a", "b", "c", "d"]),
|
|
323
|
+
dice: rng1.rollDice(6),
|
|
324
|
+
coin: rng1.flipCoin(),
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const rng2 = new SeededRNG("complex-seed");
|
|
328
|
+
const results2 = {
|
|
329
|
+
random: rng2.random(),
|
|
330
|
+
int: rng2.randomInt(1, 100),
|
|
331
|
+
pick: rng2.pick(["a", "b", "c", "d"]),
|
|
332
|
+
dice: rng2.rollDice(6),
|
|
333
|
+
coin: rng2.flipCoin(),
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
expect(results1).toEqual(results2);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { nanoid } from "nanoid";
|
|
2
|
+
import seedrandom from "seedrandom";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Seeded random number generator for deterministic randomness
|
|
6
|
+
* Wraps seedrandom library to provide game-specific RNG operations
|
|
7
|
+
*/
|
|
8
|
+
export class SeededRNG {
|
|
9
|
+
private seed: string;
|
|
10
|
+
private prng: seedrandom.PRNG;
|
|
11
|
+
|
|
12
|
+
constructor(seed?: string) {
|
|
13
|
+
this.seed = seed ?? nanoid();
|
|
14
|
+
this.prng = seedrandom(this.seed);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get the current seed
|
|
19
|
+
*/
|
|
20
|
+
getSeed(): string {
|
|
21
|
+
return this.seed;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Set a new seed and reset the generator
|
|
26
|
+
*/
|
|
27
|
+
setSeed(newSeed: string): void {
|
|
28
|
+
this.seed = newSeed;
|
|
29
|
+
this.prng = seedrandom(this.seed);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Generate a random float in range [0, 1)
|
|
34
|
+
*/
|
|
35
|
+
random(): number {
|
|
36
|
+
return this.prng();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Generate a random integer in range [min, max] (inclusive)
|
|
41
|
+
* If only one argument is provided, range is [0, max]
|
|
42
|
+
*/
|
|
43
|
+
randomInt(min: number, max?: number): number {
|
|
44
|
+
const actualMin = max === undefined ? 0 : min;
|
|
45
|
+
const actualMax = max === undefined ? min : max;
|
|
46
|
+
|
|
47
|
+
if (actualMin === actualMax) {
|
|
48
|
+
return actualMin;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return Math.floor(this.random() * (actualMax - actualMin + 1)) + actualMin;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Pick a random element from an array
|
|
56
|
+
* Returns undefined if array is empty
|
|
57
|
+
*/
|
|
58
|
+
pick<T>(array: readonly T[]): T | undefined {
|
|
59
|
+
if (array.length === 0) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (array.length === 1) {
|
|
64
|
+
return array[0];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const index = this.randomInt(0, array.length - 1);
|
|
68
|
+
return array[index];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Shuffle an array using Fisher-Yates algorithm
|
|
73
|
+
* Returns a new array, does not mutate the original
|
|
74
|
+
*/
|
|
75
|
+
shuffle<T>(array: readonly T[]): T[] {
|
|
76
|
+
if (array.length <= 1) {
|
|
77
|
+
return [...array];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const result = [...array];
|
|
81
|
+
|
|
82
|
+
for (let i = result.length - 1; i > 0; i--) {
|
|
83
|
+
const j = this.randomInt(0, i);
|
|
84
|
+
const itemI = result[i];
|
|
85
|
+
const itemJ = result[j];
|
|
86
|
+
if (itemI !== undefined && itemJ !== undefined) {
|
|
87
|
+
result[i] = itemJ;
|
|
88
|
+
result[j] = itemI;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Roll dice with specified number of sides
|
|
97
|
+
* Returns a single number for one die, or an array for multiple dice
|
|
98
|
+
*/
|
|
99
|
+
rollDice(sides: number, count?: number): number | number[] {
|
|
100
|
+
if (count === undefined || count === 1) {
|
|
101
|
+
return this.randomInt(1, sides);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return Array.from({ length: count }, () => this.randomInt(1, sides));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Flip a coin
|
|
109
|
+
* @param bias - Probability of returning true (default: 0.5)
|
|
110
|
+
*/
|
|
111
|
+
flipCoin(bias = 0.5): boolean {
|
|
112
|
+
return this.random() < bias;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Create a child RNG with a derived seed
|
|
117
|
+
* Useful for creating independent RNG instances for sub-operations
|
|
118
|
+
*/
|
|
119
|
+
createChild(namespace: string): SeededRNG {
|
|
120
|
+
const childSeed = `${this.seed}:${namespace}`;
|
|
121
|
+
return new SeededRNG(childSeed);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Targeting Module
|
|
3
|
+
*
|
|
4
|
+
* Provides DSL and utilities for expressing and resolving card/player targets
|
|
5
|
+
* in a game-agnostic way. Game engines extend these types for game-specific
|
|
6
|
+
* targeting needs.
|
|
7
|
+
*
|
|
8
|
+
* @module targeting
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Core DSL types
|
|
12
|
+
export {
|
|
13
|
+
type BaseContext,
|
|
14
|
+
DEFAULT_SELF_TARGET,
|
|
15
|
+
DEFAULT_SINGLE_TARGET,
|
|
16
|
+
getMaxCount,
|
|
17
|
+
getMinCount,
|
|
18
|
+
isMultipleTargetSelector,
|
|
19
|
+
isOptionalCount,
|
|
20
|
+
type OwnerScope,
|
|
21
|
+
type PlayerTargetDSL,
|
|
22
|
+
requiresPlayerChoice,
|
|
23
|
+
type SelectorScope,
|
|
24
|
+
type TargetCount,
|
|
25
|
+
type TargetDSL,
|
|
26
|
+
type TargetingUIHint,
|
|
27
|
+
} from "./target-dsl";
|
|
28
|
+
// Target resolution
|
|
29
|
+
export {
|
|
30
|
+
BaseTargetResolver,
|
|
31
|
+
invalidSelection,
|
|
32
|
+
type TargetIssue,
|
|
33
|
+
type TargetResolutionContext,
|
|
34
|
+
type TargetResolver,
|
|
35
|
+
type TargetValidationResult,
|
|
36
|
+
validateTargetCount,
|
|
37
|
+
validSelection,
|
|
38
|
+
} from "./target-resolver";
|
|
39
|
+
|
|
40
|
+
// Target validation utilities
|
|
41
|
+
export {
|
|
42
|
+
enumerateTargetCombinations,
|
|
43
|
+
getLegalTargets,
|
|
44
|
+
isLegalTarget,
|
|
45
|
+
type TargetContext,
|
|
46
|
+
type ValidationResult,
|
|
47
|
+
validateTargetSelection,
|
|
48
|
+
} from "./target-validation";
|