@gamepark/zenith 0.1.4 → 0.1.7

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.
Files changed (88) hide show
  1. package/dist/ZenithBot.d.ts +15 -0
  2. package/dist/ZenithBot.d.ts.map +1 -0
  3. package/dist/ZenithBot.js +106 -0
  4. package/dist/ZenithBot.js.map +1 -0
  5. package/dist/ZenithRules.d.ts +4 -1
  6. package/dist/ZenithRules.d.ts.map +1 -1
  7. package/dist/ZenithRules.js +6 -3
  8. package/dist/ZenithRules.js.map +1 -1
  9. package/dist/ZenithSetup.d.ts +2 -1
  10. package/dist/ZenithSetup.d.ts.map +1 -1
  11. package/dist/ZenithSetup.js +24 -17
  12. package/dist/ZenithSetup.js.map +1 -1
  13. package/dist/index.d.ts +2 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +2 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/material/Agents.d.ts +1 -2
  18. package/dist/material/Agents.d.ts.map +1 -1
  19. package/dist/material/LocationType.d.ts +2 -1
  20. package/dist/material/LocationType.d.ts.map +1 -1
  21. package/dist/material/LocationType.js +1 -0
  22. package/dist/material/LocationType.js.map +1 -1
  23. package/dist/rules/CustomMoveType.d.ts +3 -1
  24. package/dist/rules/CustomMoveType.d.ts.map +1 -1
  25. package/dist/rules/CustomMoveType.js +2 -0
  26. package/dist/rules/CustomMoveType.js.map +1 -1
  27. package/dist/rules/Memory.d.ts +7 -1
  28. package/dist/rules/Memory.d.ts.map +1 -1
  29. package/dist/rules/Memory.js +2 -0
  30. package/dist/rules/Memory.js.map +1 -1
  31. package/dist/rules/PlayCardRule.d.ts +4 -2
  32. package/dist/rules/PlayCardRule.d.ts.map +1 -1
  33. package/dist/rules/PlayCardRule.js +39 -1
  34. package/dist/rules/PlayCardRule.js.map +1 -1
  35. package/dist/rules/RefillRule.d.ts +2 -4
  36. package/dist/rules/RefillRule.d.ts.map +1 -1
  37. package/dist/rules/RefillRule.js +15 -18
  38. package/dist/rules/RefillRule.js.map +1 -1
  39. package/dist/rules/RuleId.d.ts +3 -1
  40. package/dist/rules/RuleId.d.ts.map +1 -1
  41. package/dist/rules/RuleId.js +2 -0
  42. package/dist/rules/RuleId.js.map +1 -1
  43. package/dist/rules/discard-action/DiplomacyBoardRule.d.ts +1 -0
  44. package/dist/rules/discard-action/DiplomacyBoardRule.d.ts.map +1 -1
  45. package/dist/rules/discard-action/DiplomacyBoardRule.js +7 -0
  46. package/dist/rules/discard-action/DiplomacyBoardRule.js.map +1 -1
  47. package/dist/rules/discard-action/TechnologyBoardRule.d.ts +1 -0
  48. package/dist/rules/discard-action/TechnologyBoardRule.d.ts.map +1 -1
  49. package/dist/rules/discard-action/TechnologyBoardRule.js +4 -6
  50. package/dist/rules/discard-action/TechnologyBoardRule.js.map +1 -1
  51. package/dist/rules/effect/AgentEffects.spec.d.ts +2 -0
  52. package/dist/rules/effect/AgentEffects.spec.d.ts.map +1 -0
  53. package/dist/rules/effect/AgentEffects.spec.js +1088 -0
  54. package/dist/rules/effect/AgentEffects.spec.js.map +1 -0
  55. package/dist/rules/effect/EffectEdgeCases.spec.d.ts +2 -0
  56. package/dist/rules/effect/EffectEdgeCases.spec.d.ts.map +1 -0
  57. package/dist/rules/effect/EffectEdgeCases.spec.js +706 -0
  58. package/dist/rules/effect/EffectEdgeCases.spec.js.map +1 -0
  59. package/dist/rules/effect/GiveInfluenceRule.js +3 -3
  60. package/dist/rules/effect/GiveInfluenceRule.js.map +1 -1
  61. package/dist/rules/effect/MobilizeRule.d.ts +0 -1
  62. package/dist/rules/effect/MobilizeRule.d.ts.map +1 -1
  63. package/dist/rules/effect/MobilizeRule.js +32 -17
  64. package/dist/rules/effect/MobilizeRule.js.map +1 -1
  65. package/dist/rules/effect/WinInfluenceRule.d.ts +1 -0
  66. package/dist/rules/effect/WinInfluenceRule.d.ts.map +1 -1
  67. package/dist/rules/effect/WinInfluenceRule.js +39 -11
  68. package/dist/rules/effect/WinInfluenceRule.js.map +1 -1
  69. package/dist/rules/helper/DeckHelper.d.ts +9 -0
  70. package/dist/rules/helper/DeckHelper.d.ts.map +1 -0
  71. package/dist/rules/helper/DeckHelper.js +24 -0
  72. package/dist/rules/helper/DeckHelper.js.map +1 -0
  73. package/dist/rules/helper/MobilizeHelper.d.ts +1 -1
  74. package/dist/rules/helper/MobilizeHelper.d.ts.map +1 -1
  75. package/dist/rules/helper/MobilizeHelper.js +5 -6
  76. package/dist/rules/helper/MobilizeHelper.js.map +1 -1
  77. package/dist/rules/helper/PlanetHelper.d.ts +1 -0
  78. package/dist/rules/helper/PlanetHelper.d.ts.map +1 -1
  79. package/dist/rules/helper/PlanetHelper.js +13 -4
  80. package/dist/rules/helper/PlanetHelper.js.map +1 -1
  81. package/dist/rules/helper/PlayerHelper.d.ts +2 -0
  82. package/dist/rules/helper/PlayerHelper.d.ts.map +1 -1
  83. package/dist/rules/helper/PlayerHelper.js +9 -0
  84. package/dist/rules/helper/PlayerHelper.js.map +1 -1
  85. package/dist/tutorial/TutorialSetup.d.ts.map +1 -1
  86. package/dist/tutorial/TutorialSetup.js +0 -1
  87. package/dist/tutorial/TutorialSetup.js.map +1 -1
  88. package/package.json +3 -3
@@ -0,0 +1,706 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { Agent, agents } from '../../material/Agent';
3
+ import { Agents } from '../../material/Agents';
4
+ import { Credit } from '../../material/Credit';
5
+ import { ConditionType } from '../../material/effect/Effect';
6
+ import { EffectType } from '../../material/effect/EffectType';
7
+ import { Faction } from '../../material/Faction';
8
+ import { Influence, influences } from '../../material/Influence';
9
+ import { LocationType } from '../../material/LocationType';
10
+ import { MaterialType } from '../../material/MaterialType';
11
+ import { TeamColor } from '../../TeamColor';
12
+ import { ZenithRules } from '../../ZenithRules';
13
+ import { ZenithSetup } from '../../ZenithSetup';
14
+ import { Memory } from '../Memory';
15
+ import { RuleId } from '../RuleId';
16
+ const player1 = 1;
17
+ const player2 = 2;
18
+ function playConsequences(rules, move) {
19
+ const consequences = rules.play(move);
20
+ while (consequences.length > 0) {
21
+ consequences.push(...rules.play(consequences.shift()));
22
+ }
23
+ }
24
+ function resolveAutoMoves(rules) {
25
+ let autoMoves = rules.getAutomaticMoves();
26
+ while (autoMoves.length > 0) {
27
+ for (const auto of autoMoves) {
28
+ playConsequences(rules, auto);
29
+ }
30
+ autoMoves = rules.getAutomaticMoves();
31
+ }
32
+ }
33
+ function resolveAllEffects(rules, player, maxIterations = 50) {
34
+ let iterations = 0;
35
+ while (iterations < maxIterations) {
36
+ const ruleId = rules.game.rule?.id;
37
+ if (ruleId === RuleId.Refill || ruleId === RuleId.PlayCard || ruleId === undefined) {
38
+ return { finalRule: ruleId };
39
+ }
40
+ resolveAutoMoves(rules);
41
+ const newRuleId = rules.game.rule?.id;
42
+ if (newRuleId === RuleId.Refill || newRuleId === RuleId.PlayCard || newRuleId === undefined) {
43
+ return { finalRule: newRuleId };
44
+ }
45
+ const legalMoves = rules.getLegalMoves(player);
46
+ if (legalMoves.length === 0) {
47
+ const p2Moves = rules.getLegalMoves(player === player1 ? player2 : player1);
48
+ if (p2Moves.length > 0)
49
+ return { finalRule: newRuleId };
50
+ return { error: `No legal moves at rule ${RuleId[newRuleId] ?? newRuleId}` };
51
+ }
52
+ playConsequences(rules, legalMoves[0]);
53
+ iterations++;
54
+ }
55
+ return { error: `Max iterations at rule ${RuleId[rules.game.rule?.id] ?? rules.game.rule?.id}` };
56
+ }
57
+ class EffectTestSetup extends ZenithSetup {
58
+ testAgent;
59
+ effects;
60
+ deckCount;
61
+ discardCount;
62
+ opponentInfluenceCards;
63
+ playerInfluenceCards;
64
+ planetPositions;
65
+ playerCredits;
66
+ opponentCredits;
67
+ playerZenithium;
68
+ playerIsLeader;
69
+ constructor(opts = {}) {
70
+ super();
71
+ this.testAgent = opts.agent ?? Agent.Elisabeth;
72
+ this.effects = opts.effects ?? [];
73
+ this.deckCount = opts.deckCount ?? 20;
74
+ this.discardCount = opts.discardCount ?? 0;
75
+ this.opponentInfluenceCards = opts.opponentInfluenceCards ?? [];
76
+ this.playerInfluenceCards = opts.playerInfluenceCards ?? [];
77
+ this.planetPositions = opts.planetPositions ?? {};
78
+ this.playerCredits = opts.playerCredits ?? 10;
79
+ this.opponentCredits = opts.opponentCredits ?? 10;
80
+ this.playerZenithium = opts.playerZenithium ?? 3;
81
+ this.playerIsLeader = opts.playerIsLeader ?? false;
82
+ }
83
+ setupMaterial() {
84
+ this.setupTurnOrder();
85
+ this.setupTestHands();
86
+ this.setupDeckAndDiscard();
87
+ this.setupInfluences();
88
+ this.setupInfluenceCards();
89
+ this.setupTeams();
90
+ this.setupLeaderBadge();
91
+ this.setupTechnologyBoard();
92
+ this.setupTestBonuses();
93
+ }
94
+ setupTurnOrder() {
95
+ this.memorize(Memory.TurnOrder, [player1, player2]);
96
+ }
97
+ setupTestHands() {
98
+ this.material(MaterialType.AgentCard).createItem({
99
+ id: this.testAgent,
100
+ location: { type: LocationType.PlayerHand, player: player1 }
101
+ });
102
+ const fillers = agents.filter(a => a !== this.testAgent && !this.opponentInfluenceCards.includes(a) && !this.playerInfluenceCards.includes(a)).slice(0, 3);
103
+ for (const agent of fillers) {
104
+ this.material(MaterialType.AgentCard).createItem({
105
+ id: agent,
106
+ location: { type: LocationType.PlayerHand, player: player1 }
107
+ });
108
+ }
109
+ const p2Fillers = agents.filter(a => a !== this.testAgent && !fillers.includes(a) && !this.opponentInfluenceCards.includes(a) && !this.playerInfluenceCards.includes(a)).slice(0, 4);
110
+ for (const agent of p2Fillers) {
111
+ this.material(MaterialType.AgentCard).createItem({
112
+ id: agent,
113
+ location: { type: LocationType.PlayerHand, player: player2 }
114
+ });
115
+ }
116
+ }
117
+ setupDeckAndDiscard() {
118
+ const usedAgents = new Set([
119
+ this.testAgent,
120
+ ...this.opponentInfluenceCards,
121
+ ...this.playerInfluenceCards,
122
+ ...agents.filter(a => a !== this.testAgent && !this.opponentInfluenceCards.includes(a) && !this.playerInfluenceCards.includes(a)).slice(0, 7)
123
+ ]);
124
+ const remaining = agents.filter(a => !usedAgents.has(a));
125
+ let idx = 0;
126
+ for (let i = 0; i < this.deckCount && idx < remaining.length; i++, idx++) {
127
+ this.material(MaterialType.AgentCard).createItem({
128
+ id: remaining[idx],
129
+ location: { type: LocationType.AgentDeck }
130
+ });
131
+ }
132
+ for (let i = 0; i < this.discardCount && idx < remaining.length; i++, idx++) {
133
+ this.material(MaterialType.AgentCard).createItem({
134
+ id: remaining[idx],
135
+ location: { type: LocationType.AgentDiscard }
136
+ });
137
+ }
138
+ }
139
+ setupInfluences() {
140
+ for (const planet of influences) {
141
+ const x = this.planetPositions[planet] ?? 0;
142
+ this.material(MaterialType.InfluenceDisc).createItem({
143
+ id: planet,
144
+ location: {
145
+ type: LocationType.PlanetBoardInfluenceDiscSpace,
146
+ id: planet,
147
+ x
148
+ }
149
+ });
150
+ }
151
+ }
152
+ setupInfluenceCards() {
153
+ for (let i = 0; i < this.opponentInfluenceCards.length; i++) {
154
+ const agent = this.opponentInfluenceCards[i];
155
+ this.material(MaterialType.AgentCard).createItem({
156
+ id: agent,
157
+ location: {
158
+ type: LocationType.Influence,
159
+ id: Agents[agent].influence,
160
+ player: TeamColor.Black,
161
+ x: i
162
+ }
163
+ });
164
+ }
165
+ for (let i = 0; i < this.playerInfluenceCards.length; i++) {
166
+ const agent = this.playerInfluenceCards[i];
167
+ this.material(MaterialType.AgentCard).createItem({
168
+ id: agent,
169
+ location: {
170
+ type: LocationType.Influence,
171
+ id: Agents[agent].influence,
172
+ player: TeamColor.White,
173
+ x: i
174
+ }
175
+ });
176
+ }
177
+ }
178
+ setupTeams() {
179
+ for (const team of [TeamColor.White, TeamColor.Black]) {
180
+ const credits = team === TeamColor.White ? this.playerCredits : this.opponentCredits;
181
+ this.material(MaterialType.CreditToken).createItem({
182
+ id: Credit.Credit1,
183
+ location: { type: LocationType.TeamCredit, player: team },
184
+ quantity: credits
185
+ });
186
+ this.material(MaterialType.CreditToken).createItem({
187
+ id: Credit.Credit5,
188
+ location: { type: LocationType.TeamCredit, player: team }
189
+ });
190
+ const zenithium = team === TeamColor.White ? this.playerZenithium : 3;
191
+ this.material(MaterialType.ZenithiumToken).createItem({
192
+ location: { type: LocationType.TeamZenithium, player: team },
193
+ quantity: zenithium
194
+ });
195
+ }
196
+ }
197
+ setupTestBonuses() {
198
+ const bonusIds = [1, 2, 3, 4, 5];
199
+ for (let i = 0; i < influences.length; i++) {
200
+ this.material(MaterialType.BonusToken).createItem({
201
+ id: bonusIds[i],
202
+ location: { type: LocationType.PlanetBoardBonusSpace, id: influences[i] }
203
+ });
204
+ }
205
+ const techBoards = this.material(MaterialType.TechnologyBoard).getIndexes();
206
+ for (const boardIndex of techBoards) {
207
+ this.material(MaterialType.BonusToken).createItem({
208
+ id: bonusIds.length + boardIndex + 1,
209
+ location: { type: LocationType.TechnologyBoardBonusSpace, parent: boardIndex, x: 2 }
210
+ });
211
+ }
212
+ }
213
+ setupPlayers() { }
214
+ start() {
215
+ this.startPlayerTurn(RuleId.PlayCard, player1);
216
+ }
217
+ }
218
+ function setupEffects(rules, effects) {
219
+ const expandedEffects = effects.map(e => ({
220
+ ...e,
221
+ effectSource: { type: MaterialType.AgentCard, value: Agent.Elisabeth }
222
+ }));
223
+ rules.game.memory[Memory.Effects] = JSON.parse(JSON.stringify(expandedEffects));
224
+ }
225
+ function createRulesWithEffects(opts, effects) {
226
+ const setup = new EffectTestSetup(opts);
227
+ const game = setup.setup({ players: 2 });
228
+ const rules = new ZenithRules(game);
229
+ setupEffects(rules, effects);
230
+ return rules;
231
+ }
232
+ describe('WinInfluence edge cases', () => {
233
+ it('opponentSide with no planets on opponent side should skip effect', () => {
234
+ const rules = createRulesWithEffects({}, [
235
+ { type: EffectType.WinInfluence, quantity: 1, opponentSide: true }
236
+ ]);
237
+ playConsequences(rules, { type: 'rule', id: RuleId.WinInfluence });
238
+ const result = resolveAllEffects(rules, player1);
239
+ expect(result.error).toBeUndefined();
240
+ });
241
+ it('differentPlanet with only 1 planet remaining should skip after first pull', () => {
242
+ const rules = createRulesWithEffects({}, [
243
+ { type: EffectType.WinInfluence, quantity: 1, differentPlanet: true, resetDifferentPlanet: true }
244
+ ]);
245
+ const result = resolveAllEffects(rules, player1);
246
+ expect(result.error).toBeUndefined();
247
+ });
248
+ it('fromCenter with no planets at center should skip', () => {
249
+ const rules = createRulesWithEffects({
250
+ planetPositions: {
251
+ [Influence.Mercury]: 2, [Influence.Venus]: 2, [Influence.Terra]: 2,
252
+ [Influence.Mars]: 2, [Influence.Jupiter]: 2
253
+ }
254
+ }, [
255
+ { type: EffectType.WinInfluence, quantity: 1, fromCenter: true }
256
+ ]);
257
+ const result = resolveAllEffects(rules, player1);
258
+ expect(result.error).toBeUndefined();
259
+ });
260
+ it('planet already at max position (x=4 for White) should still trigger capture', () => {
261
+ const rules = createRulesWithEffects({
262
+ planetPositions: { [Influence.Mars]: 3 }
263
+ }, [
264
+ { type: EffectType.WinInfluence, influence: Influence.Mars, quantity: 1 }
265
+ ]);
266
+ playConsequences(rules, rules.play({ type: 'rule', id: RuleId.WinInfluence })[0] ?? { type: 'rule', id: RuleId.WinInfluence });
267
+ resolveAutoMoves(rules);
268
+ const result = resolveAllEffects(rules, player1);
269
+ expect(result.error).toBeUndefined();
270
+ });
271
+ });
272
+ describe('GiveInfluence edge cases', () => {
273
+ it('all planets at boundary (x=-4 for White push) should skip', () => {
274
+ const rules = createRulesWithEffects({
275
+ planetPositions: {
276
+ [Influence.Mercury]: -4, [Influence.Venus]: -4, [Influence.Terra]: -4,
277
+ [Influence.Mars]: -4, [Influence.Jupiter]: -4
278
+ }
279
+ }, [
280
+ { type: EffectType.GiveInfluence }
281
+ ]);
282
+ const result = resolveAllEffects(rules, player1);
283
+ expect(result.error).toBeUndefined();
284
+ });
285
+ it('push planet to -4 should capture for opponent', () => {
286
+ const rules = createRulesWithEffects({
287
+ planetPositions: { [Influence.Mars]: -3 }
288
+ }, [
289
+ { type: EffectType.GiveInfluence }
290
+ ]);
291
+ const result = resolveAllEffects(rules, player1);
292
+ expect(result.error).toBeUndefined();
293
+ });
294
+ it('except planet filter works', () => {
295
+ const rules = createRulesWithEffects({
296
+ planetPositions: { [Influence.Mars]: -4 }
297
+ }, [
298
+ { type: EffectType.GiveInfluence, except: Influence.Mars }
299
+ ]);
300
+ const result = resolveAllEffects(rules, player1);
301
+ expect(result.error).toBeUndefined();
302
+ });
303
+ });
304
+ describe('Exile edge cases', () => {
305
+ it('exile with no influence cards should skip', () => {
306
+ const rules = createRulesWithEffects({}, [
307
+ { type: EffectType.Exile }
308
+ ]);
309
+ const result = resolveAllEffects(rules, player1);
310
+ expect(result.error).toBeUndefined();
311
+ });
312
+ it('exile opponent with no opponent influence cards should skip', () => {
313
+ const rules = createRulesWithEffects({
314
+ playerInfluenceCards: [Agent.Pkd1ck]
315
+ }, [
316
+ { type: EffectType.Exile, opponent: true }
317
+ ]);
318
+ const result = resolveAllEffects(rules, player1);
319
+ expect(result.error).toBeUndefined();
320
+ });
321
+ it('exile quantity=2 with only 1 card should exile 1 then skip', () => {
322
+ const rules = createRulesWithEffects({
323
+ playerInfluenceCards: [Agent.Pkd1ck]
324
+ }, [
325
+ { type: EffectType.Exile, quantity: 2 }
326
+ ]);
327
+ const result = resolveAllEffects(rules, player1);
328
+ expect(result.error).toBeUndefined();
329
+ });
330
+ it('exile with specific influence filter and no matching cards should skip', () => {
331
+ const rules = createRulesWithEffects({
332
+ playerInfluenceCards: [Agent.Pkd1ck]
333
+ }, [
334
+ { type: EffectType.Exile, influence: Influence.Venus }
335
+ ]);
336
+ const result = resolveAllEffects(rules, player1);
337
+ expect(result.error).toBeUndefined();
338
+ });
339
+ });
340
+ describe('Transfer edge cases', () => {
341
+ it('transfer with no opponent influence cards should skip', () => {
342
+ const rules = createRulesWithEffects({}, [
343
+ { type: EffectType.Transfer }
344
+ ]);
345
+ const result = resolveAllEffects(rules, player1);
346
+ expect(result.error).toBeUndefined();
347
+ });
348
+ it('transfer quantity=2 with only 1 opponent card should skip (isPossible checks quantity)', () => {
349
+ const rules = createRulesWithEffects({
350
+ opponentInfluenceCards: [Agent.Pkd1ck]
351
+ }, [
352
+ { type: EffectType.Transfer, quantity: 2 }
353
+ ]);
354
+ const result = resolveAllEffects(rules, player1);
355
+ expect(result.error).toBeUndefined();
356
+ });
357
+ it('transfer with specific influence and matching opponent cards should not block', () => {
358
+ const rules = createRulesWithEffects({
359
+ opponentInfluenceCards: [Agent.Pkd1ck]
360
+ }, [
361
+ { type: EffectType.Transfer, influence: Influence.Mercury }
362
+ ]);
363
+ const result = resolveAllEffects(rules, player1);
364
+ expect(result.error).toBeUndefined();
365
+ });
366
+ });
367
+ describe('GiveCredit edge cases', () => {
368
+ it('give credits with 0 credits should skip', () => {
369
+ const rules = createRulesWithEffects({
370
+ playerCredits: 0
371
+ }, [
372
+ { type: EffectType.GiveCredit, quantity: 3 }
373
+ ]);
374
+ const result = resolveAllEffects(rules, player1);
375
+ expect(result.error).toBeUndefined();
376
+ });
377
+ it('give credits with insufficient credits should skip', () => {
378
+ const rules = createRulesWithEffects({
379
+ playerCredits: 2
380
+ }, [
381
+ { type: EffectType.GiveCredit, quantity: 5 }
382
+ ]);
383
+ const result = resolveAllEffects(rules, player1);
384
+ expect(result.error).toBeUndefined();
385
+ });
386
+ });
387
+ describe('StealCredit edge cases', () => {
388
+ it('steal credits from opponent with 0 credits should skip', () => {
389
+ const rules = createRulesWithEffects({
390
+ opponentCredits: 0
391
+ }, [
392
+ { type: EffectType.StealCredit, quantity: 2 }
393
+ ]);
394
+ const result = resolveAllEffects(rules, player1);
395
+ expect(result.error).toBeUndefined();
396
+ });
397
+ });
398
+ describe('GiveZenithium edge cases', () => {
399
+ it('give zenithium with 0 zenithium should skip', () => {
400
+ const rules = createRulesWithEffects({
401
+ playerZenithium: 0
402
+ }, [
403
+ { type: EffectType.GiveZenithium, quantity: 1 }
404
+ ]);
405
+ const result = resolveAllEffects(rules, player1);
406
+ expect(result.error).toBeUndefined();
407
+ });
408
+ });
409
+ describe('SpendCredit edge cases', () => {
410
+ it('spend credits with 0 credits should skip', () => {
411
+ const rules = createRulesWithEffects({
412
+ playerCredits: 0
413
+ }, [
414
+ { type: EffectType.SpendCredit, quantities: [2, 4, 6], factors: [1, 2, 3] }
415
+ ]);
416
+ const result = resolveAllEffects(rules, player1);
417
+ expect(result.error).toBeUndefined();
418
+ });
419
+ it('spend credits with insufficient for all tiers should skip', () => {
420
+ const rules = createRulesWithEffects({
421
+ playerCredits: 1
422
+ }, [
423
+ { type: EffectType.SpendCredit, quantities: [2, 4, 6], factors: [1, 2, 3] }
424
+ ]);
425
+ const result = resolveAllEffects(rules, player1);
426
+ expect(result.error).toBeUndefined();
427
+ });
428
+ });
429
+ describe('SpendZenithium edge cases', () => {
430
+ it('spend zenithium with 0 zenithium should skip', () => {
431
+ const rules = createRulesWithEffects({
432
+ playerZenithium: 0
433
+ }, [
434
+ { type: EffectType.SpendZenithium, quantities: [1, 2], factors: [1, 2] }
435
+ ]);
436
+ const result = resolveAllEffects(rules, player1);
437
+ expect(result.error).toBeUndefined();
438
+ });
439
+ });
440
+ describe('Discard edge cases', () => {
441
+ it('discard with empty hand should skip', () => {
442
+ const setup = new EffectTestSetup({});
443
+ const game = setup.setup({ players: 2 });
444
+ const rules = new ZenithRules(game);
445
+ const hand = rules.material(MaterialType.AgentCard).location(LocationType.PlayerHand).player(player1);
446
+ for (const index of hand.getIndexes()) {
447
+ playConsequences(rules, hand.index(index).moveItem({ type: LocationType.AgentDiscard }));
448
+ }
449
+ setupEffects(rules, [{ type: EffectType.Discard }]);
450
+ const result = resolveAllEffects(rules, player1);
451
+ expect(result.error).toBeUndefined();
452
+ });
453
+ });
454
+ describe('ResetInfluence edge cases', () => {
455
+ it('reset influence with no planets on player side should skip', () => {
456
+ const rules = createRulesWithEffects({
457
+ planetPositions: {
458
+ [Influence.Mercury]: -2, [Influence.Venus]: -1, [Influence.Terra]: 0,
459
+ [Influence.Mars]: -1, [Influence.Jupiter]: -2
460
+ }
461
+ }, [
462
+ { type: EffectType.ResetInfluence }
463
+ ]);
464
+ const result = resolveAllEffects(rules, player1);
465
+ expect(result.error).toBeUndefined();
466
+ });
467
+ });
468
+ describe('Conditional DoEffect edge cases', () => {
469
+ it('mandatory mobilize condition with empty deck+discard should skip', () => {
470
+ const rules = createRulesWithEffects({
471
+ deckCount: 0,
472
+ discardCount: 0
473
+ }, [
474
+ {
475
+ type: EffectType.Conditional,
476
+ mandatory: true,
477
+ condition: {
478
+ type: ConditionType.DoEffect,
479
+ effect: { type: EffectType.Mobilize }
480
+ },
481
+ effect: {
482
+ type: EffectType.WinInfluence,
483
+ quantity: 1
484
+ }
485
+ }
486
+ ]);
487
+ const result = resolveAllEffects(rules, player1);
488
+ expect(result.error).toBeUndefined();
489
+ });
490
+ it('non-mandatory exile condition with no cards should allow pass', () => {
491
+ const rules = createRulesWithEffects({}, [
492
+ {
493
+ type: EffectType.Conditional,
494
+ mandatory: false,
495
+ condition: {
496
+ type: ConditionType.DoEffect,
497
+ effect: { type: EffectType.Exile }
498
+ },
499
+ effect: {
500
+ type: EffectType.WinInfluence,
501
+ quantity: 1
502
+ }
503
+ }
504
+ ]);
505
+ const result = resolveAllEffects(rules, player1);
506
+ expect(result.error).toBeUndefined();
507
+ });
508
+ it('mandatory transfer condition with no opponent cards should skip', () => {
509
+ const rules = createRulesWithEffects({}, [
510
+ {
511
+ type: EffectType.Conditional,
512
+ mandatory: true,
513
+ condition: {
514
+ type: ConditionType.DoEffect,
515
+ effect: { type: EffectType.Transfer }
516
+ },
517
+ effect: {
518
+ type: EffectType.WinInfluence,
519
+ quantity: 1
520
+ }
521
+ }
522
+ ]);
523
+ const result = resolveAllEffects(rules, player1);
524
+ expect(result.error).toBeUndefined();
525
+ });
526
+ it('HaveCredits condition with insufficient credits should skip', () => {
527
+ const rules = createRulesWithEffects({
528
+ playerCredits: 2
529
+ }, [
530
+ {
531
+ type: EffectType.Conditional,
532
+ condition: {
533
+ type: ConditionType.HaveCredits,
534
+ min: 6
535
+ },
536
+ effect: {
537
+ type: EffectType.WinInfluence,
538
+ quantity: 1
539
+ }
540
+ }
541
+ ]);
542
+ const result = resolveAllEffects(rules, player1);
543
+ expect(result.error).toBeUndefined();
544
+ });
545
+ it('Leader condition when not leader should skip', () => {
546
+ const rules = createRulesWithEffects({
547
+ playerIsLeader: false
548
+ }, [
549
+ {
550
+ type: EffectType.Conditional,
551
+ condition: {
552
+ type: ConditionType.Leader
553
+ },
554
+ effect: {
555
+ type: EffectType.WinInfluence,
556
+ quantity: 1
557
+ }
558
+ }
559
+ ]);
560
+ const result = resolveAllEffects(rules, player1);
561
+ expect(result.error).toBeUndefined();
562
+ });
563
+ });
564
+ describe('Chained effect sequences', () => {
565
+ it('WinInfluence + GiveInfluence + WinCredit chain should resolve', () => {
566
+ const rules = createRulesWithEffects({}, [
567
+ { type: EffectType.WinInfluence, influence: Influence.Mars, quantity: 1 },
568
+ { type: EffectType.GiveInfluence },
569
+ { type: EffectType.WinCredit, quantity: 2 }
570
+ ]);
571
+ const result = resolveAllEffects(rules, player1);
572
+ expect(result.error).toBeUndefined();
573
+ });
574
+ it('multiple impossible effects in a row should all skip gracefully', () => {
575
+ const rules = createRulesWithEffects({
576
+ playerCredits: 0,
577
+ playerZenithium: 0
578
+ }, [
579
+ { type: EffectType.GiveCredit, quantity: 5 },
580
+ { type: EffectType.GiveZenithium, quantity: 2 },
581
+ { type: EffectType.StealCredit, quantity: 3 },
582
+ { type: EffectType.WinInfluence, influence: Influence.Mars, quantity: 1 }
583
+ ]);
584
+ const result = resolveAllEffects(rules, player1);
585
+ expect(result.error).toBeUndefined();
586
+ });
587
+ it('Domitian-like: WinInfluence + 3x Conditional(Mobilize -> WinInfluence) should resolve', () => {
588
+ const rules = createRulesWithEffects({
589
+ deckCount: 10
590
+ }, [
591
+ { type: EffectType.WinInfluence, influence: Influence.Mars, quantity: 1 },
592
+ {
593
+ type: EffectType.Conditional, mandatory: true,
594
+ condition: { type: ConditionType.DoEffect, effect: { type: EffectType.Mobilize } },
595
+ effect: { type: EffectType.WinInfluence, quantity: 1 }
596
+ },
597
+ {
598
+ type: EffectType.Conditional, mandatory: true,
599
+ condition: { type: ConditionType.DoEffect, effect: { type: EffectType.Mobilize } },
600
+ effect: { type: EffectType.WinInfluence, quantity: 1 }
601
+ },
602
+ {
603
+ type: EffectType.Conditional, mandatory: true,
604
+ condition: { type: ConditionType.DoEffect, effect: { type: EffectType.Mobilize } },
605
+ effect: { type: EffectType.WinInfluence, quantity: 1 }
606
+ }
607
+ ]);
608
+ const result = resolveAllEffects(rules, player1);
609
+ expect(result.error).toBeUndefined();
610
+ });
611
+ it('Domitian-like with empty deck should skip mobilize conditions', () => {
612
+ const rules = createRulesWithEffects({
613
+ deckCount: 0,
614
+ discardCount: 0
615
+ }, [
616
+ { type: EffectType.WinInfluence, influence: Influence.Mars, quantity: 1 },
617
+ {
618
+ type: EffectType.Conditional, mandatory: true,
619
+ condition: { type: ConditionType.DoEffect, effect: { type: EffectType.Mobilize } },
620
+ effect: { type: EffectType.WinInfluence, quantity: 1 }
621
+ },
622
+ {
623
+ type: EffectType.Conditional, mandatory: true,
624
+ condition: { type: ConditionType.DoEffect, effect: { type: EffectType.Mobilize } },
625
+ effect: { type: EffectType.WinInfluence, quantity: 1 }
626
+ }
627
+ ]);
628
+ const result = resolveAllEffects(rules, player1);
629
+ expect(result.error).toBeUndefined();
630
+ });
631
+ });
632
+ describe('Mobilize edge cases', () => {
633
+ it('mobilize quantity=2 with 2 cards in deck should resolve in one batch without crash', () => {
634
+ const rules = createRulesWithEffects({
635
+ deckCount: 2,
636
+ discardCount: 0
637
+ }, [
638
+ { type: EffectType.Mobilize, quantity: 2 }
639
+ ]);
640
+ const result = resolveAllEffects(rules, player1);
641
+ expect(result.error).toBeUndefined();
642
+ });
643
+ it('mobilize quantity=3 with 1 card in deck and 0 in discard should mobilize 1 then skip', () => {
644
+ const rules = createRulesWithEffects({
645
+ deckCount: 1,
646
+ discardCount: 0
647
+ }, [
648
+ { type: EffectType.Mobilize, quantity: 3 }
649
+ ]);
650
+ const result = resolveAllEffects(rules, player1);
651
+ expect(result.error).toBeUndefined();
652
+ });
653
+ it('conditional mandatory mobilize quantity=2 should not crash afterItemMove', () => {
654
+ const rules = createRulesWithEffects({
655
+ deckCount: 5
656
+ }, [
657
+ {
658
+ type: EffectType.Conditional, mandatory: true,
659
+ condition: { type: ConditionType.DoEffect, effect: { type: EffectType.Mobilize, quantity: 2 } },
660
+ effect: { type: EffectType.WinInfluence, quantity: 1 }
661
+ }
662
+ ]);
663
+ const result = resolveAllEffects(rules, player1);
664
+ expect(result.error).toBeUndefined();
665
+ });
666
+ });
667
+ describe('WinCredit edge cases', () => {
668
+ it('perLevel1Technology with no techs should give 0 credits (not block)', () => {
669
+ const rules = createRulesWithEffects({}, [
670
+ { type: EffectType.WinCredit, perLevel1Technology: [Faction.Human] }
671
+ ]);
672
+ const result = resolveAllEffects(rules, player1);
673
+ expect(result.error).toBeUndefined();
674
+ });
675
+ it('factorPerDifferentInfluence with no influence cards should give 0', () => {
676
+ const rules = createRulesWithEffects({}, [
677
+ { type: EffectType.WinCredit, factorPerDifferentInfluence: 2 }
678
+ ]);
679
+ const result = resolveAllEffects(rules, player1);
680
+ expect(result.error).toBeUndefined();
681
+ });
682
+ it('factorPerDifferentOpponentInfluence with no opponent cards should give 0', () => {
683
+ const rules = createRulesWithEffects({}, [
684
+ { type: EffectType.WinCredit, factorPerDifferentOpponentInfluence: 2 }
685
+ ]);
686
+ const result = resolveAllEffects(rules, player1);
687
+ expect(result.error).toBeUndefined();
688
+ });
689
+ });
690
+ describe('WinZenithium edge cases', () => {
691
+ it('perLevel1Technology with no techs should give 0 zenithium', () => {
692
+ const rules = createRulesWithEffects({}, [
693
+ { type: EffectType.WinZenithium, perLevel1Technology: [Faction.Human] }
694
+ ]);
695
+ const result = resolveAllEffects(rules, player1);
696
+ expect(result.error).toBeUndefined();
697
+ });
698
+ it('opponent=true should give zenithium to opponent', () => {
699
+ const rules = createRulesWithEffects({}, [
700
+ { type: EffectType.WinZenithium, quantity: 1, opponent: true }
701
+ ]);
702
+ const result = resolveAllEffects(rules, player1);
703
+ expect(result.error).toBeUndefined();
704
+ });
705
+ });
706
+ //# sourceMappingURL=EffectEdgeCases.spec.js.map