powergrid-engine 1.9.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 (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +31 -0
  3. package/dist/index.d.ts +6 -0
  4. package/dist/index.js +17 -0
  5. package/dist/src/available-moves.d.ts +24 -0
  6. package/dist/src/available-moves.js +363 -0
  7. package/dist/src/engine.d.ts +20 -0
  8. package/dist/src/engine.js +1937 -0
  9. package/dist/src/gamestate.d.ts +135 -0
  10. package/dist/src/gamestate.js +30 -0
  11. package/dist/src/log.d.ts +14 -0
  12. package/dist/src/log.js +2 -0
  13. package/dist/src/maps/america.d.ts +55 -0
  14. package/dist/src/maps/america.js +411 -0
  15. package/dist/src/maps/australia.d.ts +46 -0
  16. package/dist/src/maps/australia.js +138 -0
  17. package/dist/src/maps/badenwurttemberg.d.ts +46 -0
  18. package/dist/src/maps/badenwurttemberg.js +163 -0
  19. package/dist/src/maps/benelux.d.ts +46 -0
  20. package/dist/src/maps/benelux.js +210 -0
  21. package/dist/src/maps/brazil.d.ts +54 -0
  22. package/dist/src/maps/brazil.js +292 -0
  23. package/dist/src/maps/centraleurope.d.ts +54 -0
  24. package/dist/src/maps/centraleurope.js +236 -0
  25. package/dist/src/maps/china.d.ts +54 -0
  26. package/dist/src/maps/china.js +262 -0
  27. package/dist/src/maps/france.d.ts +54 -0
  28. package/dist/src/maps/france.js +290 -0
  29. package/dist/src/maps/germany.d.ts +57 -0
  30. package/dist/src/maps/germany.js +328 -0
  31. package/dist/src/maps/indian.d.ts +54 -0
  32. package/dist/src/maps/indian.js +283 -0
  33. package/dist/src/maps/italy.d.ts +54 -0
  34. package/dist/src/maps/italy.js +190 -0
  35. package/dist/src/maps/japan.d.ts +46 -0
  36. package/dist/src/maps/japan.js +144 -0
  37. package/dist/src/maps/korea.d.ts +54 -0
  38. package/dist/src/maps/korea.js +186 -0
  39. package/dist/src/maps/middleeast.d.ts +54 -0
  40. package/dist/src/maps/middleeast.js +225 -0
  41. package/dist/src/maps/northerneurope.d.ts +54 -0
  42. package/dist/src/maps/northerneurope.js +197 -0
  43. package/dist/src/maps/quebec.d.ts +54 -0
  44. package/dist/src/maps/quebec.js +283 -0
  45. package/dist/src/maps/russia.d.ts +54 -0
  46. package/dist/src/maps/russia.js +286 -0
  47. package/dist/src/maps/southafrica.d.ts +46 -0
  48. package/dist/src/maps/southafrica.js +152 -0
  49. package/dist/src/maps/spainportugal.d.ts +54 -0
  50. package/dist/src/maps/spainportugal.js +289 -0
  51. package/dist/src/maps/ukireland.d.ts +52 -0
  52. package/dist/src/maps/ukireland.js +176 -0
  53. package/dist/src/maps.d.ts +50 -0
  54. package/dist/src/maps.js +61 -0
  55. package/dist/src/move.d.ts +63 -0
  56. package/dist/src/move.js +15 -0
  57. package/dist/src/powerPlants.d.ts +4 -0
  58. package/dist/src/powerPlants.js +60 -0
  59. package/dist/src/prices.d.ts +7 -0
  60. package/dist/src/prices.js +10 -0
  61. package/dist/src/randomizeMap.d.ts +3 -0
  62. package/dist/src/randomizeMap.js +244 -0
  63. package/dist/src/utils.d.ts +2 -0
  64. package/dist/src/utils.js +24 -0
  65. package/dist/wrapper.d.ts +30 -0
  66. package/dist/wrapper.js +127 -0
  67. package/index.ts +6 -0
  68. package/package.json +51 -0
  69. package/src/available-moves.ts +450 -0
  70. package/src/engine.spec.ts +163 -0
  71. package/src/engine.ts +2270 -0
  72. package/src/fixtures/GermanyRecharged.json +6627 -0
  73. package/src/fixtures/USAOriginal.json +5216 -0
  74. package/src/fixtures/supply.json +5792 -0
  75. package/src/fixtures/undo.json +4102 -0
  76. package/src/gamestate.ts +164 -0
  77. package/src/log.ts +17 -0
  78. package/src/maps/america.ts +411 -0
  79. package/src/maps/australia.ts +137 -0
  80. package/src/maps/badenwurttemberg.ts +162 -0
  81. package/src/maps/benelux.ts +210 -0
  82. package/src/maps/brazil.ts +306 -0
  83. package/src/maps/centraleurope.ts +235 -0
  84. package/src/maps/china.ts +268 -0
  85. package/src/maps/france.ts +295 -0
  86. package/src/maps/germany.ts +328 -0
  87. package/src/maps/indian.ts +289 -0
  88. package/src/maps/italy.ts +189 -0
  89. package/src/maps/japan.ts +143 -0
  90. package/src/maps/korea.ts +185 -0
  91. package/src/maps/middleeast.ts +225 -0
  92. package/src/maps/northerneurope.ts +196 -0
  93. package/src/maps/quebec.ts +304 -0
  94. package/src/maps/russia.ts +292 -0
  95. package/src/maps/southafrica.ts +151 -0
  96. package/src/maps/spainportugal.ts +295 -0
  97. package/src/maps/ukireland.ts +175 -0
  98. package/src/maps.ts +123 -0
  99. package/src/move.ts +83 -0
  100. package/src/powerPlants.ts +59 -0
  101. package/src/prices.ts +10 -0
  102. package/src/randomizeMap.ts +288 -0
  103. package/src/rankings.spec.ts +18 -0
  104. package/src/utils.spec.ts +13 -0
  105. package/src/utils.ts +23 -0
  106. package/tsconfig.json +17 -0
  107. package/wrapper.ts +126 -0
package/src/engine.ts ADDED
@@ -0,0 +1,2270 @@
1
+ import assert from 'assert';
2
+ import { cloneDeep, isEqual, range } from 'lodash';
3
+ import seedrandom from 'seedrandom';
4
+ import { availableMoves } from './available-moves';
5
+ import { GameOptions, GameState, Phase, Player, PowerPlant, PowerPlantType, ResourceType } from './gamestate';
6
+ import { LogMove } from './log';
7
+ import { GameMap, maps, mapsRecharged } from './maps';
8
+ import { Move, MoveName, Moves } from './move';
9
+ import { indiaPowerPlants, powerPlants } from './powerPlants';
10
+ import prices from './prices';
11
+ import { createRandomizedMap } from './randomizeMap';
12
+ import { asserts, shuffle } from './utils';
13
+
14
+ export const playerColors = ['limegreen', 'mediumorchid', 'red', 'dodgerblue', 'yellow', 'brown'];
15
+
16
+ const citiesToStep2 = [10, 7, 7, 7, 6];
17
+ const citiesToEndGame = [21, 17, 17, 15, 14];
18
+ const cityIncome = [10, 22, 33, 44, 54, 64, 73, 82, 90, 98, 105, 112, 118, 124, 129, 134, 138, 142, 145, 148, 150, 150];
19
+ const regionsInPlay = [3, 3, 4, 5, 5];
20
+
21
+ export function defaultSetupDeck(
22
+ numPlayers: number,
23
+ variant: string,
24
+ rng: seedrandom.prng,
25
+ useNewRechargedSetup: boolean
26
+ ) {
27
+ let actualMarket: PowerPlant[];
28
+ let futureMarket: PowerPlant[];
29
+ let powerPlantsDeck: PowerPlant[] = cloneDeep(powerPlants);
30
+
31
+ if (variant == 'original') {
32
+ powerPlantsDeck = powerPlantsDeck.slice(8);
33
+ const powerPlant13 = powerPlantsDeck.splice(2, 1)[0];
34
+ const step3 = powerPlantsDeck.pop()!;
35
+
36
+ powerPlantsDeck = shuffle(powerPlantsDeck, rng() + '');
37
+ if (numPlayers == 2 || numPlayers == 3) {
38
+ powerPlantsDeck = powerPlantsDeck.slice(8);
39
+ } else if (numPlayers == 4) {
40
+ powerPlantsDeck = powerPlantsDeck.slice(4);
41
+ }
42
+
43
+ powerPlantsDeck.unshift(powerPlant13);
44
+ powerPlantsDeck.push(step3);
45
+
46
+ actualMarket = [getPowerPlant(3), getPowerPlant(4), getPowerPlant(5), getPowerPlant(6)];
47
+ futureMarket = [getPowerPlant(7), getPowerPlant(8), getPowerPlant(9), getPowerPlant(10)];
48
+ } else {
49
+ let initialPowerPlants = shuffle(powerPlantsDeck.splice(0, 13), rng() + '');
50
+ let initialPlantMarket = initialPowerPlants.splice(0, 8);
51
+ initialPlantMarket = initialPlantMarket.sort((a, b) => a.number - b.number);
52
+ actualMarket = initialPlantMarket.slice(0, 4);
53
+ futureMarket = initialPlantMarket.slice(4);
54
+
55
+ const first = initialPowerPlants.shift()!;
56
+ const step3 = powerPlantsDeck.pop()!;
57
+
58
+ powerPlantsDeck = shuffle(powerPlantsDeck, rng() + '');
59
+ if (numPlayers == 2 || numPlayers == 3) {
60
+ initialPowerPlants = initialPowerPlants.slice(2);
61
+ powerPlantsDeck = shuffle(powerPlantsDeck.slice(6).concat(initialPowerPlants), rng() + '');
62
+ } else if (numPlayers == 4) {
63
+ initialPowerPlants = initialPowerPlants.slice(1);
64
+ powerPlantsDeck = shuffle(powerPlantsDeck.slice(3).concat(initialPowerPlants), rng() + '');
65
+ } else if (useNewRechargedSetup) {
66
+ // TODO: This flag exists solely to make old tests pass. We should eventually
67
+ // fix the test and remove the flag.
68
+ powerPlantsDeck = shuffle(powerPlantsDeck.concat(initialPowerPlants), rng() + '');
69
+ }
70
+
71
+ powerPlantsDeck.unshift(first);
72
+ powerPlantsDeck.push(step3);
73
+ }
74
+
75
+ return { actualMarket, futureMarket, powerPlantsDeck };
76
+ }
77
+
78
+ export function setup(
79
+ numPlayers: number,
80
+ {
81
+ fastBid = false,
82
+ map = 'USA',
83
+ variant = 'original',
84
+ showMoney = false,
85
+ useNewRechargedSetup = true,
86
+ trackTotalSpent = true,
87
+ randomizeMap = false,
88
+ }: GameOptions,
89
+ seed?: string,
90
+ forceDeck?: PowerPlant[],
91
+ forceMap?: GameMap
92
+ ): GameState {
93
+ seed = seed ?? Math.random().toString();
94
+ const rng = seedrandom(seed);
95
+
96
+ const chosenMap = cloneDeep(
97
+ variant == 'original' ? maps.find((m) => m.name == map)! : mapsRecharged.find((m) => m.name == map)!
98
+ );
99
+
100
+ let actualMarket: PowerPlant[];
101
+ let futureMarket: PowerPlant[];
102
+ let powerPlantsDeck: PowerPlant[];
103
+
104
+ if (forceDeck) {
105
+ powerPlantsDeck = forceDeck;
106
+ actualMarket = powerPlantsDeck.splice(0, 4);
107
+ futureMarket = powerPlantsDeck.splice(0, 4);
108
+ } else {
109
+ if (chosenMap.setupDeck) {
110
+ ({ actualMarket, futureMarket, powerPlantsDeck } = chosenMap.setupDeck(numPlayers, variant, rng));
111
+ } else {
112
+ ({ actualMarket, futureMarket, powerPlantsDeck } = defaultSetupDeck(
113
+ numPlayers,
114
+ variant,
115
+ rng,
116
+ useNewRechargedSetup
117
+ ));
118
+ }
119
+ }
120
+
121
+ const players: Player[] = range(numPlayers).map((id) => ({
122
+ id,
123
+ powerPlants: [],
124
+ coalCapacity: 0,
125
+ coalLeft: 0,
126
+ oilCapacity: 0,
127
+ oilLeft: 0,
128
+ garbageCapacity: 0,
129
+ garbageLeft: 0,
130
+ uraniumCapacity: 0,
131
+ uraniumLeft: 0,
132
+ hybridCapacity: 0,
133
+ hybridLeft: 0,
134
+ money: 50,
135
+ housesLeft: 22,
136
+ cities: [],
137
+ powerPlantsNotUsed: [],
138
+ availableMoves: null,
139
+ lastMove: null,
140
+ isDropped: false,
141
+ isAI: false,
142
+ bid: 0,
143
+ passed: false,
144
+ skipAuction: false,
145
+ citiesPowered: 0,
146
+ resourcesUsed: [],
147
+ totalIncome: 0,
148
+ totalSpentCities: 0,
149
+ totalSpentConnections: 0,
150
+ totalSpentPlants: 0,
151
+ totalSpentResources: 0,
152
+ }));
153
+
154
+ const p = players.length - 2;
155
+
156
+ const playerOrder = range(numPlayers);
157
+ const startingPlayer = playerOrder[0];
158
+
159
+ let coalResupply: number[][];
160
+ let oilResupply: number[][];
161
+ let garbageResupply: number[][];
162
+ let uraniumResupply: number[][];
163
+ if (chosenMap.resupply) {
164
+ coalResupply = chosenMap.resupply[0];
165
+ oilResupply = chosenMap.resupply[1];
166
+ garbageResupply = chosenMap.resupply[2];
167
+ uraniumResupply = chosenMap.resupply[3];
168
+ } else {
169
+ coalResupply = [
170
+ [3, 4, 3],
171
+ [4, 5, 3],
172
+ [5, 6, 4],
173
+ [5, 7, 5],
174
+ [7, 9, 6],
175
+ ];
176
+ oilResupply = [
177
+ [2, 2, 4],
178
+ [2, 3, 4],
179
+ [3, 4, 5],
180
+ [4, 5, 6],
181
+ [5, 6, 7],
182
+ ];
183
+ garbageResupply = [
184
+ [1, 2, 3],
185
+ [1, 2, 3],
186
+ [2, 3, 4],
187
+ [3, 3, 5],
188
+ [3, 5, 6],
189
+ ];
190
+ uraniumResupply = [
191
+ [1, 1, 1],
192
+ [1, 1, 1],
193
+ [1, 2, 2],
194
+ [2, 3, 2],
195
+ [2, 3, 3],
196
+ ];
197
+ }
198
+
199
+ if (chosenMap.layout == 'Portrait' || randomizeMap) {
200
+ const isUsaRecharged = chosenMap.name === 'USA' && variant === 'recharged';
201
+ chosenMap.viewBox = chosenMap.viewBox || [1480, 1060];
202
+ chosenMap.adjustRatio = chosenMap.adjustRatio || [1, 1];
203
+ chosenMap.playerOrderPosition = chosenMap.playerOrderPosition || [1160, 160];
204
+ chosenMap.cityCountPosition = chosenMap.cityCountPosition || [0, 0];
205
+ chosenMap.powerPlantMarketPosition = chosenMap.powerPlantMarketPosition || [745, 0];
206
+ chosenMap.actualMarketWidth = chosenMap.actualMarketWidth || 430;
207
+ chosenMap.mapPosition = chosenMap.mapPosition || [0, 0];
208
+ chosenMap.buttonsPosition = chosenMap.buttonsPosition || [1305, 0];
209
+ chosenMap.playerBoardsPosition = chosenMap.playerBoardsPosition || [1105, 240];
210
+ chosenMap.roundInfoPosition = chosenMap.roundInfoPosition || [20, 920];
211
+ chosenMap.supplyPosition = chosenMap.supplyPosition || [isUsaRecharged ? 480 : 675, 920];
212
+ } else {
213
+ chosenMap.viewBox = chosenMap.viewBox || [1465, 860];
214
+ chosenMap.adjustRatio = chosenMap.adjustRatio || [1, 1];
215
+ chosenMap.playerOrderPosition = chosenMap.playerOrderPosition || [1160, 140];
216
+ chosenMap.cityCountPosition = chosenMap.cityCountPosition || [0, 0];
217
+ chosenMap.powerPlantMarketPosition = chosenMap.powerPlantMarketPosition || [745, 0];
218
+ chosenMap.actualMarketWidth = chosenMap.actualMarketWidth || 430;
219
+ chosenMap.mapPosition = chosenMap.mapPosition || [-10, 0];
220
+ chosenMap.buttonsPosition = chosenMap.buttonsPosition || [1305, 0];
221
+ chosenMap.playerBoardsPosition = chosenMap.playerBoardsPosition || [1105, 200];
222
+ chosenMap.roundInfoPosition = chosenMap.roundInfoPosition || [20, 590];
223
+ chosenMap.supplyPosition = chosenMap.supplyPosition || [0, 720];
224
+ }
225
+
226
+ let finalMap: GameMap;
227
+ if (randomizeMap) {
228
+ finalMap = createRandomizedMap(chosenMap, regionsInPlay[p], rng);
229
+ } else {
230
+ chosenMap.cities = chosenMap.cities.map((city) => ({
231
+ ...city,
232
+ x: city.x * chosenMap.adjustRatio![0],
233
+ y: city.y * chosenMap.adjustRatio![1],
234
+ }));
235
+
236
+ const regions = chosenMap.cities
237
+ .filter((c, i) => chosenMap.cities.findIndex((cc) => cc.region == c.region) == i)
238
+ .map((c) => c.region);
239
+ const connections = chosenMap.connections.map((con) =>
240
+ con.nodes.map((n) => chosenMap.cities.find((city) => city.name == n)!.region)
241
+ );
242
+ const regionConnections = regions.map((region) =>
243
+ regions.filter(
244
+ (area2) => region != area2 && connections.some((con) => con.includes(region) && con.includes(area2))
245
+ )
246
+ );
247
+
248
+ const playRegions = new Set<string>();
249
+ while (playRegions.size != Math.min(regionsInPlay[p], regions.length)) {
250
+ const region = regions[Math.floor(rng() * regions.length)];
251
+ if (
252
+ playRegions.size == 0 ||
253
+ regionConnections[regions.indexOf(region)].some((con) => playRegions.has(con))
254
+ ) {
255
+ playRegions.add(region);
256
+
257
+ // Avoid italy Red Green Blue
258
+ if (chosenMap.name === 'Italy') {
259
+ if (playRegions.has('red') && playRegions.has('green') && playRegions.has('blue')) {
260
+ playRegions.clear();
261
+ }
262
+ }
263
+ }
264
+ }
265
+
266
+ const filteredMap = cloneDeep(chosenMap);
267
+ filteredMap.cities = filteredMap.cities.filter((city) => playRegions.has(city.region));
268
+ filteredMap.connections = filteredMap.connections.filter((con) =>
269
+ con.nodes
270
+ .map((n) => chosenMap.cities.find((city) => city.name == n)!.region)
271
+ .every((r) => playRegions.has(r))
272
+ );
273
+
274
+ finalMap = filteredMap;
275
+ }
276
+
277
+ const coalMarket = chosenMap.startingResources ? chosenMap.startingResources[0] : 24;
278
+ const oilMarket = chosenMap.startingResources ? chosenMap.startingResources[1] : 18;
279
+ const garbageMarket = chosenMap.startingResources ? chosenMap.startingResources[2] : variant == 'original' ? 6 : 9;
280
+ const uraniumMarket = chosenMap.startingResources ? chosenMap.startingResources[3] : 2;
281
+
282
+ const totalCoal = chosenMap.startingSupply ? chosenMap.startingSupply[0] : 24;
283
+ const totalOil = chosenMap.startingSupply ? chosenMap.startingSupply[1] : 24;
284
+ const totalGarbage = chosenMap.startingSupply ? chosenMap.startingSupply[2] : 24;
285
+ const totalUranium = chosenMap.startingSupply ? chosenMap.startingSupply[3] : 12;
286
+
287
+ const coalSupply = totalCoal - coalMarket;
288
+ const oilSupply = totalOil - oilMarket;
289
+ const garbageSupply = totalGarbage - garbageMarket;
290
+ const uraniumSupply = totalUranium - uraniumMarket;
291
+
292
+ const coalPrices = cloneDeep(chosenMap.coalPrices ?? prices.coal);
293
+ const oilPrices = cloneDeep(chosenMap.oilPrices ?? prices.oil);
294
+ const garbagePrices = cloneDeep(chosenMap.garbagePrices ?? prices.garbage);
295
+ const uraniumPrices = cloneDeep(chosenMap.uraniumPrices ?? prices.uranium);
296
+
297
+ const G: GameState = {
298
+ map: forceMap || finalMap,
299
+ players,
300
+ playerOrder,
301
+ currentPlayers: [startingPlayer],
302
+ powerPlantsDeck,
303
+ coalSupply,
304
+ oilSupply,
305
+ garbageSupply,
306
+ uraniumSupply,
307
+ coalResupply,
308
+ oilResupply,
309
+ garbageResupply,
310
+ uraniumResupply,
311
+ coalMarket,
312
+ oilMarket,
313
+ garbageMarket,
314
+ uraniumMarket,
315
+ coalPrices,
316
+ oilPrices,
317
+ garbagePrices,
318
+ uraniumPrices,
319
+ actualMarket,
320
+ futureMarket,
321
+ chosenPowerPlant: undefined,
322
+ currentBid: undefined,
323
+ highestBidders: [],
324
+ auctioningPlayer: undefined,
325
+ step: 1,
326
+ phase: Phase.Auction,
327
+ options: { fastBid, map, variant, showMoney, useNewRechargedSetup, trackTotalSpent },
328
+ log: [],
329
+ hiddenLog: [],
330
+ seed,
331
+ round: 1,
332
+ auctionSkips: 0,
333
+ citiesToStep2: citiesToStep2[numPlayers - 2],
334
+ citiesToEndGame: citiesToEndGame[numPlayers - 2],
335
+ resourceResupply: [
336
+ `[${coalResupply[p][0]}, ${oilResupply[p][0]}, ${garbageResupply[p][0]}, ${uraniumResupply[p][0]}]`,
337
+ `[${coalResupply[p][1]}, ${oilResupply[p][1]}, ${garbageResupply[p][1]}, ${uraniumResupply[p][1]}]`,
338
+ `[${coalResupply[p][2]}, ${oilResupply[p][2]}, ${garbageResupply[p][2]}, ${uraniumResupply[p][2]}]`,
339
+ ],
340
+ paymentTable: cityIncome,
341
+ variant,
342
+ minimunBid: 0,
343
+ plantDiscountActive:
344
+ variant == 'recharged' && (forceMap || finalMap).name != 'China' && (forceMap || finalMap).name != 'Russia',
345
+ discardSmallestPlant: false,
346
+ cardsLeft: powerPlantsDeck.length,
347
+ nextCardWeak: variant == 'recharged',
348
+ card39Bought: false,
349
+ knownPowerPlantDeck: actualMarket.concat(futureMarket),
350
+ knownPowerPlantDeckStep3: [],
351
+ powerPlantDeckAfterStep3: undefined,
352
+ } as GameState;
353
+
354
+ G.log.push({ type: 'event', event: 'Game Start!' });
355
+
356
+ if (G.map.name == 'Middle East') {
357
+ removePlantsForMiddleEastStep1(G);
358
+ }
359
+
360
+ G.players[startingPlayer].availableMoves = availableMoves(G, G.players[startingPlayer]);
361
+
362
+ return G;
363
+ }
364
+
365
+ export function stripSecret(G: GameState, player?: number): GameState {
366
+ return {
367
+ ...G,
368
+ seed: 'secret',
369
+ hiddenLog: [],
370
+ powerPlantsDeck: [],
371
+ players: G.players.map((pl, i) => {
372
+ if (player === i) {
373
+ return pl;
374
+ } else {
375
+ return {
376
+ ...pl,
377
+ availableMoves: pl.availableMoves ? {} : null,
378
+ money: ended(G) || G.options.showMoney ? pl.money : -1,
379
+ bid: G.options.fastBid ? 0 : pl.bid,
380
+ };
381
+ }
382
+ }),
383
+ log: G.log,
384
+ };
385
+ }
386
+
387
+ export function currentPlayers(G: GameState): number[] {
388
+ return G.currentPlayers;
389
+ }
390
+
391
+ export function move(G: GameState, move: Move, playerNumber: number, isUndo = false): GameState {
392
+ const player = G.players[playerNumber];
393
+ const available = player.availableMoves?.[move.name];
394
+
395
+ updateGameState(G);
396
+
397
+ assert(G.currentPlayers.includes(playerNumber), 'It is not your turn!');
398
+ assert(available, 'You are not allowed to run the command ' + move.name);
399
+
400
+ // Fix for issue 8: can't undo because of a move (discaring the pp you just bought) that is now invalid
401
+ if (
402
+ !isUndo ||
403
+ move.name != MoveName.DiscardPowerPlant ||
404
+ player.powerPlants[player.powerPlants.length - 1].number != move.data
405
+ ) {
406
+ assert(
407
+ available.some((x) => isEqual(x, move.data)),
408
+ 'Wrong argument for the command ' + move.name
409
+ );
410
+ }
411
+
412
+ switch (move.name) {
413
+ case MoveName.ChoosePowerPlant: {
414
+ asserts<Moves.MoveChoosePowerPlant>(move);
415
+
416
+ G.chosenPowerPlant = getPowerPlant(move.data, G.map.name);
417
+ G.auctioningPlayer = player.id;
418
+
419
+ if (move.data == 39) {
420
+ G.card39Bought = true;
421
+ }
422
+
423
+ if (
424
+ G.options.variant == 'recharged' &&
425
+ G.plantDiscountActive &&
426
+ G.chosenPowerPlant.number == G.actualMarket[0].number
427
+ ) {
428
+ G.minimunBid = 1;
429
+ G.plantDiscountActive = false;
430
+ move.usedPlantDiscount = true;
431
+ } else {
432
+ G.minimunBid = move.data;
433
+ }
434
+
435
+ const notPassed = G.players.filter((p) => !p.skipAuction && !p.isDropped);
436
+ if (notPassed.length == 1) {
437
+ G.log.push({
438
+ type: 'move',
439
+ player: playerNumber,
440
+ move,
441
+ simple: `${player.name} chooses Power Plant ${move.data}.`,
442
+ pretty: `${playerNameHTML(player)} chooses Power Plant <b>${move.data}</b>.`,
443
+ });
444
+
445
+ const winningPlayer = notPassed[0];
446
+ endAuction(G, winningPlayer, G.minimunBid);
447
+
448
+ if (G.round == 1) {
449
+ setPlayerOrder(G);
450
+ }
451
+
452
+ if (
453
+ winningPlayer.powerPlants.length <= 3 ||
454
+ (G.players.length == 2 && winningPlayer.powerPlants.length == 4)
455
+ ) {
456
+ if (G.map.name != 'China' || G.step == 3) {
457
+ addPowerPlant(G);
458
+ }
459
+
460
+ toResourcesPhase(G);
461
+ }
462
+ } else {
463
+ G.log.push({
464
+ type: 'move',
465
+ player: playerNumber,
466
+ move,
467
+ simple: `${player.name} chooses Power Plant ${move.data} to initiate an auction.`,
468
+ pretty: `${playerNameHTML(player)} chooses Power Plant <b>${move.data}</b> to initiate an auction.`,
469
+ });
470
+
471
+ if (G.options.fastBid) {
472
+ G.currentPlayers = G.playerOrder.filter(
473
+ (p) => !G.players[p].skipAuction && !G.players[p].isDropped
474
+ );
475
+ }
476
+ }
477
+
478
+ break;
479
+ }
480
+
481
+ case MoveName.Bid: {
482
+ asserts<Moves.MoveBid>(move);
483
+
484
+ if (G.options.fastBid) {
485
+ G.hiddenLog.push({
486
+ type: 'move',
487
+ player: playerNumber,
488
+ move,
489
+ simple: `${player.name} bids $${move.data}.`,
490
+ pretty: `${playerNameHTML(player)} bids <span style="color: green">$${move.data}</span>.`,
491
+ });
492
+
493
+ fastAuction(G, player, move.data);
494
+ } else {
495
+ G.currentBid = player.bid = move.data;
496
+
497
+ G.log.push({
498
+ type: 'move',
499
+ player: playerNumber,
500
+ move,
501
+ simple: `${player.name} bids $${move.data}.`,
502
+ pretty: `${playerNameHTML(player)} bids <span style="color: green">$${move.data}</span>.`,
503
+ });
504
+
505
+ nextPlayerClockwise(G);
506
+ }
507
+
508
+ break;
509
+ }
510
+
511
+ case MoveName.Pass: {
512
+ asserts<Moves.MovePass>(move);
513
+
514
+ if (!G.options.fastBid || G.phase != Phase.Auction || !G.chosenPowerPlant) {
515
+ G.log.push({
516
+ type: 'move',
517
+ player: playerNumber,
518
+ move,
519
+ simple: `${player.name} passes.`,
520
+ pretty: `${playerNameHTML(player)} passes.`,
521
+ });
522
+ }
523
+
524
+ switch (G.phase) {
525
+ case Phase.Auction: {
526
+ if (G.chosenPowerPlant == undefined) {
527
+ player.skipAuction = true;
528
+ G.auctionSkips++;
529
+ if (G.auctionSkips == 1 && G.map.name == 'Russia' && G.actualMarket?.length > 0) {
530
+ G.log.push({
531
+ type: 'event',
532
+ event: `First pass, removing lowest numbered Power Plant (${G.actualMarket[0].number}).`,
533
+ });
534
+
535
+ G.actualMarket.shift();
536
+ addPowerPlant(G);
537
+ }
538
+
539
+ if (G.players.some((p) => !p.skipAuction && !p.isDropped)) {
540
+ nextPlayerAuction(G);
541
+ } else {
542
+ if (
543
+ G.auctionSkips == G.players.length &&
544
+ G.options.variant == 'original' &&
545
+ G.map.name != 'China' &&
546
+ G.map.name != 'Russia'
547
+ ) {
548
+ G.log.push({
549
+ type: 'event',
550
+ event: `Everyone passed, removing lowest numbered Power Plant (${G.actualMarket[0].number}).`,
551
+ });
552
+
553
+ G.actualMarket.shift();
554
+ addPowerPlant(G);
555
+ }
556
+
557
+ toResourcesPhase(G);
558
+ }
559
+ } else {
560
+ if (G.options.fastBid) {
561
+ G.hiddenLog.push({
562
+ type: 'move',
563
+ player: playerNumber,
564
+ move,
565
+ simple: `${player.name} passes.`,
566
+ pretty: `${playerNameHTML(player)} passes.`,
567
+ });
568
+
569
+ fastAuction(G, player, 0);
570
+ } else {
571
+ player.passed = true;
572
+
573
+ const notPassed = G.players.filter((p) => !p.passed && !p.skipAuction && !p.isDropped);
574
+ if (notPassed.length == 1) {
575
+ const winningPlayer = notPassed[0];
576
+ endAuction(G, winningPlayer, winningPlayer.bid);
577
+
578
+ if (
579
+ (winningPlayer.powerPlants.length > 4 ||
580
+ (G.players.length > 2 && winningPlayer.powerPlants.length > 3)) &&
581
+ !winningPlayer.isDropped
582
+ ) {
583
+ setCurrentPlayer(G, winningPlayer.id);
584
+ } else {
585
+ if (G.map.name != 'China' || G.step == 3) {
586
+ addPowerPlant(G);
587
+ }
588
+
589
+ if (G.players.some((p) => !p.skipAuction && !p.isDropped)) {
590
+ G.players.forEach((p) => {
591
+ p.bid = 0;
592
+ p.passed = p.isDropped;
593
+ });
594
+
595
+ nextPlayerAuction(G, true);
596
+ } else {
597
+ toResourcesPhase(G);
598
+ }
599
+ }
600
+ } else {
601
+ nextPlayerClockwise(G);
602
+ }
603
+ }
604
+ }
605
+
606
+ break;
607
+ }
608
+
609
+ case Phase.Resources: {
610
+ if (G.map.name == 'India') {
611
+ if (G.chosenResource) {
612
+ G.chosenResource = undefined;
613
+ } else {
614
+ player.passed = true;
615
+ }
616
+ } else {
617
+ player.passed = true;
618
+ }
619
+
620
+ if (G.players.filter((p) => !p.passed && !p.isDropped).length == 0) {
621
+ G.players.forEach((p) => {
622
+ p.passed = p.isDropped;
623
+ });
624
+ G.phase = Phase.Building;
625
+
626
+ if (G.map.name == 'India') {
627
+ G.citiesBuiltInCurrentRound = 0;
628
+ }
629
+
630
+ setCurrentPlayer(G, G.playerOrder[G.players.length - 1]);
631
+
632
+ if (G.players[G.currentPlayers[0]].isDropped && G.players.some((p) => !p.isDropped)) {
633
+ G.players[G.currentPlayers[0]].passed = true;
634
+ nextPlayerReverse(G);
635
+ }
636
+ } else {
637
+ nextPlayerReverse(G);
638
+ }
639
+
640
+ if (G.map.name == 'India') {
641
+ while (G.players[G.currentPlayers[0]].passed) {
642
+ nextPlayerReverse(G);
643
+ }
644
+ }
645
+
646
+ break;
647
+ }
648
+
649
+ case Phase.Building: {
650
+ player.passed = true;
651
+
652
+ if (G.players.filter((p) => !p.passed && !p.isDropped).length == 0) {
653
+ const maxCities = Math.max(...G.players.map((p) => p.cities.length));
654
+ if (G.step == 1) {
655
+ if (maxCities >= G.citiesToStep2 && G.map.name != 'Middle East') {
656
+ const powerPlant = G.actualMarket.shift()!;
657
+ G.log.push({
658
+ type: 'event',
659
+ event: `Starting Step 2, Power Plant ${powerPlant?.number} discarded.`,
660
+ });
661
+ G.step = 2;
662
+
663
+ // Spain & Portugal: put plants 18, 22 and 27 on top
664
+ if (G.map.name == 'Spain & Portugal') {
665
+ if (!G.powerPlantsDeck.find((p) => p.number == 18)) {
666
+ G.powerPlantsDeck.unshift(getPowerPlant(27));
667
+ G.powerPlantsDeck.unshift(getPowerPlant(22));
668
+ G.powerPlantsDeck.unshift(getPowerPlant(18));
669
+ }
670
+ }
671
+
672
+ addPowerPlant(G);
673
+ }
674
+ }
675
+
676
+ if (maxCities >= G.citiesToEndGame) {
677
+ G.phase = Phase.GameEnd;
678
+ G.currentPlayers = [];
679
+ calculateCitiesPowered(G);
680
+
681
+ // Include payouts in phase 5 if there is a power outage in India.
682
+ if (G.map.name == 'India' && G.citiesBuiltInCurrentRound! > G.players.length * 2) {
683
+ G.players.forEach((player) => {
684
+ const payment = G.paymentTable[player.citiesPowered] - 3 * player.cities.length;
685
+ player.money += Math.max(payment, 0);
686
+ player.totalIncome += Math.max(payment, 0);
687
+ });
688
+ }
689
+
690
+ G.log.push({ type: 'event', event: 'Game Ended!' });
691
+ } else {
692
+ G.players.forEach((p) => {
693
+ p.passed = p.isDropped;
694
+ p.powerPlantsNotUsed = p.powerPlants.map((pp) => pp.number);
695
+ });
696
+ G.phase = Phase.Bureaucracy;
697
+ G.currentPlayers = G.playerOrder.filter(
698
+ (p) => !G.players[p].passed && !G.players[p].isDropped
699
+ );
700
+
701
+ if (G.map.name == 'India') {
702
+ // Compute the maximum number of cities each player can power.
703
+ G.players.forEach(
704
+ (player) => (player.targetCitiesPowered = calculateMaxCitiesPowered(player))
705
+ );
706
+
707
+ // Output log for power outage.
708
+ if (G.citiesBuiltInCurrentRound! > G.players.length * 2) {
709
+ G.log.push({
710
+ type: 'event',
711
+ event: `Power outage! ${G.citiesBuiltInCurrentRound} built this round, which is more than twice the number of players.`,
712
+ });
713
+ }
714
+ }
715
+
716
+ if (G.map.name == 'China' && G.step <= 2) {
717
+ rebuildPlantMarketForChina(G);
718
+ } else if (G.futureMarket.length == 0) {
719
+ G.step = 3;
720
+ }
721
+ }
722
+ } else {
723
+ nextPlayerReverse(G);
724
+ }
725
+
726
+ break;
727
+ }
728
+
729
+ case Phase.Bureaucracy: {
730
+ player.passed = true;
731
+ const citiesPowered: number = Math.min(player.cities.length, player.citiesPowered);
732
+ let payment: number = G.paymentTable[citiesPowered];
733
+
734
+ // For the India map, if the number of cities built in the current round is more than twice
735
+ // the number of players, each player is penalized three Elektro per city (power outage).
736
+ if (G.map.name == 'India' && G.citiesBuiltInCurrentRound! > G.players.length * 2) {
737
+ payment -= 3 * player.cities.length;
738
+ payment = Math.max(payment, 0); // No negative income
739
+ }
740
+
741
+ player.money += payment;
742
+
743
+ if (G.options.trackTotalSpent) {
744
+ player.totalIncome += payment;
745
+ }
746
+
747
+ player.citiesPowered = 0;
748
+
749
+ if (G.map.name == 'India') {
750
+ player.targetCitiesPowered = 0;
751
+ }
752
+
753
+ if (G.players.filter((p) => !p.passed && !p.isDropped).length == 0) {
754
+ const coalResupplyValue = Math.min(
755
+ G.coalSupply,
756
+ G.coalResupply![G.players.length - 2][G.step - 1]
757
+ );
758
+ G.coalMarket += coalResupplyValue;
759
+ G.coalSupply -= coalResupplyValue;
760
+
761
+ let oilResupplyValue: number;
762
+ if (G.map.name == 'Middle East') {
763
+ if (G.oilMarket == 0) {
764
+ G.oilPrices = prices[ResourceType.Oil];
765
+ }
766
+
767
+ oilResupplyValue = G.oilResupply![G.players.length - 2][G.step - 1];
768
+ for (let i = 0; i < oilResupplyValue; i++) {
769
+ if (G.oilSupply > 0) {
770
+ G.oilMarket++;
771
+ G.oilSupply--;
772
+ } else {
773
+ // If we have more oil to stock than is in the supply, we shift the prices downward.
774
+ G.oilPrices!.pop()!;
775
+ G.oilPrices!.unshift(1);
776
+ }
777
+ }
778
+ } else {
779
+ oilResupplyValue = Math.min(G.oilSupply, G.oilResupply![G.players.length - 2][G.step - 1]);
780
+ G.oilMarket += oilResupplyValue;
781
+ G.oilSupply -= oilResupplyValue;
782
+ }
783
+
784
+ const garbageResupplyValue = Math.min(
785
+ G.garbageSupply,
786
+ G.garbageResupply![G.players.length - 2][G.step - 1]
787
+ );
788
+ G.garbageMarket += garbageResupplyValue;
789
+ G.garbageSupply -= garbageResupplyValue;
790
+
791
+ let uraniumResupplyValue = 0;
792
+ if (
793
+ G.options.variant != 'recharged' ||
794
+ (G.options.map != 'Germany' && G.options.map != 'Italy') ||
795
+ !G.card39Bought
796
+ ) {
797
+ uraniumResupplyValue = Math.min(
798
+ G.uraniumSupply,
799
+ G.uraniumResupply![G.players.length - 2][G.step - 1]
800
+ );
801
+ G.uraniumMarket += uraniumResupplyValue;
802
+ G.uraniumSupply -= uraniumResupplyValue;
803
+ }
804
+
805
+ G.log.push({
806
+ type: 'event',
807
+ event: `Resupplying resources: [${coalResupplyValue}, ${oilResupplyValue}, ${garbageResupplyValue}, ${uraniumResupplyValue}].`,
808
+ });
809
+
810
+ if (G.map.name == 'Middle East' && G.step == 2 && G.futureMarket.length > 0) {
811
+ // If we aren't about to enter step 3, discard top two plants instead of one.
812
+ let powerPlantToPush: PowerPlant = G.futureMarket.pop()!;
813
+ G.log.push({
814
+ type: 'event',
815
+ event: `Putting Power Plant ${powerPlantToPush.number} on the bottom of the deck.`,
816
+ });
817
+ G.powerPlantsDeck.push(powerPlantToPush);
818
+ addPowerPlant(G);
819
+
820
+ // If Step 3 was drawn above, futureMarket will be empty so use actualMarket instead
821
+ powerPlantToPush = G.futureMarket.length ? G.futureMarket.pop()! : G.actualMarket.pop()!;
822
+ G.log.push({
823
+ type: 'event',
824
+ event: `Putting Power Plant ${powerPlantToPush.number} on the bottom of the deck.`,
825
+ });
826
+ G.powerPlantsDeck.push(powerPlantToPush);
827
+ addPowerPlant(G);
828
+ } else if (G.futureMarket.length > 0) {
829
+ if (
830
+ G.map.name == 'Benelux' &&
831
+ (G.options.variant == 'original' || G.discardSmallestPlant)
832
+ ) {
833
+ const removedPlant = G.actualMarket[0];
834
+ G.log.push({
835
+ type: 'event',
836
+ event: `Removing smallest plant, Power Plant ${removedPlant.number}.`,
837
+ });
838
+ G.actualMarket.shift();
839
+ addPowerPlant(G);
840
+ G.discardSmallestPlant = false;
841
+ }
842
+
843
+ let powerPlantToPush: PowerPlant | undefined;
844
+ if (G.map.name == 'Quebec') {
845
+ // For the Quebec map, ecological plants will never be put on the bottom of the deck.
846
+ const nonEcoPlants = G.futureMarket.filter((pp) => pp.type != PowerPlantType.Wind);
847
+ powerPlantToPush = nonEcoPlants.pop();
848
+
849
+ if (powerPlantToPush) {
850
+ G.futureMarket = G.futureMarket.filter(
851
+ (pp) => pp.number != powerPlantToPush?.number
852
+ );
853
+ } else {
854
+ const nonEcoPlants = G.actualMarket.filter((pp) => pp.type != PowerPlantType.Wind);
855
+ powerPlantToPush = nonEcoPlants.pop();
856
+
857
+ if (powerPlantToPush) {
858
+ G.actualMarket = G.actualMarket.filter(
859
+ (pp) => pp.number != powerPlantToPush?.number
860
+ );
861
+ }
862
+ }
863
+ } else {
864
+ powerPlantToPush = G.futureMarket.pop()!;
865
+ }
866
+
867
+ // This check covers the rare case in which a Quebec game might have a futures market consisting of
868
+ // all ecological plants. In that case, we do not draw a new plant.
869
+ if (powerPlantToPush) {
870
+ G.log.push({
871
+ type: 'event',
872
+ event: `Putting Power Plant ${powerPlantToPush.number} on the bottom of the deck.`,
873
+ });
874
+ G.powerPlantsDeck.push(powerPlantToPush);
875
+ addPowerPlant(G);
876
+ }
877
+ } else if (G.actualMarket.length > 0 && (G.map.name != 'China' || G.step == 3)) {
878
+ G.log.push({ type: 'event', event: `Discarding Power Plant ${G.actualMarket[0].number}.` });
879
+ G.actualMarket.shift();
880
+ addPowerPlant(G);
881
+ }
882
+
883
+ G.round++;
884
+
885
+ setPlayerOrder(G);
886
+
887
+ G.players.forEach((p) => {
888
+ p.passed = p.isDropped;
889
+ });
890
+ G.auctionSkips = 0;
891
+
892
+ if (G.actualMarket.length > 0) {
893
+ G.phase = Phase.Auction;
894
+
895
+ if (G.futureMarket.length == 0 && G.map.name != 'China') {
896
+ G.step = 3;
897
+ }
898
+
899
+ G.plantDiscountActive =
900
+ G.options.variant == 'recharged' && G.map.name != 'China' && G.map.name != 'Russia';
901
+ setCurrentPlayer(G, G.playerOrder[0]);
902
+ } else {
903
+ toResourcesPhase(G);
904
+ }
905
+ } else {
906
+ G.currentPlayers = G.playerOrder.filter((p) => !G.players[p].passed && !G.players[p].isDropped);
907
+ }
908
+
909
+ break;
910
+ }
911
+ }
912
+
913
+ break;
914
+ }
915
+
916
+ case MoveName.DiscardPowerPlant: {
917
+ asserts<Moves.MoveDiscardPowerPlant>(move);
918
+
919
+ if (!move.extra) {
920
+ G.log.push({
921
+ type: 'move',
922
+ player: playerNumber,
923
+ move,
924
+ simple: `${player.name} discards Power Plant ${move.data}.`,
925
+ pretty: `${playerNameHTML(player)} discards Power Plant <b>${move.data}</b>.`,
926
+ });
927
+ }
928
+
929
+ const powerPlant = player.powerPlants.find((p) => p.number == move.data)!;
930
+ player.powerPlants = player.powerPlants.filter((p) => p.number != move.data);
931
+
932
+ updatePlayerCapacity(player);
933
+
934
+ if (move.extra) {
935
+ const discarded: string[] = [];
936
+ switch (powerPlant.type) {
937
+ case PowerPlantType.Coal:
938
+ G.coalSupply += move.extra[0];
939
+ player.coalLeft -= move.extra[0];
940
+ discarded.push(move.extra[0] + ' Coal');
941
+ break;
942
+
943
+ case PowerPlantType.Oil:
944
+ G.oilSupply += move.extra[0];
945
+ player.oilLeft -= move.extra[0];
946
+ discarded.push(move.extra[0] + ' Oil');
947
+ break;
948
+
949
+ case PowerPlantType.Garbage:
950
+ G.garbageSupply += move.extra[0];
951
+ player.garbageLeft -= move.extra[0];
952
+ discarded.push(move.extra[0] + ' Garbage');
953
+ break;
954
+
955
+ case PowerPlantType.Uranium:
956
+ G.uraniumSupply += move.extra[0];
957
+ player.uraniumLeft -= move.extra[0];
958
+ discarded.push(move.extra[0] + ' Uranium');
959
+ break;
960
+
961
+ case PowerPlantType.Hybrid:
962
+ if (move.extra[0] > 0) {
963
+ G.coalSupply += move.extra[0];
964
+ player.coalLeft -= move.extra[0];
965
+ discarded.push(move.extra[0] + ' Coal');
966
+ }
967
+
968
+ if (move.extra[1] > 0) {
969
+ G.oilSupply += move.extra[1];
970
+ player.oilLeft -= move.extra[1];
971
+ discarded.push(move.extra[1] + ' Oil');
972
+ }
973
+
974
+ break;
975
+ }
976
+
977
+ G.log.push({
978
+ type: 'move',
979
+ player: playerNumber,
980
+ move,
981
+ simple: `${player.name} discards Power Plant ${move.data} and ${discarded.join(', ')}.`,
982
+ pretty: `${playerNameHTML(player)} discards Power Plant <b>${move.data}</b> and ${discarded.join(
983
+ ', '
984
+ )}.`,
985
+ });
986
+
987
+ if (G.map.name != 'China' || G.step == 3) {
988
+ addPowerPlant(G);
989
+ }
990
+
991
+ G.players.forEach((p) => {
992
+ p.bid = 0;
993
+ p.passed = p.isDropped;
994
+ });
995
+
996
+ if (G.players.some((p) => !p.skipAuction && !p.isDropped)) {
997
+ nextPlayerAuction(G, true);
998
+ } else {
999
+ toResourcesPhase(G);
1000
+ }
1001
+ } else {
1002
+ const toDiscard: ResourceType[] = [];
1003
+ let hybridCapacityUsed =
1004
+ player.hybridCapacity > 0 ? Math.max(0, player.oilLeft - player.oilCapacity) : 0;
1005
+ if (player.coalCapacity + player.hybridCapacity < player.coalLeft + hybridCapacityUsed) {
1006
+ toDiscard.push(ResourceType.Coal);
1007
+ }
1008
+
1009
+ hybridCapacityUsed = player.hybridCapacity > 0 ? Math.max(0, player.coalLeft - player.coalCapacity) : 0;
1010
+ if (player.oilCapacity + player.hybridCapacity < player.oilLeft + hybridCapacityUsed) {
1011
+ toDiscard.push(ResourceType.Oil);
1012
+ }
1013
+
1014
+ if (player.garbageLeft > player.garbageCapacity) {
1015
+ G.garbageSupply += player.garbageLeft - player.garbageCapacity;
1016
+ player.garbageLeft = player.garbageCapacity;
1017
+ }
1018
+
1019
+ if (player.uraniumLeft > player.uraniumCapacity) {
1020
+ G.uraniumSupply += player.uraniumLeft - player.uraniumCapacity;
1021
+ player.uraniumLeft = player.uraniumCapacity;
1022
+ }
1023
+
1024
+ if (toDiscard.length == 1) {
1025
+ if (toDiscard[0] == ResourceType.Coal) {
1026
+ G.coalSupply += player.coalLeft - player.coalCapacity;
1027
+ player.coalLeft = player.coalCapacity;
1028
+ } else if (toDiscard[0] == ResourceType.Oil) {
1029
+ G.oilSupply += player.oilLeft - player.oilCapacity;
1030
+ player.oilLeft = player.oilCapacity;
1031
+ }
1032
+
1033
+ toDiscard.pop();
1034
+ }
1035
+
1036
+ if (toDiscard.length == 0) {
1037
+ if (G.map.name != 'China' || G.step == 3) {
1038
+ addPowerPlant(G);
1039
+ }
1040
+ G.players.forEach((p) => {
1041
+ p.bid = 0;
1042
+ p.passed = p.isDropped;
1043
+ });
1044
+
1045
+ if (G.players.some((p) => !p.skipAuction && !p.isDropped)) {
1046
+ nextPlayerAuction(G, true);
1047
+ } else {
1048
+ toResourcesPhase(G);
1049
+ }
1050
+ }
1051
+ }
1052
+
1053
+ break;
1054
+ }
1055
+
1056
+ case MoveName.DiscardResources: {
1057
+ asserts<Moves.MoveDiscardResources>(move);
1058
+
1059
+ G.log.push({
1060
+ type: 'move',
1061
+ player: playerNumber,
1062
+ move,
1063
+ simple: `${player.name} discarded a ${move.data}.`,
1064
+ pretty: `${playerNameHTML(player)} discarded a <b>${move.data}</b>.`,
1065
+ });
1066
+
1067
+ if (move.data == ResourceType.Coal) {
1068
+ player.coalLeft--;
1069
+ G.coalSupply++;
1070
+ } else if (move.data == ResourceType.Oil) {
1071
+ player.oilLeft--;
1072
+ G.oilSupply++;
1073
+ }
1074
+
1075
+ const toDiscard: ResourceType[] = [];
1076
+ let hybridCapacityUsed = player.hybridCapacity > 0 ? Math.max(0, player.oilLeft - player.oilCapacity) : 0;
1077
+ if (player.coalCapacity + player.hybridCapacity < player.coalLeft + hybridCapacityUsed) {
1078
+ toDiscard.push(ResourceType.Coal);
1079
+ }
1080
+
1081
+ hybridCapacityUsed = player.hybridCapacity > 0 ? Math.max(0, player.coalLeft - player.coalCapacity) : 0;
1082
+ if (player.oilCapacity + player.hybridCapacity < player.oilLeft + hybridCapacityUsed) {
1083
+ toDiscard.push(ResourceType.Oil);
1084
+ }
1085
+
1086
+ if (toDiscard.length == 1) {
1087
+ if (toDiscard[0] == ResourceType.Coal) {
1088
+ player.coalLeft--;
1089
+ } else if (toDiscard[0] == ResourceType.Oil) {
1090
+ player.oilLeft--;
1091
+ }
1092
+
1093
+ toDiscard.pop();
1094
+ }
1095
+
1096
+ if (toDiscard.length == 0) {
1097
+ if (G.map.name != 'China' || G.step == 3) {
1098
+ addPowerPlant(G);
1099
+ }
1100
+ G.players.forEach((p) => {
1101
+ p.bid = 0;
1102
+ p.passed = p.isDropped;
1103
+ });
1104
+
1105
+ if (G.players.some((p) => !p.skipAuction && !p.isDropped)) {
1106
+ nextPlayerAuction(G, true);
1107
+ } else {
1108
+ toResourcesPhase(G);
1109
+ }
1110
+ }
1111
+
1112
+ break;
1113
+ }
1114
+
1115
+ case MoveName.BuyResource: {
1116
+ asserts<Moves.MoveBuyResource>(move);
1117
+ G.chosenResource = move.data.resource;
1118
+
1119
+ let price: number;
1120
+ switch (move.data.resource) {
1121
+ case ResourceType.Coal: {
1122
+ if (G.coalMarket == 0) {
1123
+ price = 8;
1124
+ player.coalLeft++;
1125
+ G.coalSupply--;
1126
+ move.fromSupply = true;
1127
+ } else {
1128
+ const coalPrices = G.coalPrices ?? prices[ResourceType.Coal];
1129
+ price = coalPrices[coalPrices.length - G.coalMarket];
1130
+ player.coalLeft++;
1131
+ G.coalMarket--;
1132
+ }
1133
+
1134
+ break;
1135
+ }
1136
+
1137
+ case ResourceType.Oil: {
1138
+ const oilPrices = G.oilPrices ?? prices[ResourceType.Oil];
1139
+ price = oilPrices[oilPrices.length - G.oilMarket];
1140
+ player.oilLeft++;
1141
+ G.oilMarket--;
1142
+ break;
1143
+ }
1144
+
1145
+ case ResourceType.Garbage: {
1146
+ const garbagePrices = G.garbagePrices ?? prices[ResourceType.Garbage];
1147
+ price = garbagePrices[garbagePrices.length - G.garbageMarket];
1148
+
1149
+ // $1 cheaper for players in Wien in Central Europe
1150
+ if (G.map.name == 'Central Europe') {
1151
+ const wienCity = player.cities.filter((c) => c.name == 'Wien');
1152
+ if (wienCity?.length > 0) {
1153
+ price--;
1154
+ }
1155
+ }
1156
+
1157
+ player.garbageLeft++;
1158
+ G.garbageMarket--;
1159
+ break;
1160
+ }
1161
+
1162
+ case ResourceType.Uranium: {
1163
+ const uraniumPrices = G.uraniumPrices ?? prices[ResourceType.Uranium];
1164
+ price = uraniumPrices[uraniumPrices.length - G.uraniumMarket];
1165
+ player.uraniumLeft++;
1166
+ G.uraniumMarket--;
1167
+ break;
1168
+ }
1169
+ }
1170
+
1171
+ player.money -= price;
1172
+
1173
+ if (G.options.trackTotalSpent) {
1174
+ player.totalSpentResources += price;
1175
+ }
1176
+
1177
+ G.log.push({
1178
+ type: 'move',
1179
+ player: playerNumber,
1180
+ move,
1181
+ simple: `${player.name} buys ${move.data.resource} for $${price}.`,
1182
+ pretty: `${playerNameHTML(player)} buys <b>${
1183
+ move.data.resource
1184
+ }</b> for <span style="color: green">$${price}</span>.`,
1185
+ });
1186
+
1187
+ break;
1188
+ }
1189
+
1190
+ case MoveName.Build: {
1191
+ asserts<Moves.MoveBuild>(move);
1192
+
1193
+ const position = G.players.filter((p) => p.cities.find((c) => c.name == move.data.name)).length;
1194
+ player.cities.push({ name: move.data.name, position });
1195
+ player.money -= move.data.price;
1196
+
1197
+ if (G.options.trackTotalSpent) {
1198
+ player.totalSpentCities += 10 + position * 5;
1199
+ player.totalSpentConnections += move.data.price - (10 + position * 5);
1200
+ }
1201
+
1202
+ G.log.push({
1203
+ type: 'move',
1204
+ player: playerNumber,
1205
+ move,
1206
+ simple: `${player.name} builds on ${move.data.name} for $${move.data.price}.`,
1207
+ pretty: `${playerNameHTML(player)} builds on <b>${move.data.name}</b> for <span style="color: green">$${
1208
+ move.data.price
1209
+ }</span>.`,
1210
+ });
1211
+
1212
+ if (G.map.name == 'India') {
1213
+ G.citiesBuiltInCurrentRound!++;
1214
+ }
1215
+
1216
+ if (G.options.variant == 'original') {
1217
+ if (
1218
+ G.actualMarket.length > 0 &&
1219
+ player.cities.length >= G.actualMarket[0].number &&
1220
+ G.map.name != 'China' &&
1221
+ G.map.name != 'Russia'
1222
+ ) {
1223
+ G.actualMarket.shift();
1224
+ addPowerPlant(G);
1225
+ }
1226
+ }
1227
+
1228
+ break;
1229
+ }
1230
+
1231
+ case MoveName.UsePowerPlant: {
1232
+ asserts<Moves.MoveUsePowerPlant>(move);
1233
+
1234
+ player.powerPlantsNotUsed = player.powerPlantsNotUsed.filter((x) => x != move.data.powerPlant);
1235
+ move.data.resourcesSpent.forEach((resourceType) => {
1236
+ switch (resourceType) {
1237
+ case ResourceType.Coal:
1238
+ player.coalLeft--;
1239
+ G.coalSupply++;
1240
+ break;
1241
+
1242
+ case ResourceType.Oil:
1243
+ player.oilLeft--;
1244
+ G.oilSupply++;
1245
+ break;
1246
+
1247
+ case ResourceType.Garbage:
1248
+ player.garbageLeft--;
1249
+ G.garbageSupply++;
1250
+ break;
1251
+
1252
+ case ResourceType.Uranium:
1253
+ player.uraniumLeft--;
1254
+ G.uraniumSupply++;
1255
+ break;
1256
+ }
1257
+ });
1258
+
1259
+ player.citiesPowered += move.data.citiesPowered;
1260
+
1261
+ G.log.push({
1262
+ type: 'move',
1263
+ player: playerNumber,
1264
+ move,
1265
+ simple: `${player.name} uses Power Plant ${move.data.powerPlant}.`,
1266
+ pretty: `${playerNameHTML(player)} uses Power Plant <b>${move.data.powerPlant}</b>.`,
1267
+ });
1268
+
1269
+ break;
1270
+ }
1271
+
1272
+ case MoveName.Undo: {
1273
+ asserts<Moves.MoveUndo>(move);
1274
+
1275
+ const lastMove = player.lastMove;
1276
+ switch (lastMove?.name) {
1277
+ case MoveName.ChoosePowerPlant: {
1278
+ if (lastMove.data == 39) {
1279
+ G.card39Bought = false;
1280
+ }
1281
+
1282
+ if (lastMove.usedPlantDiscount) {
1283
+ G.plantDiscountActive = true;
1284
+ }
1285
+
1286
+ G.chosenPowerPlant = undefined;
1287
+ G.auctioningPlayer = undefined;
1288
+
1289
+ G.currentPlayers = [player.id];
1290
+
1291
+ G.log.pop();
1292
+
1293
+ break;
1294
+ }
1295
+
1296
+ case MoveName.BuyResource: {
1297
+ let price: number;
1298
+ switch (lastMove.data.resource) {
1299
+ case ResourceType.Coal:
1300
+ if (lastMove.fromSupply) {
1301
+ price = 8;
1302
+ player.coalLeft--;
1303
+ G.coalSupply++;
1304
+ } else {
1305
+ player.coalLeft--;
1306
+ G.coalMarket++;
1307
+ const coalPrices = G.coalPrices ?? prices[ResourceType.Coal];
1308
+ price = coalPrices[coalPrices.length - G.coalMarket];
1309
+ }
1310
+
1311
+ break;
1312
+
1313
+ case ResourceType.Oil: {
1314
+ player.oilLeft--;
1315
+ G.oilMarket++;
1316
+ const oilPrices = G.oilPrices ?? prices[ResourceType.Oil];
1317
+ price = oilPrices[oilPrices.length - G.oilMarket];
1318
+ break;
1319
+ }
1320
+
1321
+ case ResourceType.Garbage: {
1322
+ player.garbageLeft--;
1323
+ G.garbageMarket++;
1324
+ const garbagePrices = G.garbagePrices ?? prices[ResourceType.Garbage];
1325
+ price = garbagePrices[garbagePrices.length - G.garbageMarket];
1326
+
1327
+ // $1 cheaper for players in Wien in Central Europe
1328
+ if (G.map.name == 'Central Europe') {
1329
+ const wienCity = player.cities.filter((c) => c.name == 'Wien');
1330
+ if (wienCity?.length > 0) {
1331
+ price--;
1332
+ }
1333
+ }
1334
+
1335
+ break;
1336
+ }
1337
+
1338
+ case ResourceType.Uranium: {
1339
+ player.uraniumLeft--;
1340
+ G.uraniumMarket++;
1341
+ const uraniumPrices = G.uraniumPrices ?? prices[ResourceType.Uranium];
1342
+ price = uraniumPrices[uraniumPrices.length - G.uraniumMarket];
1343
+ break;
1344
+ }
1345
+ }
1346
+
1347
+ player.money += price;
1348
+
1349
+ if (G.options.trackTotalSpent) {
1350
+ player.totalSpentResources -= price;
1351
+ }
1352
+
1353
+ if (G.map.name == 'India') {
1354
+ G.chosenResource = undefined;
1355
+ }
1356
+
1357
+ G.log.pop();
1358
+ break;
1359
+ }
1360
+
1361
+ case MoveName.Build: {
1362
+ player.cities.pop();
1363
+ player.money += lastMove.data.price;
1364
+
1365
+ const position = G.players.filter((p) => p.cities.find((c) => c.name == lastMove.data.name)).length;
1366
+
1367
+ if (G.options.trackTotalSpent) {
1368
+ player.totalSpentCities -= 10 + position * 5;
1369
+ player.totalSpentConnections -= lastMove.data.price - (10 + position * 5);
1370
+ }
1371
+
1372
+ G.log.pop();
1373
+
1374
+ if (G.map.name == 'India') {
1375
+ G.citiesBuiltInCurrentRound!--;
1376
+ }
1377
+
1378
+ break;
1379
+ }
1380
+
1381
+ case MoveName.UsePowerPlant: {
1382
+ player.powerPlantsNotUsed.push(lastMove.data.powerPlant);
1383
+ lastMove.data.resourcesSpent.forEach((resourceType) => {
1384
+ switch (resourceType) {
1385
+ case ResourceType.Coal:
1386
+ player.coalLeft++;
1387
+ G.coalSupply--;
1388
+ break;
1389
+
1390
+ case ResourceType.Oil:
1391
+ player.oilLeft++;
1392
+ G.oilSupply--;
1393
+ break;
1394
+
1395
+ case ResourceType.Garbage:
1396
+ player.garbageLeft++;
1397
+ G.garbageSupply--;
1398
+ break;
1399
+
1400
+ case ResourceType.Uranium:
1401
+ player.uraniumLeft++;
1402
+ G.uraniumSupply--;
1403
+ break;
1404
+ }
1405
+ });
1406
+
1407
+ player.citiesPowered -= lastMove.data.citiesPowered;
1408
+
1409
+ const reverseLog = G.log.slice().reverse();
1410
+ const index =
1411
+ G.log.length - reverseLog.findIndex((l) => l.type == 'move' && l.player == player.id) - 1;
1412
+ G.log.splice(index, 1);
1413
+
1414
+ break;
1415
+ }
1416
+ }
1417
+ }
1418
+ }
1419
+
1420
+ player.availableMoves = null;
1421
+
1422
+ if (move.name == MoveName.Undo) {
1423
+ const reverseLog = G.log.slice().reverse();
1424
+ const logMove = reverseLog.find((m) => m.type == 'move' && m.player == player.id) as LogMove;
1425
+ player.lastMove = logMove?.move;
1426
+ } else {
1427
+ player.lastMove = move;
1428
+ }
1429
+
1430
+ G.cardsLeft = G.powerPlantsDeck.length;
1431
+ G.nextCardWeak = G.options.variant == 'recharged' && G.cardsLeft > 0 && G.powerPlantsDeck[0].number <= 15;
1432
+
1433
+ G.currentPlayers.forEach((p) => (G.players[p].availableMoves = availableMoves(G, G.players[p])));
1434
+
1435
+ return G;
1436
+ }
1437
+
1438
+ export function moveAI(G: GameState, playerNumber: number): GameState {
1439
+ const player = G.players[playerNumber];
1440
+ const availableMoves = player.availableMoves;
1441
+ let chosenMove: Move = { name: MoveName.Pass, data: true };
1442
+
1443
+ switch (G.phase) {
1444
+ case Phase.Auction: {
1445
+ if (availableMoves?.ChoosePowerPlant) {
1446
+ if (
1447
+ !availableMoves.Pass ||
1448
+ (Math.random() > 0.5 && player.money - availableMoves.ChoosePowerPlant[0] >= 20)
1449
+ ) {
1450
+ chosenMove = {
1451
+ name: MoveName.ChoosePowerPlant,
1452
+ data: chooseRandom(availableMoves.ChoosePowerPlant),
1453
+ };
1454
+ } else {
1455
+ chosenMove = { name: MoveName.Pass, data: true };
1456
+ }
1457
+ } else if (availableMoves?.Bid) {
1458
+ if (
1459
+ !availableMoves.Pass ||
1460
+ (availableMoves.Bid.length > 0 &&
1461
+ Math.random() > 0.5 &&
1462
+ player.money - availableMoves?.Bid[0] >= 15)
1463
+ ) {
1464
+ if (G.options.fastBid) {
1465
+ const bid = Math.floor((Math.random() * availableMoves.Bid.length) / 2);
1466
+ chosenMove = { name: MoveName.Bid, data: availableMoves?.Bid[bid] };
1467
+ } else {
1468
+ chosenMove = { name: MoveName.Bid, data: availableMoves?.Bid[0] };
1469
+ }
1470
+ } else {
1471
+ chosenMove = { name: MoveName.Pass, data: true };
1472
+ }
1473
+ } else if (availableMoves?.DiscardPowerPlant) {
1474
+ chosenMove = { name: MoveName.DiscardPowerPlant, data: player.powerPlants[0].number };
1475
+ } else if (availableMoves?.DiscardResources) {
1476
+ chosenMove = { name: MoveName.DiscardResources, data: chooseRandom(availableMoves.DiscardResources) };
1477
+ }
1478
+
1479
+ break;
1480
+ }
1481
+
1482
+ case Phase.Resources: {
1483
+ if (availableMoves?.BuyResource && player.money > 20) {
1484
+ const buyCoal = availableMoves.BuyResource.find((r) => r.resource == ResourceType.Coal);
1485
+ const buyOil = availableMoves.BuyResource.find((r) => r.resource == ResourceType.Oil);
1486
+ const buyGarbage = availableMoves.BuyResource.find((r) => r.resource == ResourceType.Garbage);
1487
+ const buyUranium = availableMoves.BuyResource.find((r) => r.resource == ResourceType.Uranium);
1488
+
1489
+ if (buyCoal && player.coalLeft < (player.coalCapacity + player.hybridCapacity) / 2) {
1490
+ chosenMove = { name: MoveName.BuyResource, data: buyCoal };
1491
+ } else if (buyOil && player.oilLeft < (player.oilCapacity + player.hybridCapacity) / 2) {
1492
+ chosenMove = { name: MoveName.BuyResource, data: buyOil };
1493
+ } else if (buyGarbage && player.garbageLeft < player.garbageCapacity / 2) {
1494
+ chosenMove = { name: MoveName.BuyResource, data: buyGarbage };
1495
+ } else if (buyUranium && player.uraniumLeft < player.uraniumCapacity / 2) {
1496
+ chosenMove = { name: MoveName.BuyResource, data: buyUranium };
1497
+ }
1498
+ }
1499
+
1500
+ break;
1501
+ }
1502
+
1503
+ case Phase.Building: {
1504
+ const capacity = player.powerPlants.map((pp) => pp.citiesPowered).reduce((a, b) => a + b);
1505
+
1506
+ if (availableMoves?.Build && (player.money >= 30 || capacity > player.cities.length)) {
1507
+ const minPrice = availableMoves.Build.sort((a, b) => a.price - b.price)[0].price;
1508
+ const cheapestCities = availableMoves.Build.filter((x) => x.price == minPrice);
1509
+ chosenMove = { name: MoveName.Build, data: chooseRandom(cheapestCities) };
1510
+ }
1511
+
1512
+ break;
1513
+ }
1514
+
1515
+ case Phase.Bureaucracy: {
1516
+ if (availableMoves?.UsePowerPlant && player.cities.length > player.citiesPowered) {
1517
+ chosenMove = {
1518
+ name: MoveName.UsePowerPlant,
1519
+ data: availableMoves.UsePowerPlant.sort((a, b) => a.citiesPowered - b.citiesPowered)[0],
1520
+ };
1521
+ }
1522
+
1523
+ break;
1524
+ }
1525
+ }
1526
+
1527
+ console.log('ai move', chosenMove);
1528
+ return move(G, chosenMove, playerNumber);
1529
+ }
1530
+
1531
+ function chooseRandom(moves: any[]) {
1532
+ return moves[Math.floor(Math.random() * moves.length)];
1533
+ }
1534
+
1535
+ export function ended(G: GameState): boolean {
1536
+ return G.phase == Phase.GameEnd;
1537
+ }
1538
+
1539
+ export function scores(G: GameState): number[] {
1540
+ return ended(G) ? G.players.map((p) => p.citiesPowered) : G.players.map((_) => 0);
1541
+ }
1542
+
1543
+ export function reconstructState(gameState: GameState, to?: number): GameState {
1544
+ const initialState = getBaseState(gameState);
1545
+ const G = cloneDeep(initialState);
1546
+
1547
+ if (to != undefined && gameState.seed == 'secret') {
1548
+ if (gameState.knownPowerPlantDeck) {
1549
+ G.map = gameState.map;
1550
+ G.powerPlantsDeck = cloneDeep(gameState.knownPowerPlantDeck);
1551
+ G.actualMarket = G.powerPlantsDeck.splice(0, 4);
1552
+ G.futureMarket = G.powerPlantsDeck.splice(0, 4);
1553
+ G.players[G.currentPlayers[0]].availableMoves = availableMoves(G, G.players[G.currentPlayers[0]]);
1554
+ G.powerPlantDeckAfterStep3 = gameState.knownPowerPlantDeckStep3;
1555
+ G.knownPowerPlantDeck = G.actualMarket.concat(G.futureMarket);
1556
+ }
1557
+ }
1558
+
1559
+ const log = to != null ? gameState.log.slice(0, to) : gameState.log;
1560
+ for (const item of log) {
1561
+ switch (item.type) {
1562
+ case 'event': {
1563
+ if (item.event.endsWith('was dropped')) {
1564
+ const playerNum = +item.event.split(' ')[1];
1565
+ G.players[playerNum].isDropped = true;
1566
+ }
1567
+
1568
+ break;
1569
+ }
1570
+
1571
+ case 'move': {
1572
+ move(G, item.move, item.player, true);
1573
+ break;
1574
+ }
1575
+ }
1576
+ }
1577
+
1578
+ return G;
1579
+ }
1580
+
1581
+ function updatePlayerCapacity(player: Player) {
1582
+ player.coalCapacity =
1583
+ player.oilCapacity =
1584
+ player.garbageCapacity =
1585
+ player.uraniumCapacity =
1586
+ player.hybridCapacity =
1587
+ 0;
1588
+
1589
+ player.powerPlants.forEach((powerPlant) => {
1590
+ switch (powerPlant.type) {
1591
+ case PowerPlantType.Coal: {
1592
+ player.coalCapacity += powerPlant.cost * 2;
1593
+ break;
1594
+ }
1595
+
1596
+ case PowerPlantType.Oil: {
1597
+ player.oilCapacity += powerPlant.cost * 2;
1598
+ break;
1599
+ }
1600
+
1601
+ case PowerPlantType.Garbage: {
1602
+ if (powerPlant.storage) {
1603
+ // For the India map, garbage plants have cost one higher, but have no additional storage.
1604
+ player.garbageCapacity += powerPlant.storage;
1605
+ } else {
1606
+ player.garbageCapacity += powerPlant.cost * 2;
1607
+ }
1608
+
1609
+ break;
1610
+ }
1611
+
1612
+ case PowerPlantType.Uranium: {
1613
+ player.uraniumCapacity += powerPlant.cost * 2;
1614
+ break;
1615
+ }
1616
+
1617
+ case PowerPlantType.Hybrid: {
1618
+ player.hybridCapacity += powerPlant.cost * 2;
1619
+ break;
1620
+ }
1621
+ }
1622
+ });
1623
+ }
1624
+
1625
+ function addPowerPlant(G: GameState) {
1626
+ let powerPlant = G.powerPlantsDeck.shift();
1627
+
1628
+ if (powerPlant) {
1629
+ if (G.step == 3) {
1630
+ if (G.knownPowerPlantDeckStep3) {
1631
+ G.knownPowerPlantDeckStep3.push(powerPlant);
1632
+ }
1633
+ } else {
1634
+ if (G.knownPowerPlantDeck) {
1635
+ G.knownPowerPlantDeck.push(powerPlant);
1636
+ }
1637
+ }
1638
+
1639
+ if (G.options.variant == 'original' && G.map.name != 'China') {
1640
+ const maxCities = Math.max(...G.players.map((p) => p.cities.length));
1641
+ while (powerPlant.number <= maxCities) {
1642
+ G.log.push({
1643
+ type: 'event',
1644
+ event: `Power plant ${powerPlant?.number} discarded.`,
1645
+ });
1646
+
1647
+ if (G.powerPlantsDeck.length > 0) {
1648
+ powerPlant = G.powerPlantsDeck.shift()!;
1649
+
1650
+ if (G.step == 3) {
1651
+ if (G.knownPowerPlantDeckStep3) {
1652
+ G.knownPowerPlantDeckStep3.push(powerPlant);
1653
+ }
1654
+ } else {
1655
+ if (G.knownPowerPlantDeck) {
1656
+ G.knownPowerPlantDeck.push(powerPlant);
1657
+ }
1658
+ }
1659
+ } else {
1660
+ break;
1661
+ }
1662
+ }
1663
+ }
1664
+
1665
+ let skipAdd = false;
1666
+ if (powerPlant.number == 99) {
1667
+ if (G.powerPlantDeckAfterStep3) {
1668
+ G.powerPlantsDeck = G.powerPlantDeckAfterStep3;
1669
+ } else if (G.map.name == 'China') {
1670
+ G.step = 3;
1671
+ } else {
1672
+ G.powerPlantsDeck = shuffle(G.powerPlantsDeck, G.seed);
1673
+ }
1674
+
1675
+ if (G.phase != Phase.Auction) {
1676
+ if (G.map.name == 'Middle East' && G.step == 1) {
1677
+ // Add step 3 card to market, then trigger step 2 process.
1678
+ const market = [...G.actualMarket, ...G.futureMarket, powerPlant];
1679
+ market.sort((a, b) => a.number - b.number);
1680
+ G.actualMarket = market.slice(0, 4);
1681
+ G.futureMarket = market.slice(4);
1682
+ enterStepTwoMiddleEast(G);
1683
+ skipAdd = true;
1684
+ } else {
1685
+ const powerPlantDiscarded = G.actualMarket.shift();
1686
+ G.log.push({
1687
+ type: 'event',
1688
+ event: `Step 3 will begin next phase, Power Plant ${powerPlantDiscarded?.number} discarded.`,
1689
+ });
1690
+
1691
+ const market = [...G.actualMarket, ...G.futureMarket];
1692
+ market.sort((a, b) => a.number - b.number);
1693
+ G.actualMarket = market;
1694
+ G.futureMarket = [];
1695
+ }
1696
+ }
1697
+ } else {
1698
+ if (G.plantDiscountActive && powerPlant.number < G.actualMarket[0].number) {
1699
+ G.log.push({
1700
+ type: 'event',
1701
+ event: `Power Plant ${powerPlant.number} drawn from the deck and discarded.`,
1702
+ });
1703
+ G.plantDiscountActive = false;
1704
+ addPowerPlant(G);
1705
+ return;
1706
+ } else {
1707
+ G.log.push({ type: 'event', event: `Power Plant ${powerPlant.number} drawn from the deck.` });
1708
+ }
1709
+ }
1710
+
1711
+ if (G.map.name == 'China' && powerPlant.type != PowerPlantType.Step3) {
1712
+ const market = [...G.actualMarket, powerPlant];
1713
+ market.sort((a, b) => a.number - b.number);
1714
+ G.actualMarket = market;
1715
+ } else {
1716
+ if (!skipAdd) {
1717
+ const market = [...G.actualMarket, ...G.futureMarket, powerPlant];
1718
+ market.sort((a, b) => a.number - b.number);
1719
+ if (G.futureMarket.length == 0) {
1720
+ if (G.map.name == 'Russia') {
1721
+ // Only four plants in market.
1722
+ G.actualMarket = market.slice(0, 4);
1723
+ G.futureMarket = [];
1724
+ } else {
1725
+ G.actualMarket = market.slice(0, 6);
1726
+ G.futureMarket = [];
1727
+ }
1728
+ } else {
1729
+ if (G.map.name == 'Russia') {
1730
+ // Only 3 plants in actual market and 3 in future market.
1731
+ G.actualMarket = market.slice(0, 3);
1732
+ G.futureMarket = market.slice(3);
1733
+ } else if (G.map.name == 'Benelux' && market[4].type == PowerPlantType.Wind) {
1734
+ // Add extra ecological plant to actual market.
1735
+ G.actualMarket = market.slice(0, 5);
1736
+ G.futureMarket = market.slice(5);
1737
+ } else {
1738
+ G.actualMarket = market.slice(0, 4);
1739
+ G.futureMarket = market.slice(4);
1740
+ }
1741
+ }
1742
+
1743
+ if (G.map.name == 'Middle East' && G.step == 1) {
1744
+ removePlantsForMiddleEastStep1(G);
1745
+ }
1746
+ }
1747
+ }
1748
+ }
1749
+ }
1750
+
1751
+ // During step 1 for the Middle East map, we remove garbage and uranium plants from the actual market.
1752
+ // If the number is 6, 11, or 14, the plant is removed from the game. Otherwise, it's put under the deck.
1753
+ function removePlantsForMiddleEastStep1(G: GameState) {
1754
+ let plantToRemove: PowerPlant | undefined = G.actualMarket.find(
1755
+ (pp: PowerPlant) => pp.type == PowerPlantType.Garbage || pp.type == PowerPlantType.Uranium
1756
+ );
1757
+
1758
+ while (plantToRemove) {
1759
+ removePowerPlant(G, plantToRemove);
1760
+
1761
+ if ([6, 11, 14].includes(plantToRemove.number)) {
1762
+ G.log.push({
1763
+ type: 'event',
1764
+ event: `Removing Power Plant ${plantToRemove.number} from game.`,
1765
+ });
1766
+ } else {
1767
+ G.powerPlantsDeck.push(plantToRemove);
1768
+ G.log.push({
1769
+ type: 'event',
1770
+ event: `Sending Power Plant ${plantToRemove.number} to the bottom of the deck.`,
1771
+ });
1772
+ }
1773
+
1774
+ // Prevent infinite loop cycling through power plants.
1775
+ const availableFuturePlants = G.futureMarket.filter(
1776
+ (pp) => pp.type != PowerPlantType.Garbage && pp.type != PowerPlantType.Uranium
1777
+ );
1778
+ const nextFuturePlantNumber = availableFuturePlants.length > 0 ? availableFuturePlants[0].number : 100;
1779
+ if (
1780
+ G.powerPlantsDeck.filter(
1781
+ (pp) =>
1782
+ (pp.type != PowerPlantType.Garbage && pp.type != PowerPlantType.Uranium) ||
1783
+ pp.number > nextFuturePlantNumber
1784
+ ).length == 0
1785
+ ) {
1786
+ G.log.push({
1787
+ type: 'event',
1788
+ event: 'No suitable power plants available to draw.',
1789
+ });
1790
+ break;
1791
+ }
1792
+
1793
+ addPowerPlant(G);
1794
+ plantToRemove = G.actualMarket.find(
1795
+ (pp: PowerPlant) => pp.type == PowerPlantType.Garbage || pp.type == PowerPlantType.Uranium
1796
+ );
1797
+ }
1798
+ }
1799
+
1800
+ function enterStepTwoMiddleEast(G: GameState) {
1801
+ // Shuffle deck of remaining power plants and put step 3 card back underneath.
1802
+ const step3 = G.futureMarket.pop()!;
1803
+ G.powerPlantsDeck = shuffle(G.powerPlantsDeck, G.seed);
1804
+ G.powerPlantsDeck.push(step3);
1805
+
1806
+ // Draw new plant to replace step 3 card.
1807
+ addPowerPlant(G);
1808
+
1809
+ // Discard two lowest power plants from current market.
1810
+ G.log.push({
1811
+ type: 'event',
1812
+ event: 'Step 2 will begin next phase, discarding two power plants.',
1813
+ });
1814
+ G.step = 2;
1815
+
1816
+ const powerPlantDiscarded1 = G.actualMarket.shift();
1817
+ if (powerPlantDiscarded1) {
1818
+ G.log.push({
1819
+ type: 'event',
1820
+ event: `Power Plant ${powerPlantDiscarded1.number} discarded to start step 2.`,
1821
+ });
1822
+ addPowerPlant(G);
1823
+ }
1824
+
1825
+ const powerPlantDiscarded2 = G.actualMarket.shift();
1826
+ if (powerPlantDiscarded2) {
1827
+ G.log.push({
1828
+ type: 'event',
1829
+ event: `Power Plant ${powerPlantDiscarded2.number} discarded to start step 2.`,
1830
+ });
1831
+ addPowerPlant(G);
1832
+ }
1833
+ }
1834
+
1835
+ function rebuildPlantMarketForChina(G: GameState) {
1836
+ /*At the beginning of phase 5, the players fill the power plant market with new power plants. Depending on the
1837
+ number of players, the players always add a minimum of 1, 2, or 3 power plants to the market from the supply:
1838
+ with 2 and 3 players, add at least 1 power plant.
1839
+ with 4 and 5 players, add at least 2 power plants.
1840
+ with 6 players, add at least 3 power plants.
1841
+ The players add more than the minimum if the number of plants in the market is still more than 1 less than the number of players.
1842
+ Exception: with 2 players, add plants until there are 2 in the market.*/
1843
+ let minPlantsToAdd = 0;
1844
+ if (G.players.length == 2 || G.players.length == 3) {
1845
+ minPlantsToAdd = 1;
1846
+ } else if (G.players.length == 4 || G.players.length == 5) {
1847
+ minPlantsToAdd = 2;
1848
+ } else if (G.players.length == 6) {
1849
+ minPlantsToAdd = 3;
1850
+ }
1851
+
1852
+ const currentActualSize = G.actualMarket.length;
1853
+ const minSize = G.players.length - 1;
1854
+ const numPlantsToAdd = Math.max(minPlantsToAdd, minSize - currentActualSize);
1855
+ for (let i = 0; i < numPlantsToAdd; i++) {
1856
+ if (G.step == 3) {
1857
+ break;
1858
+ } else {
1859
+ addPowerPlant(G);
1860
+ }
1861
+ }
1862
+
1863
+ // Special rule to move the market for two players
1864
+ while (G.players.length == 2 && G.actualMarket.length < 2 && G.step != 3) {
1865
+ addPowerPlant(G);
1866
+ }
1867
+
1868
+ if (G.step == 3) {
1869
+ G.actualMarket = G.actualMarket.filter((pp) => pp.type != PowerPlantType.Step3);
1870
+ while (G.actualMarket.length < 4 && G.powerPlantsDeck.length > 0) {
1871
+ addPowerPlant(G);
1872
+ }
1873
+ } else {
1874
+ // Target size is one less than number of players, or 2 for a 2-player game.
1875
+ const targetSize = Math.max(2, G.players.length - 1);
1876
+ while (G.actualMarket.length > targetSize) {
1877
+ G.actualMarket.shift();
1878
+ }
1879
+ }
1880
+ }
1881
+
1882
+ function removePowerPlant(G: GameState, powerPlant: PowerPlant) {
1883
+ G.actualMarket.splice(
1884
+ G.actualMarket.findIndex((pp) => pp.number == powerPlant.number),
1885
+ 1
1886
+ );
1887
+ }
1888
+
1889
+ export function getPowerPlant(num: number, mapName = ''): PowerPlant {
1890
+ if (mapName == 'India') {
1891
+ return indiaPowerPlants.find((p) => p.number == num)!;
1892
+ } else {
1893
+ return powerPlants.find((p) => p.number == num)!;
1894
+ }
1895
+ }
1896
+
1897
+ function getBaseState(G: GameState): GameState {
1898
+ const baseState = setup(G.players.length, G.options, G.seed);
1899
+ baseState.players.forEach((player, i) => {
1900
+ player.name = G.players[i].name;
1901
+ player.isAI = G.players[i].isAI;
1902
+ });
1903
+
1904
+ return baseState;
1905
+ }
1906
+
1907
+ function playerNameHTML(player) {
1908
+ return `<span style="background-color: ${playerColors[player.id]}; font-weight: bold; padding: 0 3px;">${
1909
+ player.name
1910
+ }</span>`;
1911
+ }
1912
+
1913
+ export function playersSortedByScore(G: GameState): Player[] {
1914
+ return cloneDeep(G.players)
1915
+ .sort((p1, p2) => {
1916
+ if (p1.citiesPowered == p2.citiesPowered) {
1917
+ if (p1.money == p2.money) {
1918
+ return p1.cities.length - p2.cities.length;
1919
+ }
1920
+
1921
+ return p1.money - p2.money;
1922
+ }
1923
+
1924
+ return p1.citiesPowered - p2.citiesPowered;
1925
+ })
1926
+ .reverse();
1927
+ }
1928
+
1929
+ function calculateCitiesPowered(G: GameState) {
1930
+ G.players.forEach((player) => {
1931
+ player.citiesPowered = calculateMaxCitiesPowered(player);
1932
+ });
1933
+ }
1934
+
1935
+ function calculateMaxCitiesPowered(player: Player) {
1936
+ const permutations: PowerPlant[][] = [];
1937
+ for (let i = 0; i < Math.pow(2, player.powerPlants.length); i++) {
1938
+ const perm: PowerPlant[] = [];
1939
+ player.powerPlants.forEach((pp, index) => {
1940
+ if (i & Math.pow(2, index)) {
1941
+ perm.push(pp);
1942
+ }
1943
+ });
1944
+ permutations.push(perm);
1945
+ }
1946
+
1947
+ let max = 0;
1948
+ permutations.forEach((permutation) => {
1949
+ if (isValid(player, permutation)) {
1950
+ const citiesPowered = permutation.map((p) => p.citiesPowered).reduce((a, b) => a + b, 0);
1951
+ max = Math.max(max, citiesPowered);
1952
+ }
1953
+ });
1954
+
1955
+ return Math.min(player.cities.length, max);
1956
+ }
1957
+
1958
+ function isValid(player: Player, powerPlants: PowerPlant[]) {
1959
+ const coalUsed = powerPlants
1960
+ .filter((pp) => pp.type == PowerPlantType.Coal)
1961
+ .map((pp) => pp.cost)
1962
+ .reduce((a, b) => a + b, 0);
1963
+ const oilUsed = powerPlants
1964
+ .filter((pp) => pp.type == PowerPlantType.Oil)
1965
+ .map((pp) => pp.cost)
1966
+ .reduce((a, b) => a + b, 0);
1967
+ const garbageUsed = powerPlants
1968
+ .filter((pp) => pp.type == PowerPlantType.Garbage)
1969
+ .map((pp) => pp.cost)
1970
+ .reduce((a, b) => a + b, 0);
1971
+ const uraniumUsed = powerPlants
1972
+ .filter((pp) => pp.type == PowerPlantType.Uranium)
1973
+ .map((pp) => pp.cost)
1974
+ .reduce((a, b) => a + b, 0);
1975
+ const hybridUsed = powerPlants
1976
+ .filter((pp) => pp.type == PowerPlantType.Hybrid)
1977
+ .map((pp) => pp.cost)
1978
+ .reduce((a, b) => a + b, 0);
1979
+
1980
+ if (
1981
+ coalUsed > player.coalLeft ||
1982
+ oilUsed > player.oilLeft ||
1983
+ garbageUsed > player.garbageLeft ||
1984
+ uraniumUsed > player.uraniumLeft
1985
+ ) {
1986
+ return false;
1987
+ }
1988
+
1989
+ if (hybridUsed > player.coalLeft - coalUsed + player.oilLeft - oilUsed) {
1990
+ return false;
1991
+ }
1992
+
1993
+ return true;
1994
+ }
1995
+
1996
+ function toResourcesPhase(G: GameState) {
1997
+ G.players.forEach((p) => {
1998
+ p.bid = 0;
1999
+ p.passed = p.isDropped;
2000
+ });
2001
+
2002
+ G.players.forEach((p) => {
2003
+ p.skipAuction = false;
2004
+ });
2005
+
2006
+ if (G.options.variant == 'recharged') {
2007
+ if (G.plantDiscountActive) {
2008
+ G.plantDiscountActive = false;
2009
+ if (G.actualMarket.length > 0) {
2010
+ G.log.push({ type: 'event', event: `Discarding Power Plant ${G.actualMarket[0].number}.` });
2011
+ G.actualMarket.shift();
2012
+ addPowerPlant(G);
2013
+ }
2014
+ } else if (G.map.name == 'Benelux') {
2015
+ G.discardSmallestPlant = true;
2016
+ }
2017
+ }
2018
+
2019
+ if (G.futureMarket.find((pp) => pp.number == 99)) {
2020
+ if (G.map.name == 'Middle East' && G.step == 1) {
2021
+ enterStepTwoMiddleEast(G);
2022
+ } else {
2023
+ const powerPlantDiscarded = G.actualMarket.shift();
2024
+ G.futureMarket.pop();
2025
+ G.log.push({
2026
+ type: 'event',
2027
+ event: `Starting Step 3, Power Plant ${powerPlantDiscarded?.number} discarded.`,
2028
+ });
2029
+ G.step = 3;
2030
+
2031
+ G.actualMarket = [...G.actualMarket, ...G.futureMarket];
2032
+ G.futureMarket = [];
2033
+ }
2034
+ }
2035
+
2036
+ G.phase = Phase.Resources;
2037
+ setCurrentPlayer(G, G.playerOrder[G.players.length - 1]);
2038
+ }
2039
+
2040
+ function endAuction(G: GameState, winningPlayer: Player, bid: number) {
2041
+ winningPlayer.powerPlants.push(G.chosenPowerPlant!);
2042
+ winningPlayer.money -= bid;
2043
+
2044
+ if (G.options.trackTotalSpent) {
2045
+ winningPlayer.totalSpentPlants += bid;
2046
+ }
2047
+
2048
+ winningPlayer.skipAuction = true;
2049
+ updatePlayerCapacity(winningPlayer);
2050
+
2051
+ G.log.push({
2052
+ type: 'event',
2053
+ event: `${winningPlayer.name} wins the bid and pays ${bid}.`,
2054
+ pretty: `${playerNameHTML(winningPlayer)} wins the bid and pays <span style="color: green">$${bid}</span>.`,
2055
+ });
2056
+
2057
+ removePowerPlant(G, G.chosenPowerPlant!);
2058
+ G.chosenPowerPlant = G.currentBid = undefined;
2059
+ }
2060
+
2061
+ function setPlayerOrder(G: GameState) {
2062
+ G.playerOrder = cloneDeep(G.players)
2063
+ .sort((a, b) => {
2064
+ const citiesA = a.cities.length;
2065
+ const citiesB = b.cities.length;
2066
+
2067
+ if (citiesA == citiesB) {
2068
+ return (
2069
+ Math.max(...a.powerPlants.map((pp) => pp.number)) -
2070
+ Math.max(...b.powerPlants.map((pp) => pp.number))
2071
+ );
2072
+ }
2073
+
2074
+ return citiesA - citiesB;
2075
+ })
2076
+ .map((p) => p.id)
2077
+ .reverse();
2078
+ }
2079
+
2080
+ function setCurrentPlayer(G: GameState, playerNum: number) {
2081
+ G.currentPlayers = [playerNum];
2082
+
2083
+ if (G.players[playerNum].isDropped && G.players.some((p) => !p.isDropped)) {
2084
+ G.players[playerNum].passed = true;
2085
+ nextPlayer(G);
2086
+ }
2087
+ }
2088
+
2089
+ function nextPlayer(G: GameState) {
2090
+ if (G.phase == Phase.Auction) {
2091
+ if (G.chosenPowerPlant == undefined) {
2092
+ nextPlayerAuction(G);
2093
+ } else {
2094
+ nextPlayerClockwise(G);
2095
+ }
2096
+ } else {
2097
+ nextPlayerReverse(G);
2098
+ }
2099
+ }
2100
+
2101
+ function nextPlayerClockwise(G: GameState) {
2102
+ const index = G.currentPlayers[0];
2103
+ G.currentPlayers = [(index + 1) % G.players.length];
2104
+
2105
+ if (G.players[G.currentPlayers[0]].isDropped && G.players.some((p) => !p.isDropped)) {
2106
+ G.players[G.currentPlayers[0]].passed = true;
2107
+ G.players[G.currentPlayers[0]].skipAuction = true;
2108
+ nextPlayerClockwise(G);
2109
+ }
2110
+
2111
+ if (
2112
+ (G.players[G.currentPlayers[0]].skipAuction || G.players[G.currentPlayers[0]].passed) &&
2113
+ G.players.some((p) => !p.skipAuction && !p.passed && !p.isDropped)
2114
+ ) {
2115
+ nextPlayerClockwise(G);
2116
+ }
2117
+ }
2118
+
2119
+ function nextPlayerReverse(G: GameState) {
2120
+ const index = G.playerOrder.indexOf(G.currentPlayers[0]);
2121
+ G.currentPlayers = [G.playerOrder[(index - 1 + G.players.length) % G.players.length]];
2122
+
2123
+ if (G.players[G.currentPlayers[0]].isDropped && G.players.some((p) => !p.isDropped)) {
2124
+ G.players[G.currentPlayers[0]].passed = true;
2125
+ nextPlayerReverse(G);
2126
+ }
2127
+ }
2128
+
2129
+ function nextPlayerAuction(G: GameState, reset = false) {
2130
+ let playerNum: number;
2131
+ if (reset) {
2132
+ playerNum = G.playerOrder[0];
2133
+ } else {
2134
+ const index = G.playerOrder.indexOf(G.currentPlayers[0]);
2135
+ playerNum = G.playerOrder[(index + 1) % G.players.length];
2136
+ }
2137
+
2138
+ G.currentPlayers = [playerNum];
2139
+ const player = G.players[playerNum];
2140
+
2141
+ if (player.isDropped) {
2142
+ player.passed = true;
2143
+ player.skipAuction = true;
2144
+ }
2145
+
2146
+ if ((player.skipAuction || player.passed) && G.players.some((p) => !p.skipAuction && !p.passed && !p.isDropped)) {
2147
+ nextPlayerAuction(G);
2148
+ }
2149
+ }
2150
+
2151
+ function updateGameState(G: GameState) {
2152
+ if (!G.coalResupply) {
2153
+ const map = maps.find((map) => map.name === G.map.name);
2154
+
2155
+ if (map?.resupply) {
2156
+ G.coalResupply = map.resupply[0];
2157
+ G.oilResupply = map.resupply[1];
2158
+ G.garbageResupply = map.resupply[2];
2159
+ G.uraniumResupply = map.resupply[3];
2160
+ } else {
2161
+ G.coalResupply = [
2162
+ [3, 4, 3],
2163
+ [4, 5, 3],
2164
+ [5, 6, 4],
2165
+ [5, 7, 5],
2166
+ [7, 9, 6],
2167
+ ];
2168
+ G.oilResupply = [
2169
+ [2, 2, 4],
2170
+ [2, 3, 4],
2171
+ [3, 4, 5],
2172
+ [4, 5, 6],
2173
+ [5, 6, 7],
2174
+ ];
2175
+ G.garbageResupply = [
2176
+ [1, 2, 3],
2177
+ [1, 2, 3],
2178
+ [2, 3, 4],
2179
+ [3, 3, 5],
2180
+ [3, 5, 6],
2181
+ ];
2182
+ G.uraniumResupply = [
2183
+ [1, 1, 1],
2184
+ [1, 1, 1],
2185
+ [1, 2, 2],
2186
+ [2, 3, 2],
2187
+ [2, 3, 3],
2188
+ ];
2189
+ }
2190
+
2191
+ const p = G.players.length - 2;
2192
+ G.resourceResupply = [
2193
+ `[${G.coalResupply[p][0]}, ${G.oilResupply[p][0]}, ${G.garbageResupply[p][0]}, ${G.uraniumResupply[p][0]}]`,
2194
+ `[${G.coalResupply[p][1]}, ${G.oilResupply[p][1]}, ${G.garbageResupply[p][1]}, ${G.uraniumResupply[p][1]}]`,
2195
+ `[${G.coalResupply[p][2]}, ${G.oilResupply[p][2]}, ${G.garbageResupply[p][2]}, ${G.uraniumResupply[p][2]}]`,
2196
+ ];
2197
+ }
2198
+ }
2199
+
2200
+ function fastAuction(G: GameState, player: Player, bid: number) {
2201
+ player.bid = bid;
2202
+ G.currentPlayers = G.currentPlayers.filter((id) => id !== player.id);
2203
+
2204
+ if (G.currentPlayers.length === 0) {
2205
+ G.log.push(...G.hiddenLog);
2206
+ G.hiddenLog = [];
2207
+
2208
+ const bids = G.players.map((p) => p.bid).filter((b) => b > 0);
2209
+ let cost = G.minimunBid;
2210
+ const highestBid = Math.max(...bids);
2211
+ const highestBidders = G.players.filter((p) => !p.isDropped && p.bid === highestBid);
2212
+ let winnerId = highestBidders[0].id;
2213
+
2214
+ if (bids.length > 1) {
2215
+ bids.splice(bids.indexOf(highestBid), 1);
2216
+ const secondHighestBid = Math.max(...bids);
2217
+
2218
+ // In case of a tie, use turn order
2219
+ if (highestBidders.length > 1) {
2220
+ let index = G.auctioningPlayer!;
2221
+
2222
+ while (!highestBidders.find((p) => p.id == index)) {
2223
+ index = (index + 1) % G.players.length;
2224
+ }
2225
+
2226
+ cost = secondHighestBid;
2227
+ winnerId = index;
2228
+ } else {
2229
+ let index = G.auctioningPlayer!;
2230
+
2231
+ cost = 0;
2232
+ while (cost == 0) {
2233
+ if (highestBidders[0].id == index) {
2234
+ cost = secondHighestBid;
2235
+ } else if (G.players[index].bid == secondHighestBid) {
2236
+ cost = secondHighestBid + 1;
2237
+ }
2238
+
2239
+ index = (index + 1) % G.players.length;
2240
+ }
2241
+ }
2242
+ }
2243
+
2244
+ const winningPlayer = G.players[winnerId];
2245
+
2246
+ endAuction(G, winningPlayer, cost);
2247
+
2248
+ if (
2249
+ (winningPlayer.powerPlants.length > 4 || (G.players.length > 2 && winningPlayer.powerPlants.length > 3)) &&
2250
+ !winningPlayer.isDropped
2251
+ ) {
2252
+ setCurrentPlayer(G, winningPlayer.id);
2253
+ } else {
2254
+ if (G.map.name != 'China' || G.step == 3) {
2255
+ addPowerPlant(G);
2256
+ }
2257
+
2258
+ if (G.players.some((p) => !p.skipAuction && !p.isDropped)) {
2259
+ G.players.forEach((p) => {
2260
+ p.bid = 0;
2261
+ p.passed = p.isDropped;
2262
+ });
2263
+
2264
+ nextPlayerAuction(G, true);
2265
+ } else {
2266
+ toResourcesPhase(G);
2267
+ }
2268
+ }
2269
+ }
2270
+ }