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/dist/src/available-moves.d.ts +2 -0
- package/dist/src/available-moves.js +110 -9
- package/dist/src/engine.js +290 -57
- package/dist/src/gamestate.d.ts +13 -1
- package/dist/src/maps/europe.d.ts +62 -0
- package/dist/src/maps/europe.js +356 -0
- package/dist/src/maps/korea.js +107 -0
- package/dist/src/maps/northamerica.d.ts +62 -0
- package/dist/src/maps/northamerica.js +330 -0
- package/dist/src/maps/northerneurope.js +75 -0
- package/dist/src/maps/southafrica.d.ts +26 -26
- package/dist/src/maps/southafrica.js +239 -116
- package/dist/src/maps/ukireland.js +163 -40
- package/dist/src/maps.d.ts +16 -0
- package/dist/src/maps.js +19 -8
- package/dist/src/move.d.ts +2 -0
- package/package.json +1 -1
- package/src/available-moves.ts +158 -10
- package/src/engine.spec.ts +17 -0
- package/src/engine.ts +305 -52
- package/src/gamestate.ts +33 -6
- package/src/maps/europe.ts +391 -0
- package/src/maps/korea.ts +109 -0
- package/src/maps/northamerica.ts +360 -0
- package/src/maps/northerneurope.ts +76 -0
- package/src/maps/southafrica.ts +247 -116
- package/src/maps/ukireland.ts +170 -40
- package/src/maps.ts +53 -13
- package/src/move.ts +7 -0
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
|
-
|
|
289
|
-
const
|
|
290
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
G.
|
|
782
|
-
G.
|
|
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(
|
|
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.
|
|
829
|
-
|
|
830
|
-
|
|
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 (
|
|
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
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
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
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
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
|
-
|
|
1187
|
-
|
|
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
|
-
|
|
1228
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
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
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
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;
|