powergrid-engine 1.10.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/engine.ts CHANGED
@@ -15,6 +15,7 @@ export const playerColors = ['limegreen', 'mediumorchid', 'red', 'dodgerblue', '
15
15
 
16
16
  const citiesToStep2 = [10, 7, 7, 7, 6];
17
17
  const citiesToStep2BadenWurttemberg = [9, 6, 6, 6, 5];
18
+ const citiesToStep2UKIreland = [7, 7, 7, 7, 6];
18
19
  const citiesToEndGame = [21, 17, 17, 15, 14];
19
20
  const cityIncome = [10, 22, 33, 44, 54, 64, 73, 82, 90, 98, 105, 112, 118, 124, 129, 134, 138, 142, 145, 148, 150, 150];
20
21
  const regionsInPlay = [3, 3, 4, 5, 5];
@@ -161,6 +162,15 @@ export function setup(
161
162
  let oilResupply: number[][];
162
163
  let garbageResupply: number[][];
163
164
  let uraniumResupply: number[][];
165
+ // Korea: parallel North-side resupply tables (no uranium row).
166
+ let coalResupplyNorth: number[][] | undefined;
167
+ let oilResupplyNorth: number[][] | undefined;
168
+ let garbageResupplyNorth: number[][] | undefined;
169
+ if (chosenMap.resupplyNorth) {
170
+ coalResupplyNorth = chosenMap.resupplyNorth[0];
171
+ oilResupplyNorth = chosenMap.resupplyNorth[1];
172
+ garbageResupplyNorth = chosenMap.resupplyNorth[2];
173
+ }
164
174
  if (chosenMap.resupply) {
165
175
  coalResupply = chosenMap.resupply[0];
166
176
  oilResupply = chosenMap.resupply[1];
@@ -251,7 +261,14 @@ export function setup(
251
261
  const region = regions[Math.floor(rng() * regions.length)];
252
262
  if (
253
263
  playRegions.size == 0 ||
254
- regionConnections[regions.indexOf(region)].some((con) => playRegions.has(con))
264
+ regionConnections[regions.indexOf(region)].some((con) => playRegions.has(con)) ||
265
+ // UK & Ireland: regions on the two islands have no edges between
266
+ // them (no sea connection). Skipping the connectivity check lets
267
+ // the random selection span both islands; the cross-island
268
+ // surcharge handles the disconnect at build time. Without this,
269
+ // requiring 5-of-6 regions for 5p would loop forever (GB has 4
270
+ // regions, IE has 2).
271
+ chosenMap.name === 'UK & Ireland'
255
272
  ) {
256
273
  playRegions.add(region);
257
274
 
@@ -273,6 +290,23 @@ export function setup(
273
290
  );
274
291
 
275
292
  finalMap = filteredMap;
293
+
294
+ if (chosenMap.regionalPowerPlants) {
295
+ for (const region of playRegions) {
296
+ const replacements = chosenMap.regionalPowerPlants[region];
297
+ if (replacements) {
298
+ for (const newPlant of replacements) {
299
+ const swapIn = (arr: PowerPlant[]) => {
300
+ const idx = arr.findIndex((p) => p.number === newPlant.number);
301
+ if (idx !== -1) arr[idx] = { ...newPlant };
302
+ };
303
+ swapIn(actualMarket);
304
+ swapIn(futureMarket);
305
+ swapIn(powerPlantsDeck);
306
+ }
307
+ }
308
+ }
309
+ }
276
310
  }
277
311
 
278
312
  const coalMarket = chosenMap.startingResources ? chosenMap.startingResources[0] : 24;
@@ -285,9 +319,17 @@ export function setup(
285
319
  const totalGarbage = chosenMap.startingSupply ? chosenMap.startingSupply[2] : 24;
286
320
  const totalUranium = chosenMap.startingSupply ? chosenMap.startingSupply[3] : 12;
287
321
 
288
- const coalSupply = totalCoal - coalMarket;
289
- const oilSupply = totalOil - oilMarket;
290
- const garbageSupply = totalGarbage - garbageMarket;
322
+ // Korea: parallel North-side market and prices. Supply is shared — see below.
323
+ const coalMarketNorth = chosenMap.startingResourcesNorth?.[0];
324
+ const oilMarketNorth = chosenMap.startingResourcesNorth?.[1];
325
+ const garbageMarketNorth = chosenMap.startingResourcesNorth?.[2];
326
+
327
+ // Supply pools are shared between both sides for Korea. `startingSupply`
328
+ // represents the TOTAL cubes in the game; the supply pool is whatever is
329
+ // left after both markets are filled.
330
+ const coalSupply = totalCoal - coalMarket - (coalMarketNorth ?? 0);
331
+ const oilSupply = totalOil - oilMarket - (oilMarketNorth ?? 0);
332
+ const garbageSupply = totalGarbage - garbageMarket - (garbageMarketNorth ?? 0);
291
333
  const uraniumSupply = totalUranium - uraniumMarket;
292
334
 
293
335
  const coalPrices = cloneDeep(chosenMap.coalPrices ?? prices.coal);
@@ -295,6 +337,12 @@ export function setup(
295
337
  const garbagePrices = cloneDeep(chosenMap.garbagePrices ?? prices.garbage);
296
338
  const uraniumPrices = cloneDeep(chosenMap.uraniumPrices ?? prices.uranium);
297
339
 
340
+ const coalPricesNorth = chosenMap.coalPricesNorth ? cloneDeep(chosenMap.coalPricesNorth) : undefined;
341
+ const oilPricesNorth = chosenMap.oilPricesNorth ? cloneDeep(chosenMap.oilPricesNorth) : undefined;
342
+ const garbagePricesNorth = chosenMap.garbagePricesNorth ? cloneDeep(chosenMap.garbagePricesNorth) : undefined;
343
+
344
+ const isSouthAfrica = (forceMap || finalMap).name == 'South Africa';
345
+
298
346
  const G: GameState = {
299
347
  map: forceMap || finalMap,
300
348
  players,
@@ -305,6 +353,9 @@ export function setup(
305
353
  oilSupply,
306
354
  garbageSupply,
307
355
  uraniumSupply,
356
+ // South Africa: separate coal pool below the market. Starts empty;
357
+ // used coal returns here; refill draws from here first.
358
+ coalStorage: isSouthAfrica ? 0 : undefined,
308
359
  coalResupply,
309
360
  oilResupply,
310
361
  garbageResupply,
@@ -317,6 +368,17 @@ export function setup(
317
368
  oilPrices,
318
369
  garbagePrices,
319
370
  uraniumPrices,
371
+ // Korea: parallel North-side fields. Undefined for non-Korea maps.
372
+ // Supply is shared with the primary `*Supply` fields above.
373
+ coalMarketNorth,
374
+ oilMarketNorth,
375
+ garbageMarketNorth,
376
+ coalResupplyNorth,
377
+ oilResupplyNorth,
378
+ garbageResupplyNorth,
379
+ coalPricesNorth,
380
+ oilPricesNorth,
381
+ garbagePricesNorth,
320
382
  actualMarket,
321
383
  futureMarket,
322
384
  chosenPowerPlant: undefined,
@@ -334,6 +396,8 @@ export function setup(
334
396
  citiesToStep2:
335
397
  (forceMap || finalMap).name == 'Baden-Württemberg'
336
398
  ? citiesToStep2BadenWurttemberg[numPlayers - 2]
399
+ : (forceMap || finalMap).name == 'UK & Ireland'
400
+ ? citiesToStep2UKIreland[numPlayers - 2]
337
401
  : citiesToStep2[numPlayers - 2],
338
402
  citiesToEndGame: citiesToEndGame[numPlayers - 2],
339
403
  resourceResupply: [
@@ -341,6 +405,14 @@ export function setup(
341
405
  `[${coalResupply[p][1]}, ${oilResupply[p][1]}, ${garbageResupply[p][1]}, ${uraniumResupply[p][1]}]`,
342
406
  `[${coalResupply[p][2]}, ${oilResupply[p][2]}, ${garbageResupply[p][2]}, ${uraniumResupply[p][2]}]`,
343
407
  ],
408
+ resourceResupplyNorth:
409
+ coalResupplyNorth && oilResupplyNorth && garbageResupplyNorth
410
+ ? [
411
+ `[${coalResupplyNorth[p][0]}, ${oilResupplyNorth[p][0]}, ${garbageResupplyNorth[p][0]}]`,
412
+ `[${coalResupplyNorth[p][1]}, ${oilResupplyNorth[p][1]}, ${garbageResupplyNorth[p][1]}]`,
413
+ `[${coalResupplyNorth[p][2]}, ${oilResupplyNorth[p][2]}, ${garbageResupplyNorth[p][2]}]`,
414
+ ]
415
+ : undefined,
344
416
  paymentTable: cityIncome,
345
417
  variant,
346
418
  minimunBid: 0,
@@ -640,6 +712,12 @@ export function move(G: GameState, move: Move, playerNumber: number, isUndo = fa
640
712
  player.passed = true;
641
713
  }
642
714
 
715
+ // Korea: end of this player's buying turn — clear the side lock
716
+ // so the next player can pick freely.
717
+ if (G.map.name == 'Korea') {
718
+ G.chosenSide = undefined;
719
+ }
720
+
643
721
  if (G.players.filter((p) => !p.passed && !p.isDropped).length == 0) {
644
722
  G.players.forEach((p) => {
645
723
  p.passed = p.isDropped;
@@ -692,7 +770,17 @@ export function move(G: GameState, move: Move, playerNumber: number, isUndo = fa
692
770
  }
693
771
  }
694
772
 
695
- addPowerPlant(G);
773
+ if (G.map.name == 'Europe') {
774
+ // Europe: do NOT draw a replacement from the deck.
775
+ // The future market shrinks from 5 to 4; reorganize
776
+ // the remaining 8 plants so actual stays at 4.
777
+ const market = [...G.actualMarket, ...G.futureMarket];
778
+ market.sort((a, b) => a.number - b.number);
779
+ G.actualMarket = market.slice(0, 4);
780
+ G.futureMarket = market.slice(4);
781
+ } else {
782
+ addPowerPlant(G);
783
+ }
696
784
  }
697
785
  }
698
786
 
@@ -774,12 +862,72 @@ export function move(G: GameState, move: Move, playerNumber: number, isUndo = fa
774
862
  }
775
863
 
776
864
  if (G.players.filter((p) => !p.passed && !p.isDropped).length == 0) {
777
- const coalResupplyValue = Math.min(
778
- G.coalSupply,
779
- G.coalResupply![G.players.length - 2][G.step - 1]
780
- );
781
- G.coalMarket += coalResupplyValue;
782
- G.coalSupply -= coalResupplyValue;
865
+ // Resupply is also capped by remaining market capacity (the
866
+ // prices array length minus current market size). Without
867
+ // this, smaller markets like Korea's can overflow past the
868
+ // number of slots and break price lookups.
869
+ const coalCapSouth = (G.coalPrices?.length ?? prices[ResourceType.Coal].length) - G.coalMarket;
870
+ const oilCapSouth = (G.oilPrices?.length ?? prices[ResourceType.Oil].length) - G.oilMarket;
871
+ const garbageCapSouth =
872
+ (G.garbagePrices?.length ?? prices[ResourceType.Garbage].length) - G.garbageMarket;
873
+ const uraniumCapSouth =
874
+ (G.uraniumPrices?.length ?? prices[ResourceType.Uranium].length) - G.uraniumMarket;
875
+
876
+ // Korea: North restocks FIRST from the shared supply pool, then
877
+ // South takes whatever remains. If supply runs short, South gets less.
878
+ let coalResupplyNorthValue = 0;
879
+ let oilResupplyNorthValue = 0;
880
+ let garbageResupplyNorthValue = 0;
881
+ if (G.coalResupplyNorth) {
882
+ const coalCapNorth = G.coalPricesNorth!.length - G.coalMarketNorth!;
883
+ const oilCapNorth = G.oilPricesNorth!.length - G.oilMarketNorth!;
884
+ const garbageCapNorth = G.garbagePricesNorth!.length - G.garbageMarketNorth!;
885
+
886
+ coalResupplyNorthValue = Math.min(
887
+ G.coalSupply,
888
+ G.coalResupplyNorth[G.players.length - 2][G.step - 1],
889
+ coalCapNorth
890
+ );
891
+ G.coalMarketNorth! += coalResupplyNorthValue;
892
+ G.coalSupply -= coalResupplyNorthValue;
893
+
894
+ oilResupplyNorthValue = Math.min(
895
+ G.oilSupply,
896
+ G.oilResupplyNorth![G.players.length - 2][G.step - 1],
897
+ oilCapNorth
898
+ );
899
+ G.oilMarketNorth! += oilResupplyNorthValue;
900
+ G.oilSupply -= oilResupplyNorthValue;
901
+
902
+ garbageResupplyNorthValue = Math.min(
903
+ G.garbageSupply,
904
+ G.garbageResupplyNorth![G.players.length - 2][G.step - 1],
905
+ garbageCapNorth
906
+ );
907
+ G.garbageMarketNorth! += garbageResupplyNorthValue;
908
+ G.garbageSupply -= garbageResupplyNorthValue;
909
+ }
910
+
911
+ // South Africa pulls from coalStorage first, then coalSupply.
912
+ // Other maps just pull from coalSupply.
913
+ let coalResupplyValue: number;
914
+ if (G.coalStorage !== undefined) {
915
+ const wantCoal = Math.min(G.coalResupply![G.players.length - 2][G.step - 1], coalCapSouth);
916
+ const fromStorage = Math.min(G.coalStorage, wantCoal);
917
+ const fromSupply = Math.min(G.coalSupply, wantCoal - fromStorage);
918
+ coalResupplyValue = fromStorage + fromSupply;
919
+ G.coalMarket += coalResupplyValue;
920
+ G.coalStorage -= fromStorage;
921
+ G.coalSupply -= fromSupply;
922
+ } else {
923
+ coalResupplyValue = Math.min(
924
+ G.coalSupply,
925
+ G.coalResupply![G.players.length - 2][G.step - 1],
926
+ coalCapSouth
927
+ );
928
+ G.coalMarket += coalResupplyValue;
929
+ G.coalSupply -= coalResupplyValue;
930
+ }
783
931
 
784
932
  let oilResupplyValue: number;
785
933
  if (G.map.name == 'Middle East') {
@@ -799,14 +947,19 @@ export function move(G: GameState, move: Move, playerNumber: number, isUndo = fa
799
947
  }
800
948
  }
801
949
  } else {
802
- oilResupplyValue = Math.min(G.oilSupply, G.oilResupply![G.players.length - 2][G.step - 1]);
950
+ oilResupplyValue = Math.min(
951
+ G.oilSupply,
952
+ G.oilResupply![G.players.length - 2][G.step - 1],
953
+ oilCapSouth
954
+ );
803
955
  G.oilMarket += oilResupplyValue;
804
956
  G.oilSupply -= oilResupplyValue;
805
957
  }
806
958
 
807
959
  const garbageResupplyValue = Math.min(
808
960
  G.garbageSupply,
809
- G.garbageResupply![G.players.length - 2][G.step - 1]
961
+ G.garbageResupply![G.players.length - 2][G.step - 1],
962
+ garbageCapSouth
810
963
  );
811
964
  G.garbageMarket += garbageResupplyValue;
812
965
  G.garbageSupply -= garbageResupplyValue;
@@ -819,16 +972,24 @@ export function move(G: GameState, move: Move, playerNumber: number, isUndo = fa
819
972
  ) {
820
973
  uraniumResupplyValue = Math.min(
821
974
  G.uraniumSupply,
822
- G.uraniumResupply![G.players.length - 2][G.step - 1]
975
+ G.uraniumResupply![G.players.length - 2][G.step - 1],
976
+ uraniumCapSouth
823
977
  );
824
978
  G.uraniumMarket += uraniumResupplyValue;
825
979
  G.uraniumSupply -= uraniumResupplyValue;
826
980
  }
827
981
 
828
- G.log.push({
829
- type: 'event',
830
- event: `Resupplying resources: [${coalResupplyValue}, ${oilResupplyValue}, ${garbageResupplyValue}, ${uraniumResupplyValue}].`,
831
- });
982
+ if (G.coalResupplyNorth) {
983
+ G.log.push({
984
+ type: 'event',
985
+ event: `Resupplying resources — North: [${coalResupplyNorthValue}, ${oilResupplyNorthValue}, ${garbageResupplyNorthValue}], South: [${coalResupplyValue}, ${oilResupplyValue}, ${garbageResupplyValue}, ${uraniumResupplyValue}].`,
986
+ });
987
+ } else {
988
+ G.log.push({
989
+ type: 'event',
990
+ event: `Resupplying resources: [${coalResupplyValue}, ${oilResupplyValue}, ${garbageResupplyValue}, ${uraniumResupplyValue}].`,
991
+ });
992
+ }
832
993
 
833
994
  if (G.map.name == 'Middle East' && G.step == 2 && G.futureMarket.length > 0) {
834
995
  // If we aren't about to enter step 3, discard top two plants instead of one.
@@ -1145,10 +1306,28 @@ export function move(G: GameState, move: Move, playerNumber: number, isUndo = fa
1145
1306
  asserts<Moves.MoveBuyResource>(move);
1146
1307
  G.chosenResource = move.data.resource;
1147
1308
 
1309
+ // Korea: lock the player to the side of their first buy this turn.
1310
+ // Subsequent buys must come from the same side until they pass.
1311
+ if (move.data.side) {
1312
+ G.chosenSide = move.data.side;
1313
+ }
1314
+
1315
+ const isNorth = move.data.side === 'north';
1316
+
1148
1317
  let price: number;
1149
1318
  switch (move.data.resource) {
1150
1319
  case ResourceType.Coal: {
1151
- if (G.coalMarket == 0) {
1320
+ if (isNorth) {
1321
+ const coalPrices = G.coalPricesNorth!;
1322
+ price = coalPrices[coalPrices.length - G.coalMarketNorth!];
1323
+ player.coalLeft++;
1324
+ G.coalMarketNorth!--;
1325
+ } else if (move.data.fromStorage) {
1326
+ // South Africa: $8 flat from the storage pool below the market.
1327
+ price = 8;
1328
+ player.coalLeft++;
1329
+ G.coalStorage!--;
1330
+ } else if (G.coalMarket == 0) {
1152
1331
  price = 8;
1153
1332
  player.coalLeft++;
1154
1333
  G.coalSupply--;
@@ -1164,31 +1343,46 @@ export function move(G: GameState, move: Move, playerNumber: number, isUndo = fa
1164
1343
  }
1165
1344
 
1166
1345
  case ResourceType.Oil: {
1167
- const oilPrices = G.oilPrices ?? prices[ResourceType.Oil];
1168
- price = oilPrices[oilPrices.length - G.oilMarket];
1169
- player.oilLeft++;
1170
- G.oilMarket--;
1346
+ if (isNorth) {
1347
+ const oilPrices = G.oilPricesNorth!;
1348
+ price = oilPrices[oilPrices.length - G.oilMarketNorth!];
1349
+ player.oilLeft++;
1350
+ G.oilMarketNorth!--;
1351
+ } else {
1352
+ const oilPrices = G.oilPrices ?? prices[ResourceType.Oil];
1353
+ price = oilPrices[oilPrices.length - G.oilMarket];
1354
+ player.oilLeft++;
1355
+ G.oilMarket--;
1356
+ }
1171
1357
  break;
1172
1358
  }
1173
1359
 
1174
1360
  case ResourceType.Garbage: {
1175
- const garbagePrices = G.garbagePrices ?? prices[ResourceType.Garbage];
1176
- price = garbagePrices[garbagePrices.length - G.garbageMarket];
1177
-
1178
- // $1 cheaper for players in Wien in Central Europe
1179
- if (G.map.name == 'Central Europe') {
1180
- const wienCity = player.cities.filter((c) => c.name == 'Wien');
1181
- if (wienCity?.length > 0) {
1182
- price--;
1361
+ if (isNorth) {
1362
+ const garbagePrices = G.garbagePricesNorth!;
1363
+ price = garbagePrices[garbagePrices.length - G.garbageMarketNorth!];
1364
+ player.garbageLeft++;
1365
+ G.garbageMarketNorth!--;
1366
+ } else {
1367
+ const garbagePrices = G.garbagePrices ?? prices[ResourceType.Garbage];
1368
+ price = garbagePrices[garbagePrices.length - G.garbageMarket];
1369
+
1370
+ // $1 cheaper for players in Wien in Central Europe
1371
+ if (G.map.name == 'Central Europe') {
1372
+ const wienCity = player.cities.filter((c) => c.name == 'Wien');
1373
+ if (wienCity?.length > 0) {
1374
+ price--;
1375
+ }
1183
1376
  }
1184
- }
1185
1377
 
1186
- player.garbageLeft++;
1187
- G.garbageMarket--;
1378
+ player.garbageLeft++;
1379
+ G.garbageMarket--;
1380
+ }
1188
1381
  break;
1189
1382
  }
1190
1383
 
1191
1384
  case ResourceType.Uranium: {
1385
+ // Uranium is only available from the South market (or non-Korea maps).
1192
1386
  const uraniumPrices = G.uraniumPrices ?? prices[ResourceType.Uranium];
1193
1387
  price = uraniumPrices[uraniumPrices.length - G.uraniumMarket];
1194
1388
  player.uraniumLeft++;
@@ -1224,8 +1418,14 @@ export function move(G: GameState, move: Move, playerNumber: number, isUndo = fa
1224
1418
  player.money -= move.data.price;
1225
1419
 
1226
1420
  if (G.options.trackTotalSpent) {
1227
- player.totalSpentCities += 10 + position * 5;
1228
- player.totalSpentConnections += move.data.price - (10 + position * 5);
1421
+ const cityData = G.map.cities.find((c) => c.name == move.data.name)!;
1422
+ if (cityData.singleOccupancy) {
1423
+ // SA cross-border: no house base — full price is "connection".
1424
+ player.totalSpentConnections += move.data.price;
1425
+ } else {
1426
+ player.totalSpentCities += 10 + position * 5;
1427
+ player.totalSpentConnections += move.data.price - (10 + position * 5);
1428
+ }
1229
1429
  }
1230
1430
 
1231
1431
  G.log.push({
@@ -1266,7 +1466,12 @@ export function move(G: GameState, move: Move, playerNumber: number, isUndo = fa
1266
1466
  switch (resourceType) {
1267
1467
  case ResourceType.Coal:
1268
1468
  player.coalLeft--;
1269
- G.coalSupply++;
1469
+ // SA: used coal returns to the separate storage pool below the market.
1470
+ if (G.coalStorage !== undefined) {
1471
+ G.coalStorage++;
1472
+ } else {
1473
+ G.coalSupply++;
1474
+ }
1270
1475
  break;
1271
1476
 
1272
1477
  case ResourceType.Oil:
@@ -1324,10 +1529,20 @@ export function move(G: GameState, move: Move, playerNumber: number, isUndo = fa
1324
1529
  }
1325
1530
 
1326
1531
  case MoveName.BuyResource: {
1532
+ const undoIsNorth = lastMove.data.side === 'north';
1327
1533
  let price: number;
1328
1534
  switch (lastMove.data.resource) {
1329
1535
  case ResourceType.Coal:
1330
- if (lastMove.fromSupply) {
1536
+ if (undoIsNorth) {
1537
+ player.coalLeft--;
1538
+ G.coalMarketNorth!++;
1539
+ const coalPrices = G.coalPricesNorth!;
1540
+ price = coalPrices[coalPrices.length - G.coalMarketNorth!];
1541
+ } else if (lastMove.data.fromStorage) {
1542
+ price = 8;
1543
+ player.coalLeft--;
1544
+ G.coalStorage!++;
1545
+ } else if (lastMove.fromSupply) {
1331
1546
  price = 8;
1332
1547
  player.coalLeft--;
1333
1548
  G.coalSupply++;
@@ -1341,24 +1556,38 @@ export function move(G: GameState, move: Move, playerNumber: number, isUndo = fa
1341
1556
  break;
1342
1557
 
1343
1558
  case ResourceType.Oil: {
1344
- player.oilLeft--;
1345
- G.oilMarket++;
1346
- const oilPrices = G.oilPrices ?? prices[ResourceType.Oil];
1347
- price = oilPrices[oilPrices.length - G.oilMarket];
1559
+ if (undoIsNorth) {
1560
+ player.oilLeft--;
1561
+ G.oilMarketNorth!++;
1562
+ const oilPrices = G.oilPricesNorth!;
1563
+ price = oilPrices[oilPrices.length - G.oilMarketNorth!];
1564
+ } else {
1565
+ player.oilLeft--;
1566
+ G.oilMarket++;
1567
+ const oilPrices = G.oilPrices ?? prices[ResourceType.Oil];
1568
+ price = oilPrices[oilPrices.length - G.oilMarket];
1569
+ }
1348
1570
  break;
1349
1571
  }
1350
1572
 
1351
1573
  case ResourceType.Garbage: {
1352
- player.garbageLeft--;
1353
- G.garbageMarket++;
1354
- const garbagePrices = G.garbagePrices ?? prices[ResourceType.Garbage];
1355
- price = garbagePrices[garbagePrices.length - G.garbageMarket];
1356
-
1357
- // $1 cheaper for players in Wien in Central Europe
1358
- if (G.map.name == 'Central Europe') {
1359
- const wienCity = player.cities.filter((c) => c.name == 'Wien');
1360
- if (wienCity?.length > 0) {
1361
- price--;
1574
+ if (undoIsNorth) {
1575
+ player.garbageLeft--;
1576
+ G.garbageMarketNorth!++;
1577
+ const garbagePrices = G.garbagePricesNorth!;
1578
+ price = garbagePrices[garbagePrices.length - G.garbageMarketNorth!];
1579
+ } else {
1580
+ player.garbageLeft--;
1581
+ G.garbageMarket++;
1582
+ const garbagePrices = G.garbagePrices ?? prices[ResourceType.Garbage];
1583
+ price = garbagePrices[garbagePrices.length - G.garbageMarket];
1584
+
1585
+ // $1 cheaper for players in Wien in Central Europe
1586
+ if (G.map.name == 'Central Europe') {
1587
+ const wienCity = player.cities.filter((c) => c.name == 'Wien');
1588
+ if (wienCity?.length > 0) {
1589
+ price--;
1590
+ }
1362
1591
  }
1363
1592
  }
1364
1593
 
@@ -1366,6 +1595,7 @@ export function move(G: GameState, move: Move, playerNumber: number, isUndo = fa
1366
1595
  }
1367
1596
 
1368
1597
  case ResourceType.Uranium: {
1598
+ // Uranium is only available from the South market (or non-Korea maps).
1369
1599
  player.uraniumLeft--;
1370
1600
  G.uraniumMarket++;
1371
1601
  const uraniumPrices = G.uraniumPrices ?? prices[ResourceType.Uranium];
@@ -1385,6 +1615,22 @@ export function move(G: GameState, move: Move, playerNumber: number, isUndo = fa
1385
1615
  }
1386
1616
 
1387
1617
  G.log.pop();
1618
+
1619
+ // Korea: keep chosenSide locked while the player still has
1620
+ // outstanding BuyResource moves this phase, but clear it once
1621
+ // the last one is undone so they can switch sides again.
1622
+ if (G.map.name == 'Korea' && G.chosenSide) {
1623
+ let stillCommitted = false;
1624
+ for (let i = G.log.length - 1; i >= 0; i--) {
1625
+ const entry = G.log[i];
1626
+ if (entry.type !== 'move') continue;
1627
+ stillCommitted = entry.player === playerNumber && entry.move.name === MoveName.BuyResource;
1628
+ break;
1629
+ }
1630
+ if (!stillCommitted) {
1631
+ G.chosenSide = undefined;
1632
+ }
1633
+ }
1388
1634
  break;
1389
1635
  }
1390
1636
 
@@ -2229,6 +2475,13 @@ function updateGameState(G: GameState) {
2229
2475
  `[${G.coalResupply[p][1]}, ${G.oilResupply[p][1]}, ${G.garbageResupply[p][1]}, ${G.uraniumResupply[p][1]}]`,
2230
2476
  `[${G.coalResupply[p][2]}, ${G.oilResupply[p][2]}, ${G.garbageResupply[p][2]}, ${G.uraniumResupply[p][2]}]`,
2231
2477
  ];
2478
+ if (G.coalResupplyNorth && G.oilResupplyNorth && G.garbageResupplyNorth) {
2479
+ G.resourceResupplyNorth = [
2480
+ `[${G.coalResupplyNorth[p][0]}, ${G.oilResupplyNorth[p][0]}, ${G.garbageResupplyNorth[p][0]}]`,
2481
+ `[${G.coalResupplyNorth[p][1]}, ${G.oilResupplyNorth[p][1]}, ${G.garbageResupplyNorth[p][1]}]`,
2482
+ `[${G.coalResupplyNorth[p][2]}, ${G.oilResupplyNorth[p][2]}, ${G.garbageResupplyNorth[p][2]}]`,
2483
+ ];
2484
+ }
2232
2485
  }
2233
2486
  }
2234
2487
 
package/src/gamestate.ts CHANGED
@@ -16,14 +16,16 @@ export type MapName =
16
16
  | 'China'
17
17
  | 'Benelux'
18
18
  | 'Russia'
19
- | 'Central Europe';
19
+ | 'Central Europe'
20
+ | 'Baden-Württemberg'
21
+ | 'Northern Europe'
22
+ | 'Korea'
23
+ | 'Europe'
24
+ | 'North America'
25
+ | 'South Africa'
26
+ | 'UK & Ireland';
20
27
  // | 'Australia'
21
- // | 'Baden-Württemberg'
22
28
  // | 'Japan'
23
- // | 'Korea'
24
- // | 'Northern Europe'
25
- // | 'South Africa'
26
- // | 'UK & Ireland'
27
29
  export type Variant = 'original' | 'recharged';
28
30
 
29
31
  export interface GameOptions {
@@ -121,6 +123,11 @@ export interface GameState {
121
123
  oilSupply: number;
122
124
  garbageSupply: number;
123
125
  uraniumSupply: number;
126
+ // South Africa: coal used to power plants returns here instead of coalSupply.
127
+ // Market refills draw from this pool first, then fall back to coalSupply.
128
+ // Players can always buy a coal cube from here for $8 (on top of the normal
129
+ // market). Undefined on non-SA maps.
130
+ coalStorage?: number;
124
131
  coalMarket: number;
125
132
  oilMarket: number;
126
133
  garbageMarket: number;
@@ -129,6 +136,22 @@ export interface GameState {
129
136
  oilResupply?: number[][];
130
137
  garbageResupply?: number[][];
131
138
  uraniumResupply?: number[][];
139
+ // Korea: parallel North-side markets and resupply tables. The primary
140
+ // `*Market`/`*Resupply` fields above act as the South side. The supply pools
141
+ // (`coalSupply` etc.) are SHARED — used cubes return there and both sides
142
+ // restock from the same pool, with North restocking first.
143
+ // North has no uranium track (nuclear plants are banned on the North side).
144
+ coalMarketNorth?: number;
145
+ oilMarketNorth?: number;
146
+ garbageMarketNorth?: number;
147
+ coalResupplyNorth?: number[][];
148
+ oilResupplyNorth?: number[][];
149
+ garbageResupplyNorth?: number[][];
150
+ // Korea: per-side price arrays. Each side's market has different slot counts
151
+ // per price space, so the price arrays differ. (No uranium on the North side.)
152
+ coalPricesNorth?: number[];
153
+ oilPricesNorth?: number[];
154
+ garbagePricesNorth?: number[];
132
155
  coalPrices?: number[];
133
156
  oilPrices?: number[];
134
157
  garbagePrices?: number[];
@@ -137,6 +160,9 @@ export interface GameState {
137
160
  futureMarket: PowerPlant[];
138
161
  chosenPowerPlant: PowerPlant | undefined;
139
162
  chosenResource?: ResourceType | undefined; // Used for India map, where only one resource can be bought at a time.
163
+ // Korea: locked to the side a player bought their first resource from this turn.
164
+ // All subsequent buys this turn must be from the same side. Cleared when the player passes.
165
+ chosenSide?: 'north' | 'south';
140
166
  currentBid: number | undefined;
141
167
  auctioningPlayer: number | undefined;
142
168
  step: number;
@@ -151,6 +177,7 @@ export interface GameState {
151
177
  citiesToEndGame: number;
152
178
  citiesBuiltInCurrentRound?: number; // In India, if the players build too many cities in a single round, a power outage will occur.
153
179
  resourceResupply: string[];
180
+ resourceResupplyNorth?: string[]; // Korea: per-step North resupply, parallel to resourceResupply (no uranium).
154
181
  paymentTable: number[];
155
182
  minimunBid: number;
156
183
  plantDiscountActive: boolean;