powergrid-engine 1.11.0 → 1.12.1

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.
@@ -0,0 +1,391 @@
1
+ // by John and Cici
2
+ import { cloneDeep } from 'lodash';
3
+ import seedrandom from 'seedrandom';
4
+ import { PowerPlantType } from '../gamestate';
5
+ import { shuffle } from '../utils';
6
+ import { GameMap } from './../maps';
7
+ import { powerPlants } from './../powerPlants';
8
+
9
+ export enum Regions {
10
+ Pink = 'pink', // UK / Ireland / Low Countries
11
+ Red = 'red', // France / Iberia
12
+ Brown = 'brown', // Central North (Germany / Czech / Poland-south)
13
+ Yellow = 'yellow', // Alpine / Italy / Hungary
14
+ Orange = 'orange', // Scandinavia / Baltic
15
+ Blue = 'blue', // Eastern Europe (incl. Bucureşti)
16
+ Green = 'green', // Balkans / Turkey
17
+ }
18
+
19
+ export enum Cities {
20
+ // Pink — UK / Ireland / Low Countries
21
+ Glasgow = 'Glasgow',
22
+ Dublin = 'Dublin',
23
+ Birmingham = 'Birmingham',
24
+ London = 'London',
25
+ Randstad = 'Randstad',
26
+ Vlaanderen = 'Vlaanderen',
27
+ RheinRuhr = 'Rhein-Ruhr',
28
+
29
+ // Red — France / Iberia
30
+ Paris = 'Paris',
31
+ Bordeaux = 'Bordeaux',
32
+ Lyon = 'Lyon',
33
+ Marseille = 'Marseille',
34
+ Lisboa = 'Lisboa',
35
+ Madrid = 'Madrid',
36
+ Barcelona = 'Barcelona',
37
+
38
+ // Brown — Central North
39
+ Bremen = 'Bremen',
40
+ Berlin = 'Berlin',
41
+ RheinMain = 'Rhein-Main',
42
+ Stuttgart = 'Stuttgart',
43
+ Praha = 'Praha',
44
+ Katowice = 'Katowice',
45
+ München = 'München',
46
+
47
+ // Yellow — Alpine / Italy / Hungary
48
+ Zürich = 'Zürich',
49
+ Milano = 'Milano',
50
+ Roma = 'Roma',
51
+ Napoli = 'Napoli',
52
+ Wien = 'Wien',
53
+ Budapest = 'Budapest',
54
+ Zagreb = 'Zagreb',
55
+
56
+ // Orange — Scandinavia / Baltic
57
+ Oslo = 'Oslo',
58
+ Stockholm = 'Stockholm',
59
+ Helsinki = 'Helsinki',
60
+ Tallinn = 'Tallinn',
61
+ Kobenhavn = 'København',
62
+ Riga = 'Riga',
63
+ StPetersburg = 'St. Petersburg',
64
+
65
+ // Blue — Eastern Europe
66
+ Minsk = 'Minsk',
67
+ Moskwa = 'Moskwa',
68
+ Warszawa = 'Warszawa',
69
+ Kyjiv = 'Kyjiv',
70
+ Kharkiv = 'Kharkiv',
71
+ Odessa = 'Odessa',
72
+ Bucuresti = 'Bucureşti',
73
+
74
+ // Green — Balkans / Turkey
75
+ Beograd = 'Beograd',
76
+ Sofia = 'Sofia',
77
+ Tirana = 'Tirana',
78
+ Athina = 'Athina',
79
+ Istanbul = 'Istanbul',
80
+ Izmir = 'Izmir',
81
+ Ankara = 'Ankara',
82
+ }
83
+
84
+ export const map: GameMap = {
85
+ name: 'Europe',
86
+ cities: [
87
+ // Pink — UK / Ireland / Low Countries
88
+ { name: Cities.Glasgow, region: Regions.Pink, x: 490, y: 510 },
89
+ { name: Cities.Dublin, region: Regions.Pink, x: 390, y: 660 },
90
+ { name: Cities.Birmingham, region: Regions.Pink, x: 485, y: 695 },
91
+ { name: Cities.London, region: Regions.Pink, x: 545, y: 800 },
92
+ { name: Cities.Randstad, region: Regions.Pink, x: 770, y: 770 },
93
+ { name: Cities.Vlaanderen, region: Regions.Pink, x: 700, y: 830 },
94
+ { name: Cities.RheinRuhr, region: Regions.Pink, x: 845, y: 860 },
95
+
96
+ // Red — France / Iberia
97
+ { name: Cities.Paris, region: Regions.Red, x: 730, y: 920 },
98
+ { name: Cities.Bordeaux, region: Regions.Red, x: 655, y: 1095 },
99
+ { name: Cities.Lyon, region: Regions.Red, x: 785, y: 1100 },
100
+ { name: Cities.Marseille, region: Regions.Red, x: 815, y: 1240 },
101
+ { name: Cities.Barcelona, region: Regions.Red, x: 650, y: 1280 },
102
+ { name: Cities.Madrid, region: Regions.Red, x: 435, y: 1335 },
103
+ { name: Cities.Lisboa, region: Regions.Red, x: 255, y: 1335 },
104
+
105
+ // Brown — Central North (Germany / Czech / Poland-south)
106
+ { name: Cities.Bremen, region: Regions.Brown, x: 1010, y: 770 },
107
+ { name: Cities.Berlin, region: Regions.Brown, x: 1180, y: 800 },
108
+ { name: Cities.RheinMain, region: Regions.Brown, x: 970, y: 920 },
109
+ { name: Cities.Stuttgart, region: Regions.Brown, x: 995, y: 1000 },
110
+ { name: Cities.München, region: Regions.Brown, x: 1100, y: 1050 },
111
+ { name: Cities.Praha, region: Regions.Brown, x: 1235, y: 940 },
112
+ { name: Cities.Katowice, region: Regions.Brown, x: 1380, y: 940 },
113
+
114
+ // Yellow — Alpine / Italy / Hungary
115
+ { name: Cities.Zürich, region: Regions.Yellow, x: 1010, y: 1100 },
116
+ { name: Cities.Milano, region: Regions.Yellow, x: 1075, y: 1180 },
117
+ { name: Cities.Roma, region: Regions.Yellow, x: 1180, y: 1310 },
118
+ { name: Cities.Napoli, region: Regions.Yellow, x: 1235, y: 1430 },
119
+ { name: Cities.Wien, region: Regions.Yellow, x: 1265, y: 1080 },
120
+ { name: Cities.Budapest, region: Regions.Yellow, x: 1390, y: 1080 },
121
+ { name: Cities.Zagreb, region: Regions.Yellow, x: 1300, y: 1180 },
122
+
123
+ // Orange — Scandinavia / Baltic
124
+ { name: Cities.Oslo, region: Regions.Orange, x: 1015, y: 365 },
125
+ { name: Cities.Stockholm, region: Regions.Orange, x: 1170, y: 415 },
126
+ { name: Cities.Helsinki, region: Regions.Orange, x: 1395, y: 360 },
127
+ { name: Cities.Tallinn, region: Regions.Orange, x: 1410, y: 470 },
128
+ { name: Cities.Riga, region: Regions.Orange, x: 1395, y: 575 },
129
+ { name: Cities.Kobenhavn, region: Regions.Orange, x: 1180, y: 660 },
130
+ { name: Cities.StPetersburg, region: Regions.Orange, x: 1620, y: 460 },
131
+
132
+ // Blue — Eastern Europe
133
+ { name: Cities.Warszawa, region: Regions.Blue, x: 1500, y: 815 },
134
+ { name: Cities.Minsk, region: Regions.Blue, x: 1610, y: 815 },
135
+ { name: Cities.Moskwa, region: Regions.Blue, x: 1830, y: 600 },
136
+ { name: Cities.Kyjiv, region: Regions.Blue, x: 1690, y: 980 },
137
+ { name: Cities.Kharkiv, region: Regions.Blue, x: 1860, y: 980 },
138
+ { name: Cities.Odessa, region: Regions.Blue, x: 1720, y: 1100 },
139
+ { name: Cities.Bucuresti, region: Regions.Blue, x: 1610, y: 1175 },
140
+
141
+ // Green — Balkans / Turkey
142
+ { name: Cities.Beograd, region: Regions.Green, x: 1390, y: 1175 },
143
+ { name: Cities.Sofia, region: Regions.Green, x: 1505, y: 1240 },
144
+ { name: Cities.Tirana, region: Regions.Green, x: 1370, y: 1330 },
145
+ { name: Cities.Athina, region: Regions.Green, x: 1480, y: 1450 },
146
+ { name: Cities.Istanbul, region: Regions.Green, x: 1665, y: 1335 },
147
+ { name: Cities.Izmir, region: Regions.Green, x: 1730, y: 1430 },
148
+ { name: Cities.Ankara, region: Regions.Green, x: 1850, y: 1380 },
149
+ ],
150
+ connections: [
151
+ // Pink — UK / Ireland / Low Countries
152
+ { nodes: [Cities.Dublin, Cities.Glasgow], cost: 17 },
153
+ { nodes: [Cities.Glasgow, Cities.Birmingham], cost: 13 },
154
+ { nodes: [Cities.Dublin, Cities.Birmingham], cost: 15 },
155
+ { nodes: [Cities.Birmingham, Cities.London], cost: 4 },
156
+ { nodes: [Cities.London, Cities.Vlaanderen], cost: 15 },
157
+ { nodes: [Cities.London, Cities.Randstad], cost: 18 },
158
+ { nodes: [Cities.Randstad, Cities.Vlaanderen], cost: 4 },
159
+ { nodes: [Cities.Vlaanderen, Cities.RheinRuhr], cost: 4 },
160
+ { nodes: [Cities.Randstad, Cities.RheinRuhr], cost: 4 },
161
+
162
+ // Red — France / Iberia (internal)
163
+ { nodes: [Cities.Paris, Cities.Bordeaux], cost: 12 },
164
+ { nodes: [Cities.Paris, Cities.Lyon], cost: 11 },
165
+ { nodes: [Cities.Bordeaux, Cities.Lyon], cost: 12 },
166
+ { nodes: [Cities.Lyon, Cities.Marseille], cost: 8 },
167
+ { nodes: [Cities.Bordeaux, Cities.Marseille], cost: 12 },
168
+ { nodes: [Cities.Marseille, Cities.Barcelona], cost: 11 },
169
+ { nodes: [Cities.Bordeaux, Cities.Barcelona], cost: 15 },
170
+ { nodes: [Cities.Bordeaux, Cities.Madrid], cost: 15 },
171
+ { nodes: [Cities.Madrid, Cities.Barcelona], cost: 14 },
172
+ { nodes: [Cities.Madrid, Cities.Lisboa], cost: 13 },
173
+
174
+ // Red ↔ Pink
175
+ { nodes: [Cities.Paris, Cities.London], cost: 16 },
176
+ { nodes: [Cities.Paris, Cities.Vlaanderen], cost: 7 },
177
+ { nodes: [Cities.Paris, Cities.RheinRuhr], cost: 10 },
178
+
179
+ // Brown — Central North (internal)
180
+ { nodes: [Cities.Bremen, Cities.Berlin], cost: 6 },
181
+ { nodes: [Cities.Bremen, Cities.RheinMain], cost: 9 },
182
+ { nodes: [Cities.Berlin, Cities.RheinMain], cost: 10 },
183
+ { nodes: [Cities.Berlin, Cities.Praha], cost: 7 },
184
+ { nodes: [Cities.RheinMain, Cities.Praha], cost: 10 },
185
+ { nodes: [Cities.RheinMain, Cities.München], cost: 6 },
186
+ { nodes: [Cities.RheinMain, Cities.Stuttgart], cost: 3 },
187
+ { nodes: [Cities.Stuttgart, Cities.München], cost: 5 },
188
+ { nodes: [Cities.München, Cities.Praha], cost: 6 },
189
+ { nodes: [Cities.Praha, Cities.Katowice], cost: 8 },
190
+
191
+ // Brown ↔ Pink
192
+ { nodes: [Cities.Bremen, Cities.Randstad], cost: 8 },
193
+ { nodes: [Cities.Bremen, Cities.Vlaanderen], cost: 10 },
194
+ { nodes: [Cities.RheinMain, Cities.Vlaanderen], cost: 6 },
195
+ { nodes: [Cities.RheinMain, Cities.RheinRuhr], cost: 3 },
196
+
197
+ // Brown ↔ Red
198
+ { nodes: [Cities.Stuttgart, Cities.Paris], cost: 14 },
199
+
200
+ // Yellow — Alpine / Italy / Hungary (internal)
201
+ { nodes: [Cities.Zürich, Cities.Milano], cost: 11 },
202
+ { nodes: [Cities.Milano, Cities.Zagreb], cost: 17 },
203
+ { nodes: [Cities.Milano, Cities.Roma], cost: 19 },
204
+ { nodes: [Cities.Roma, Cities.Napoli], cost: 7 },
205
+ { nodes: [Cities.Wien, Cities.Budapest], cost: 5 },
206
+ { nodes: [Cities.Wien, Cities.Zagreb], cost: 8 },
207
+ { nodes: [Cities.Budapest, Cities.Zagreb], cost: 7 },
208
+
209
+ // Yellow ↔ Red
210
+ { nodes: [Cities.Paris, Cities.Zürich], cost: 14 },
211
+ { nodes: [Cities.Lyon, Cities.Zürich], cost: 14 },
212
+ { nodes: [Cities.Lyon, Cities.Milano], cost: 11 },
213
+ { nodes: [Cities.Marseille, Cities.Milano], cost: 13 },
214
+
215
+ // Yellow ↔ Brown
216
+ { nodes: [Cities.Zürich, Cities.München], cost: 8 },
217
+ { nodes: [Cities.Zürich, Cities.Stuttgart], cost: 5 },
218
+ { nodes: [Cities.München, Cities.Wien], cost: 9 },
219
+ { nodes: [Cities.Praha, Cities.Wien], cost: 7 },
220
+ { nodes: [Cities.Wien, Cities.Katowice], cost: 8 },
221
+ { nodes: [Cities.Budapest, Cities.Katowice], cost: 11 },
222
+
223
+ // Orange — Scandinavia / Baltic (internal)
224
+ { nodes: [Cities.Oslo, Cities.Kobenhavn], cost: 17 },
225
+ { nodes: [Cities.Oslo, Cities.Stockholm], cost: 13 },
226
+ { nodes: [Cities.Stockholm, Cities.Kobenhavn], cost: 18 },
227
+ { nodes: [Cities.Stockholm, Cities.Helsinki], cost: 21 },
228
+ { nodes: [Cities.Helsinki, Cities.StPetersburg], cost: 11 },
229
+ { nodes: [Cities.Tallinn, Cities.StPetersburg], cost: 9 },
230
+ { nodes: [Cities.Tallinn, Cities.Riga], cost: 7 },
231
+ { nodes: [Cities.Riga, Cities.StPetersburg], cost: 13 },
232
+
233
+ // Orange ↔ Brown
234
+ { nodes: [Cities.Kobenhavn, Cities.Bremen], cost: 12 },
235
+ { nodes: [Cities.Kobenhavn, Cities.Berlin], cost: 15 },
236
+
237
+ // Blue — Eastern Europe (internal)
238
+ { nodes: [Cities.Warszawa, Cities.Minsk], cost: 10 },
239
+ { nodes: [Cities.Warszawa, Cities.Kyjiv], cost: 14 },
240
+ { nodes: [Cities.Minsk, Cities.Moskwa], cost: 14 },
241
+ { nodes: [Cities.Minsk, Cities.Kyjiv], cost: 10 },
242
+ { nodes: [Cities.Moskwa, Cities.Kharkiv], cost: 16 },
243
+ { nodes: [Cities.Kyjiv, Cities.Kharkiv], cost: 9 },
244
+ { nodes: [Cities.Kyjiv, Cities.Odessa], cost: 9 },
245
+ { nodes: [Cities.Kharkiv, Cities.Odessa], cost: 13 },
246
+ { nodes: [Cities.Odessa, Cities.Bucuresti], cost: 10 },
247
+
248
+ // Blue ↔ Brown
249
+ { nodes: [Cities.Berlin, Cities.Warszawa], cost: 11 },
250
+ { nodes: [Cities.Praha, Cities.Warszawa], cost: 11 },
251
+ { nodes: [Cities.Katowice, Cities.Warszawa], cost: 5 },
252
+
253
+ // Blue ↔ Orange
254
+ { nodes: [Cities.Kobenhavn, Cities.Warszawa], cost: 25 },
255
+ { nodes: [Cities.Riga, Cities.Warszawa], cost: 12 },
256
+ { nodes: [Cities.Riga, Cities.Minsk], cost: 8 },
257
+ { nodes: [Cities.Riga, Cities.Moskwa], cost: 18 },
258
+ { nodes: [Cities.StPetersburg, Cities.Moskwa], cost: 14 },
259
+
260
+ // Blue ↔ Yellow
261
+ { nodes: [Cities.Kyjiv, Cities.Budapest], cost: 21 },
262
+ { nodes: [Cities.Odessa, Cities.Budapest], cost: 25 },
263
+ { nodes: [Cities.Bucuresti, Cities.Budapest], cost: 16 },
264
+
265
+ // Green — Balkans / Turkey (internal)
266
+ { nodes: [Cities.Beograd, Cities.Sofia], cost: 11 },
267
+ { nodes: [Cities.Beograd, Cities.Tirana], cost: 15 },
268
+ { nodes: [Cities.Sofia, Cities.Tirana], cost: 13 },
269
+ { nodes: [Cities.Sofia, Cities.Athina], cost: 17 },
270
+ { nodes: [Cities.Sofia, Cities.Istanbul], cost: 13 },
271
+ { nodes: [Cities.Tirana, Cities.Athina], cost: 16 },
272
+ { nodes: [Cities.Istanbul, Cities.Izmir], cost: 8 },
273
+ { nodes: [Cities.Istanbul, Cities.Ankara], cost: 9 },
274
+ { nodes: [Cities.Izmir, Cities.Ankara], cost: 10 },
275
+
276
+ // Green ↔ Blue
277
+ { nodes: [Cities.Bucuresti, Cities.Beograd], cost: 12 },
278
+ { nodes: [Cities.Bucuresti, Cities.Sofia], cost: 9 },
279
+ { nodes: [Cities.Bucuresti, Cities.Istanbul], cost: 13 },
280
+
281
+ // Green ↔ Yellow
282
+ { nodes: [Cities.Beograd, Cities.Zagreb], cost: 9 },
283
+ { nodes: [Cities.Beograd, Cities.Napoli], cost: 18 },
284
+ { nodes: [Cities.Beograd, Cities.Budapest], cost: 10 },
285
+ { nodes: [Cities.Tirana, Cities.Napoli], cost: 25 },
286
+ ],
287
+ layout: 'Landscape',
288
+ // Coords were authored against the printed board (~1860x1450). The map area
289
+ // in the default landscape viewBox is roughly 0..1100 x 0..720 (player boards
290
+ // start at x=1105, resource market at y=720). Shrink uniformly to fit and
291
+ // shift up-left so the map sits in the top-left of that area.
292
+ adjustRatio: [0.5, 0.5],
293
+ mapPosition: [-30, -50],
294
+ // Slight clockwise tilt — fits the map's NW-tall / SE-tall extents into the
295
+ // available area better than the un-rotated layout.
296
+ mapRotation: 10,
297
+ // Resupply table verified with John from the Europe refill summary cards.
298
+ // Indexed [resource][playerCount-2][step-1].
299
+ resupply: [
300
+ // Coal
301
+ [
302
+ [2, 6, 2], // 2P
303
+ [2, 6, 2], // 3P
304
+ [3, 7, 4], // 4P
305
+ [3, 8, 4], // 5P
306
+ [5, 10, 5], // 6P
307
+ ],
308
+ // Oil
309
+ [
310
+ [2, 2, 3], // 2P
311
+ [2, 2, 3], // 3P
312
+ [3, 3, 4], // 4P
313
+ [4, 3, 5], // 5P
314
+ [4, 5, 6], // 6P
315
+ ],
316
+ // Garbage
317
+ [
318
+ [2, 3, 5], // 2P
319
+ [2, 3, 5], // 3P
320
+ [3, 4, 5], // 4P
321
+ [3, 5, 7], // 5P
322
+ [4, 6, 8], // 6P
323
+ ],
324
+ // Uranium
325
+ [
326
+ [1, 1, 2], // 2P
327
+ [1, 1, 2], // 3P
328
+ [1, 2, 2], // 4P
329
+ [2, 3, 3], // 5P
330
+ [2, 3, 4], // 6P
331
+ ],
332
+ ],
333
+ // Europe uses price spaces 1–9 (not the standard 1–8 with uranium ending at 16);
334
+ // every resource — including uranium — caps at price 9.
335
+ //
336
+ // Per-price slot counts (verified with John from the board):
337
+ // Coal [4,4,4,2,2,2,2,2,2] total 24 market slots
338
+ // Oil [2,2,2,2,2,2,2,2,4] total 20 market slots
339
+ // Garbage [3,3,3,3,3,3,3,3,0] total 24 market slots
340
+ // Uranium [1,1,1,1,1,1,2,2,2] total 12 market slots
341
+ //
342
+ // Oil note: market capacity is 20 but we keep the total cube count at 24 for now
343
+ // (per Mike's guidance while he tries to reach 2F-Spiele). This means oil can
344
+ // overflow back to the supply pile and a player holding 1–2 oil on power plants
345
+ // can briefly leave the market with oil still available at price 1. If the
346
+ // publisher confirms otherwise, drop startingSupply oil from 24 to 20.
347
+ coalPrices: [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9],
348
+ oilPrices: [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 9, 9],
349
+ garbagePrices: [1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8],
350
+ uraniumPrices: [1, 2, 3, 4, 5, 6, 7, 7, 8, 8, 9, 9],
351
+ // Initial market fill from the rules (coal 2–9, oil 3–9, garbage 3–8, uranium 8–9):
352
+ // coal = 4+4+2+2+2+2+2+2 = 20
353
+ // oil = 2+2+2+2+2+2+4 = 16
354
+ // garbage = 3+3+3+3+3+3 = 18
355
+ // uranium = 2+2 = 4
356
+ startingResources: [20, 16, 18, 4],
357
+ // Total cubes in the game (used cubes return here; refill draws from here).
358
+ startingSupply: [24, 24, 24, 12],
359
+ // Europe market setup: shuffle the deck, draw 9 plants, place the 4 lowest in
360
+ // the current market and the 5 next in the future market, with the Step 3 card
361
+ // buried at the bottom of the deck.
362
+ //
363
+ // The rules call for the New Power Plants Set 2 deck ("plug-back" cards) and a
364
+ // rule against placing a plug-back card on top of the deck — both are no-ops
365
+ // here since this engine ships only one power-plant set. Revisit if/when Set 2
366
+ // is added.
367
+ //
368
+ // The Step 2 trigger for Europe is handled in engine.ts (special branch): it
369
+ // removes the lowest plant from the current market once, then re-sorts the
370
+ // remaining 8 plants into 4 actual + 4 future without drawing from the deck.
371
+ setupDeck(numPlayers: number, variant: string, rng: seedrandom.prng) {
372
+ let powerPlantsDeck = cloneDeep(powerPlants);
373
+
374
+ const step3Index = powerPlantsDeck.findIndex((p) => p.type === PowerPlantType.Step3);
375
+ const step3 = powerPlantsDeck.splice(step3Index, 1)[0];
376
+
377
+ powerPlantsDeck = shuffle(powerPlantsDeck, rng() + '');
378
+
379
+ const initialPlants = powerPlantsDeck.splice(0, 9).sort((a, b) => a.number - b.number);
380
+ const actualMarket = initialPlants.slice(0, 4);
381
+ const futureMarket = initialPlants.slice(4); // 5 plants
382
+
383
+ powerPlantsDeck.push(step3);
384
+
385
+ return { actualMarket, futureMarket, powerPlantsDeck };
386
+ },
387
+ mapSpecificRules:
388
+ 'Europe uses the New Power Plants Set 2 deck (cards with a plug on the back). ' +
389
+ 'The power plant market starts as 4 current + 5 future power plants (9 total). ' +
390
+ 'When Step 2 begins, remove the lowest-numbered plant from the current market from the game once and do not replace it; the future market then has 4 plants instead of 5.',
391
+ };