powergrid-viewer 2.0.4 → 2.0.5
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/powergrid-viewer.css +1 -1
- package/dist/powergrid-viewer.umd.min.js +4 -4
- package/package.json +1 -1
- package/src/components/Game.vue +172 -6
- package/src/components/boards/ResourceBoxes.vue +212 -0
- package/src/launch.ts +8 -3
- package/src/util/resource-rows.ts +246 -0
- package/dist/img/help.ba7779cc.svg +0 -1
- package/dist/img/log.04ef6981.svg +0 -1
- package/dist/img/pass.da9065dc.svg +0 -3
- package/dist/img/rules.64f9aae5.svg +0 -28
- package/dist/img/sound-off.72ada995.svg +0 -3
- package/dist/img/sound-on.c55edd90.svg +0 -3
- package/dist/img/undo.208666d2.svg +0 -3
- package/dist/media/notification.55fa47dd.ogg +0 -0
- package/dist/media/notification.ac905963.mp3 +0 -0
- package/dist/media/piece-drop.eef5f607.mp3 +0 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import type { GameState } from 'powergrid-engine';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The row model behind the portrait resource market (`boards/ResourceBoxes.vue`).
|
|
5
|
+
*
|
|
6
|
+
* Kept out of the component so it can be tested against the engine directly: every
|
|
7
|
+
* row's `price` is the money the engine will actually take for the next cube bought
|
|
8
|
+
* from that source, and `resource-rows.spec.ts` asserts exactly that by buying one.
|
|
9
|
+
*
|
|
10
|
+
* Written without optional chaining: webpack 4 parses the unit-test bundle and
|
|
11
|
+
* rejects `?.` (same note as `util/turn-buffer.ts`).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type BuyMove = { resource: string; side?: 'north' | 'south'; fromStorage?: boolean };
|
|
15
|
+
|
|
16
|
+
export interface ResourceRow {
|
|
17
|
+
label: string;
|
|
18
|
+
color: string;
|
|
19
|
+
/** What a tap sends. Identical in shape to the engine's BuyResource payload. */
|
|
20
|
+
move: BuyMove;
|
|
21
|
+
/** What the engine charges for the next cube here, or null when it is empty. */
|
|
22
|
+
price: number | null;
|
|
23
|
+
/** The price once the cubes at the current price run out. */
|
|
24
|
+
next: number | null;
|
|
25
|
+
/** How many cubes are still on sale at `price`. Zero for a flat pool. */
|
|
26
|
+
atPrice: number;
|
|
27
|
+
/** Cubes left in this source altogether. */
|
|
28
|
+
cubes: number;
|
|
29
|
+
buyable: boolean;
|
|
30
|
+
/** On the board but above the per-step price cap, so nobody may buy it yet. */
|
|
31
|
+
capped: boolean;
|
|
32
|
+
/** A flat-price pool with no track behind it: every cube costs the same. */
|
|
33
|
+
flat: boolean;
|
|
34
|
+
title: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ResourceBlock {
|
|
38
|
+
title?: string;
|
|
39
|
+
restock?: string;
|
|
40
|
+
rows: ResourceRow[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const RESOURCE_COLORS: Record<string, string> = {
|
|
44
|
+
coal: '#6b2028',
|
|
45
|
+
oil: '#15130f',
|
|
46
|
+
garbage: '#f2d024',
|
|
47
|
+
uranium: '#dc2626',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** The engine's own defaults, for maps that do not author their own arrays. */
|
|
51
|
+
const DEFAULT_PRICES: Record<string, number[]> = {
|
|
52
|
+
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],
|
|
53
|
+
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],
|
|
54
|
+
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],
|
|
55
|
+
uranium: [1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const FLAT_POOL_PRICE = 8;
|
|
59
|
+
|
|
60
|
+
export interface RowOptions {
|
|
61
|
+
/** The seat reading the board — only needed for Central Europe's Wien discount. */
|
|
62
|
+
player?: number;
|
|
63
|
+
buyable?: BuyMove[];
|
|
64
|
+
/** Resupply for this step, indexed coal / oil / garbage / uranium. */
|
|
65
|
+
resupply?: (number | string)[];
|
|
66
|
+
resupplyNorth?: (number | string)[];
|
|
67
|
+
isUsaRecharged?: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function capitalize(s: string): string {
|
|
71
|
+
return s[0].toUpperCase() + s.slice(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isKorea(G: GameState): boolean {
|
|
75
|
+
return G.coalPricesNorth !== undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** India and friends only sell up to a per-step price. 16 = no cap in practice. */
|
|
79
|
+
function maxPrice(G: GameState): number {
|
|
80
|
+
const table = (G.map as { maxPriceAvailable?: number[] }).maxPriceAvailable;
|
|
81
|
+
return table ? table[G.step - 1] : 16;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function pricesFor(G: GameState, resource: string, north: boolean): number[] {
|
|
85
|
+
const bag = G as unknown as Record<string, number[] | undefined>;
|
|
86
|
+
const authored = bag[resource + 'Prices' + (north ? 'North' : '')];
|
|
87
|
+
return authored !== undefined ? authored : DEFAULT_PRICES[resource];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function cubesIn(G: GameState, resource: string, north: boolean): number {
|
|
91
|
+
const bag = G as unknown as Record<string, number | undefined>;
|
|
92
|
+
const count = bag[resource + 'Market' + (north ? 'North' : '')];
|
|
93
|
+
return count === undefined ? 0 : count;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function sameMove(a: BuyMove, b: BuyMove): boolean {
|
|
97
|
+
return a.resource === b.resource && a.side === b.side && !!a.fromStorage === !!b.fromStorage;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Central Europe sells garbage a dollar cheaper to whoever holds Wien — and the
|
|
102
|
+
* engine really does charge the lower price, not just offer the move. The box shows
|
|
103
|
+
* the price THIS player would pay, because that is the only number a tap can act on.
|
|
104
|
+
*/
|
|
105
|
+
function wienDiscount(G: GameState, resource: string, player: number | undefined): number {
|
|
106
|
+
if (resource !== 'garbage' || G.map.name !== 'Central Europe' || player === undefined) return 0;
|
|
107
|
+
const me = G.players[player];
|
|
108
|
+
if (!me) return 0;
|
|
109
|
+
return me.cities.some((c) => c.name === 'Wien') ? 1 : 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function marketRow(G: GameState, resource: string, north: boolean, opts: RowOptions): ResourceRow {
|
|
113
|
+
const prices = pricesFor(G, resource, north);
|
|
114
|
+
const cubes = cubesIn(G, resource, north);
|
|
115
|
+
// Cubes fill from the dear end, so the next one sold is at `length - cubes`.
|
|
116
|
+
// Clamped only so a market holding more cubes than its track has slots reads as
|
|
117
|
+
// the cheapest price rather than as a blank — the engine indexes the same array
|
|
118
|
+
// the same way, so it could not price such a state either. Middle East's "surplus
|
|
119
|
+
// oil" is not that case: its track is the full 24 and the surplus IS the $1 end.
|
|
120
|
+
const idx = Math.max(0, prices.length - cubes);
|
|
121
|
+
|
|
122
|
+
let price: number | null = null;
|
|
123
|
+
let atPrice = 0;
|
|
124
|
+
let next: number | null = null;
|
|
125
|
+
|
|
126
|
+
if (cubes > 0) {
|
|
127
|
+
price = prices[idx] - (north ? 0 : wienDiscount(G, resource, opts.player));
|
|
128
|
+
for (let i = idx; i < prices.length && prices[i] === prices[idx]; i++) atPrice++;
|
|
129
|
+
for (let i = idx; i < prices.length; i++) {
|
|
130
|
+
if (prices[i] !== prices[idx]) {
|
|
131
|
+
next = prices[i] - (north ? 0 : wienDiscount(G, resource, opts.player));
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const move: BuyMove = isKorea(G) ? { resource, side: north ? 'north' : 'south' } : { resource };
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
label: capitalize(resource),
|
|
141
|
+
color: RESOURCE_COLORS[resource],
|
|
142
|
+
move,
|
|
143
|
+
price,
|
|
144
|
+
next,
|
|
145
|
+
atPrice,
|
|
146
|
+
cubes,
|
|
147
|
+
buyable: !!opts.buyable && opts.buyable.some((b) => sameMove(b, move)),
|
|
148
|
+
capped: price !== null && price > maxPrice(G),
|
|
149
|
+
flat: false,
|
|
150
|
+
title:
|
|
151
|
+
cubes > 0
|
|
152
|
+
? `${capitalize(resource)} — $${price} each, ${atPrice} at that price, ${cubes} left`
|
|
153
|
+
: `${capitalize(resource)} — market empty`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** South Africa's storage pool and USA recharged's supply: a flat $8, no track. */
|
|
158
|
+
function flatRow(G: GameState, cubes: number, fromStorage: boolean, label: string, opts: RowOptions): ResourceRow {
|
|
159
|
+
const move: BuyMove = fromStorage ? { resource: 'coal', fromStorage: true } : { resource: 'coal' };
|
|
160
|
+
return {
|
|
161
|
+
label,
|
|
162
|
+
color: RESOURCE_COLORS.coal,
|
|
163
|
+
move,
|
|
164
|
+
price: cubes > 0 ? FLAT_POOL_PRICE : null,
|
|
165
|
+
next: null,
|
|
166
|
+
atPrice: 0,
|
|
167
|
+
cubes,
|
|
168
|
+
buyable: !!opts.buyable && opts.buyable.some((b) => sameMove(b, move)),
|
|
169
|
+
capped: cubes > 0 && FLAT_POOL_PRICE > maxPrice(G),
|
|
170
|
+
flat: true,
|
|
171
|
+
title: `${label} — $${FLAT_POOL_PRICE} each, ${cubes} left`,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Does this map sell uranium on the main track at all? Australia moves it to a
|
|
177
|
+
* separate mine market and Bremen has no nuclear, so on both the row would be a
|
|
178
|
+
* permanently empty box.
|
|
179
|
+
*/
|
|
180
|
+
function uraniumOnTrack(G: GameState): boolean {
|
|
181
|
+
return G.map.name !== 'Australia' && G.map.name !== 'Bremen';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function rowsFor(G: GameState, north: boolean, opts: RowOptions): ResourceRow[] {
|
|
185
|
+
const rows: ResourceRow[] = [];
|
|
186
|
+
|
|
187
|
+
// USA recharged keeps selling coal at a flat $8 out of the supply once the printed
|
|
188
|
+
// track is bare, so the row would otherwise read "out" while the move is on offer.
|
|
189
|
+
if (!north && opts.isUsaRecharged && G.coalMarket === 0 && G.coalSupply > 0) {
|
|
190
|
+
rows.push(flatRow(G, G.coalSupply, false, 'Coal (from supply)', opts));
|
|
191
|
+
} else {
|
|
192
|
+
rows.push(marketRow(G, 'coal', north, opts));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// South Africa: used coal returns to a storage pool below the market and can
|
|
196
|
+
// always be bought back at $8, alongside whatever the track is charging. It sits
|
|
197
|
+
// directly under the market coal rather than at the bottom of the list, so the two
|
|
198
|
+
// coal prices are read together — the track is usually the cheaper of the two and
|
|
199
|
+
// that should be obvious without hunting.
|
|
200
|
+
if (!north && G.coalStorage !== undefined) {
|
|
201
|
+
rows.push(flatRow(G, G.coalStorage, true, 'Coal (from storage)', opts));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
rows.push(marketRow(G, 'oil', north, opts));
|
|
205
|
+
rows.push(marketRow(G, 'garbage', north, opts));
|
|
206
|
+
|
|
207
|
+
// Korea's north side has no uranium row.
|
|
208
|
+
if (!north && uraniumOnTrack(G)) {
|
|
209
|
+
rows.push(marketRow(G, 'uranium', north, opts));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return rows;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* The resupply table is indexed coal / oil / garbage / uranium, and the track rows
|
|
217
|
+
* are drawn in that order — so the numbers line up with the boxes below them. Rows
|
|
218
|
+
* that are not part of the printed track (the flat pools) get no number.
|
|
219
|
+
*/
|
|
220
|
+
function restockText(table: (number | string)[] | undefined, rows: ResourceRow[]): string | undefined {
|
|
221
|
+
if (!table) return undefined;
|
|
222
|
+
const numbers = rows.filter((r) => !r.flat).map((r, i) => table[i]);
|
|
223
|
+
return numbers.every((n) => n === undefined) ? undefined : numbers.filter((n) => n !== undefined).join(' / ');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function resourceBlocks(G: GameState, opts: RowOptions = {}): ResourceBlock[] {
|
|
227
|
+
const blocks: ResourceBlock[] = [];
|
|
228
|
+
|
|
229
|
+
if (isKorea(G)) {
|
|
230
|
+
const northRows = rowsFor(G, true, opts);
|
|
231
|
+
blocks.push({
|
|
232
|
+
title: 'North Market',
|
|
233
|
+
restock: restockText(opts.resupplyNorth, northRows),
|
|
234
|
+
rows: northRows,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const southRows = rowsFor(G, false, opts);
|
|
239
|
+
blocks.push({
|
|
240
|
+
title: isKorea(G) ? 'South Market' : undefined,
|
|
241
|
+
restock: restockText(opts.resupply, southRows),
|
|
242
|
+
rows: southRows,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
return blocks;
|
|
246
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M14.601 21.5c0 1.38-1.116 2.5-2.499 2.5-1.378 0-2.499-1.12-2.499-2.5s1.121-2.5 2.499-2.5c1.383 0 2.499 1.119 2.499 2.5zm-2.42-21.5c-4.029 0-7.06 2.693-7.06 8h3.955c0-2.304.906-4.189 3.024-4.189 1.247 0 2.57.828 2.684 2.411.123 1.666-.767 2.511-1.892 3.582-2.924 2.78-2.816 4.049-2.816 7.196h3.943c0-1.452-.157-2.508 1.838-4.659 1.331-1.436 2.986-3.222 3.021-5.943.047-3.963-2.751-6.398-6.697-6.398z"/></svg>
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M24 12c0 6.627-5.373 12-12 12s-12-5.373-12-12h2c0 5.514 4.486 10 10 10s10-4.486 10-10-4.486-10-10-10c-2.777 0-5.287 1.141-7.099 2.977l2.061 2.061-6.962 1.354 1.305-7.013 2.179 2.18c2.172-2.196 5.182-3.559 8.516-3.559 6.627 0 12 5.373 12 12zm-13-6v8h7v-2h-5v-6h-2z"/></svg>
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="475.452px" height="475.451px" viewBox="0 0 475.452 475.451">
|
|
2
|
-
<g>
|
|
3
|
-
<path d="M468.083,118.385c-3.99-5.33-9.61-9.419-16.854-12.275c0.387,6.665-0.086,12.09-1.42,16.281l-85.65,281.789
|
|
4
|
-
c-1.526,4.948-4.859,8.897-9.992,11.848c-5.141,2.953-10.469,4.428-15.989,4.428H74.66c-22.84,0-36.542-6.652-41.112-19.985
|
|
5
|
-
c-1.903-5.14-1.807-9.229,0.288-12.275c2.092-2.857,5.708-4.288,10.85-4.288h248.102c17.702,0,29.93-3.285,36.688-9.852
|
|
6
|
-
c6.763-6.567,13.565-21.177,20.413-43.824l78.228-258.669c4.186-14.084,2.474-26.457-5.141-37.113s-18.462-15.987-32.548-15.987
|
|
7
|
-
H173.163c-2.474,0-7.329,0.854-14.562,2.568l0.284-0.859c-5.33-1.14-9.851-1.662-13.562-1.571
|
|
8
|
-
c-3.71,0.099-7.137,1.192-10.277,3.289c-3.14,2.094-5.664,4.328-7.566,6.706c-1.903,2.38-3.761,5.426-5.568,9.136
|
|
9
|
-
c-1.805,3.715-3.33,7.142-4.567,10.282c-1.237,3.14-2.666,6.473-4.281,9.998c-1.62,3.521-3.186,6.423-4.71,8.706
|
|
10
|
-
c-1.143,1.523-2.758,3.521-4.854,5.996c-2.091,2.474-3.805,4.664-5.137,6.567c-1.331,1.903-2.19,3.616-2.568,5.14
|
|
11
|
-
c-0.378,1.711-0.19,4.233,0.571,7.566c0.76,3.328,1.047,5.753,0.854,7.277c-0.76,7.232-3.378,16.414-7.849,27.552
|
|
12
|
-
c-4.471,11.136-8.52,19.18-12.135,24.126c-0.761,0.95-2.853,3.092-6.28,6.424c-3.427,3.33-5.52,6.23-6.279,8.704
|
|
13
|
-
c-0.762,0.951-0.81,3.617-0.144,7.994c0.666,4.38,0.907,7.423,0.715,9.136c-0.765,6.473-3.14,15.037-7.139,25.697
|
|
14
|
-
c-3.999,10.657-7.994,19.414-11.993,26.265c-0.569,1.141-2.185,3.328-4.853,6.567c-2.662,3.237-4.283,5.902-4.853,7.99
|
|
15
|
-
c-0.38,1.523-0.33,4.188,0.144,7.994c0.473,3.806,0.426,6.66-0.144,8.562c-1.521,7.228-4.377,15.94-8.565,26.125
|
|
16
|
-
c-4.187,10.178-8.47,18.896-12.851,26.121c-1.138,1.906-2.712,4.145-4.708,6.711c-1.999,2.566-3.568,4.805-4.711,6.707
|
|
17
|
-
c-1.141,1.903-1.903,3.901-2.284,5.996c-0.19,1.143,0.098,2.998,0.859,5.571c0.76,2.566,1.047,4.612,0.854,6.14
|
|
18
|
-
c-0.192,2.662-0.57,6.187-1.141,10.567c-0.572,4.373-0.859,6.939-0.859,7.699c-4.187,11.424-3.999,23.511,0.572,36.269
|
|
19
|
-
c5.33,14.838,14.797,27.36,28.406,37.541c13.61,10.185,27.74,15.27,42.398,15.27h263.521c12.367,0,24.026-4.141,34.971-12.416
|
|
20
|
-
c10.944-8.281,18.227-18.507,21.837-30.696l78.511-258.662C477.412,141.51,475.701,129.234,468.083,118.385z M164.31,118.956
|
|
21
|
-
l5.997-18.274c0.76-2.474,2.329-4.615,4.709-6.423c2.38-1.805,4.808-2.712,7.282-2.712h173.589c2.663,0,4.565,0.903,5.708,2.712
|
|
22
|
-
c1.14,1.809,1.335,3.949,0.575,6.423l-6.002,18.274c-0.764,2.475-2.327,4.611-4.713,6.424c-2.382,1.805-4.805,2.708-7.278,2.708
|
|
23
|
-
H170.593c-2.666,0-4.568-0.9-5.711-2.708C163.74,123.567,163.55,121.431,164.31,118.956z M140.615,192.045l5.996-18.271
|
|
24
|
-
c0.76-2.474,2.331-4.615,4.709-6.423c2.38-1.809,4.805-2.712,7.282-2.712h173.583c2.666,0,4.572,0.9,5.712,2.712
|
|
25
|
-
c1.14,1.809,1.331,3.949,0.568,6.423l-5.996,18.271c-0.759,2.474-2.33,4.617-4.708,6.423c-2.383,1.809-4.805,2.712-7.283,2.712
|
|
26
|
-
H146.895c-2.664,0-4.567-0.9-5.708-2.712C140.043,196.662,139.854,194.519,140.615,192.045z"/>
|
|
27
|
-
</g>
|
|
28
|
-
</svg>
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
|
2
|
-
<path d="M5 17h-5v-10h5v10zm2-10v10l9 5v-20l-9 5zm15.324 4.993l1.646-1.659-1.324-1.324-1.651 1.67-1.665-1.648-1.316 1.318 1.67 1.657-1.65 1.669 1.318 1.317 1.658-1.672 1.666 1.653 1.324-1.325-1.676-1.656z"/>
|
|
3
|
-
</svg>
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
<svg width="497.25px" height="497.25px" viewBox="0 0 497.25 497.25" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
-
<path fill="black" d="M248.625,89.25 V0 l-127.5,127.5 l127.5,127.5 V140.25 c84.15,0,153,68.85,153,153 c0,84.15-68.85,153-153,153 c-84.15,0-153-68.85-153-153 h-51 c0,112.2,91.8,204,204,204 s204-91.8,204-204 S360.825,89.25,248.625,89.25z"/>
|
|
3
|
-
</svg>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|