powergrid-viewer 2.0.5 → 2.0.6
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 +2 -2
- package/src/components/Game.vue +52 -1
- package/src/components/boards/ResourceBoxes.vue +63 -1
- package/src/components/boards/Resources.vue +80 -16
- package/src/components/pieces/Coal.vue +1 -1
- package/src/components/pieces/Garbage.vue +1 -1
- package/src/components/pieces/Oil.vue +1 -1
- package/src/components/pieces/Piece.vue +14 -0
- package/src/components/pieces/Uranium.vue +1 -1
- package/src/types/ui-data.ts +5 -0
- package/src/util/turn-buffer.ts +66 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "powergrid-viewer",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.6",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/boardgamers/powergrid.git",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"vue": "^2.6.11",
|
|
19
19
|
"vue-class-component": "^7.2.3",
|
|
20
20
|
"vue-property-decorator": "^8.4.2",
|
|
21
|
-
"powergrid-engine": "2.0.
|
|
21
|
+
"powergrid-engine": "2.0.4"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/assert": "^1.4.7",
|
package/src/components/Game.vue
CHANGED
|
@@ -156,7 +156,9 @@
|
|
|
156
156
|
:buyableResources="buyableResources()"
|
|
157
157
|
:resourceResupply="getResourceResupply()"
|
|
158
158
|
:resourceResupplyNorth="getResourceResupplyNorth()"
|
|
159
|
+
:bufferedBuys="bufferedBuys"
|
|
159
160
|
@buyResource="buyResource($event)"
|
|
161
|
+
@unbuyResource="unbuyResource($event)"
|
|
160
162
|
/>
|
|
161
163
|
<Resources
|
|
162
164
|
v-else
|
|
@@ -174,7 +176,9 @@
|
|
|
174
176
|
:coalStorage="G.coalStorage"
|
|
175
177
|
:resourceResupply="getResourceResupply()"
|
|
176
178
|
:resourceResupplyNorth="getResourceResupplyNorth()"
|
|
179
|
+
:bufferedBuys="bufferedBuys"
|
|
177
180
|
@buyResource="buyResource($event)"
|
|
181
|
+
@unbuyResource="unbuyResource($event)"
|
|
178
182
|
/>
|
|
179
183
|
</g>
|
|
180
184
|
|
|
@@ -721,7 +725,13 @@ import { Vue, Component, Prop, Watch, Provide, ProvideReactive, Ref } from 'vue-
|
|
|
721
725
|
import { MoveName, ended, playersSortedByScore, reconstructState } from 'powergrid-engine';
|
|
722
726
|
import type { GameState, LogItem, Move, Player } from 'powergrid-engine';
|
|
723
727
|
import { EventEmitter } from 'events';
|
|
724
|
-
import {
|
|
728
|
+
import {
|
|
729
|
+
bufferedBuyCounts,
|
|
730
|
+
lastBuyIndex,
|
|
731
|
+
matchesTurnBuffer,
|
|
732
|
+
rebaseTurnBuffer,
|
|
733
|
+
replayTurnBuffer as replayBuffer,
|
|
734
|
+
} from '../util/turn-buffer';
|
|
725
735
|
import { UIData, Preferences } from '../types/ui-data';
|
|
726
736
|
import { Card, House, Coal, Oil, Garbage, Uranium } from './pieces';
|
|
727
737
|
import {
|
|
@@ -1261,6 +1271,47 @@ export default class Game extends Vue {
|
|
|
1261
1271
|
this.sendMove({ name: MoveName.BuyResource, data });
|
|
1262
1272
|
}
|
|
1263
1273
|
|
|
1274
|
+
/**
|
|
1275
|
+
* Take back a resource bought earlier in this same turn (#127): a market's empty
|
|
1276
|
+
* spaces are clickable while the buffer still holds a purchase from that source.
|
|
1277
|
+
*
|
|
1278
|
+
* This is not an engine undo — there is no Undo move any more (2.0.0). A tentative
|
|
1279
|
+
* turn lives only in this buffer, so a purchase is taken back by dropping it and
|
|
1280
|
+
* replaying the rest, exactly as `undo()` drops the last move. Removing a buy only
|
|
1281
|
+
* ever frees money and plant capacity, so the remaining buffer always replays.
|
|
1282
|
+
*/
|
|
1283
|
+
unbuyResource(payload: { resource: ResourceType, side?: 'north' | 'south', fromStorage?: boolean }) {
|
|
1284
|
+
if (this.paused || !this.committedState) {
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
const index = lastBuyIndex(this.turnMoves, payload);
|
|
1289
|
+
|
|
1290
|
+
if (index < 0) {
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
this.turnMoves.splice(index, 1);
|
|
1295
|
+
|
|
1296
|
+
if (this.turnMoves.length > 0) {
|
|
1297
|
+
const preview = this.replayTurnBuffer();
|
|
1298
|
+
this.emitter.emit('move', [...this.turnMoves]);
|
|
1299
|
+
this.replaceState(preview, false);
|
|
1300
|
+
} else {
|
|
1301
|
+
// Empty buffer: nothing to send — nothing was ever persisted for this turn,
|
|
1302
|
+
// so the last committed state IS the turn start.
|
|
1303
|
+
this.replaceState(this.committedState, false);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
/**
|
|
1308
|
+
* Cubes the turn buffer would give back, per source — how many of each market's
|
|
1309
|
+
* empty spaces are clickable.
|
|
1310
|
+
*/
|
|
1311
|
+
get bufferedBuys(): Record<string, number> {
|
|
1312
|
+
return bufferedBuyCounts(this.turnMoves);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1264
1315
|
bid(bid: number) {
|
|
1265
1316
|
this.sendMove({ name: MoveName.Bid, data: bid });
|
|
1266
1317
|
}
|
|
@@ -116,6 +116,44 @@
|
|
|
116
116
|
step {{ step }} sells to ${{ maxPrice }}
|
|
117
117
|
</text>
|
|
118
118
|
|
|
119
|
+
<!-- A purchase made this turn can be handed back (#127). The box
|
|
120
|
+
market has no empty spaces to click — the printed track's
|
|
121
|
+
ghosts are exactly what it replaces — so the take-back lives
|
|
122
|
+
on a chip, and stops the click reaching the buy target under
|
|
123
|
+
it. Sits in the free band above the cubes, and appears whether
|
|
124
|
+
or not the row is still buyable: being unable to afford the
|
|
125
|
+
next cube is precisely when you want the last one back. -->
|
|
126
|
+
<g v-if="canUnbuy(row)" class="canClick" @click.stop="$emit('unbuyResource', row.move)">
|
|
127
|
+
<rect
|
|
128
|
+
:x="chipBox(row).hx"
|
|
129
|
+
:y="chipBox(row).hy"
|
|
130
|
+
:width="chipBox(row).hw"
|
|
131
|
+
:height="chipBox(row).hh"
|
|
132
|
+
fill="transparent"
|
|
133
|
+
pointer-events="all"
|
|
134
|
+
/>
|
|
135
|
+
<rect
|
|
136
|
+
:x="chipBox(row).x"
|
|
137
|
+
:y="chipBox(row).y"
|
|
138
|
+
:width="chipBox(row).w"
|
|
139
|
+
:height="chipBox(row).h"
|
|
140
|
+
rx="10"
|
|
141
|
+
fill="#f0e6c8"
|
|
142
|
+
stroke="#6b5312"
|
|
143
|
+
stroke-width="2"
|
|
144
|
+
/>
|
|
145
|
+
<text
|
|
146
|
+
:x="chipBox(row).x + chipBox(row).w / 2"
|
|
147
|
+
:y="chipBox(row).y + chipBox(row).h / 2 + 8"
|
|
148
|
+
text-anchor="middle"
|
|
149
|
+
font-weight="700"
|
|
150
|
+
fill="#3a2c08"
|
|
151
|
+
:style="`font-size: ${chipBox(row).fontSize}px`"
|
|
152
|
+
>
|
|
153
|
+
− take back
|
|
154
|
+
</text>
|
|
155
|
+
</g>
|
|
156
|
+
|
|
119
157
|
<title>{{ row.title }}</title>
|
|
120
158
|
</g>
|
|
121
159
|
</g>
|
|
@@ -126,7 +164,8 @@
|
|
|
126
164
|
<script lang="ts">
|
|
127
165
|
import { GameState } from 'powergrid-engine';
|
|
128
166
|
import { Component, Prop, Vue } from 'vue-property-decorator';
|
|
129
|
-
import { BuyMove, ResourceBlock, resourceBlocks } from '../../util/resource-rows';
|
|
167
|
+
import { BuyMove, ResourceBlock, ResourceRow, resourceBlocks } from '../../util/resource-rows';
|
|
168
|
+
import { buySourceKey } from '../../util/turn-buffer';
|
|
130
169
|
|
|
131
170
|
const BOX_W = 700;
|
|
132
171
|
const BOX_H = 104;
|
|
@@ -167,6 +206,8 @@ export default class ResourceBoxes extends Vue {
|
|
|
167
206
|
@Prop() isUsaRecharged?: boolean;
|
|
168
207
|
/** The seat looking at the board — only needed for Central Europe's Wien discount. */
|
|
169
208
|
@Prop() player?: number;
|
|
209
|
+
/** Purchases still sitting in this turn's buffer, per source (#127). */
|
|
210
|
+
@Prop() bufferedBuys?: Record<string, number>;
|
|
170
211
|
|
|
171
212
|
BOX_W = BOX_W;
|
|
172
213
|
BOX_H = BOX_H;
|
|
@@ -174,6 +215,27 @@ export default class ResourceBoxes extends Vue {
|
|
|
174
215
|
HEAD_H = HEAD_H;
|
|
175
216
|
CUBE_BAND_CENTRE = CUBE_BAND_CENTRE;
|
|
176
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Where a row's take-back button sits: in the gap between the price block and the
|
|
220
|
+
* cubes, and big enough for a thumb (~28 css px tall on a phone).
|
|
221
|
+
*
|
|
222
|
+
* The two pool rows carry a long label right across that gap — but they are
|
|
223
|
+
* flat-priced, so nothing sits below them, and the button drops under the label at
|
|
224
|
+
* the same size rather than shrinking.
|
|
225
|
+
*/
|
|
226
|
+
chipBox(row: ResourceRow) {
|
|
227
|
+
const labelFillsTheTop = row.label.length > 8;
|
|
228
|
+
|
|
229
|
+
return labelFillsTheTop
|
|
230
|
+
? { x: 130, y: 36, w: 140, h: 52, hx: 120, hy: 32, hw: 164, hh: 60, fontSize: 22 }
|
|
231
|
+
: { x: 130, y: 6, w: 140, h: 52, hx: 120, hy: 2, hw: 164, hh: 60, fontSize: 22 };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Does this turn's buffer hold a purchase from this row's source? */
|
|
235
|
+
canUnbuy(row: ResourceRow): boolean {
|
|
236
|
+
return !!this.bufferedBuys && !!this.bufferedBuys[buySourceKey(row.move)];
|
|
237
|
+
}
|
|
238
|
+
|
|
177
239
|
/** Centre the cube cluster in that band, however many cubes there are. */
|
|
178
240
|
cubeX(count: number, n: number): number {
|
|
179
241
|
return CUBE_BAND_CENTRE - ((count - 1) * CUBE_PITCH) / 2 + (n - 1) * CUBE_PITCH;
|
|
@@ -75,10 +75,13 @@
|
|
|
75
75
|
:key="coal.id"
|
|
76
76
|
:pieceId="coal.id"
|
|
77
77
|
:targetState="{ x: coal.x, y: coal.y }"
|
|
78
|
-
:canClick="
|
|
78
|
+
:canClick="
|
|
79
|
+
coal.transparent ? canUnbuy('coal', coal) : canBuyResource('coal', coal.side, coal.fromStorage)
|
|
80
|
+
"
|
|
79
81
|
:transparent="coal.transparent"
|
|
82
|
+
:restorable="canUnbuy('coal', coal)"
|
|
80
83
|
:scale="0.08"
|
|
81
|
-
@click="
|
|
84
|
+
@click="clickResource('coal', coal)"
|
|
82
85
|
/>
|
|
83
86
|
</template>
|
|
84
87
|
<template v-for="oil in oilsNorth">
|
|
@@ -86,9 +89,10 @@
|
|
|
86
89
|
:key="oil.id"
|
|
87
90
|
:pieceId="oil.id"
|
|
88
91
|
:targetState="{ x: oil.x, y: oil.y }"
|
|
89
|
-
:canClick="
|
|
92
|
+
:canClick="oil.transparent ? canUnbuy('oil', oil) : canBuyResource('oil', oil.side)"
|
|
90
93
|
:transparent="oil.transparent"
|
|
91
|
-
|
|
94
|
+
:restorable="canUnbuy('oil', oil)"
|
|
95
|
+
@click="clickResource('oil', oil)"
|
|
92
96
|
/>
|
|
93
97
|
</template>
|
|
94
98
|
<template v-for="garbage in garbagesNorth">
|
|
@@ -96,9 +100,12 @@
|
|
|
96
100
|
:key="garbage.id"
|
|
97
101
|
:pieceId="garbage.id"
|
|
98
102
|
:targetState="{ x: garbage.x, y: garbage.y }"
|
|
99
|
-
:canClick="
|
|
103
|
+
:canClick="
|
|
104
|
+
garbage.transparent ? canUnbuy('garbage', garbage) : canBuyResource('garbage', garbage.side)
|
|
105
|
+
"
|
|
100
106
|
:transparent="garbage.transparent"
|
|
101
|
-
|
|
107
|
+
:restorable="canUnbuy('garbage', garbage)"
|
|
108
|
+
@click="clickResource('garbage', garbage)"
|
|
102
109
|
/>
|
|
103
110
|
</template>
|
|
104
111
|
<text x="20" y="35" font-weight="700" fill="black" style="font-size: 22px">South Market</text>
|
|
@@ -326,10 +333,13 @@
|
|
|
326
333
|
:key="coal.id"
|
|
327
334
|
:pieceId="coal.id"
|
|
328
335
|
:targetState="{ x: coal.x, y: coal.y }"
|
|
329
|
-
:canClick="
|
|
336
|
+
:canClick="
|
|
337
|
+
coal.transparent ? canUnbuy('coal', coal) : canBuyResource('coal', coal.side, coal.fromStorage)
|
|
338
|
+
"
|
|
330
339
|
:transparent="coal.transparent"
|
|
340
|
+
:restorable="canUnbuy('coal', coal)"
|
|
331
341
|
:scale="isIndiaResourceMarket ? 0.06 : 0.08"
|
|
332
|
-
@click="
|
|
342
|
+
@click="clickResource('coal', coal)"
|
|
333
343
|
/>
|
|
334
344
|
</template>
|
|
335
345
|
|
|
@@ -338,9 +348,10 @@
|
|
|
338
348
|
:key="oil.id"
|
|
339
349
|
:pieceId="oil.id"
|
|
340
350
|
:targetState="{ x: oil.x, y: oil.y }"
|
|
341
|
-
:canClick="
|
|
351
|
+
:canClick="oil.transparent ? canUnbuy('oil', oil) : canBuyResource('oil', oil.side)"
|
|
342
352
|
:transparent="oil.transparent"
|
|
343
|
-
|
|
353
|
+
:restorable="canUnbuy('oil', oil)"
|
|
354
|
+
@click="clickResource('oil', oil)"
|
|
344
355
|
/>
|
|
345
356
|
</template>
|
|
346
357
|
|
|
@@ -349,10 +360,11 @@
|
|
|
349
360
|
:key="garbage.id"
|
|
350
361
|
:pieceId="garbage.id"
|
|
351
362
|
:targetState="{ x: garbage.x, y: garbage.y }"
|
|
352
|
-
:canClick="
|
|
363
|
+
:canClick="garbage.transparent ? canUnbuy('garbage', garbage) : canBuyResource('garbage', garbage.side)"
|
|
353
364
|
:transparent="garbage.transparent"
|
|
365
|
+
:restorable="canUnbuy('garbage', garbage)"
|
|
354
366
|
:scale="isIndiaResourceMarket ? 0.8 : 1"
|
|
355
|
-
@click="
|
|
367
|
+
@click="clickResource('garbage', garbage)"
|
|
356
368
|
/>
|
|
357
369
|
</template>
|
|
358
370
|
|
|
@@ -361,9 +373,10 @@
|
|
|
361
373
|
:key="uranium.id"
|
|
362
374
|
:pieceId="uranium.id"
|
|
363
375
|
:targetState="{ x: uranium.x, y: uranium.y }"
|
|
364
|
-
:canClick="
|
|
376
|
+
:canClick="uranium.transparent ? canUnbuy('uranium', uranium) : canBuyResource('uranium', uranium.side)"
|
|
365
377
|
:transparent="uranium.transparent"
|
|
366
|
-
|
|
378
|
+
:restorable="canUnbuy('uranium', uranium)"
|
|
379
|
+
@click="clickResource('uranium', uranium)"
|
|
367
380
|
/>
|
|
368
381
|
</template>
|
|
369
382
|
|
|
@@ -426,6 +439,7 @@ import { Vue, Component, Prop, Inject } from 'vue-property-decorator';
|
|
|
426
439
|
import { Coal, Garbage, Oil, Uranium } from '../pieces';
|
|
427
440
|
import { Piece, Preferences } from '../../types/ui-data';
|
|
428
441
|
import { range } from 'lodash';
|
|
442
|
+
import { buySourceKey } from '../../util/turn-buffer';
|
|
429
443
|
|
|
430
444
|
@Component({
|
|
431
445
|
components: {
|
|
@@ -443,6 +457,9 @@ export default class Resources extends Vue {
|
|
|
443
457
|
// South Africa: number of coal cubes in the storage pool below the market.
|
|
444
458
|
// Undefined on all other maps (panel is hidden).
|
|
445
459
|
@Prop() coalStorage?: number;
|
|
460
|
+
// Purchases still sitting in this turn's buffer, per source (#127). Each one can be
|
|
461
|
+
// taken back by clicking the empty space it left behind.
|
|
462
|
+
@Prop() bufferedBuys?: Record<string, number>;
|
|
446
463
|
|
|
447
464
|
@Inject() preferences!: Preferences;
|
|
448
465
|
|
|
@@ -452,6 +469,7 @@ export default class Resources extends Vue {
|
|
|
452
469
|
uraniums: Piece[] = [];
|
|
453
470
|
|
|
454
471
|
isKorea: boolean = false;
|
|
472
|
+
coalComesFromSupply: boolean = false;
|
|
455
473
|
isNinePriceMarket: boolean = false;
|
|
456
474
|
// Australia: coal/oil/garbage-only market that can reach $10 after the Step 3
|
|
457
475
|
// CO2 tax. co2TaxActive closes the $1/$2 columns once that shift has happened.
|
|
@@ -501,6 +519,7 @@ export default class Resources extends Vue {
|
|
|
501
519
|
x,
|
|
502
520
|
y,
|
|
503
521
|
transparent: i < (prices.length - market),
|
|
522
|
+
ghostRank: (prices.length - market) - i,
|
|
504
523
|
side: options.side,
|
|
505
524
|
});
|
|
506
525
|
}
|
|
@@ -511,6 +530,10 @@ export default class Resources extends Vue {
|
|
|
511
530
|
createPieces(gameState: GameState) {
|
|
512
531
|
if (!gameState) return;
|
|
513
532
|
|
|
533
|
+
// USA recharged: once the printed track is bare, coal is bought from the $8
|
|
534
|
+
// supply pile, and a cube taken back returns THERE — so the empty track spaces
|
|
535
|
+
// must not offer to give it back (#127).
|
|
536
|
+
this.coalComesFromSupply = !!this.isUsaRecharged && gameState.coalMarket === 0 && gameState.coalSupply > 0;
|
|
514
537
|
this.isAustraliaMarket = gameState.map?.name === 'Australia';
|
|
515
538
|
this.co2TaxActive = this.isAustraliaMarket && gameState.step >= 3;
|
|
516
539
|
this.isBremenMarket = gameState.map?.name === 'Bremen';
|
|
@@ -551,6 +574,7 @@ export default class Resources extends Vue {
|
|
|
551
574
|
id: `uranium_${i}`,
|
|
552
575
|
x, y,
|
|
553
576
|
transparent: i < (uPrices.length - gameState.uraniumMarket),
|
|
577
|
+
ghostRank: (uPrices.length - gameState.uraniumMarket) - i,
|
|
554
578
|
side: 'south',
|
|
555
579
|
});
|
|
556
580
|
});
|
|
@@ -665,7 +689,8 @@ export default class Resources extends Vue {
|
|
|
665
689
|
id: 'coal_' + i,
|
|
666
690
|
x: 672 - 17 * index - 17 * Math.floor(index / 4),
|
|
667
691
|
y: 48,
|
|
668
|
-
transparent: i >= gameState!.coalMarket
|
|
692
|
+
transparent: i >= gameState!.coalMarket,
|
|
693
|
+
ghostRank: i - (gameState!.coalMarket) + 1,
|
|
669
694
|
});
|
|
670
695
|
});
|
|
671
696
|
} else {
|
|
@@ -677,6 +702,7 @@ export default class Resources extends Vue {
|
|
|
677
702
|
x: 668 - 23.5 * i - 14.5 * Math.floor(i / 3),
|
|
678
703
|
y: 48,
|
|
679
704
|
transparent: i >= gameState!.coalMarket,
|
|
705
|
+
ghostRank: i - (gameState!.coalMarket) + 1,
|
|
680
706
|
});
|
|
681
707
|
});
|
|
682
708
|
}
|
|
@@ -720,6 +746,7 @@ export default class Resources extends Vue {
|
|
|
720
746
|
x: 651 - 16 * adjustedIndex - 37 * Math.floor(adjustedIndex / 3),
|
|
721
747
|
y: 70,
|
|
722
748
|
transparent: i >= availableRegularOil,
|
|
749
|
+
ghostRank: i - (availableRegularOil) + 1,
|
|
723
750
|
});
|
|
724
751
|
});
|
|
725
752
|
} else {
|
|
@@ -731,6 +758,7 @@ export default class Resources extends Vue {
|
|
|
731
758
|
x: 651 - 16 * i - 37 * Math.floor(i / 3),
|
|
732
759
|
y: 70,
|
|
733
760
|
transparent: i >= gameState!.oilMarket,
|
|
761
|
+
ghostRank: i - (gameState!.oilMarket) + 1,
|
|
734
762
|
});
|
|
735
763
|
});
|
|
736
764
|
}
|
|
@@ -746,6 +774,7 @@ export default class Resources extends Vue {
|
|
|
746
774
|
x: 672 - 17 * index - 17 * Math.floor(index / 4),
|
|
747
775
|
y: 94,
|
|
748
776
|
transparent: i >= gameState!.garbageMarket,
|
|
777
|
+
ghostRank: i - (gameState!.garbageMarket) + 1,
|
|
749
778
|
});
|
|
750
779
|
});
|
|
751
780
|
} else {
|
|
@@ -757,6 +786,7 @@ export default class Resources extends Vue {
|
|
|
757
786
|
x: 668 - 23.5 * i - 14.5 * Math.floor(i / 3),
|
|
758
787
|
y: 94,
|
|
759
788
|
transparent: i >= gameState!.garbageMarket,
|
|
789
|
+
ghostRank: i - (gameState!.garbageMarket) + 1,
|
|
760
790
|
});
|
|
761
791
|
});
|
|
762
792
|
}
|
|
@@ -770,7 +800,8 @@ export default class Resources extends Vue {
|
|
|
770
800
|
id: 'uranium_' + i,
|
|
771
801
|
x: 670 - 85 * i,
|
|
772
802
|
y: 72,
|
|
773
|
-
transparent: i >= gameState!.uraniumMarket
|
|
803
|
+
transparent: i >= gameState!.uraniumMarket,
|
|
804
|
+
ghostRank: i - (gameState!.uraniumMarket) + 1,
|
|
774
805
|
});
|
|
775
806
|
});
|
|
776
807
|
} else {
|
|
@@ -783,6 +814,7 @@ export default class Resources extends Vue {
|
|
|
783
814
|
x: 750,
|
|
784
815
|
y: 91,
|
|
785
816
|
transparent: i >= gameState!.uraniumMarket,
|
|
817
|
+
ghostRank: i - (gameState!.uraniumMarket) + 1,
|
|
786
818
|
});
|
|
787
819
|
} else if (i == 1) {
|
|
788
820
|
this.uraniums.push({
|
|
@@ -790,6 +822,7 @@ export default class Resources extends Vue {
|
|
|
790
822
|
x: 710,
|
|
791
823
|
y: 91,
|
|
792
824
|
transparent: i >= gameState!.uraniumMarket,
|
|
825
|
+
ghostRank: i - (gameState!.uraniumMarket) + 1,
|
|
793
826
|
});
|
|
794
827
|
} else if (i == 2) {
|
|
795
828
|
this.uraniums.push({
|
|
@@ -797,6 +830,7 @@ export default class Resources extends Vue {
|
|
|
797
830
|
x: 750,
|
|
798
831
|
y: 52,
|
|
799
832
|
transparent: i >= gameState!.uraniumMarket,
|
|
833
|
+
ghostRank: i - (gameState!.uraniumMarket) + 1,
|
|
800
834
|
});
|
|
801
835
|
} else if (i == 3) {
|
|
802
836
|
this.uraniums.push({
|
|
@@ -804,6 +838,7 @@ export default class Resources extends Vue {
|
|
|
804
838
|
x: 710,
|
|
805
839
|
y: 52,
|
|
806
840
|
transparent: i >= gameState!.uraniumMarket,
|
|
841
|
+
ghostRank: i - (gameState!.uraniumMarket) + 1,
|
|
807
842
|
});
|
|
808
843
|
} else {
|
|
809
844
|
this.uraniums.push({
|
|
@@ -811,6 +846,7 @@ export default class Resources extends Vue {
|
|
|
811
846
|
x: 1010 - 85 * i,
|
|
812
847
|
y: 72,
|
|
813
848
|
transparent: i >= gameState!.uraniumMarket,
|
|
849
|
+
ghostRank: i - (gameState!.uraniumMarket) + 1,
|
|
814
850
|
});
|
|
815
851
|
}
|
|
816
852
|
});
|
|
@@ -835,5 +871,33 @@ export default class Resources extends Vue {
|
|
|
835
871
|
buyResource(resource: string, side?: 'north' | 'south', fromStorage?: boolean) {
|
|
836
872
|
this.$emit('buyResource', { resource, side, fromStorage });
|
|
837
873
|
}
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* Can this empty space give a cube back? Buying empties a track from its cheap end,
|
|
877
|
+
* so the `n` spaces nearest the fill line hold the `n` purchases this turn's buffer
|
|
878
|
+
* still has from that source — `ghostRank` counts out from that line.
|
|
879
|
+
*/
|
|
880
|
+
canUnbuy(resource: string, piece: Piece) {
|
|
881
|
+
if (!piece.transparent || !piece.ghostRank || !this.bufferedBuys) {
|
|
882
|
+
return false;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
if (this.coalComesFromSupply && resource === 'coal' && !piece.fromStorage) {
|
|
886
|
+
return false;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const buffered = this.bufferedBuys[buySourceKey({ resource, side: piece.side, fromStorage: piece.fromStorage })];
|
|
890
|
+
|
|
891
|
+
return !!buffered && piece.ghostRank <= buffered;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/** A cube buys; the empty space it left takes the purchase back. */
|
|
895
|
+
clickResource(resource: string, piece: Piece) {
|
|
896
|
+
if (piece.transparent) {
|
|
897
|
+
this.$emit('unbuyResource', { resource, side: piece.side, fromStorage: piece.fromStorage });
|
|
898
|
+
} else {
|
|
899
|
+
this.buyResource(resource, piece.side, piece.fromStorage);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
838
902
|
}
|
|
839
903
|
</script>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
:id="elId"
|
|
4
4
|
:class="[{ canClick: canClick }]"
|
|
5
5
|
:transform="`translate(${currentX}, ${currentY}) scale(${scale})`"
|
|
6
|
-
:opacity="
|
|
6
|
+
:opacity="pieceOpacity"
|
|
7
7
|
@click="canClick && $emit('click')"
|
|
8
8
|
>
|
|
9
9
|
<path d="M2,5 A9,5,0 0,1 18,5 L18,15 A9,5 0 0,1 2,15Z" fill="none" stroke="black" stroke-miterlimit="0" />
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
:id="elId"
|
|
4
4
|
:class="[{ canClick: canClick }]"
|
|
5
5
|
:transform="`translate(${currentX}, ${currentY}) scale(${scale})`"
|
|
6
|
-
:opacity="
|
|
6
|
+
:opacity="pieceOpacity"
|
|
7
7
|
@click="canClick && $emit('click')"
|
|
8
8
|
>
|
|
9
9
|
<path d="M6,4 A6,4,0 0,1 14,4 L14,16 A6,4 0 0,1 6,16Z" fill="none" stroke="black" />
|
|
@@ -27,6 +27,12 @@ export default class Piece extends Vue {
|
|
|
27
27
|
@Prop()
|
|
28
28
|
transparent?: boolean;
|
|
29
29
|
|
|
30
|
+
// An empty market space holding a purchase that can still be taken back (#127).
|
|
31
|
+
// Drawn brighter than a plain empty space so the click target is visible — there
|
|
32
|
+
// is no hover cursor to discover it with on a phone.
|
|
33
|
+
@Prop()
|
|
34
|
+
restorable?: boolean;
|
|
35
|
+
|
|
30
36
|
@Inject()
|
|
31
37
|
readonly communicator!: EventEmitter;
|
|
32
38
|
|
|
@@ -43,6 +49,14 @@ export default class Piece extends Vue {
|
|
|
43
49
|
|
|
44
50
|
moving = false;
|
|
45
51
|
|
|
52
|
+
get pieceOpacity(): number {
|
|
53
|
+
if (!this.transparent) {
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return this.restorable ? 0.6 : 0.3;
|
|
58
|
+
}
|
|
59
|
+
|
|
46
60
|
startTransitioning() {
|
|
47
61
|
if (this.transitioning) {
|
|
48
62
|
return;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
:id="elId"
|
|
4
4
|
:class="[{ canClick: canClick }]"
|
|
5
5
|
:transform="`translate(${currentX}, ${currentY})`"
|
|
6
|
-
:opacity="
|
|
6
|
+
:opacity="pieceOpacity"
|
|
7
7
|
@click="canClick && $emit('click')"
|
|
8
8
|
>
|
|
9
9
|
<path d="M3,3 l4,-2 l6,0 l4,2 l0,11 l-4,2 l-6,0 l-4,-2Z" fill="#F00" stroke="black" />
|
package/src/types/ui-data.ts
CHANGED
|
@@ -30,6 +30,11 @@ export interface Piece {
|
|
|
30
30
|
// South Africa: this coal cube sits in the storage pool below the market.
|
|
31
31
|
// Clicking emits buyResource with fromStorage:true (flat $8 buy).
|
|
32
32
|
fromStorage?: boolean;
|
|
33
|
+
// Empty spaces only: distance from the market's fill line, 1 for the space the
|
|
34
|
+
// next cube would come back to. A purchase made this turn can be taken back by
|
|
35
|
+
// clicking the ghost that holds it (#127), so the first `n` ranks are clickable
|
|
36
|
+
// when the turn buffer holds `n` buys from that source.
|
|
37
|
+
ghostRank?: number;
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
export enum PieceType {
|
package/src/util/turn-buffer.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isEqual } from 'lodash';
|
|
2
2
|
import type { GameState, LogItem, LogMove, Move } from 'powergrid-engine';
|
|
3
|
-
import { move as engineMove } from 'powergrid-engine';
|
|
3
|
+
import { move as engineMove, MoveName } from 'powergrid-engine';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Pure helpers for the viewer's tentative-turn buffer.
|
|
@@ -147,3 +147,68 @@ export function replayTurnBuffer(
|
|
|
147
147
|
|
|
148
148
|
return { state, applied };
|
|
149
149
|
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Where a resource purchase came from. The same resource bought from the north or the
|
|
153
|
+
* south table, or out of South Africa's storage pool, is a different source with its
|
|
154
|
+
* own cubes — so a decrement must give back a cube from the source that was clicked.
|
|
155
|
+
*/
|
|
156
|
+
export interface BuySource {
|
|
157
|
+
resource: string;
|
|
158
|
+
side?: 'north' | 'south';
|
|
159
|
+
fromStorage?: boolean;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Stable key for a buy source, so counts can be looked up per market row or track. */
|
|
163
|
+
export function buySourceKey(source: BuySource): string {
|
|
164
|
+
return `${source.resource}|${source.side || ''}|${source.fromStorage ? 'storage' : ''}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function isBuyFrom(move: Move, source: BuySource): boolean {
|
|
168
|
+
const data = move.data as BuySource | undefined;
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
move.name === MoveName.BuyResource &&
|
|
172
|
+
!!data &&
|
|
173
|
+
data.resource === source.resource &&
|
|
174
|
+
(data.side || '') === (source.side || '') &&
|
|
175
|
+
!!data.fromStorage === !!source.fromStorage
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* How many cubes of each source the buffer would give back, keyed by `buySourceKey`.
|
|
181
|
+
* The market draws one ghost per cube taken off it, so this is also how many of those
|
|
182
|
+
* ghosts can be clicked to take a purchase back.
|
|
183
|
+
*/
|
|
184
|
+
export function bufferedBuyCounts(turnMoves: Move[]): Record<string, number> {
|
|
185
|
+
const counts: Record<string, number> = {};
|
|
186
|
+
|
|
187
|
+
for (const move of turnMoves) {
|
|
188
|
+
if (move.name !== MoveName.BuyResource || !move.data) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const key = buySourceKey(move.data as BuySource);
|
|
193
|
+
counts[key] = (counts[key] || 0) + 1;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return counts;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Index of the purchase a decrement click should remove: the LAST buffered buy from
|
|
201
|
+
* that source. Cubes leave a track cheapest-first, so the last buy is the one the
|
|
202
|
+
* market hands back — and removing a purchase only ever frees money and plant capacity,
|
|
203
|
+
* so the rest of the buffer always replays (verified over 653 mid-buffer removals).
|
|
204
|
+
* Returns -1 when the buffer holds no purchase from that source.
|
|
205
|
+
*/
|
|
206
|
+
export function lastBuyIndex(turnMoves: Move[], source: BuySource): number {
|
|
207
|
+
for (let i = turnMoves.length - 1; i >= 0; i--) {
|
|
208
|
+
if (isBuyFrom(turnMoves[i], source)) {
|
|
209
|
+
return i;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return -1;
|
|
214
|
+
}
|