powergrid-engine 1.9.7

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.
Files changed (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +31 -0
  3. package/dist/index.d.ts +6 -0
  4. package/dist/index.js +17 -0
  5. package/dist/src/available-moves.d.ts +24 -0
  6. package/dist/src/available-moves.js +363 -0
  7. package/dist/src/engine.d.ts +20 -0
  8. package/dist/src/engine.js +1937 -0
  9. package/dist/src/gamestate.d.ts +135 -0
  10. package/dist/src/gamestate.js +30 -0
  11. package/dist/src/log.d.ts +14 -0
  12. package/dist/src/log.js +2 -0
  13. package/dist/src/maps/america.d.ts +55 -0
  14. package/dist/src/maps/america.js +411 -0
  15. package/dist/src/maps/australia.d.ts +46 -0
  16. package/dist/src/maps/australia.js +138 -0
  17. package/dist/src/maps/badenwurttemberg.d.ts +46 -0
  18. package/dist/src/maps/badenwurttemberg.js +163 -0
  19. package/dist/src/maps/benelux.d.ts +46 -0
  20. package/dist/src/maps/benelux.js +210 -0
  21. package/dist/src/maps/brazil.d.ts +54 -0
  22. package/dist/src/maps/brazil.js +292 -0
  23. package/dist/src/maps/centraleurope.d.ts +54 -0
  24. package/dist/src/maps/centraleurope.js +236 -0
  25. package/dist/src/maps/china.d.ts +54 -0
  26. package/dist/src/maps/china.js +262 -0
  27. package/dist/src/maps/france.d.ts +54 -0
  28. package/dist/src/maps/france.js +290 -0
  29. package/dist/src/maps/germany.d.ts +57 -0
  30. package/dist/src/maps/germany.js +328 -0
  31. package/dist/src/maps/indian.d.ts +54 -0
  32. package/dist/src/maps/indian.js +283 -0
  33. package/dist/src/maps/italy.d.ts +54 -0
  34. package/dist/src/maps/italy.js +190 -0
  35. package/dist/src/maps/japan.d.ts +46 -0
  36. package/dist/src/maps/japan.js +144 -0
  37. package/dist/src/maps/korea.d.ts +54 -0
  38. package/dist/src/maps/korea.js +186 -0
  39. package/dist/src/maps/middleeast.d.ts +54 -0
  40. package/dist/src/maps/middleeast.js +225 -0
  41. package/dist/src/maps/northerneurope.d.ts +54 -0
  42. package/dist/src/maps/northerneurope.js +197 -0
  43. package/dist/src/maps/quebec.d.ts +54 -0
  44. package/dist/src/maps/quebec.js +283 -0
  45. package/dist/src/maps/russia.d.ts +54 -0
  46. package/dist/src/maps/russia.js +286 -0
  47. package/dist/src/maps/southafrica.d.ts +46 -0
  48. package/dist/src/maps/southafrica.js +152 -0
  49. package/dist/src/maps/spainportugal.d.ts +54 -0
  50. package/dist/src/maps/spainportugal.js +289 -0
  51. package/dist/src/maps/ukireland.d.ts +52 -0
  52. package/dist/src/maps/ukireland.js +176 -0
  53. package/dist/src/maps.d.ts +50 -0
  54. package/dist/src/maps.js +61 -0
  55. package/dist/src/move.d.ts +63 -0
  56. package/dist/src/move.js +15 -0
  57. package/dist/src/powerPlants.d.ts +4 -0
  58. package/dist/src/powerPlants.js +60 -0
  59. package/dist/src/prices.d.ts +7 -0
  60. package/dist/src/prices.js +10 -0
  61. package/dist/src/randomizeMap.d.ts +3 -0
  62. package/dist/src/randomizeMap.js +244 -0
  63. package/dist/src/utils.d.ts +2 -0
  64. package/dist/src/utils.js +24 -0
  65. package/dist/wrapper.d.ts +30 -0
  66. package/dist/wrapper.js +127 -0
  67. package/index.ts +6 -0
  68. package/package.json +51 -0
  69. package/src/available-moves.ts +450 -0
  70. package/src/engine.spec.ts +163 -0
  71. package/src/engine.ts +2270 -0
  72. package/src/fixtures/GermanyRecharged.json +6627 -0
  73. package/src/fixtures/USAOriginal.json +5216 -0
  74. package/src/fixtures/supply.json +5792 -0
  75. package/src/fixtures/undo.json +4102 -0
  76. package/src/gamestate.ts +164 -0
  77. package/src/log.ts +17 -0
  78. package/src/maps/america.ts +411 -0
  79. package/src/maps/australia.ts +137 -0
  80. package/src/maps/badenwurttemberg.ts +162 -0
  81. package/src/maps/benelux.ts +210 -0
  82. package/src/maps/brazil.ts +306 -0
  83. package/src/maps/centraleurope.ts +235 -0
  84. package/src/maps/china.ts +268 -0
  85. package/src/maps/france.ts +295 -0
  86. package/src/maps/germany.ts +328 -0
  87. package/src/maps/indian.ts +289 -0
  88. package/src/maps/italy.ts +189 -0
  89. package/src/maps/japan.ts +143 -0
  90. package/src/maps/korea.ts +185 -0
  91. package/src/maps/middleeast.ts +225 -0
  92. package/src/maps/northerneurope.ts +196 -0
  93. package/src/maps/quebec.ts +304 -0
  94. package/src/maps/russia.ts +292 -0
  95. package/src/maps/southafrica.ts +151 -0
  96. package/src/maps/spainportugal.ts +295 -0
  97. package/src/maps/ukireland.ts +175 -0
  98. package/src/maps.ts +123 -0
  99. package/src/move.ts +83 -0
  100. package/src/powerPlants.ts +59 -0
  101. package/src/prices.ts +10 -0
  102. package/src/randomizeMap.ts +288 -0
  103. package/src/rankings.spec.ts +18 -0
  104. package/src/utils.spec.ts +13 -0
  105. package/src/utils.ts +23 -0
  106. package/tsconfig.json +17 -0
  107. package/wrapper.ts +126 -0
package/src/maps.ts ADDED
@@ -0,0 +1,123 @@
1
+ import seedrandom from 'seedrandom';
2
+ import { PowerPlant } from './gamestate';
3
+ import { map as america, mapRecharged as americaRecharged } from './maps/america';
4
+ import { map as benelux } from './maps/benelux';
5
+ import { map as brazil } from './maps/brazil';
6
+ import { map as centraleurope } from './maps/centraleurope';
7
+ import { map as china } from './maps/china';
8
+ import { map as france } from './maps/france';
9
+ import { map as germany, mapRecharged as germanyRecharged } from './maps/germany';
10
+ import { map as indian } from './maps/indian';
11
+ import { map as italy } from './maps/italy';
12
+ import { map as middleeast } from './maps/middleeast';
13
+ import { map as quebec } from './maps/quebec';
14
+ import { map as russia } from './maps/russia';
15
+ import { map as spainportugal } from './maps/spainportugal';
16
+ // import { map as australia } from './maps/australia';
17
+ // import { map as badenwurttemberg } from './maps/badenwurttemberg';
18
+ // import { map as japan } from './maps/japan';
19
+ // import { map as korea } from './maps/korea';
20
+ // import { map as northerneurope } from './maps/northerneurope';
21
+ // import { map as southafrica } from './maps/southafrica';
22
+ // import { map as ukireland } from './maps/ukireland';
23
+
24
+ export interface City {
25
+ name: string;
26
+ region: string;
27
+ x: number;
28
+ y: number;
29
+ }
30
+
31
+ export interface Connection {
32
+ nodes: string[];
33
+ cost: number;
34
+ }
35
+
36
+ export interface Polygon {
37
+ region: string;
38
+ points: number[][];
39
+ }
40
+
41
+ export interface GameMap {
42
+ name: string;
43
+ cities: City[];
44
+ connections: Connection[];
45
+ polygons?: Polygon[];
46
+ layout?: 'Portrait' | 'Landscape';
47
+ adjustRatio?: [number, number];
48
+ viewBox?: [number, number];
49
+ playerOrderPosition?: [number, number];
50
+ cityCountPosition?: [number, number];
51
+ powerPlantMarketPosition?: [number, number];
52
+ actualMarketWidth?: number;
53
+ mapPosition?: [number, number];
54
+ buttonsPosition?: [number, number];
55
+ playerBoardsPosition?: [number, number];
56
+ supplyPosition?: [number, number];
57
+ roundInfoPosition?: [number, number];
58
+ resupply?: number[][][];
59
+ startingResources?: number[];
60
+ startingSupply?: number[];
61
+ maxPriceAvailable?: number[]; // For India, only limited sections of the supply are available in step 1 and 2.
62
+ coalPrices?: number[];
63
+ oilPrices?: number[];
64
+ garbagePrices?: number[];
65
+ uraniumPrices?: number[];
66
+ setupDeck?: (
67
+ numPlayers: number,
68
+ variant: string,
69
+ rng: seedrandom.prng
70
+ ) => {
71
+ actualMarket: PowerPlant[];
72
+ futureMarket: PowerPlant[];
73
+ powerPlantsDeck: PowerPlant[];
74
+ };
75
+ mapSpecificRules?: string;
76
+ }
77
+
78
+ export const maps: GameMap[] = [
79
+ america,
80
+ germany,
81
+ brazil,
82
+ spainportugal,
83
+ france,
84
+ indian,
85
+ italy,
86
+ quebec,
87
+ middleeast,
88
+ china,
89
+ benelux,
90
+ russia,
91
+ centraleurope,
92
+ // australia,
93
+ // badenwurttemberg,
94
+ // japan,
95
+ // korea,
96
+ // northerneurope,
97
+ // southafrica,
98
+ // ukireland,
99
+ ];
100
+
101
+ export const mapsRecharged: GameMap[] = [
102
+ americaRecharged,
103
+ germanyRecharged,
104
+ brazil,
105
+ spainportugal,
106
+ france,
107
+ indian,
108
+ italy,
109
+ quebec,
110
+ middleeast,
111
+ china,
112
+ benelux,
113
+ russia,
114
+ centraleurope,
115
+ // australia,
116
+ // badenwurttemberg,
117
+ // china,
118
+ // japan,
119
+ // korea,
120
+ // northerneurope,
121
+ // southafrica,
122
+ // ukireland,
123
+ ];
package/src/move.ts ADDED
@@ -0,0 +1,83 @@
1
+ import { ResourceType } from './gamestate';
2
+
3
+ export declare namespace Moves {
4
+ export interface MoveChoosePowerPlant {
5
+ name: MoveName.ChoosePowerPlant;
6
+ data: number;
7
+ usedPlantDiscount?: boolean;
8
+ }
9
+
10
+ export interface MoveBid {
11
+ name: MoveName.Bid;
12
+ data: number;
13
+ }
14
+
15
+ export interface MoveDiscardPowerPlant {
16
+ name: MoveName.DiscardPowerPlant;
17
+ data: number;
18
+ extra?: number[];
19
+ }
20
+
21
+ export interface MoveDiscardResources {
22
+ name: MoveName.DiscardResources;
23
+ data: ResourceType;
24
+ }
25
+
26
+ export interface MoveBuyResource {
27
+ name: MoveName.BuyResource;
28
+ data: {
29
+ resource: ResourceType;
30
+ };
31
+ fromSupply?: boolean;
32
+ }
33
+
34
+ export interface MoveBuild {
35
+ name: MoveName.Build;
36
+ data: {
37
+ name: string;
38
+ price: number;
39
+ };
40
+ }
41
+
42
+ export interface MoveUsePowerPlant {
43
+ name: MoveName.UsePowerPlant;
44
+ data: {
45
+ powerPlant: number;
46
+ resourcesSpent: ResourceType[];
47
+ citiesPowered: number;
48
+ };
49
+ }
50
+
51
+ export interface MovePass {
52
+ name: MoveName.Pass;
53
+ data: true;
54
+ }
55
+
56
+ export interface MoveUndo {
57
+ name: MoveName.Undo;
58
+ data: boolean;
59
+ }
60
+ }
61
+
62
+ export type Move =
63
+ | Moves.MoveChoosePowerPlant
64
+ | Moves.MoveBid
65
+ | Moves.MoveDiscardPowerPlant
66
+ | Moves.MoveDiscardResources
67
+ | Moves.MoveBuyResource
68
+ | Moves.MoveBuild
69
+ | Moves.MoveUsePowerPlant
70
+ | Moves.MovePass
71
+ | Moves.MoveUndo;
72
+
73
+ export enum MoveName {
74
+ ChoosePowerPlant = 'ChoosePowerPlant',
75
+ Bid = 'Bid',
76
+ DiscardPowerPlant = 'DiscardPowerPlant',
77
+ DiscardResources = 'DiscardResources',
78
+ BuyResource = 'BuyResource',
79
+ Build = 'Build',
80
+ UsePowerPlant = 'UsePowerPlant',
81
+ Pass = 'Pass',
82
+ Undo = 'Undo',
83
+ }
@@ -0,0 +1,59 @@
1
+ import { cloneDeep } from 'lodash';
2
+ import { PowerPlant, PowerPlantType } from './gamestate';
3
+
4
+ const powerPlants: PowerPlant[] = [
5
+ { number: 3, type: PowerPlantType.Oil, cost: 2, citiesPowered: 1 },
6
+ { number: 4, type: PowerPlantType.Coal, cost: 2, citiesPowered: 1 },
7
+ { number: 5, type: PowerPlantType.Hybrid, cost: 2, citiesPowered: 1 },
8
+ { number: 6, type: PowerPlantType.Garbage, cost: 1, citiesPowered: 1 },
9
+ { number: 7, type: PowerPlantType.Oil, cost: 3, citiesPowered: 2 },
10
+ { number: 8, type: PowerPlantType.Coal, cost: 3, citiesPowered: 2 },
11
+ { number: 9, type: PowerPlantType.Oil, cost: 1, citiesPowered: 1 },
12
+ { number: 10, type: PowerPlantType.Coal, cost: 2, citiesPowered: 2 },
13
+ { number: 11, type: PowerPlantType.Uranium, cost: 1, citiesPowered: 2 },
14
+ { number: 12, type: PowerPlantType.Hybrid, cost: 2, citiesPowered: 2 },
15
+ { number: 13, type: PowerPlantType.Wind, cost: 0, citiesPowered: 1 },
16
+ { number: 14, type: PowerPlantType.Garbage, cost: 2, citiesPowered: 2 },
17
+ { number: 15, type: PowerPlantType.Coal, cost: 2, citiesPowered: 3 },
18
+ { number: 16, type: PowerPlantType.Oil, cost: 2, citiesPowered: 3 },
19
+ { number: 17, type: PowerPlantType.Uranium, cost: 1, citiesPowered: 2 },
20
+ { number: 18, type: PowerPlantType.Wind, cost: 0, citiesPowered: 2 },
21
+ { number: 19, type: PowerPlantType.Garbage, cost: 2, citiesPowered: 3 },
22
+ { number: 20, type: PowerPlantType.Coal, cost: 3, citiesPowered: 5 },
23
+ { number: 21, type: PowerPlantType.Hybrid, cost: 2, citiesPowered: 4 },
24
+ { number: 22, type: PowerPlantType.Wind, cost: 0, citiesPowered: 2 },
25
+ { number: 23, type: PowerPlantType.Uranium, cost: 1, citiesPowered: 3 },
26
+ { number: 24, type: PowerPlantType.Garbage, cost: 2, citiesPowered: 4 },
27
+ { number: 25, type: PowerPlantType.Coal, cost: 2, citiesPowered: 5 },
28
+ { number: 26, type: PowerPlantType.Oil, cost: 2, citiesPowered: 5 },
29
+ { number: 27, type: PowerPlantType.Wind, cost: 0, citiesPowered: 3 },
30
+ { number: 28, type: PowerPlantType.Uranium, cost: 1, citiesPowered: 4 },
31
+ { number: 29, type: PowerPlantType.Hybrid, cost: 1, citiesPowered: 4 },
32
+ { number: 30, type: PowerPlantType.Garbage, cost: 3, citiesPowered: 6 },
33
+ { number: 31, type: PowerPlantType.Coal, cost: 3, citiesPowered: 6 },
34
+ { number: 32, type: PowerPlantType.Oil, cost: 3, citiesPowered: 6 },
35
+ { number: 33, type: PowerPlantType.Wind, cost: 0, citiesPowered: 4 },
36
+ { number: 34, type: PowerPlantType.Uranium, cost: 1, citiesPowered: 5 },
37
+ { number: 35, type: PowerPlantType.Oil, cost: 1, citiesPowered: 5 },
38
+ { number: 36, type: PowerPlantType.Coal, cost: 3, citiesPowered: 7 },
39
+ { number: 37, type: PowerPlantType.Wind, cost: 0, citiesPowered: 4 },
40
+ { number: 38, type: PowerPlantType.Garbage, cost: 3, citiesPowered: 7 },
41
+ { number: 39, type: PowerPlantType.Uranium, cost: 1, citiesPowered: 6 },
42
+ { number: 40, type: PowerPlantType.Oil, cost: 2, citiesPowered: 6 },
43
+ { number: 42, type: PowerPlantType.Coal, cost: 2, citiesPowered: 6 },
44
+ { number: 44, type: PowerPlantType.Wind, cost: 0, citiesPowered: 5 },
45
+ { number: 46, type: PowerPlantType.Hybrid, cost: 3, citiesPowered: 7 },
46
+ { number: 50, type: PowerPlantType.Nuclear, cost: 0, citiesPowered: 6 },
47
+ { number: 99, type: PowerPlantType.Step3, cost: 0, citiesPowered: 6 },
48
+ ];
49
+
50
+ const indiaPowerPlants = cloneDeep(powerPlants).filter((pp) => pp.number != 11);
51
+ // Garbage plants cost one more garbage to run, but have no additional storage.
52
+ indiaPowerPlants.forEach((pp) => {
53
+ if (pp.type == PowerPlantType.Garbage) {
54
+ pp.storage = 2 * pp.cost;
55
+ pp.cost++;
56
+ }
57
+ });
58
+
59
+ export { powerPlants, indiaPowerPlants };
package/src/prices.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { ResourceType } from './gamestate';
2
+
3
+ const prices = {
4
+ [ResourceType.Coal]: [1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8],
5
+ [ResourceType.Oil]: [1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8],
6
+ [ResourceType.Garbage]: [1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8],
7
+ [ResourceType.Uranium]: [1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16],
8
+ };
9
+
10
+ export default prices;
@@ -0,0 +1,288 @@
1
+ import Delaunator from 'delaunator';
2
+ import { upperFirst } from 'lodash';
3
+ import seedrandom from 'seedrandom';
4
+ import { City, Connection, GameMap } from './maps';
5
+
6
+ export function createRandomizedMap(map: GameMap, regionCount: number, rng: seedrandom.prng): GameMap {
7
+ const regions = [...new Set(map.cities.map((c) => c.region))];
8
+ const cities = Array(7 * regionCount)
9
+ .fill(0)
10
+ .map((_) => ({ name: '', region: '', x: 0, y: 0 }));
11
+ const widthByRegions = [420, 490, 490];
12
+ const heightByRegions = [490, 560, 700];
13
+ const width = widthByRegions[regionCount - 3];
14
+ const height = heightByRegions[regionCount - 3];
15
+ const gridCellSize = 70;
16
+ const gridWidth = Math.ceil(width / gridCellSize);
17
+ const gridHeight = Math.ceil(height / gridCellSize);
18
+ const gridLength = gridWidth * gridHeight;
19
+ const grid = Array(gridLength).fill(-1);
20
+
21
+ map.mapPosition = [100, 200];
22
+ map.layout = 'Portrait';
23
+
24
+ let emptyGrids = Array(gridLength)
25
+ .fill(0)
26
+ .map((_, i) => i);
27
+ cities.forEach((city, i) => {
28
+ const emptyGrid = emptyGrids[Math.floor(rng() * emptyGrids.length)];
29
+ const emptyGridY = Math.floor(emptyGrid / gridWidth);
30
+ const emptyGridX = emptyGrid % gridWidth;
31
+ city.x = Math.floor(emptyGridX * 2 * gridCellSize);
32
+ city.y = Math.floor(emptyGridY * gridCellSize);
33
+ if (emptyGridY % 2 === 0) {
34
+ city.x += gridCellSize;
35
+ }
36
+
37
+ const t = rng() * 2 * Math.PI;
38
+ const r = (rng() * gridCellSize) / 2;
39
+ city.x += r * Math.cos(t);
40
+ city.y += r * Math.sin(t);
41
+
42
+ emptyGrids = emptyGrids.filter((g) => g !== emptyGrid);
43
+ grid[emptyGrid] = i;
44
+ });
45
+
46
+ const cityDists: number[][] = Array(cities.length)
47
+ .fill(0)
48
+ .map((_) => Array(cities.length).fill(0));
49
+ const cityConnections: boolean[][] = Array(cities.length)
50
+ .fill(0)
51
+ .map((_) => Array(cities.length).fill(null));
52
+ const d = Delaunator.from(
53
+ cities,
54
+ (c: City) => c.x,
55
+ (c: City) => c.y
56
+ );
57
+ for (let i = 0; i < d.triangles.length; i += 3) {
58
+ const city1 = cities[d.triangles[i]];
59
+ const city2 = cities[d.triangles[i + 1]];
60
+ const city3 = cities[d.triangles[i + 2]];
61
+
62
+ let dist = cityDists[d.triangles[i]][d.triangles[i + 1]];
63
+ const dist12Squared = dist
64
+ ? Math.pow(dist, 2)
65
+ : Math.pow(city1.x - city2.x, 2) + Math.pow(city1.y - city2.y, 2);
66
+ const dist12 =
67
+ dist ||
68
+ (cityDists[d.triangles[i + 1]][d.triangles[i]] = cityDists[d.triangles[i]][d.triangles[i + 1]] =
69
+ Math.sqrt(dist12Squared));
70
+
71
+ dist = cityDists[d.triangles[i]][d.triangles[i + 2]];
72
+ const dist13Squared = dist
73
+ ? Math.pow(dist, 2)
74
+ : Math.pow(city1.x - city3.x, 2) + Math.pow(city1.y - city3.y, 2);
75
+ const dist13 =
76
+ dist ||
77
+ (cityDists[d.triangles[i + 2]][d.triangles[i]] = cityDists[d.triangles[i]][d.triangles[i + 2]] =
78
+ Math.sqrt(dist13Squared));
79
+
80
+ dist = cityDists[d.triangles[i + 1]][d.triangles[i + 2]];
81
+ const dist23Squared = dist
82
+ ? Math.pow(dist, 2)
83
+ : Math.pow(city2.x - city3.x, 2) + Math.pow(city2.y - city3.y, 2);
84
+ const dist23 =
85
+ dist ||
86
+ (cityDists[d.triangles[i + 2]][d.triangles[i + 1]] = cityDists[d.triangles[i + 1]][d.triangles[i + 2]] =
87
+ Math.sqrt(dist23Squared));
88
+
89
+ // arccos((P122 + P132 - P232) / (2 * P12 * P13))
90
+ const angle1 = Math.acos((dist12Squared + dist13Squared - dist23Squared) / (2 * dist12 * dist13));
91
+ const angle2 = Math.acos((dist12Squared + dist23Squared - dist13Squared) / (2 * dist12 * dist23));
92
+ const angle3 = Math.PI - angle1 - angle2;
93
+
94
+ const maxAngle = (105 * Math.PI) / 180;
95
+ if (cityConnections[d.triangles[i]][d.triangles[i + 1]] !== false && angle3 < maxAngle) {
96
+ cityConnections[d.triangles[i]][d.triangles[i + 1]] = true;
97
+ cityConnections[d.triangles[i + 1]][d.triangles[i]] = true;
98
+ } else {
99
+ cityConnections[d.triangles[i]][d.triangles[i + 1]] = false;
100
+ cityConnections[d.triangles[i + 1]][d.triangles[i]] = false;
101
+ }
102
+
103
+ if (cityConnections[d.triangles[i]][d.triangles[i + 2]] !== false && angle2 < maxAngle) {
104
+ cityConnections[d.triangles[i]][d.triangles[i + 2]] = true;
105
+ cityConnections[d.triangles[i + 2]][d.triangles[i]] = true;
106
+ } else {
107
+ cityConnections[d.triangles[i]][d.triangles[i + 2]] = false;
108
+ cityConnections[d.triangles[i + 2]][d.triangles[i]] = false;
109
+ }
110
+
111
+ if (cityConnections[d.triangles[i + 1]][d.triangles[i + 2]] !== false && angle1 < maxAngle) {
112
+ cityConnections[d.triangles[i + 1]][d.triangles[i + 2]] = true;
113
+ cityConnections[d.triangles[i + 2]][d.triangles[i + 1]] = true;
114
+ } else {
115
+ cityConnections[d.triangles[i + 1]][d.triangles[i + 2]] = false;
116
+ cityConnections[d.triangles[i + 2]][d.triangles[i + 1]] = false;
117
+ }
118
+ }
119
+
120
+ let maxDist = 0;
121
+ let minDist = 1000;
122
+ for (let i = 0; i < cities.length; i++) {
123
+ for (let j = 0; j < i; j++) {
124
+ if (cityConnections[i][j]) {
125
+ const dist = cityDists[i][j];
126
+ maxDist = Math.max(maxDist, dist);
127
+ minDist = Math.min(minDist, dist);
128
+ }
129
+ }
130
+ }
131
+
132
+ const centroids =
133
+ regionCount === 5
134
+ ? [
135
+ [width / 4, height / 4],
136
+ [width / 4, (3 * height) / 4],
137
+ [width / 2, height / 2],
138
+ [(3 * width) / 4, height / 4],
139
+ [(3 * width) / 4, (3 * height) / 4],
140
+ ]
141
+ : regionCount === 4
142
+ ? [
143
+ [width / 4, height / 4],
144
+ [width / 4, (3 * height) / 4],
145
+ [(3 * width) / 4, height / 4],
146
+ [(3 * width) / 4, (3 * height) / 4],
147
+ ]
148
+ : [
149
+ [width / 4, height / 4],
150
+ [width / 2, height / 2],
151
+ [(3 * width) / 4, (3 * height) / 4],
152
+ ];
153
+ const indexes = bkmeans(
154
+ cities.map((c) => [c.x, c.y]),
155
+ regionCount,
156
+ centroids,
157
+ cityConnections
158
+ );
159
+ const countRegions = Array(regionCount).fill(0);
160
+ cities.forEach((city, index) => {
161
+ city.region = regions[indexes[index]];
162
+ city.name = upperFirst(city.region) + ' ' + (countRegions[indexes[index]]++ + 1);
163
+ });
164
+
165
+ const connections: Connection[] = [];
166
+ for (let i = 0; i < cities.length; i++) {
167
+ for (let j = 0; j < i; j++) {
168
+ if (cityConnections[i][j]) {
169
+ const city1 = cities[i];
170
+ const city2 = cities[j];
171
+ const dist = cityDists[i][j];
172
+ connections.push({
173
+ nodes: [city1.name, city2.name],
174
+ cost: Math.floor((5 * dist) / gridCellSize),
175
+ });
176
+ }
177
+ }
178
+ }
179
+
180
+ map.cities = cities;
181
+ map.connections = connections;
182
+
183
+ return map;
184
+ }
185
+
186
+ function bkmeans(
187
+ points: number[][],
188
+ regions: number,
189
+ centroids: number[][],
190
+ connections: boolean[][],
191
+ iterations = 100
192
+ ): number[] {
193
+ const indexes = Array(points.length).fill(0);
194
+ const pointsPerRegion = Math.ceil(points.length / regions);
195
+
196
+ let conv = false;
197
+ do {
198
+ // Assign
199
+ const regionsCount = Array(regions).fill(0);
200
+ const pointCentDists = Array(points.length)
201
+ .fill(0)
202
+ .map((_) => Array(regions).fill(0));
203
+ points.forEach((point, pi) => {
204
+ let minDist: number | null = null;
205
+ let index = 0;
206
+ centroids.forEach((centroid, ci) => {
207
+ const d = dist(point, centroid);
208
+ if (minDist === null || d < minDist) {
209
+ if (regionsCount[ci] < pointsPerRegion) {
210
+ minDist = d;
211
+ index = ci;
212
+ }
213
+ }
214
+
215
+ pointCentDists[pi][ci] = d;
216
+ });
217
+
218
+ indexes[pi] = index;
219
+ regionsCount[index]++;
220
+ });
221
+
222
+ // Update
223
+ const newCentroids = Array(centroids.length)
224
+ .fill(0)
225
+ .map((_) => [0, 0]);
226
+ const centroidsCount = Array(centroids.length).fill(0);
227
+ points.forEach((point, pi) => {
228
+ newCentroids[indexes[pi]][0] += point[0];
229
+ newCentroids[indexes[pi]][1] += point[1];
230
+ centroidsCount[indexes[pi]]++;
231
+ });
232
+
233
+ conv = true;
234
+ centroids.forEach((centroid, ci) => {
235
+ newCentroids[ci][0] = newCentroids[ci][0] / centroidsCount[ci];
236
+ newCentroids[ci][1] = newCentroids[ci][1] / centroidsCount[ci];
237
+
238
+ if (centroid[0] !== newCentroids[ci][0] || centroid[1] !== newCentroids[ci][1]) {
239
+ conv = false;
240
+ }
241
+ });
242
+
243
+ centroids = newCentroids;
244
+
245
+ console.log('iterations', iterations);
246
+ console.log('centroids', centroids);
247
+
248
+ conv = conv || --iterations === 0;
249
+ } while (!conv);
250
+
251
+ // Refine
252
+ let maxRefine = 10;
253
+ do {
254
+ console.log('Refining');
255
+ conv = true;
256
+ points.forEach((_, p1) => {
257
+ const index = indexes[p1];
258
+ const hasSameColorNeighbor = connections[p1].some((connection, p2) => connection && index === indexes[p2]);
259
+ if (!hasSameColorNeighbor) {
260
+ const centroid = centroids[index];
261
+ let minDist: number | null = null;
262
+ let closestNeighbor: number | null = null;
263
+ connections[p1].forEach((connection, p2) => {
264
+ if (connection) {
265
+ const neighbor = points[p2];
266
+ const d = dist(neighbor, centroid);
267
+ if (minDist === null || d < minDist) {
268
+ minDist = d;
269
+ closestNeighbor = p2;
270
+ }
271
+ }
272
+ });
273
+
274
+ indexes[p1] = indexes[closestNeighbor!];
275
+ indexes[closestNeighbor!] = index;
276
+
277
+ conv = false;
278
+ }
279
+ });
280
+ conv = conv || --maxRefine === 0;
281
+ } while (!conv);
282
+
283
+ return indexes;
284
+ }
285
+
286
+ function dist(p1: number[], p2: number[]): number {
287
+ return (p1[0] - p2[0]) * (p1[0] - p2[0]) + (p1[1] - p2[1]) * (p1[1] - p2[1]);
288
+ }
@@ -0,0 +1,18 @@
1
+ import { expect } from 'chai';
2
+ import { rankings } from '../wrapper';
3
+ import { GameState } from './gamestate';
4
+
5
+ describe('rankings', () => {
6
+ it('should rank players correctly', () => {
7
+ const players: any[] = [
8
+ { id: 0, citiesPowered: 20, money: 30, cities: { length: 21 } },
9
+ { id: 1, citiesPowered: 20, money: 20, cities: { length: 21 } },
10
+ { id: 2, citiesPowered: 20, money: 30, cities: { length: 21 } },
11
+ { id: 4, citiesPowered: 18, money: 30, cities: { length: 21 } },
12
+ { id: 3, citiesPowered: 18, money: 30, cities: { length: 20 } },
13
+ { id: 5, citiesPowered: 19, money: 30, cities: { length: 21 } },
14
+ ];
15
+
16
+ expect(rankings({ players } as GameState)).to.have.ordered.members([1, 3, 1, 5, 6, 4]);
17
+ });
18
+ });
@@ -0,0 +1,13 @@
1
+ import { expect } from 'chai';
2
+ import { shuffle } from './utils';
3
+
4
+ describe('utils', () => {
5
+ describe('shuffle', () => {
6
+ it('should shuffle the same way based on the seed', () => {
7
+ expect(shuffle([1, 2, 3, 4], 'a')).to.have.ordered.members([4, 2, 3, 1]);
8
+ expect(shuffle([1, 2, 3, 4], 'b')).to.have.ordered.members([3, 1, 2, 4]);
9
+ expect(shuffle([1, 2, 3, 4], 'b')).to.have.ordered.members([3, 1, 2, 4]);
10
+ expect(shuffle([1, 2, 3, 4], 'a')).to.have.ordered.members([4, 2, 3, 1]);
11
+ });
12
+ });
13
+ });
package/src/utils.ts ADDED
@@ -0,0 +1,23 @@
1
+ import seedrandom from 'seedrandom';
2
+
3
+ /* eslint-disable */
4
+ export function asserts<T>(move: any): asserts move is T {}
5
+
6
+ /* eslint-enable */
7
+
8
+ export function shuffle<T>(array: T[], seed: string): T[] {
9
+ const rng = seedrandom.alea(seed);
10
+ const reverse = new Map<number, number>();
11
+
12
+ array.forEach((item, i) => {
13
+ let n = rng.int32();
14
+
15
+ while (reverse.has(n)) {
16
+ n = rng.int32();
17
+ }
18
+
19
+ reverse.set(n, i);
20
+ });
21
+
22
+ return [...reverse.keys()].sort().map((n) => array[reverse.get(n)!]);
23
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ /* Basic Options */
4
+ "target": "es2018",
5
+ "module": "commonjs",
6
+ "esModuleInterop": true,
7
+ "strictNullChecks": true,
8
+ "declaration": true /* Generates corresponding '.d.ts' file. */,
9
+ "sourceMap": false /* Generates corresponding '.map' file. */,
10
+ // "outFile": "./", /* Concatenate and emit output to single file. */
11
+ "outDir": "./dist" /* Redirect output structure to the directory. */,
12
+ "resolveJsonModule": true
13
+ },
14
+ "types": ["node"],
15
+ "include": ["src/**/*.ts", "index.ts", "wrapper.ts"],
16
+ "exclude": ["node_modules", "dist", "**/*.spec.ts"]
17
+ }