powergrid-engine 1.12.1 → 1.13.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.
@@ -9,7 +9,7 @@ const gamestate_1 = require("./gamestate");
9
9
  const move_1 = require("./move");
10
10
  const prices_1 = __importDefault(require("./prices"));
11
11
  function availableMoves(G, player) {
12
- var _a, _b, _c, _d, _e, _f;
12
+ var _a, _b, _c, _d, _e, _f, _g;
13
13
  const moves = {};
14
14
  const lastLog = G.log[G.log.length - 1];
15
15
  if (lastLog.type == 'move' && G.currentPlayers.includes(player.id)) {
@@ -260,9 +260,36 @@ function availableMoves(G, player) {
260
260
  }
261
261
  case gamestate_1.Phase.Building: {
262
262
  if (player.cities.length < 21) {
263
- let toBuild = player.cities.length == 0
264
- ? G.map.cities.map((c) => ({ name: c.name, price: 0 }))
265
- : dijkstra(G, player).map((c) => ({ name: c.name, price: c.price }));
263
+ const isJapan = G.map.name === 'Japan';
264
+ const japanStartingCities = isJapan ? new Set((_g = G.map.startingCities) !== null && _g !== void 0 ? _g : []) : new Set();
265
+ let toBuild;
266
+ if (player.cities.length == 0) {
267
+ // Japan: first house must go in one of the 6 designated starting cities.
268
+ const candidates = isJapan
269
+ ? G.map.cities.filter((c) => japanStartingCities.has(c.name))
270
+ : G.map.cities;
271
+ toBuild = candidates.map((c) => ({ name: c.name, price: 0 }));
272
+ }
273
+ else if (isJapan && G.round === 1) {
274
+ // Japan round 1: any additional house must also be a starting city.
275
+ // Players cannot extend to adjacent non-starting cities until round 2.
276
+ toBuild = G.map.cities
277
+ .filter((c) => japanStartingCities.has(c.name))
278
+ .map((c) => ({ name: c.name, price: 0 }));
279
+ }
280
+ else {
281
+ toBuild = dijkstra(G, player).map((c) => ({ name: c.name, price: c.price }));
282
+ // Japan: a player may start a second disconnected network at any available
283
+ // starting city, paying only the slot cost (no connection fee).
284
+ if (isJapan &&
285
+ countNetworks(G.map.connections, player.cities.map((c) => c.name)) < 2) {
286
+ toBuild.forEach((city) => {
287
+ if (japanStartingCities.has(city.name)) {
288
+ city.price = 0;
289
+ }
290
+ });
291
+ }
292
+ }
266
293
  toBuild.forEach((city) => {
267
294
  const cityData = G.map.cities.find((c) => c.name == city.name);
268
295
  const othersCount = G.players.filter((p) => p.cities.find((c) => city.name == c.name)).length;
@@ -322,10 +349,15 @@ function availableMoves(G, player) {
322
349
  }
323
350
  return;
324
351
  }
325
- city.price += 10 + othersCount * 5;
326
- if (othersCount == G.step) {
352
+ const slotCosts = cityData.slotCosts;
353
+ const maxSlotsThisStep = cityData.stepSlots ? cityData.stepSlots[G.step - 1] : G.step;
354
+ const totalSlots = slotCosts ? slotCosts.length : 3;
355
+ if (othersCount >= maxSlotsThisStep || othersCount >= totalSlots) {
327
356
  city.price = 9999;
328
357
  }
358
+ else {
359
+ city.price += slotCosts ? slotCosts[othersCount] : 10 + othersCount * 5;
360
+ }
329
361
  if (player.cities.find((c) => c.name == city.name)) {
330
362
  city.price = 9999;
331
363
  }
@@ -441,6 +473,32 @@ function availableMoves(G, player) {
441
473
  return moves;
442
474
  }
443
475
  exports.availableMoves = availableMoves;
476
+ function countNetworks(connections, cityNames) {
477
+ const citySet = new Set(cityNames);
478
+ const visited = new Set();
479
+ let count = 0;
480
+ for (const city of cityNames) {
481
+ if (visited.has(city))
482
+ continue;
483
+ count++;
484
+ const stack = [city];
485
+ while (stack.length > 0) {
486
+ const current = stack.pop();
487
+ if (visited.has(current))
488
+ continue;
489
+ visited.add(current);
490
+ for (const conn of connections) {
491
+ if (conn.nodes.includes(current)) {
492
+ const neighbor = conn.nodes.find((n) => n !== current);
493
+ if (citySet.has(neighbor) && !visited.has(neighbor)) {
494
+ stack.push(neighbor);
495
+ }
496
+ }
497
+ }
498
+ }
499
+ }
500
+ return count;
501
+ }
444
502
  function dijkstra(G, player) {
445
503
  const nodes = G.map.cities.map((c) => ({
446
504
  name: c.name,
@@ -18,7 +18,7 @@ const utils_1 = require("./utils");
18
18
  exports.playerColors = ['limegreen', 'mediumorchid', 'red', 'dodgerblue', 'yellow', 'brown'];
19
19
  const citiesToStep2 = [10, 7, 7, 7, 6];
20
20
  const citiesToStep2BadenWurttemberg = [9, 6, 6, 6, 5];
21
- const citiesToStep2UKIreland = [7, 7, 7, 7, 6];
21
+ const citiesToStep2UKIreland = [10, 7, 7, 7, 6];
22
22
  const citiesToEndGame = [21, 17, 17, 15, 14];
23
23
  const cityIncome = [10, 22, 33, 44, 54, 64, 73, 82, 90, 98, 105, 112, 118, 124, 129, 134, 138, 142, 145, 148, 150, 150];
24
24
  const regionsInPlay = [3, 3, 4, 5, 5];
@@ -531,31 +531,30 @@ function move(G, move, playerNumber, isUndo = false) {
531
531
  nextPlayerAuction(G);
532
532
  }
533
533
  else {
534
- if (G.auctionSkips == G.players.length &&
535
- G.options.variant == 'original' &&
536
- G.map.name != 'China' &&
537
- G.map.name != 'Russia') {
538
- if (G.map.name == 'Baden-Württemberg') {
539
- // Baden-Württemberg: remove the two lowest plants
540
- const removed = [];
541
- for (let i = 0; i < 2 && G.actualMarket.length > 0; i++) {
542
- removed.push(G.actualMarket[0].number);
543
- G.actualMarket.shift();
544
- addPowerPlant(G);
545
- }
546
- G.log.push({
547
- type: 'event',
548
- event: `Everyone passed, removing the two lowest numbered Power Plants (${removed.join(', ')}).`,
549
- });
550
- }
551
- else {
552
- G.log.push({
553
- type: 'event',
554
- event: `Everyone passed, removing lowest numbered Power Plant (${G.actualMarket[0].number}).`,
555
- });
534
+ if (G.auctionSkips == G.players.length && G.map.name == 'Baden-Württemberg') {
535
+ // Baden-Württemberg: remove the two lowest plants when no one buys.
536
+ // This applies regardless of variant.
537
+ const removed = [];
538
+ for (let i = 0; i < 2 && G.actualMarket.length > 0; i++) {
539
+ removed.push(G.actualMarket[0].number);
556
540
  G.actualMarket.shift();
557
541
  addPowerPlant(G);
558
542
  }
543
+ G.log.push({
544
+ type: 'event',
545
+ event: `Everyone passed, removing the two lowest numbered Power Plants (${removed.join(', ')}).`,
546
+ });
547
+ }
548
+ else if (G.auctionSkips == G.players.length &&
549
+ G.options.variant == 'original' &&
550
+ G.map.name != 'China' &&
551
+ G.map.name != 'Russia') {
552
+ G.log.push({
553
+ type: 'event',
554
+ event: `Everyone passed, removing lowest numbered Power Plant (${G.actualMarket[0].number}).`,
555
+ });
556
+ G.actualMarket.shift();
557
+ addPowerPlant(G);
559
558
  }
560
559
  toResourcesPhase(G);
561
560
  }
@@ -2,7 +2,7 @@ import { AvailableMoves } from './available-moves';
2
2
  import { LogItem } from './log';
3
3
  import { GameMap } from './maps';
4
4
  import { Move } from './move';
5
- export declare type MapName = 'USA' | 'Germany' | 'Brazil' | 'Spain & Portugal' | 'France' | 'Italy' | 'Quebec' | 'Middle East' | 'India' | 'China' | 'Benelux' | 'Russia' | 'Central Europe' | 'Baden-Württemberg' | 'Northern Europe' | 'Korea' | 'Europe' | 'North America' | 'South Africa' | 'UK & Ireland';
5
+ export declare type MapName = 'USA' | 'Germany' | 'Brazil' | 'Spain & Portugal' | 'France' | 'Italy' | 'Quebec' | 'Middle East' | 'India' | 'China' | 'Benelux' | 'Russia' | 'Central Europe' | 'Baden-Württemberg' | 'Northern Europe' | 'Korea' | 'Europe' | 'North America' | 'South Africa' | 'UK & Ireland' | 'Japan';
6
6
  export declare type Variant = 'original' | 'recharged';
7
7
  export interface GameOptions {
8
8
  fastBid?: boolean;
@@ -56,8 +56,24 @@ exports.map = {
56
56
  { name: Cities.Sigmarincen, region: Regions.Purple, x: 1111, y: 1803 },
57
57
  { name: Cities.Konstanz, region: Regions.Purple, x: 1088, y: 2163 },
58
58
  { name: Cities.Ulm, region: Regions.Purple, x: 1541, y: 1499 },
59
- { name: Cities.Augsburg, region: Regions.Purple, x: 1830, y: 1505, transregional: true },
60
- { name: Cities.Basel, region: Regions.Green, x: 103, y: 2271, transregional: true },
59
+ {
60
+ name: Cities.Augsburg,
61
+ region: Regions.Purple,
62
+ x: 1830,
63
+ y: 1505,
64
+ transregional: true,
65
+ slotCosts: [15, 20],
66
+ stepSlots: [0, 1, 2],
67
+ },
68
+ {
69
+ name: Cities.Basel,
70
+ region: Regions.Green,
71
+ x: 103,
72
+ y: 2271,
73
+ transregional: true,
74
+ slotCosts: [15, 20],
75
+ stepSlots: [0, 1, 2],
76
+ },
61
77
  { name: Cities.Waldshuttiencen, region: Regions.Green, x: 502, y: 2166 },
62
78
  { name: Cities.Singen, region: Regions.Green, x: 874, y: 2084 },
63
79
  { name: Cities.Tuttlincen, region: Regions.Green, x: 870, y: 1854 },
@@ -70,8 +86,24 @@ exports.map = {
70
86
  { name: Cities.Sinsheim, region: Regions.Red, x: 920, y: 785 },
71
87
  { name: Cities.Heidelber, region: Regions.Red, x: 817, y: 660 },
72
88
  { name: Cities.Mannheim, region: Regions.Red, x: 710, y: 540 },
73
- { name: Cities.Luowigshafen, region: Regions.Red, x: 579, y: 588, transregional: true },
74
- { name: Cities.Nurnberg, region: Regions.Blue, x: 1837, y: 771, transregional: true },
89
+ {
90
+ name: Cities.Luowigshafen,
91
+ region: Regions.Red,
92
+ x: 579,
93
+ y: 588,
94
+ transregional: true,
95
+ slotCosts: [15, 20],
96
+ stepSlots: [0, 1, 2],
97
+ },
98
+ {
99
+ name: Cities.Nurnberg,
100
+ region: Regions.Blue,
101
+ x: 1837,
102
+ y: 771,
103
+ transregional: true,
104
+ slotCosts: [15, 20],
105
+ stepSlots: [0, 1, 2],
106
+ },
75
107
  { name: Cities.Ellwangen, region: Regions.Blue, x: 1637, y: 1030 },
76
108
  { name: Cities.Coppingen, region: Regions.Blue, x: 1387, y: 1233 },
77
109
  { name: Cities.Reutlincen, region: Regions.Blue, x: 1107, y: 1443 },
@@ -81,7 +113,15 @@ exports.map = {
81
113
  { name: Cities.Laha, region: Regions.Yellow, x: 296, y: 1575 },
82
114
  { name: Cities.Badenbaden, region: Regions.Yellow, x: 554, y: 1259 },
83
115
  { name: Cities.Offenburg, region: Regions.Yellow, x: 395, y: 1422 },
84
- { name: Cities.Strasbourg, region: Regions.Yellow, x: 211, y: 1330, transregional: true },
116
+ {
117
+ name: Cities.Strasbourg,
118
+ region: Regions.Yellow,
119
+ x: 211,
120
+ y: 1330,
121
+ transregional: true,
122
+ slotCosts: [15, 20],
123
+ stepSlots: [0, 1, 2],
124
+ },
85
125
  { name: Cities.Pforzheim, region: Regions.Yellow, x: 794, y: 1095 },
86
126
  { name: Cities.Rastatt, region: Regions.Yellow, x: 509, y: 1109 },
87
127
  { name: Cities.Karlsruhf, region: Regions.Yellow, x: 638, y: 974 },
@@ -44,3 +44,4 @@ export declare enum Cities {
44
44
  Nagasaki = "Nagasaki"
45
45
  }
46
46
  export declare const map: GameMap;
47
+ export declare const mapRecharged: GameMap;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.map = exports.Cities = exports.Regions = void 0;
3
+ exports.mapRecharged = exports.map = exports.Cities = exports.Regions = void 0;
4
4
  var Regions;
5
5
  (function (Regions) {
6
6
  Regions["Brown"] = "brown";
@@ -50,42 +50,94 @@ var Cities;
50
50
  exports.map = {
51
51
  name: 'Japan',
52
52
  cities: [
53
- { name: Cities.Asahikawa, region: Regions.Brown, x: 3384, y: 414 },
54
- { name: Cities.Sapporo, region: Regions.Brown, x: 3028, y: 382 },
55
- { name: Cities.Hakodate, region: Regions.Brown, x: 2622, y: 512 },
53
+ { name: Cities.Asahikawa, region: Regions.Brown, x: 3384, y: 1014 },
54
+ { name: Cities.Sapporo, region: Regions.Brown, x: 3028, y: 982, slotCosts: [10, 10, 20], stepSlots: [2, 2, 3] },
55
+ { name: Cities.Hakodate, region: Regions.Brown, x: 2622, y: 1112 },
56
56
  { name: Cities.Admori, region: Regions.Brown, x: 3818, y: 1336 },
57
- { name: Cities.Morioka, region: Regions.Brown, x: 3636, y: 1646 },
57
+ { name: Cities.Morioka, region: Regions.Brown, x: 3636, y: 1646, slotCosts: [15, 20], stepSlots: [0, 1, 2] },
58
58
  { name: Cities.Akita, region: Regions.Brown, x: 3472, y: 1480 },
59
59
  { name: Cities.Sendai, region: Regions.Brown, x: 3408, y: 1886 },
60
60
  { name: Cities.Niigata, region: Regions.Green, x: 2874, y: 1628 },
61
- { name: Cities.Koriyama, region: Regions.Green, x: 3086, y: 1932 },
61
+ { name: Cities.Koriyama, region: Regions.Green, x: 3086, y: 1932, slotCosts: [10, 15] },
62
62
  { name: Cities.Nagano, region: Regions.Green, x: 2486, y: 1722 },
63
- { name: Cities.Saitama, region: Regions.Green, x: 2450, y: 1986 },
64
- { name: Cities.Tokyo, region: Regions.Green, x: 2524, y: 2122 },
63
+ { name: Cities.Saitama, region: Regions.Green, x: 2450, y: 1986, slotCosts: [15, 20], stepSlots: [0, 1, 2] },
64
+ { name: Cities.Tokyo, region: Regions.Green, x: 2524, y: 2122, slotCosts: [10, 10, 20], stepSlots: [2, 2, 3] },
65
65
  { name: Cities.Chiba, region: Regions.Green, x: 2650, y: 2316 },
66
- { name: Cities.Yokohama, region: Regions.Green, x: 2306, y: 2244 },
66
+ {
67
+ name: Cities.Yokohama,
68
+ region: Regions.Green,
69
+ x: 2306,
70
+ y: 2244,
71
+ slotCosts: [10, 10, 20],
72
+ stepSlots: [2, 2, 3],
73
+ },
67
74
  { name: Cities.Kanazawa, region: Regions.Purple, x: 2207, y: 1385 },
68
- { name: Cities.Toyama, region: Regions.Purple, x: 2257, y: 1595 },
75
+ { name: Cities.Toyama, region: Regions.Purple, x: 2257, y: 1595, slotCosts: [15, 20], stepSlots: [0, 1, 2] },
69
76
  { name: Cities.Kyoto, region: Regions.Purple, x: 1765, y: 1595 },
70
- { name: Cities.Osaka, region: Regions.Purple, x: 1517, y: 1631 },
77
+ { name: Cities.Osaka, region: Regions.Purple, x: 1517, y: 1631, slotCosts: [10, 10, 20], stepSlots: [2, 2, 3] },
71
78
  { name: Cities.Nagoya, region: Regions.Purple, x: 1942, y: 1751 },
72
79
  { name: Cities.Kofu, region: Regions.Purple, x: 2179, y: 1897 },
73
- { name: Cities.Hamamatsu, region: Regions.Purple, x: 1958, y: 2024 },
74
- { name: Cities.Kobe, region: Regions.Yellow, x: 1555, y: 1451 },
75
- { name: Cities.Matsue, region: Regions.Yellow, x: 1457, y: 1006 },
80
+ { name: Cities.Hamamatsu, region: Regions.Purple, x: 1958, y: 2024, slotCosts: [10, 15] },
81
+ { name: Cities.Kobe, region: Regions.Yellow, x: 1555, y: 1451, slotCosts: [10, 10, 20], stepSlots: [2, 2, 3] },
82
+ { name: Cities.Matsue, region: Regions.Yellow, x: 1457, y: 1006, slotCosts: [15, 20], stepSlots: [0, 1, 2] },
76
83
  { name: Cities.Okayama, region: Regions.Yellow, x: 1390, y: 1286 },
77
84
  { name: Cities.Takamatsu, region: Regions.Yellow, x: 1268, y: 1507 },
78
- { name: Cities.Kochi, region: Regions.Yellow, x: 974, y: 1472 },
85
+ { name: Cities.Kochi, region: Regions.Yellow, x: 974, y: 1472, slotCosts: [10, 15] },
79
86
  { name: Cities.Matsuyama, region: Regions.Yellow, x: 1036, y: 1268 },
80
87
  { name: Cities.Hiroshima, region: Regions.Yellow, x: 1103, y: 1078 },
81
88
  { name: Cities.Shimonoseki, region: Regions.Red, x: 856, y: 962 },
82
- { name: Cities.Oita, region: Regions.Red, x: 731, y: 1166 },
83
- { name: Cities.Miyazaki, region: Regions.Red, x: 469, y: 1318 },
89
+ { name: Cities.Oita, region: Regions.Red, x: 731, y: 1166, slotCosts: [10, 15] },
90
+ { name: Cities.Miyazaki, region: Regions.Red, x: 469, y: 1318, slotCosts: [15, 20], stepSlots: [0, 1, 2] },
84
91
  { name: Cities.Kagoshima, region: Regions.Red, x: 157, y: 1255 },
85
92
  { name: Cities.Kumamoto, region: Regions.Red, x: 457, y: 1045 },
86
- { name: Cities.Fukuoka, region: Regions.Red, x: 644, y: 823 },
93
+ { name: Cities.Fukuoka, region: Regions.Red, x: 644, y: 823, slotCosts: [10, 10, 20], stepSlots: [2, 2, 3] },
87
94
  { name: Cities.Nagasaki, region: Regions.Red, x: 313, y: 875 },
88
95
  ],
96
+ layout: 'Portrait',
97
+ adjustRatio: [0.28, 0.28],
98
+ mapPosition: [0, 120],
99
+ resupply: [
100
+ // Coal
101
+ [
102
+ [3, 4, 3],
103
+ [4, 5, 3],
104
+ [5, 6, 4],
105
+ [5, 7, 5],
106
+ [7, 9, 6], // 6P
107
+ ],
108
+ // Oil
109
+ [
110
+ [2, 2, 4],
111
+ [2, 3, 4],
112
+ [3, 4, 5],
113
+ [4, 5, 6],
114
+ [5, 6, 7],
115
+ ],
116
+ // Garbage
117
+ [
118
+ [1, 2, 3],
119
+ [1, 2, 3],
120
+ [2, 3, 4],
121
+ [3, 3, 5],
122
+ [3, 5, 6],
123
+ ],
124
+ // Uranium
125
+ [
126
+ [1, 1, 1],
127
+ [1, 1, 1],
128
+ [1, 2, 2],
129
+ [2, 3, 2],
130
+ [2, 3, 3],
131
+ ],
132
+ ],
133
+ startingResources: [21, 15, 9, 3],
134
+ startingCities: ['Fukuoka', 'Kobe', 'Osaka', 'Sapporo', 'Tokyo', 'Yokohama'],
135
+ mapSpecificRules: 'Each player can have two separate networks.\n' +
136
+ 'First houses must be placed in one of six starting cities: Fukuoka, Kobe, Osaka, Sapporo, Tokyo, or Yokohama.\n' +
137
+ 'Starting cities have two first-connection spots (cost 10 Elektro each); two players can build there in Step 1.\n' +
138
+ 'In Step 3, a third connection spot opens in starting cities (cost 20 Elektro).\n' +
139
+ 'Some smaller cities have only two building spots (costs 10 and 15, or 15 and 20 from Step 2).\n' +
140
+ 'If all spots in every starting city are taken, a player is forced to play with a single network.',
89
141
  connections: [
90
142
  { nodes: [Cities.Nagasaki, Cities.Fukuoka], cost: 10 },
91
143
  { nodes: [Cities.Fukuoka, Cities.Shimonoseki], cost: 10 },
@@ -142,3 +194,4 @@ exports.map = {
142
194
  { nodes: [Cities.Kumamoto, Cities.Fukuoka], cost: 8 },
143
195
  ],
144
196
  };
197
+ exports.mapRecharged = { ...exports.map };
@@ -145,7 +145,7 @@ exports.map = {
145
145
  [Regions.Orange]: [
146
146
  // Denmark
147
147
  { number: 19, type: gamestate_1.PowerPlantType.Oil, cost: 2, citiesPowered: 4 },
148
- { number: 26, type: gamestate_1.PowerPlantType.Coal, cost: 3, citiesPowered: 5 },
148
+ { number: 26, type: gamestate_1.PowerPlantType.Oil, cost: 3, citiesPowered: 5 },
149
149
  ],
150
150
  [Regions.Red]: [
151
151
  // Norway
@@ -6,6 +6,8 @@ export interface City {
6
6
  x: number;
7
7
  y: number;
8
8
  transregional?: boolean;
9
+ slotCosts?: number[];
10
+ stepSlots?: number[];
9
11
  singleOccupancy?: boolean;
10
12
  island?: string;
11
13
  }
@@ -56,6 +58,7 @@ export interface GameMap {
56
58
  regionalPowerPlants?: Record<string, PowerPlant[]>;
57
59
  crossIslandSurcharge?: number;
58
60
  mapSpecificRules?: string;
61
+ startingCities?: string[];
59
62
  devBackdrop?: {
60
63
  src: string;
61
64
  width: number;
package/dist/src/maps.js CHANGED
@@ -13,7 +13,7 @@ const france_1 = require("./maps/france");
13
13
  const germany_1 = require("./maps/germany");
14
14
  const indian_1 = require("./maps/indian");
15
15
  const italy_1 = require("./maps/italy");
16
- // import { map as japan } from './maps/japan';
16
+ const japan_1 = require("./maps/japan");
17
17
  const korea_1 = require("./maps/korea");
18
18
  const middleeast_1 = require("./maps/middleeast");
19
19
  const northamerica_1 = require("./maps/northamerica");
@@ -45,7 +45,7 @@ exports.maps = [
45
45
  southafrica_1.map,
46
46
  ukireland_1.map,
47
47
  // australia,
48
- // japan,
48
+ japan_1.map,
49
49
  ];
50
50
  exports.mapsRecharged = [
51
51
  america_1.mapRecharged,
@@ -70,5 +70,5 @@ exports.mapsRecharged = [
70
70
  ukireland_1.map,
71
71
  // australia,
72
72
  // china,
73
- // japan,
73
+ japan_1.mapRecharged,
74
74
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "powergrid-engine",
3
- "version": "1.12.1",
3
+ "version": "1.13.1",
4
4
  "description": "An engine for Power Grid",
5
5
  "main": "dist/index.js",
6
6
  "types": "index.ts",
@@ -354,10 +354,41 @@ export function availableMoves(G: GameState, player: Player): AvailableMoves {
354
354
 
355
355
  case Phase.Building: {
356
356
  if (player.cities.length < 21) {
357
- let toBuild =
358
- player.cities.length == 0
359
- ? G.map.cities.map((c) => ({ name: c.name, price: 0 }))
360
- : dijkstra(G, player).map((c) => ({ name: c.name, price: c.price }));
357
+ const isJapan = G.map.name === 'Japan';
358
+ const japanStartingCities = isJapan ? new Set(G.map.startingCities ?? []) : new Set<string>();
359
+
360
+ let toBuild: { name: string; price: number }[];
361
+ if (player.cities.length == 0) {
362
+ // Japan: first house must go in one of the 6 designated starting cities.
363
+ const candidates = isJapan
364
+ ? G.map.cities.filter((c) => japanStartingCities.has(c.name))
365
+ : G.map.cities;
366
+ toBuild = candidates.map((c) => ({ name: c.name, price: 0 }));
367
+ } else if (isJapan && G.round === 1) {
368
+ // Japan round 1: any additional house must also be a starting city.
369
+ // Players cannot extend to adjacent non-starting cities until round 2.
370
+ toBuild = G.map.cities
371
+ .filter((c) => japanStartingCities.has(c.name))
372
+ .map((c) => ({ name: c.name, price: 0 }));
373
+ } else {
374
+ toBuild = dijkstra(G, player).map((c) => ({ name: c.name, price: c.price }));
375
+
376
+ // Japan: a player may start a second disconnected network at any available
377
+ // starting city, paying only the slot cost (no connection fee).
378
+ if (
379
+ isJapan &&
380
+ countNetworks(
381
+ G.map.connections,
382
+ player.cities.map((c) => c.name)
383
+ ) < 2
384
+ ) {
385
+ toBuild.forEach((city) => {
386
+ if (japanStartingCities.has(city.name)) {
387
+ city.price = 0;
388
+ }
389
+ });
390
+ }
391
+ }
361
392
 
362
393
  toBuild.forEach((city) => {
363
394
  const cityData = G.map.cities.find((c) => c.name == city.name)!;
@@ -428,10 +459,14 @@ export function availableMoves(G: GameState, player: Player): AvailableMoves {
428
459
  return;
429
460
  }
430
461
 
431
- city.price += 10 + othersCount * 5;
462
+ const slotCosts = cityData.slotCosts;
463
+ const maxSlotsThisStep = cityData.stepSlots ? cityData.stepSlots[G.step - 1] : G.step;
464
+ const totalSlots = slotCosts ? slotCosts.length : 3;
432
465
 
433
- if (othersCount == G.step) {
466
+ if (othersCount >= maxSlotsThisStep || othersCount >= totalSlots) {
434
467
  city.price = 9999;
468
+ } else {
469
+ city.price += slotCosts ? slotCosts[othersCount] : 10 + othersCount * 5;
435
470
  }
436
471
 
437
472
  if (player.cities.find((c) => c.name == city.name)) {
@@ -569,6 +604,31 @@ export function availableMoves(G: GameState, player: Player): AvailableMoves {
569
604
  return moves;
570
605
  }
571
606
 
607
+ function countNetworks(connections: { nodes: string[] }[], cityNames: string[]): number {
608
+ const citySet = new Set(cityNames);
609
+ const visited = new Set<string>();
610
+ let count = 0;
611
+ for (const city of cityNames) {
612
+ if (visited.has(city)) continue;
613
+ count++;
614
+ const stack = [city];
615
+ while (stack.length > 0) {
616
+ const current = stack.pop()!;
617
+ if (visited.has(current)) continue;
618
+ visited.add(current);
619
+ for (const conn of connections) {
620
+ if (conn.nodes.includes(current)) {
621
+ const neighbor = conn.nodes.find((n) => n !== current)!;
622
+ if (citySet.has(neighbor) && !visited.has(neighbor)) {
623
+ stack.push(neighbor);
624
+ }
625
+ }
626
+ }
627
+ }
628
+ }
629
+ return count;
630
+ }
631
+
572
632
  function dijkstra(G: GameState, player: Player): { name: string; price: number }[] {
573
633
  const nodes = G.map.cities.map((c) => ({
574
634
  name: c.name,
@@ -141,6 +141,50 @@ describe('Engine', () => {
141
141
  expect(ended(G)).to.be.false;
142
142
  });
143
143
 
144
+ it('should setup Korea with dual-market populated and players ready to move', () => {
145
+ // Smoke test for the Korea map (PR #74). If a deployed lobby fails to
146
+ // auto-start, setup() is the most likely culprit — this catches that
147
+ // class of bug without needing a recorded game fixture.
148
+ const G = setup(
149
+ 2,
150
+ { map: 'Korea', variant: 'recharged', randomizeMap: false, fastBid: false },
151
+ 'korea-test-seed'
152
+ );
153
+
154
+ expect(ended(G)).to.be.false;
155
+ expect(G.map.name).to.equal('Korea');
156
+
157
+ // South-side markets (standard fields, uranium only here)
158
+ expect(G.coalMarket).to.be.greaterThan(0);
159
+ expect(G.oilMarket).to.be.greaterThan(0);
160
+ expect(G.uraniumMarket).to.be.greaterThan(0);
161
+
162
+ // North-side markets (Korea-specific, no uranium row)
163
+ expect(G.coalMarketNorth).to.be.greaterThan(0);
164
+ expect(G.oilMarketNorth).to.be.greaterThan(0);
165
+ expect(G.garbageMarketNorth).to.exist;
166
+
167
+ // Korea uses parallel per-side price tables
168
+ expect(G.coalPricesNorth).to.be.an('array').that.is.not.empty;
169
+ expect(G.oilPricesNorth).to.be.an('array').that.is.not.empty;
170
+
171
+ // At least one player should have available moves (auction phase is live)
172
+ expect(G.players.some((p) => p.availableMoves && Object.keys(p.availableMoves).length > 0)).to.be.true;
173
+ });
174
+
175
+ it('should setup Northern Europe and have players ready to move', () => {
176
+ // Smoke test for Northern Europe (PR #74) — parity with Korea test.
177
+ const G = setup(
178
+ 2,
179
+ { map: 'Northern Europe', variant: 'recharged', randomizeMap: false, fastBid: false },
180
+ 'ne-test-seed'
181
+ );
182
+
183
+ expect(ended(G)).to.be.false;
184
+ expect(G.map.name).to.equal('Northern Europe');
185
+ expect(G.players.some((p) => p.availableMoves && Object.keys(p.availableMoves).length > 0)).to.be.true;
186
+ });
187
+
144
188
  it('should place UK & Ireland Step 3 card third from last with two plants below it', () => {
145
189
  // UK & Ireland rules: the Step 3 card (plant 99) goes at deck.length - 3
146
190
  // so two plants sit below it, and Step 3 fires two auctions earlier than
package/src/engine.ts CHANGED
@@ -15,7 +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
+ const citiesToStep2UKIreland = [10, 7, 7, 7, 6];
19
19
  const citiesToEndGame = [21, 17, 17, 15, 14];
20
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];
21
21
  const regionsInPlay = [3, 3, 4, 5, 5];
@@ -621,35 +621,34 @@ export function move(G: GameState, move: Move, playerNumber: number, isUndo = fa
621
621
  if (G.players.some((p) => !p.skipAuction && !p.isDropped)) {
622
622
  nextPlayerAuction(G);
623
623
  } else {
624
- if (
624
+ if (G.auctionSkips == G.players.length && G.map.name == 'Baden-Württemberg') {
625
+ // Baden-Württemberg: remove the two lowest plants when no one buys.
626
+ // This applies regardless of variant.
627
+ const removed: number[] = [];
628
+ for (let i = 0; i < 2 && G.actualMarket.length > 0; i++) {
629
+ removed.push(G.actualMarket[0].number);
630
+ G.actualMarket.shift();
631
+ addPowerPlant(G);
632
+ }
633
+ G.log.push({
634
+ type: 'event',
635
+ event: `Everyone passed, removing the two lowest numbered Power Plants (${removed.join(
636
+ ', '
637
+ )}).`,
638
+ });
639
+ } else if (
625
640
  G.auctionSkips == G.players.length &&
626
641
  G.options.variant == 'original' &&
627
642
  G.map.name != 'China' &&
628
643
  G.map.name != 'Russia'
629
644
  ) {
630
- if (G.map.name == 'Baden-Württemberg') {
631
- // Baden-Württemberg: remove the two lowest plants
632
- const removed: number[] = [];
633
- for (let i = 0; i < 2 && G.actualMarket.length > 0; i++) {
634
- removed.push(G.actualMarket[0].number);
635
- G.actualMarket.shift();
636
- addPowerPlant(G);
637
- }
638
- G.log.push({
639
- type: 'event',
640
- event: `Everyone passed, removing the two lowest numbered Power Plants (${removed.join(
641
- ', '
642
- )}).`,
643
- });
644
- } else {
645
- G.log.push({
646
- type: 'event',
647
- event: `Everyone passed, removing lowest numbered Power Plant (${G.actualMarket[0].number}).`,
648
- });
645
+ G.log.push({
646
+ type: 'event',
647
+ event: `Everyone passed, removing lowest numbered Power Plant (${G.actualMarket[0].number}).`,
648
+ });
649
649
 
650
- G.actualMarket.shift();
651
- addPowerPlant(G);
652
- }
650
+ G.actualMarket.shift();
651
+ addPowerPlant(G);
653
652
  }
654
653
 
655
654
  toResourcesPhase(G);
package/src/gamestate.ts CHANGED
@@ -23,9 +23,9 @@ export type MapName =
23
23
  | 'Europe'
24
24
  | 'North America'
25
25
  | 'South Africa'
26
- | 'UK & Ireland';
26
+ | 'UK & Ireland'
27
+ | 'Japan';
27
28
  // | 'Australia'
28
- // | 'Japan'
29
29
  export type Variant = 'original' | 'recharged';
30
30
 
31
31
  export interface GameOptions {
@@ -55,8 +55,24 @@ export const map: GameMap = {
55
55
  { name: Cities.Sigmarincen, region: Regions.Purple, x: 1111, y: 1803 },
56
56
  { name: Cities.Konstanz, region: Regions.Purple, x: 1088, y: 2163 },
57
57
  { name: Cities.Ulm, region: Regions.Purple, x: 1541, y: 1499 },
58
- { name: Cities.Augsburg, region: Regions.Purple, x: 1830, y: 1505, transregional: true },
59
- { name: Cities.Basel, region: Regions.Green, x: 103, y: 2271, transregional: true },
58
+ {
59
+ name: Cities.Augsburg,
60
+ region: Regions.Purple,
61
+ x: 1830,
62
+ y: 1505,
63
+ transregional: true,
64
+ slotCosts: [15, 20],
65
+ stepSlots: [0, 1, 2],
66
+ },
67
+ {
68
+ name: Cities.Basel,
69
+ region: Regions.Green,
70
+ x: 103,
71
+ y: 2271,
72
+ transregional: true,
73
+ slotCosts: [15, 20],
74
+ stepSlots: [0, 1, 2],
75
+ },
60
76
  { name: Cities.Waldshuttiencen, region: Regions.Green, x: 502, y: 2166 },
61
77
  { name: Cities.Singen, region: Regions.Green, x: 874, y: 2084 },
62
78
  { name: Cities.Tuttlincen, region: Regions.Green, x: 870, y: 1854 },
@@ -69,8 +85,24 @@ export const map: GameMap = {
69
85
  { name: Cities.Sinsheim, region: Regions.Red, x: 920, y: 785 },
70
86
  { name: Cities.Heidelber, region: Regions.Red, x: 817, y: 660 },
71
87
  { name: Cities.Mannheim, region: Regions.Red, x: 710, y: 540 },
72
- { name: Cities.Luowigshafen, region: Regions.Red, x: 579, y: 588, transregional: true },
73
- { name: Cities.Nurnberg, region: Regions.Blue, x: 1837, y: 771, transregional: true },
88
+ {
89
+ name: Cities.Luowigshafen,
90
+ region: Regions.Red,
91
+ x: 579,
92
+ y: 588,
93
+ transregional: true,
94
+ slotCosts: [15, 20],
95
+ stepSlots: [0, 1, 2],
96
+ },
97
+ {
98
+ name: Cities.Nurnberg,
99
+ region: Regions.Blue,
100
+ x: 1837,
101
+ y: 771,
102
+ transregional: true,
103
+ slotCosts: [15, 20],
104
+ stepSlots: [0, 1, 2],
105
+ },
74
106
  { name: Cities.Ellwangen, region: Regions.Blue, x: 1637, y: 1030 },
75
107
  { name: Cities.Coppingen, region: Regions.Blue, x: 1387, y: 1233 },
76
108
  { name: Cities.Reutlincen, region: Regions.Blue, x: 1107, y: 1443 },
@@ -80,7 +112,15 @@ export const map: GameMap = {
80
112
  { name: Cities.Laha, region: Regions.Yellow, x: 296, y: 1575 },
81
113
  { name: Cities.Badenbaden, region: Regions.Yellow, x: 554, y: 1259 },
82
114
  { name: Cities.Offenburg, region: Regions.Yellow, x: 395, y: 1422 },
83
- { name: Cities.Strasbourg, region: Regions.Yellow, x: 211, y: 1330, transregional: true },
115
+ {
116
+ name: Cities.Strasbourg,
117
+ region: Regions.Yellow,
118
+ x: 211,
119
+ y: 1330,
120
+ transregional: true,
121
+ slotCosts: [15, 20],
122
+ stepSlots: [0, 1, 2],
123
+ },
84
124
  { name: Cities.Pforzheim, region: Regions.Yellow, x: 794, y: 1095 },
85
125
  { name: Cities.Rastatt, region: Regions.Yellow, x: 509, y: 1109 },
86
126
  { name: Cities.Karlsruhf, region: Regions.Yellow, x: 638, y: 974 },
package/src/maps/japan.ts CHANGED
@@ -49,42 +49,95 @@ export enum Cities {
49
49
  export const map: GameMap = {
50
50
  name: 'Japan',
51
51
  cities: [
52
- { name: Cities.Asahikawa, region: Regions.Brown, x: 3384, y: 414 },
53
- { name: Cities.Sapporo, region: Regions.Brown, x: 3028, y: 382 },
54
- { name: Cities.Hakodate, region: Regions.Brown, x: 2622, y: 512 },
52
+ { name: Cities.Asahikawa, region: Regions.Brown, x: 3384, y: 1014 },
53
+ { name: Cities.Sapporo, region: Regions.Brown, x: 3028, y: 982, slotCosts: [10, 10, 20], stepSlots: [2, 2, 3] },
54
+ { name: Cities.Hakodate, region: Regions.Brown, x: 2622, y: 1112 },
55
55
  { name: Cities.Admori, region: Regions.Brown, x: 3818, y: 1336 },
56
- { name: Cities.Morioka, region: Regions.Brown, x: 3636, y: 1646 },
56
+ { name: Cities.Morioka, region: Regions.Brown, x: 3636, y: 1646, slotCosts: [15, 20], stepSlots: [0, 1, 2] },
57
57
  { name: Cities.Akita, region: Regions.Brown, x: 3472, y: 1480 },
58
58
  { name: Cities.Sendai, region: Regions.Brown, x: 3408, y: 1886 },
59
59
  { name: Cities.Niigata, region: Regions.Green, x: 2874, y: 1628 },
60
- { name: Cities.Koriyama, region: Regions.Green, x: 3086, y: 1932 },
60
+ { name: Cities.Koriyama, region: Regions.Green, x: 3086, y: 1932, slotCosts: [10, 15] },
61
61
  { name: Cities.Nagano, region: Regions.Green, x: 2486, y: 1722 },
62
- { name: Cities.Saitama, region: Regions.Green, x: 2450, y: 1986 },
63
- { name: Cities.Tokyo, region: Regions.Green, x: 2524, y: 2122 },
62
+ { name: Cities.Saitama, region: Regions.Green, x: 2450, y: 1986, slotCosts: [15, 20], stepSlots: [0, 1, 2] },
63
+ { name: Cities.Tokyo, region: Regions.Green, x: 2524, y: 2122, slotCosts: [10, 10, 20], stepSlots: [2, 2, 3] },
64
64
  { name: Cities.Chiba, region: Regions.Green, x: 2650, y: 2316 },
65
- { name: Cities.Yokohama, region: Regions.Green, x: 2306, y: 2244 },
65
+ {
66
+ name: Cities.Yokohama,
67
+ region: Regions.Green,
68
+ x: 2306,
69
+ y: 2244,
70
+ slotCosts: [10, 10, 20],
71
+ stepSlots: [2, 2, 3],
72
+ },
66
73
  { name: Cities.Kanazawa, region: Regions.Purple, x: 2207, y: 1385 },
67
- { name: Cities.Toyama, region: Regions.Purple, x: 2257, y: 1595 },
74
+ { name: Cities.Toyama, region: Regions.Purple, x: 2257, y: 1595, slotCosts: [15, 20], stepSlots: [0, 1, 2] },
68
75
  { name: Cities.Kyoto, region: Regions.Purple, x: 1765, y: 1595 },
69
- { name: Cities.Osaka, region: Regions.Purple, x: 1517, y: 1631 },
76
+ { name: Cities.Osaka, region: Regions.Purple, x: 1517, y: 1631, slotCosts: [10, 10, 20], stepSlots: [2, 2, 3] },
70
77
  { name: Cities.Nagoya, region: Regions.Purple, x: 1942, y: 1751 },
71
78
  { name: Cities.Kofu, region: Regions.Purple, x: 2179, y: 1897 },
72
- { name: Cities.Hamamatsu, region: Regions.Purple, x: 1958, y: 2024 },
73
- { name: Cities.Kobe, region: Regions.Yellow, x: 1555, y: 1451 },
74
- { name: Cities.Matsue, region: Regions.Yellow, x: 1457, y: 1006 },
79
+ { name: Cities.Hamamatsu, region: Regions.Purple, x: 1958, y: 2024, slotCosts: [10, 15] },
80
+ { name: Cities.Kobe, region: Regions.Yellow, x: 1555, y: 1451, slotCosts: [10, 10, 20], stepSlots: [2, 2, 3] },
81
+ { name: Cities.Matsue, region: Regions.Yellow, x: 1457, y: 1006, slotCosts: [15, 20], stepSlots: [0, 1, 2] },
75
82
  { name: Cities.Okayama, region: Regions.Yellow, x: 1390, y: 1286 },
76
83
  { name: Cities.Takamatsu, region: Regions.Yellow, x: 1268, y: 1507 },
77
- { name: Cities.Kochi, region: Regions.Yellow, x: 974, y: 1472 },
84
+ { name: Cities.Kochi, region: Regions.Yellow, x: 974, y: 1472, slotCosts: [10, 15] },
78
85
  { name: Cities.Matsuyama, region: Regions.Yellow, x: 1036, y: 1268 },
79
86
  { name: Cities.Hiroshima, region: Regions.Yellow, x: 1103, y: 1078 },
80
87
  { name: Cities.Shimonoseki, region: Regions.Red, x: 856, y: 962 },
81
- { name: Cities.Oita, region: Regions.Red, x: 731, y: 1166 },
82
- { name: Cities.Miyazaki, region: Regions.Red, x: 469, y: 1318 },
88
+ { name: Cities.Oita, region: Regions.Red, x: 731, y: 1166, slotCosts: [10, 15] },
89
+ { name: Cities.Miyazaki, region: Regions.Red, x: 469, y: 1318, slotCosts: [15, 20], stepSlots: [0, 1, 2] },
83
90
  { name: Cities.Kagoshima, region: Regions.Red, x: 157, y: 1255 },
84
91
  { name: Cities.Kumamoto, region: Regions.Red, x: 457, y: 1045 },
85
- { name: Cities.Fukuoka, region: Regions.Red, x: 644, y: 823 },
92
+ { name: Cities.Fukuoka, region: Regions.Red, x: 644, y: 823, slotCosts: [10, 10, 20], stepSlots: [2, 2, 3] },
86
93
  { name: Cities.Nagasaki, region: Regions.Red, x: 313, y: 875 },
87
94
  ],
95
+ layout: 'Portrait',
96
+ adjustRatio: [0.28, 0.28],
97
+ mapPosition: [0, 120],
98
+ resupply: [
99
+ // Coal
100
+ [
101
+ [3, 4, 3], // 2P
102
+ [4, 5, 3], // 3P
103
+ [5, 6, 4], // 4P
104
+ [5, 7, 5], // 5P
105
+ [7, 9, 6], // 6P
106
+ ],
107
+ // Oil
108
+ [
109
+ [2, 2, 4],
110
+ [2, 3, 4],
111
+ [3, 4, 5],
112
+ [4, 5, 6],
113
+ [5, 6, 7],
114
+ ],
115
+ // Garbage
116
+ [
117
+ [1, 2, 3],
118
+ [1, 2, 3],
119
+ [2, 3, 4],
120
+ [3, 3, 5],
121
+ [3, 5, 6],
122
+ ],
123
+ // Uranium
124
+ [
125
+ [1, 1, 1],
126
+ [1, 1, 1],
127
+ [1, 2, 2],
128
+ [2, 3, 2],
129
+ [2, 3, 3],
130
+ ],
131
+ ],
132
+ startingResources: [21, 15, 9, 3],
133
+ startingCities: ['Fukuoka', 'Kobe', 'Osaka', 'Sapporo', 'Tokyo', 'Yokohama'],
134
+ mapSpecificRules:
135
+ 'Each player can have two separate networks.\n' +
136
+ 'First houses must be placed in one of six starting cities: Fukuoka, Kobe, Osaka, Sapporo, Tokyo, or Yokohama.\n' +
137
+ 'Starting cities have two first-connection spots (cost 10 Elektro each); two players can build there in Step 1.\n' +
138
+ 'In Step 3, a third connection spot opens in starting cities (cost 20 Elektro).\n' +
139
+ 'Some smaller cities have only two building spots (costs 10 and 15, or 15 and 20 from Step 2).\n' +
140
+ 'If all spots in every starting city are taken, a player is forced to play with a single network.',
88
141
  connections: [
89
142
  { nodes: [Cities.Nagasaki, Cities.Fukuoka], cost: 10 },
90
143
  { nodes: [Cities.Fukuoka, Cities.Shimonoseki], cost: 10 },
@@ -141,3 +194,5 @@ export const map: GameMap = {
141
194
  { nodes: [Cities.Kumamoto, Cities.Fukuoka], cost: 8 },
142
195
  ],
143
196
  };
197
+
198
+ export const mapRecharged: GameMap = { ...map };
@@ -144,7 +144,7 @@ export const map: GameMap = {
144
144
  [Regions.Orange]: [
145
145
  // Denmark
146
146
  { number: 19, type: PowerPlantType.Oil, cost: 2, citiesPowered: 4 },
147
- { number: 26, type: PowerPlantType.Coal, cost: 3, citiesPowered: 5 },
147
+ { number: 26, type: PowerPlantType.Oil, cost: 3, citiesPowered: 5 },
148
148
  ],
149
149
  [Regions.Red]: [
150
150
  // Norway
package/src/maps.ts CHANGED
@@ -12,7 +12,7 @@ import { map as france } from './maps/france';
12
12
  import { map as germany, mapRecharged as germanyRecharged } from './maps/germany';
13
13
  import { map as indian } from './maps/indian';
14
14
  import { map as italy } from './maps/italy';
15
- // import { map as japan } from './maps/japan';
15
+ import { map as japan, mapRecharged as japanRecharged } from './maps/japan';
16
16
  import { map as korea } from './maps/korea';
17
17
  import { map as middleeast } from './maps/middleeast';
18
18
  import { map as northamerica } from './maps/northamerica';
@@ -32,6 +32,12 @@ export interface City {
32
32
  // and connecting to them costs a fixed price (transregionalConnectionCost on the GameMap)
33
33
  // instead of the normal dijkstra path cost.
34
34
  transregional?: boolean;
35
+ // Custom per-slot building costs (e.g. Japan). Length also determines total slots.
36
+ // Defaults to [10, 15, 20] if omitted.
37
+ slotCosts?: number[];
38
+ // Max slots open per step [step1, step2, step3].
39
+ // Defaults to [1, 2, 3] (standard Power Grid rules) if omitted.
40
+ stepSlots?: number[];
35
41
  // South Africa's six cross-border foreign-country spaces: only one player ever
36
42
  // builds here (cap 1 instead of the standard 3), and the build skips the
37
43
  // 10+position*5 house-base cost — the dijkstra path cost (the 30-Elektro edge)
@@ -106,6 +112,10 @@ export interface GameMap {
106
112
  // cost (there is no sea edge) and pay 10+position*5 + this surcharge.
107
113
  crossIslandSurcharge?: number;
108
114
  mapSpecificRules?: string;
115
+ // Cities where a player's first house (or second network) must be placed.
116
+ // Used by Japan: only the 6 designated starting cities are valid first builds,
117
+ // and a second disconnected network can only begin in one of these cities.
118
+ startingCities?: string[];
109
119
  // Dev-only: when set, the viewer renders an `<image>` backdrop behind the
110
120
  // map and logs click positions (in local SVG coords) to the console as
111
121
  // ready-to-paste `{ name, region, x, y }` lines. Intended for authoring
@@ -137,7 +147,7 @@ export const maps: GameMap[] = [
137
147
  southafrica,
138
148
  ukireland,
139
149
  // australia,
140
- // japan,
150
+ japan,
141
151
  ];
142
152
 
143
153
  export const mapsRecharged: GameMap[] = [
@@ -163,5 +173,5 @@ export const mapsRecharged: GameMap[] = [
163
173
  ukireland,
164
174
  // australia,
165
175
  // china,
166
- // japan,
176
+ japanRecharged,
167
177
  ];