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.
@@ -6,6 +6,8 @@ export interface City {
6
6
  x: number;
7
7
  y: number;
8
8
  transregional?: boolean;
9
+ singleOccupancy?: boolean;
10
+ island?: string;
9
11
  }
10
12
  export interface Connection {
11
13
  nodes: string[];
@@ -28,6 +30,7 @@ export interface GameMap {
28
30
  powerPlantMarketPosition?: [number, number];
29
31
  actualMarketWidth?: number;
30
32
  mapPosition?: [number, number];
33
+ mapRotation?: number;
31
34
  buttonsPosition?: [number, number];
32
35
  playerBoardsPosition?: [number, number];
33
36
  supplyPosition?: [number, number];
@@ -35,6 +38,11 @@ export interface GameMap {
35
38
  resupply?: number[][][];
36
39
  startingResources?: number[];
37
40
  startingSupply?: number[];
41
+ resupplyNorth?: number[][][];
42
+ startingResourcesNorth?: number[];
43
+ coalPricesNorth?: number[];
44
+ oilPricesNorth?: number[];
45
+ garbagePricesNorth?: number[];
38
46
  maxPriceAvailable?: number[];
39
47
  coalPrices?: number[];
40
48
  oilPrices?: number[];
@@ -45,7 +53,15 @@ export interface GameMap {
45
53
  futureMarket: PowerPlant[];
46
54
  powerPlantsDeck: PowerPlant[];
47
55
  };
56
+ regionalPowerPlants?: Record<string, PowerPlant[]>;
57
+ crossIslandSurcharge?: number;
48
58
  mapSpecificRules?: string;
59
+ devBackdrop?: {
60
+ src: string;
61
+ width: number;
62
+ height: number;
63
+ opacity?: number;
64
+ };
49
65
  }
50
66
  export declare const maps: GameMap[];
51
67
  export declare const mapsRecharged: GameMap[];
package/dist/src/maps.js CHANGED
@@ -8,14 +8,21 @@ const benelux_1 = require("./maps/benelux");
8
8
  const brazil_1 = require("./maps/brazil");
9
9
  const centraleurope_1 = require("./maps/centraleurope");
10
10
  const china_1 = require("./maps/china");
11
+ const europe_1 = require("./maps/europe");
11
12
  const france_1 = require("./maps/france");
12
13
  const germany_1 = require("./maps/germany");
13
14
  const indian_1 = require("./maps/indian");
14
15
  const italy_1 = require("./maps/italy");
16
+ // import { map as japan } from './maps/japan';
17
+ const korea_1 = require("./maps/korea");
15
18
  const middleeast_1 = require("./maps/middleeast");
19
+ const northamerica_1 = require("./maps/northamerica");
20
+ const northerneurope_1 = require("./maps/northerneurope");
16
21
  const quebec_1 = require("./maps/quebec");
17
22
  const russia_1 = require("./maps/russia");
23
+ const southafrica_1 = require("./maps/southafrica");
18
24
  const spainportugal_1 = require("./maps/spainportugal");
25
+ const ukireland_1 = require("./maps/ukireland");
19
26
  exports.maps = [
20
27
  america_1.map,
21
28
  germany_1.map,
@@ -31,12 +38,14 @@ exports.maps = [
31
38
  russia_1.map,
32
39
  centraleurope_1.map,
33
40
  badenwurttemberg_1.map,
41
+ northerneurope_1.map,
42
+ korea_1.map,
43
+ europe_1.map,
44
+ northamerica_1.map,
45
+ southafrica_1.map,
46
+ ukireland_1.map,
34
47
  // australia,
35
48
  // japan,
36
- // korea,
37
- // northerneurope,
38
- // southafrica,
39
- // ukireland,
40
49
  ];
41
50
  exports.mapsRecharged = [
42
51
  america_1.mapRecharged,
@@ -53,11 +62,13 @@ exports.mapsRecharged = [
53
62
  russia_1.map,
54
63
  centraleurope_1.map,
55
64
  badenwurttemberg_1.map,
65
+ northerneurope_1.map,
66
+ korea_1.map,
67
+ europe_1.map,
68
+ northamerica_1.map,
69
+ southafrica_1.map,
70
+ ukireland_1.map,
56
71
  // australia,
57
72
  // china,
58
73
  // japan,
59
- // korea,
60
- // northerneurope,
61
- // southafrica,
62
- // ukireland,
63
74
  ];
@@ -22,6 +22,8 @@ export declare namespace Moves {
22
22
  name: MoveName.BuyResource;
23
23
  data: {
24
24
  resource: ResourceType;
25
+ side?: 'north' | 'south';
26
+ fromStorage?: boolean;
25
27
  };
26
28
  fromSupply?: boolean;
27
29
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "powergrid-engine",
3
- "version": "1.10.0",
3
+ "version": "1.12.0",
4
4
  "description": "An engine for Power Grid",
5
5
  "main": "dist/index.js",
6
6
  "types": "index.ts",
@@ -11,6 +11,12 @@ export interface AvailableMoves {
11
11
  [MoveName.BuyResource]?: {
12
12
  resource: ResourceType;
13
13
  price: number;
14
+ // Korea: which side's market this buy option draws from.
15
+ // Omitted on all other maps.
16
+ side?: 'north' | 'south';
17
+ // South Africa: $8 flat buy from the coal storage pool below the market.
18
+ // Distinct from the regular market buy (which can also be offered alongside).
19
+ fromStorage?: boolean;
14
20
  }[];
15
21
  [MoveName.Build]?: { name: string; price: number }[];
16
22
  [MoveName.UsePowerPlant]?: {
@@ -88,6 +94,18 @@ export function availableMoves(G: GameState, player: Player): AvailableMoves {
88
94
  }
89
95
  }
90
96
 
97
+ // No nuclear plants for Republic of Ireland (Green region) on UK&I.
98
+ // The restriction lifts as soon as the player has any non-Green city
99
+ // (Scotland/Wales/England/Northern Ireland = Brown/Yellow/Red/Pink/Orange).
100
+ if (G.map.name == 'UK & Ireland') {
101
+ const playerCities = player.cities.map(
102
+ (c) => G.map.cities.find((c_) => c_.name == c.name)!
103
+ );
104
+ if (playerCities.every((c) => c.region == 'green')) {
105
+ canBid = canBid.filter((p) => p.type != PowerPlantType.Uranium);
106
+ }
107
+ }
108
+
91
109
  if (canBid.length > 0) {
92
110
  moves[MoveName.ChoosePowerPlant] = canBid.map((p) => p.number);
93
111
  }
@@ -135,6 +153,19 @@ export function availableMoves(G: GameState, player: Player): AvailableMoves {
135
153
  }
136
154
  }
137
155
 
156
+ // No nuclear plants for Republic of Ireland (Green region) on UK&I.
157
+ if (G.map.name == 'UK & Ireland') {
158
+ const playerCities = player.cities.map(
159
+ (c) => G.map.cities.find((c_) => c_.name == c.name)!
160
+ );
161
+ if (
162
+ playerCities.every((c) => c.region == 'green') &&
163
+ G.chosenPowerPlant.type == PowerPlantType.Uranium
164
+ ) {
165
+ moves[MoveName.Bid] = undefined;
166
+ }
167
+ }
168
+
138
169
  if (G.options.fastBid) {
139
170
  if (player.id != G.auctioningPlayer) {
140
171
  moves[MoveName.Pass] = [true];
@@ -157,7 +188,7 @@ export function availableMoves(G: GameState, player: Player): AvailableMoves {
157
188
  break;
158
189
  }
159
190
 
160
- const toBuy: { resource: ResourceType }[] = [];
191
+ const toBuy: { resource: ResourceType; side?: 'north' | 'south'; fromStorage?: boolean }[] = [];
161
192
  let maxPriceAvailable: number;
162
193
  if (G.map.maxPriceAvailable) {
163
194
  maxPriceAvailable = G.map.maxPriceAvailable[G.step - 1];
@@ -165,7 +196,13 @@ export function availableMoves(G: GameState, player: Player): AvailableMoves {
165
196
  maxPriceAvailable = 16;
166
197
  }
167
198
 
168
- if (G.coalMarket > 0) {
199
+ // Korea: each side is offered separately, tagged with the side. The
200
+ // player's chosenSide locks them to one side once they make a buy.
201
+ const isKorea = G.coalResupplyNorth !== undefined;
202
+ const allowSouth = !isKorea || G.chosenSide !== 'north';
203
+ const allowNorth = isKorea && G.chosenSide !== 'south';
204
+
205
+ if (allowSouth && G.coalMarket > 0) {
169
206
  const hybridCapacityUsed =
170
207
  player.hybridCapacity > 0 ? Math.max(0, player.oilLeft - player.oilCapacity) : 0;
171
208
  const coalPrices = G.coalPrices ?? prices[ResourceType.Coal];
@@ -176,9 +213,11 @@ export function availableMoves(G: GameState, player: Player): AvailableMoves {
176
213
  player.coalCapacity + player.hybridCapacity > hybridCapacityUsed + player.coalLeft &&
177
214
  price <= maxPriceAvailable
178
215
  ) {
179
- toBuy.push({ resource: ResourceType.Coal });
216
+ toBuy.push(
217
+ isKorea ? { resource: ResourceType.Coal, side: 'south' } : { resource: ResourceType.Coal }
218
+ );
180
219
  }
181
- } else {
220
+ } else if (allowSouth) {
182
221
  if (G.options.variant == 'recharged' && G.map.name == 'USA' && G.coalSupply > 0) {
183
222
  const hybridCapacityUsed =
184
223
  player.hybridCapacity > 0 ? Math.max(0, player.oilLeft - player.oilCapacity) : 0;
@@ -191,7 +230,37 @@ export function availableMoves(G: GameState, player: Player): AvailableMoves {
191
230
  }
192
231
  }
193
232
 
194
- if (G.oilMarket > 0) {
233
+ // South Africa: $8 flat coal from the storage pool below the market.
234
+ // Always available alongside the regular market option (not gated on
235
+ // market being empty), as long as there are cubes in storage.
236
+ if (allowSouth && G.coalStorage !== undefined && G.coalStorage > 0) {
237
+ const hybridCapacityUsed =
238
+ player.hybridCapacity > 0 ? Math.max(0, player.oilLeft - player.oilCapacity) : 0;
239
+ if (
240
+ player.money >= 8 &&
241
+ player.coalCapacity + player.hybridCapacity > hybridCapacityUsed + player.coalLeft &&
242
+ 8 <= maxPriceAvailable
243
+ ) {
244
+ toBuy.push({ resource: ResourceType.Coal, fromStorage: true });
245
+ }
246
+ }
247
+
248
+ if (allowNorth && G.coalMarketNorth! > 0) {
249
+ const hybridCapacityUsed =
250
+ player.hybridCapacity > 0 ? Math.max(0, player.oilLeft - player.oilCapacity) : 0;
251
+ const coalPrices = G.coalPricesNorth!;
252
+ const price = coalPrices[coalPrices.length - G.coalMarketNorth!];
253
+
254
+ if (
255
+ player.money >= price &&
256
+ player.coalCapacity + player.hybridCapacity > hybridCapacityUsed + player.coalLeft &&
257
+ price <= maxPriceAvailable
258
+ ) {
259
+ toBuy.push({ resource: ResourceType.Coal, side: 'north' });
260
+ }
261
+ }
262
+
263
+ if (allowSouth && G.oilMarket > 0) {
195
264
  const hybridCapacityUsed =
196
265
  player.hybridCapacity > 0 ? Math.max(0, player.coalLeft - player.coalCapacity) : 0;
197
266
  const oilPrices = G.oilPrices ?? prices[ResourceType.Oil];
@@ -202,11 +271,28 @@ export function availableMoves(G: GameState, player: Player): AvailableMoves {
202
271
  player.oilCapacity + player.hybridCapacity > hybridCapacityUsed + player.oilLeft &&
203
272
  price <= maxPriceAvailable
204
273
  ) {
205
- toBuy.push({ resource: ResourceType.Oil });
274
+ toBuy.push(
275
+ isKorea ? { resource: ResourceType.Oil, side: 'south' } : { resource: ResourceType.Oil }
276
+ );
206
277
  }
207
278
  }
208
279
 
209
- if (G.garbageMarket > 0) {
280
+ if (allowNorth && G.oilMarketNorth! > 0) {
281
+ const hybridCapacityUsed =
282
+ player.hybridCapacity > 0 ? Math.max(0, player.coalLeft - player.coalCapacity) : 0;
283
+ const oilPrices = G.oilPricesNorth!;
284
+ const price = oilPrices[oilPrices.length - G.oilMarketNorth!];
285
+
286
+ if (
287
+ player.money >= price &&
288
+ player.oilCapacity + player.hybridCapacity > hybridCapacityUsed + player.oilLeft &&
289
+ price <= maxPriceAvailable
290
+ ) {
291
+ toBuy.push({ resource: ResourceType.Oil, side: 'north' });
292
+ }
293
+ }
294
+
295
+ if (allowSouth && G.garbageMarket > 0) {
210
296
  const garbagePrices = G.garbagePrices ?? prices[ResourceType.Garbage];
211
297
  let price = garbagePrices[garbagePrices.length - G.garbageMarket];
212
298
 
@@ -223,11 +309,27 @@ export function availableMoves(G: GameState, player: Player): AvailableMoves {
223
309
  player.garbageCapacity > player.garbageLeft &&
224
310
  price <= maxPriceAvailable
225
311
  ) {
226
- toBuy.push({ resource: ResourceType.Garbage });
312
+ toBuy.push(
313
+ isKorea ? { resource: ResourceType.Garbage, side: 'south' } : { resource: ResourceType.Garbage }
314
+ );
315
+ }
316
+ }
317
+
318
+ if (allowNorth && G.garbageMarketNorth! > 0) {
319
+ const garbagePrices = G.garbagePricesNorth!;
320
+ const price = garbagePrices[garbagePrices.length - G.garbageMarketNorth!];
321
+
322
+ if (
323
+ player.money >= price &&
324
+ player.garbageCapacity > player.garbageLeft &&
325
+ price <= maxPriceAvailable
326
+ ) {
327
+ toBuy.push({ resource: ResourceType.Garbage, side: 'north' });
227
328
  }
228
329
  }
229
330
 
230
- if (G.uraniumMarket > 0) {
331
+ // Uranium is South only (or non-Korea maps).
332
+ if (allowSouth && G.uraniumMarket > 0) {
231
333
  const uraniumPrices = G.uraniumPrices ?? prices[ResourceType.Uranium];
232
334
  const price = uraniumPrices[uraniumPrices.length - G.uraniumMarket];
233
335
  if (
@@ -235,7 +337,9 @@ export function availableMoves(G: GameState, player: Player): AvailableMoves {
235
337
  player.uraniumCapacity > player.uraniumLeft &&
236
338
  price <= maxPriceAvailable
237
339
  ) {
238
- toBuy.push({ resource: ResourceType.Uranium });
340
+ toBuy.push(
341
+ isKorea ? { resource: ResourceType.Uranium, side: 'south' } : { resource: ResourceType.Uranium }
342
+ );
239
343
  }
240
344
  }
241
345
 
@@ -280,6 +384,50 @@ export function availableMoves(G: GameState, player: Player): AvailableMoves {
280
384
  return;
281
385
  }
282
386
 
387
+ // UK & Ireland: starting a network on an island where the player has
388
+ // no city yet pays the first-house base + crossIslandSurcharge. There
389
+ // is no sea edge so dijkstra reports the target city as unreachable
390
+ // (price=9999); we override here. The first build ever (player.cities
391
+ // empty) goes through the normal first-build path and pays no surcharge.
392
+ if (cityData.island && G.map.crossIslandSurcharge !== undefined && player.cities.length > 0) {
393
+ const playerIslands = new Set(
394
+ player.cities
395
+ .map((c) => G.map.cities.find((mc) => mc.name == c.name)?.island)
396
+ .filter((i): i is string => !!i)
397
+ );
398
+ if (!playerIslands.has(cityData.island)) {
399
+ city.price = 10 + othersCount * 5 + G.map.crossIslandSurcharge;
400
+
401
+ if (othersCount == G.step) {
402
+ city.price = 9999;
403
+ }
404
+
405
+ if (player.cities.find((c) => c.name == city.name)) {
406
+ city.price = 9999;
407
+ }
408
+ return;
409
+ }
410
+ }
411
+
412
+ // South Africa's cross-border foreign-country spaces: cap at 1 occupant
413
+ // ever, and the dijkstra path cost (30 via the cross-border edge) is the
414
+ // complete cost — no 10+position*5 house base is added. Players cannot
415
+ // start in one of these (you have to build INTO South Africa first).
416
+ if (cityData.singleOccupancy) {
417
+ if (player.cities.length == 0) {
418
+ city.price = 9999;
419
+ return;
420
+ }
421
+ if (othersCount >= 1) {
422
+ city.price = 9999;
423
+ return;
424
+ }
425
+ if (player.cities.find((c) => c.name == city.name)) {
426
+ city.price = 9999;
427
+ }
428
+ return;
429
+ }
430
+
283
431
  city.price += 10 + othersCount * 5;
284
432
 
285
433
  if (othersCount == G.step) {
@@ -141,6 +141,23 @@ describe('Engine', () => {
141
141
  expect(ended(G)).to.be.false;
142
142
  });
143
143
 
144
+ it('should place UK & Ireland Step 3 card third from last with two plants below it', () => {
145
+ // UK & Ireland rules: the Step 3 card (plant 99) goes at deck.length - 3
146
+ // so two plants sit below it, and Step 3 fires two auctions earlier than
147
+ // a standard "Step 3 at the bottom" deck.
148
+ const G = setup(5, { map: 'UK & Ireland', variant: 'recharged', randomizeMap: false }, 'ukireland-test-seed');
149
+
150
+ const step3Idx = G.powerPlantsDeck.findIndex((p) => p.number === 99);
151
+ expect(step3Idx).to.equal(G.powerPlantsDeck.length - 3);
152
+
153
+ // The two plants below Step 3 are real plants (not another Step 3 card or
154
+ // undefined slots).
155
+ expect(G.powerPlantsDeck[step3Idx + 1]).to.exist;
156
+ expect(G.powerPlantsDeck[step3Idx + 2]).to.exist;
157
+ expect(G.powerPlantsDeck[step3Idx + 1].number).to.not.equal(99);
158
+ expect(G.powerPlantsDeck[step3Idx + 2].number).to.not.equal(99);
159
+ });
160
+
144
161
  it('should allow invalid move when isUndo is true', () => {
145
162
  const game = undo;
146
163
  const options: GameOptions = {