powergrid-engine 1.12.0 → 1.13.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.js +64 -6
- package/dist/src/engine.js +6 -2
- package/dist/src/gamestate.d.ts +1 -1
- package/dist/src/maps/badenwurttemberg.js +45 -5
- package/dist/src/maps/japan.js +69 -17
- package/dist/src/maps.d.ts +3 -0
- package/dist/src/maps.js +2 -2
- package/package.json +1 -1
- package/src/available-moves.ts +66 -6
- package/src/engine.spec.ts +44 -0
- package/src/engine.ts +7 -4
- package/src/gamestate.ts +2 -2
- package/src/maps/badenwurttemberg.ts +45 -5
- package/src/maps/japan.ts +70 -17
- package/src/maps.ts +12 -2
|
@@ -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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
326
|
-
|
|
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,
|
package/dist/src/engine.js
CHANGED
|
@@ -74,7 +74,11 @@ function setup(numPlayers, { fastBid = false, map = 'USA', variant = 'original',
|
|
|
74
74
|
var _a, _b, _c, _d, _e, _f, _g;
|
|
75
75
|
seed = seed !== null && seed !== void 0 ? seed : Math.random().toString();
|
|
76
76
|
const rng = seedrandom_1.default(seed);
|
|
77
|
-
const
|
|
77
|
+
const chosenMapRaw = variant == 'original' ? maps_1.maps.find((m) => m.name == map) : maps_1.mapsRecharged.find((m) => m.name == map);
|
|
78
|
+
if (!chosenMapRaw) {
|
|
79
|
+
throw new Error(`Map "${map}" not found for variant "${variant}"`);
|
|
80
|
+
}
|
|
81
|
+
const chosenMap = lodash_1.cloneDeep(chosenMapRaw);
|
|
78
82
|
let actualMarket;
|
|
79
83
|
let futureMarket;
|
|
80
84
|
let powerPlantsDeck;
|
|
@@ -532,7 +536,7 @@ function move(G, move, playerNumber, isUndo = false) {
|
|
|
532
536
|
G.map.name != 'China' &&
|
|
533
537
|
G.map.name != 'Russia') {
|
|
534
538
|
if (G.map.name == 'Baden-Württemberg') {
|
|
535
|
-
// Baden-Württemberg: remove the two lowest plants
|
|
539
|
+
// Baden-Württemberg: remove the two lowest plants when no one buys.
|
|
536
540
|
const removed = [];
|
|
537
541
|
for (let i = 0; i < 2 && G.actualMarket.length > 0; i++) {
|
|
538
542
|
removed.push(G.actualMarket[0].number);
|
package/dist/src/gamestate.d.ts
CHANGED
|
@@ -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
|
-
{
|
|
60
|
-
|
|
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
|
-
{
|
|
74
|
-
|
|
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
|
-
{
|
|
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 },
|
package/dist/src/maps/japan.js
CHANGED
|
@@ -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:
|
|
54
|
-
{ name: Cities.Sapporo, region: Regions.Brown, x: 3028, y:
|
|
55
|
-
{ name: Cities.Hakodate, region: Regions.Brown, x: 2622, y:
|
|
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, 2, 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, 2, 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
|
-
{
|
|
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, 2, 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, 2, 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, 2, 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 },
|
package/dist/src/maps.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
48
|
+
japan_1.map,
|
|
49
49
|
];
|
|
50
50
|
exports.mapsRecharged = [
|
|
51
51
|
america_1.mapRecharged,
|
package/package.json
CHANGED
package/src/available-moves.ts
CHANGED
|
@@ -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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
package/src/engine.spec.ts
CHANGED
|
@@ -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
|
@@ -95,9 +95,12 @@ export function setup(
|
|
|
95
95
|
seed = seed ?? Math.random().toString();
|
|
96
96
|
const rng = seedrandom(seed);
|
|
97
97
|
|
|
98
|
-
const
|
|
99
|
-
variant == 'original' ? maps.find((m) => m.name == map)
|
|
100
|
-
)
|
|
98
|
+
const chosenMapRaw =
|
|
99
|
+
variant == 'original' ? maps.find((m) => m.name == map) : mapsRecharged.find((m) => m.name == map);
|
|
100
|
+
if (!chosenMapRaw) {
|
|
101
|
+
throw new Error(`Map "${map}" not found for variant "${variant}"`);
|
|
102
|
+
}
|
|
103
|
+
const chosenMap = cloneDeep(chosenMapRaw);
|
|
101
104
|
|
|
102
105
|
let actualMarket: PowerPlant[];
|
|
103
106
|
let futureMarket: PowerPlant[];
|
|
@@ -625,7 +628,7 @@ export function move(G: GameState, move: Move, playerNumber: number, isUndo = fa
|
|
|
625
628
|
G.map.name != 'Russia'
|
|
626
629
|
) {
|
|
627
630
|
if (G.map.name == 'Baden-Württemberg') {
|
|
628
|
-
// Baden-Württemberg: remove the two lowest plants
|
|
631
|
+
// Baden-Württemberg: remove the two lowest plants when no one buys.
|
|
629
632
|
const removed: number[] = [];
|
|
630
633
|
for (let i = 0; i < 2 && G.actualMarket.length > 0; i++) {
|
|
631
634
|
removed.push(G.actualMarket[0].number);
|
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
|
-
{
|
|
59
|
-
|
|
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
|
-
{
|
|
73
|
-
|
|
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
|
-
{
|
|
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:
|
|
53
|
-
{ name: Cities.Sapporo, region: Regions.Brown, x: 3028, y:
|
|
54
|
-
{ name: Cities.Hakodate, region: Regions.Brown, x: 2622, y:
|
|
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, 2, 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, 2, 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
|
-
{
|
|
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, 2, 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, 2, 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, 2, 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 },
|
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
|
-
|
|
15
|
+
import { map as japan } 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
|
-
|
|
150
|
+
japan,
|
|
141
151
|
];
|
|
142
152
|
|
|
143
153
|
export const mapsRecharged: GameMap[] = [
|