powergrid-viewer 2.1.6 → 2.1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "powergrid-viewer",
3
- "version": "2.1.6",
3
+ "version": "2.1.7",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/boardgamers/powergrid.git",
@@ -20,7 +20,7 @@
20
20
  "vue-class-component": "^7.2.3",
21
21
  "vue-property-decorator": "^9.1.2",
22
22
  "zod": "4.6.4",
23
- "powergrid-engine": "2.0.6"
23
+ "powergrid-engine": "2.0.8"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/assert": "^1.4.7",
@@ -1,7 +1,41 @@
1
1
  <template>
2
2
  <div :class="['game', { fitToScreen: preferences.fitToScreen && !stacked, stacked: stacked }]">
3
- <div class="statusBar">
4
- {{ getStatusMessage() }}
3
+ <div class="planner-header" :class="{ planning: hasPlanPanel }">
4
+ <div class="statusBar">
5
+ <span class="status-message">{{ getStatusMessage() }}</span>
6
+ <button
7
+ v-if="!tutorialMove && !paused && canPlanRound && !roundPlan"
8
+ class="plan-entry"
9
+ :title="
10
+ canPreparePremoves
11
+ ? 'Plan cities and powering for this round'
12
+ : 'Simulate the rest of this round with the game controls'
13
+ "
14
+ @click="myPhaseQueue && myPhaseQueue.phases.length ? viewQueuedPlan() : startPlanning()"
15
+ >
16
+ <svg width="16" height="16" viewBox="0 0 20 20" aria-hidden="true">
17
+ <path d="M3 2v15h15M6 12l4-5 4 3 4-6" fill="none" stroke="currentColor" stroke-width="1.6" />
18
+ </svg>
19
+ {{ canPreparePremoves ? 'Plan' : 'Simulate'
20
+ }}<span v-if="queuedMoveCount">
21
+ · {{ queuedMoveCount }} {{ queuedMoveCount === 1 ? 'premove' : 'premoves' }}
22
+ </span>
23
+ </button>
24
+ <button v-if="roundPlan" class="plan-entry" @click="stopPlanning()">Return to live game</button>
25
+ </div>
26
+ <RoundPlanner
27
+ v-if="!tutorialMove && !paused && hasPlanPanel"
28
+ :plan="roundPlan"
29
+ :queue="myPhaseQueue"
30
+ :queueable="canQueueRound"
31
+ :plays-now="planPlaysNow"
32
+ :queue-hint="queueHint"
33
+ :pending="!!pendingPlanId"
34
+ :error="planningError"
35
+ :live-changed="planningChanged"
36
+ @queue="submitRoundPlan"
37
+ @cancel="cancelPhasePlan"
38
+ />
5
39
  </div>
6
40
  <svg
7
41
  v-if="G"
@@ -366,7 +400,7 @@
366
400
  transform="translate(15, 15)"
367
401
  :enabled="canPass()"
368
402
  :highlightButton="canPass() && !preferences.disableHelp"
369
- :text="tutorialMove || canUndo() ? 'Done' : 'Pass'"
403
+ :text="tutorialMove || roundPlan || canUndo() ? 'Done' : 'Pass'"
370
404
  @click="checkPass()"
371
405
  />
372
406
  <UndoButton
@@ -750,6 +784,7 @@
750
784
  </div>
751
785
  </template>
752
786
  <script lang="ts">
787
+ import { isEqual } from 'lodash';
753
788
  import InlineLog from './InlineLog.vue';
754
789
  import { Vue, Component, Prop, Watch, Provide, ProvideReactive, Ref } from 'vue-property-decorator';
755
790
  import { MoveName, ended, playersSortedByScore, reconstructState } from 'powergrid-engine';
@@ -783,6 +818,17 @@ import PlayerOrder from './boards/PlayerOrder.vue';
783
818
  import CityCount from './boards/CityCount.vue';
784
819
  import Map from './boards/Map.vue';
785
820
  import ResourceBoxes from './boards/ResourceBoxes.vue';
821
+ import RoundPlanner from './RoundPlanner.vue';
822
+ import {
823
+ copyState,
824
+ nextPlanningPhase,
825
+ startRoundPlan,
826
+ planMove,
827
+ replayRoundPlan,
828
+ RoundPlan,
829
+ } from 'powergrid-engine/src/planning';
830
+ import { PhasePlan, PremoveCommand, canQueuePhases } from 'powergrid-engine/src/premoves';
831
+ import { completedPhases } from '../util/round-plan';
786
832
  import Resources from './boards/Resources.vue';
787
833
  import { LogMove } from 'powergrid-engine/src/log';
788
834
  import { Phase, playerTimeUsed, PowerPlant, PowerPlantType, ResourceType } from 'powergrid-engine/src/gamestate';
@@ -925,6 +971,7 @@ const round = (n: number, digits = 2) => Number(n.toFixed(digits));
925
971
  ResourceViewButton,
926
972
  Button,
927
973
  Calculator,
974
+ RoundPlanner,
928
975
  PowerPlantMarket,
929
976
  PlayerOrder,
930
977
  CityCount,
@@ -1038,6 +1085,161 @@ export default class Game extends Vue {
1038
1085
  // without any server call (the platform's saved state IS the turn start).
1039
1086
  committedState: GameState | null = null;
1040
1087
 
1088
+ roundPlan: RoundPlan | null = null;
1089
+ planningBase: GameState | null = null;
1090
+ planningError = '';
1091
+ pendingPlanId = '';
1092
+ pendingPlanIsCancel = false;
1093
+ pendingPlanTimer: ReturnType<typeof setTimeout> | undefined;
1094
+
1095
+ get canPlanRound(): boolean {
1096
+ return (
1097
+ !this.tutorialMove &&
1098
+ !this.paused &&
1099
+ !this.interactionDisabled &&
1100
+ this.player != undefined &&
1101
+ !!this.committedState &&
1102
+ !!nextPlanningPhase(this.committedState, this.player)
1103
+ );
1104
+ }
1105
+ get canPreparePremoves(): boolean {
1106
+ return !!this.committedState && this.player != null && canQueuePhases(this.committedState, this.player, []);
1107
+ }
1108
+ get hasPlanPanel(): boolean {
1109
+ return !!(
1110
+ this.roundPlan ||
1111
+ this.myPhaseQueue?.notice ||
1112
+ this.planningError ||
1113
+ this.pendingPlanId
1114
+ );
1115
+ }
1116
+ get myPhaseQueue() {
1117
+ return this.committedState?.automation?.plans[this.player!];
1118
+ }
1119
+ get queuedMoveCount(): number {
1120
+ return (this.myPhaseQueue?.phases || []).reduce(
1121
+ // An otherwise empty phase still queues the decision to finish it.
1122
+ (count, phase) => count + Math.max(1, phase.moves.filter((move) => move.name !== MoveName.Pass).length),
1123
+ 0
1124
+ );
1125
+ }
1126
+ get planningChanged(): boolean {
1127
+ if (!this.roundPlan || !this.planningBase || !this.committedState) return false;
1128
+ const base = this.planningBase,
1129
+ live = this.committedState;
1130
+ return (
1131
+ base.round !== live.round ||
1132
+ base.phase !== live.phase ||
1133
+ base.log.length !== live.log.length ||
1134
+ JSON.stringify(base.players[this.player!]) !== JSON.stringify(live.players[this.player!])
1135
+ );
1136
+ }
1137
+ get canQueueRound(): boolean {
1138
+ if (
1139
+ !this.roundPlan ||
1140
+ !this.committedState ||
1141
+ this.pendingPlanId ||
1142
+ this.turnMoves.length ||
1143
+ this.roundPlan.state.round !== this.committedState.round
1144
+ )
1145
+ return false;
1146
+ const phases = completedPhases(this.roundPlan);
1147
+ return phases.length > 0 &&
1148
+ !isEqual(phases, this.myPhaseQueue?.phases) &&
1149
+ canQueuePhases(this.committedState, this.player!, phases);
1150
+ }
1151
+ get planPlaysNow(): boolean {
1152
+ return (
1153
+ !!this.roundPlan &&
1154
+ !!this.committedState?.currentPlayers.includes(this.player!) &&
1155
+ completedPhases(this.roundPlan)[0]?.phase === this.committedState.phase
1156
+ );
1157
+ }
1158
+ get queueHint(): string {
1159
+ if (this.roundPlan && this.committedState && this.roundPlan.state.round !== this.committedState.round)
1160
+ return 'The round ended. Start a new simulation.';
1161
+ return '';
1162
+ }
1163
+ startPlanning() {
1164
+ if (!this.canPlanRound) return;
1165
+ try {
1166
+ const base = this.roundPlan ? this.committedState! : this.G!;
1167
+ this.planningBase = copyState(base);
1168
+ this.roundPlan = startRoundPlan(base, this.player!);
1169
+ this.planningError = '';
1170
+ this.showPlan();
1171
+ } catch (error) {
1172
+ this.planningError = String(error instanceof Error ? error.message : error);
1173
+ }
1174
+ }
1175
+ showPlan() {
1176
+ this.confirmVisible = this.discardVisible = this.freeJumpVisible = false;
1177
+ this.soleBuyerPlant = null;
1178
+ this.G = null;
1179
+ this.replaceState(this.roundPlan!.state, false, false);
1180
+ }
1181
+ stopPlanning() {
1182
+ this.roundPlan = null;
1183
+ this.planningBase = null;
1184
+ this.planningError = '';
1185
+ this.G = null;
1186
+ this.replaceState(this.turnMoves.length ? this.replayTurnBuffer() : this.committedState!, false, false);
1187
+ }
1188
+ undoPlan() {
1189
+ if (!this.roundPlan?.entries.length) return;
1190
+ this.roundPlan = replayRoundPlan(
1191
+ this.planningBase!,
1192
+ this.player!,
1193
+ this.roundPlan.entries.slice(0, -1),
1194
+ this.roundPlan.startPhase
1195
+ );
1196
+ this.planningError = '';
1197
+ this.showPlan();
1198
+ }
1199
+ submitRoundPlan() {
1200
+ if (!this.canQueueRound) return;
1201
+ this.sendPhasePlan(completedPhases(this.roundPlan!));
1202
+ }
1203
+ cancelPhasePlan() {
1204
+ this.sendPhasePlan([], true);
1205
+ }
1206
+ sendPhasePlan(phases: PhasePlan[], cancelling = false) {
1207
+ if (this.pendingPlanId || !this.committedState) return;
1208
+ const request: PremoveCommand = {
1209
+ type: 'premoves',
1210
+ round: this.committedState.round,
1211
+ revision: this.myPhaseQueue?.revision || 0,
1212
+ requestId: crypto.randomUUID(),
1213
+ phases,
1214
+ };
1215
+ this.pendingPlanId = request.requestId;
1216
+ this.pendingPlanIsCancel = cancelling;
1217
+ this.planningError = '';
1218
+ this.emitter.emit('premoves', request);
1219
+ this.pendingPlanTimer = setTimeout(() => {
1220
+ if (this.pendingPlanId) {
1221
+ this.pendingPlanId = '';
1222
+ this.planningError = 'The queue was not confirmed. Refresh the live game before trying again.';
1223
+ this.emitter.emit('fetchState');
1224
+ }
1225
+ }, 12000);
1226
+ }
1227
+ viewQueuedPlan() {
1228
+ if (!this.myPhaseQueue?.phases.length) return;
1229
+ this.startPlanning();
1230
+ try {
1231
+ // Viewing a powering-only queue must not add a queued pass through building.
1232
+ this.roundPlan = startRoundPlan(this.planningBase!, this.player!, this.myPhaseQueue.phases[0].phase);
1233
+ for (const phase of this.myPhaseQueue.phases) {
1234
+ for (const move of phase.moves) this.roundPlan = planMove(this.roundPlan!, move);
1235
+ }
1236
+ this.showPlan();
1237
+ } catch (error) {
1238
+ this.planningError = 'The live board changed: ' + String(error instanceof Error ? error.message : error);
1239
+ this.showPlan();
1240
+ }
1241
+ }
1242
+
1041
1243
  @Watch('state', { immediate: true })
1042
1244
  onStateChanged(state: GameState) {
1043
1245
  if (this.tutorialMove) {
@@ -1051,6 +1253,26 @@ export default class Game extends Vue {
1051
1253
  this.replaceState(state);
1052
1254
  return;
1053
1255
  }
1256
+ if (
1257
+ state &&
1258
+ state.newTurn !== false &&
1259
+ this.pendingPlanId &&
1260
+ state.automation?.plans[this.player!]?.requestId === this.pendingPlanId
1261
+ ) {
1262
+ this.pendingPlanId = '';
1263
+ if (this.pendingPlanTimer) clearTimeout(this.pendingPlanTimer);
1264
+ if (!this.pendingPlanIsCancel) {
1265
+ this.roundPlan = null;
1266
+ this.planningBase = null;
1267
+ }
1268
+ }
1269
+ if (this.roundPlan) {
1270
+ if (state && state.newTurn !== false) {
1271
+ this.committedState = copyState(state);
1272
+ this._futureState = state;
1273
+ }
1274
+ return;
1275
+ }
1054
1276
  if (state && state.newTurn !== false) {
1055
1277
  // Committed state. Usually this clears the turn buffer (our own turn came
1056
1278
  // back committed), but during the simultaneous Bureaucracy phase it can be
@@ -1248,6 +1470,10 @@ export default class Game extends Vue {
1248
1470
  }
1249
1471
 
1250
1472
  undo() {
1473
+ if (this.roundPlan) {
1474
+ this.undoPlan();
1475
+ return;
1476
+ }
1251
1477
  if (this.paused || this.turnMoves.length === 0 || !this.committedState) {
1252
1478
  return;
1253
1479
  }
@@ -1296,6 +1522,7 @@ export default class Game extends Vue {
1296
1522
  // True when it is the auction, it's this player's turn to choose a plant, and they
1297
1523
  // are the only remaining buyer — matching the engine's uncontested-purchase case.
1298
1524
  isSoleBuyer(): boolean {
1525
+ if (this.roundPlan) return false;
1299
1526
  if (!this.canMove() || this.G!.phase !== Phase.Auction || this.G!.chosenPowerPlant || !this.canChoose()) {
1300
1527
  return false;
1301
1528
  }
@@ -1344,6 +1571,19 @@ export default class Game extends Vue {
1344
1571
  * ever frees money and plant capacity, so the remaining buffer always replays.
1345
1572
  */
1346
1573
  unbuyResource(payload: { resource: ResourceType; side?: 'north' | 'south'; fromStorage?: boolean }) {
1574
+ if (this.roundPlan) {
1575
+ const entries = [...this.roundPlan.entries];
1576
+ const index = lastBuyIndex(
1577
+ entries.map((e) => e.move),
1578
+ payload
1579
+ );
1580
+ if (index >= 0 && this.roundPlan.state.phase === Phase.Resources) {
1581
+ entries.splice(index, 1);
1582
+ this.roundPlan = replayRoundPlan(this.planningBase!, this.player!, entries, this.roundPlan.startPhase);
1583
+ this.showPlan();
1584
+ }
1585
+ return;
1586
+ }
1347
1587
  if (this.paused || !this.committedState) {
1348
1588
  return;
1349
1589
  }
@@ -1372,7 +1612,13 @@ export default class Game extends Vue {
1372
1612
  * empty spaces are clickable.
1373
1613
  */
1374
1614
  get bufferedBuys(): Record<string, number> {
1375
- return bufferedBuyCounts(this.turnMoves);
1615
+ return bufferedBuyCounts(
1616
+ this.roundPlan
1617
+ ? this.roundPlan.state.phase === Phase.Resources
1618
+ ? this.roundPlan.entries.filter((e) => e.phase === Phase.Resources).map((e) => e.move)
1619
+ : []
1620
+ : this.turnMoves
1621
+ );
1376
1622
  }
1377
1623
 
1378
1624
  bid(bid: number) {
@@ -1648,6 +1894,17 @@ export default class Game extends Vue {
1648
1894
  return;
1649
1895
  }
1650
1896
 
1897
+ if (this.roundPlan) {
1898
+ try {
1899
+ this.roundPlan = planMove(this.roundPlan, move);
1900
+ this.planningError = '';
1901
+ this.showPlan();
1902
+ } catch (error) {
1903
+ this.planningError = String(error instanceof Error ? error.message : error);
1904
+ }
1905
+ return;
1906
+ }
1907
+
1651
1908
  // What the board offered when this click was judged — the same list the
1652
1909
  // button that produced it was enabled from.
1653
1910
  const offeredWhenClicked =
@@ -1771,6 +2028,7 @@ export default class Game extends Vue {
1771
2028
  }
1772
2029
 
1773
2030
  canUndo() {
2031
+ if (this.roundPlan) return this.roundPlan.entries.length > 0;
1774
2032
  if (!this.canMove()) return false;
1775
2033
 
1776
2034
  // Undo scope = the current tentative turn: anything still in the buffer
@@ -2034,6 +2292,13 @@ export default class Game extends Vue {
2034
2292
  }
2035
2293
 
2036
2294
  getStatusMessage() {
2295
+ if (this.roundPlan)
2296
+ return this.roundPlan.finished
2297
+ ? 'Simulation complete · Round ' + this.roundPlan.state.round
2298
+ : 'Planning · ' +
2299
+ (this.roundPlan.state.phase === Phase.Bureaucracy
2300
+ ? 'Powering cities'
2301
+ : this.roundPlan.state.phase);
2037
2302
  // Color draft (chooseColors): prompt the current picker, even on the very
2038
2303
  // first turn before any moves are in the log.
2039
2304
  if (
@@ -2482,6 +2747,7 @@ export default class Game extends Vue {
2482
2747
  }
2483
2748
 
2484
2749
  beforeDestroy() {
2750
+ if (this.pendingPlanTimer) clearTimeout(this.pendingPlanTimer);
2485
2751
  window.removeEventListener('resize', this.onViewportResize);
2486
2752
  window.removeEventListener('orientationchange', this.onViewportResize);
2487
2753
  window.removeEventListener('load', this.onViewportResize);
@@ -2542,13 +2808,41 @@ ul {
2542
2808
  max-height: calc(100vh - 40px);
2543
2809
  }
2544
2810
 
2811
+ .planner-header {
2812
+ display: contents;
2813
+ }
2814
+ .planner-header.planning {
2815
+ width: 100%;
2816
+ box-sizing: border-box;
2817
+ display: flex;
2818
+ flex-wrap: wrap;
2819
+ align-items: center;
2820
+ gap: 0 12px;
2821
+ padding: 0 10px;
2822
+ color: white;
2823
+ background: black;
2824
+ position: sticky;
2825
+ top: 0;
2826
+ z-index: 10;
2827
+ }
2828
+ .planner-header.planning .statusBar {
2829
+ display: contents;
2830
+ }
2831
+ .planner-header.planning .status-message {
2832
+ min-width: 150px;
2833
+ }
2545
2834
  .statusBar {
2546
- height: 40px;
2835
+ display: flex;
2836
+ align-items: center;
2837
+ gap: 12px;
2838
+ min-height: 40px;
2839
+ box-sizing: border-box;
2840
+ padding: 0 10px;
2547
2841
  width: 100%;
2548
2842
  background-color: black;
2549
2843
  color: #fff;
2550
2844
  text-align: center;
2551
- line-height: 40px;
2845
+ line-height: 1.3;
2552
2846
  font-size: 20px;
2553
2847
  position: sticky;
2554
2848
  top: 0;
@@ -2556,6 +2850,30 @@ ul {
2556
2850
  flex-shrink: 0;
2557
2851
  }
2558
2852
 
2853
+ .status-message {
2854
+ flex: 1;
2855
+ padding: 7px 0;
2856
+ }
2857
+ .plan-entry {
2858
+ display: inline-flex;
2859
+ align-items: center;
2860
+ gap: 7px;
2861
+ border: 1px solid #69776b;
2862
+ border-radius: 4px;
2863
+ padding: 5px 9px;
2864
+ color: #e5efd9;
2865
+ background: #242e23;
2866
+ font: 500 13px system-ui, sans-serif;
2867
+ cursor: pointer;
2868
+ }
2869
+ .plan-entry:hover {
2870
+ background: #3a4c32;
2871
+ }
2872
+ .plan-entry:focus-visible {
2873
+ outline: 2px solid #e0efb6;
2874
+ outline-offset: 2px;
2875
+ }
2876
+
2559
2877
  #scene {
2560
2878
  display: block;
2561
2879
  height: auto;
@@ -0,0 +1,120 @@
1
+ <template>
2
+ <section class="round-planner" aria-label="Round planning">
3
+ <template v-if="plan">
4
+ <div class="planner-strip">
5
+ <div v-if="queueable || hasQueue" class="planner-controls">
6
+ <button
7
+ v-if="queueable"
8
+ class="primary"
9
+ :disabled="pending"
10
+ :title="
11
+ playsNow
12
+ ? 'Play this phase and queue the remaining moves'
13
+ : 'Queue these moves for your turn'
14
+ "
15
+ @click="$emit('queue')"
16
+ >
17
+ Validate
18
+ </button>
19
+ <button v-if="hasQueue" :disabled="pending" @click="$emit('cancel')">Cancel premoves</button>
20
+ </div>
21
+ </div>
22
+ <p v-if="!queueable && queueHint" class="muted queue-hint">{{ queueHint }}</p>
23
+ <p v-if="liveChanged" class="notice">
24
+ The live game changed. Costs and fuel will be checked again before queueing.
25
+ </p>
26
+ <p v-if="plan.finalScoring" class="muted">
27
+ The end-game threshold is reached. Final scoring uses the most cities you can power with your fuel;
28
+ there is no normal income payment.
29
+ </p>
30
+ </template>
31
+ <p v-if="queue && queue.notice" role="status" class="notice">{{ queue.notice }}</p>
32
+ <p v-if="pending" role="status">Saving premoves…</p>
33
+ <p v-if="error" role="status" class="notice">{{ error }}</p>
34
+ </section>
35
+ </template>
36
+ <script lang="ts">
37
+ import Vue from 'vue';
38
+ import type { RoundPlan } from 'powergrid-engine/src/planning';
39
+ import type { PremovePlan } from 'powergrid-engine/src/premoves';
40
+ export default Vue.extend({
41
+ props: {
42
+ plan: { type: Object as () => RoundPlan | null, default: null },
43
+ queue: { type: Object as () => PremovePlan | undefined, default: undefined },
44
+ liveChanged: Boolean,
45
+ queueable: Boolean,
46
+ playsNow: Boolean,
47
+ pending: Boolean,
48
+ error: String,
49
+ queueHint: String,
50
+ },
51
+ computed: {
52
+ hasQueue(): boolean {
53
+ return !!this.queue?.phases.length;
54
+ },
55
+ },
56
+ });
57
+ </script>
58
+ <style scoped>
59
+ .round-planner,
60
+ .planner-strip {
61
+ display: contents;
62
+ }
63
+ .planner-controls {
64
+ display: flex;
65
+ align-items: center;
66
+ gap: 10px;
67
+ flex-wrap: wrap;
68
+ padding: 5px 10px 5px 0;
69
+ font: 14px Arial, sans-serif;
70
+ color: #f0f2e9;
71
+ }
72
+ p {
73
+ margin: 6px 0;
74
+ }
75
+ button {
76
+ font: inherit;
77
+ border: 1px solid #68725f;
78
+ border-radius: 3px;
79
+ background: transparent;
80
+ color: inherit;
81
+ padding: 5px 10px;
82
+ cursor: pointer;
83
+ }
84
+ button:hover {
85
+ background: #35412b;
86
+ }
87
+ button:disabled {
88
+ opacity: 0.45;
89
+ cursor: default;
90
+ }
91
+ .primary {
92
+ background: #9acd32;
93
+ color: #182007;
94
+ border-color: #9acd32;
95
+ font-weight: 600;
96
+ }
97
+ .primary:hover {
98
+ background: #b0df52;
99
+ }
100
+ .round-planner > p {
101
+ flex-basis: 100%;
102
+ padding: 0 12px;
103
+ margin: 0 0 8px;
104
+ font: 13px Arial, sans-serif;
105
+ }
106
+ .muted {
107
+ color: #bcc7ad;
108
+ font-size: 13px;
109
+ }
110
+ button:focus-visible {
111
+ outline: 2px solid #d7ed95;
112
+ outline-offset: 2px;
113
+ }
114
+ .queue-hint {
115
+ margin: 7px 0 0;
116
+ }
117
+ .notice {
118
+ color: #efcb88;
119
+ }
120
+ </style>
package/src/launch.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createViewer } from '@boardgamers/protocol/viewer';
2
2
  import { EventEmitter } from 'events';
3
3
  import type { GameState, Move } from 'powergrid-engine';
4
+ import type { PremoveCommand } from 'powergrid-engine/src/premoves';
4
5
  import Vue from 'vue';
5
6
  import Game from './components/Game.vue';
6
7
  import { mountGameChat } from './game-chat';
@@ -51,7 +52,7 @@ function launch(selector: string) {
51
52
  }).$mount(mountPoint);
52
53
 
53
54
  let replaying = false;
54
- const viewer = createViewer<GameState, Move[]>({
55
+ const viewer = createViewer<GameState, Move[] | PremoveCommand>({
55
56
  async onState(data) {
56
57
  params.state = data;
57
58
  app.$forceUpdate();
@@ -97,6 +98,7 @@ function launch(selector: string) {
97
98
  });
98
99
  const item = viewer.emitter;
99
100
  params.emitter.on('move', (moves: Move[]) => viewer.move(moves));
101
+ params.emitter.on('premoves', (command: PremoveCommand) => viewer.move(command));
100
102
  params.emitter.on('fetchState', () => viewer.fetchState());
101
103
  params.emitter.on('addLog', (data: string[]) => viewer.addLog(data));
102
104
  params.emitter.on('replaceLog', (data: string[]) => viewer.replaceLog(data));
@@ -1,7 +1,8 @@
1
1
  import { cloneDeep } from 'lodash';
2
- import { move as execMove, Move, Phase, setup, stripSecret } from 'powergrid-engine';
3
- import { moveAI } from 'powergrid-engine/src/engine';
2
+ import { Move, Phase, setup, stripSecret } from 'powergrid-engine';
4
3
  import type { MapName, Variant } from 'powergrid-engine/src/gamestate';
4
+ import type { PremoveCommand } from 'powergrid-engine/src/premoves';
5
+ import { move as wrapperMove, moveAI as wrapperAI } from 'powergrid-engine/wrapper';
5
6
  import { installLocalChat } from './game-chat';
6
7
  import launch from './launch';
7
8
  import { mountSoundTests } from './sounds';
@@ -60,16 +61,13 @@ function launchSelfContained(selector = '#app') {
60
61
  if (player.id != playerIndex) player.isAI = true;
61
62
  }
62
63
 
63
- emitter.on('move', async (moves: Move | Move[]) => {
64
- setTimeout(() => {
64
+ emitter.on('move', async (moves: Move[] | PremoveCommand) => {
65
+ setTimeout(async () => {
65
66
  console.log('moves received', moves);
66
67
 
67
68
  // Mimic the platform: replay the whole turn buffer from the last committed
68
69
  // state; only keep (persist) the result once the turn is committed.
69
- let newState = cloneDeep(gameState);
70
- for (const move of Array.isArray(moves) ? moves : [moves]) {
71
- newState = execMove(newState, move, playerIndex);
72
- }
70
+ const newState = await wrapperMove(cloneDeep(gameState), moves, playerIndex);
73
71
  console.log('new game state', newState);
74
72
 
75
73
  if (newState.newTurn === false) {
@@ -84,7 +82,7 @@ function launchSelfContained(selector = '#app') {
84
82
  let delay = delayBase;
85
83
  const moveAIAux = () => {
86
84
  if (gameState.players.some((pl) => pl.isAI && pl.availableMoves)) {
87
- gameState = moveAI(
85
+ gameState = wrapperAI(
88
86
  gameState,
89
87
  gameState.players.findIndex((pl) => pl.isAI && pl.availableMoves)
90
88
  );
@@ -114,7 +112,7 @@ function launchSelfContained(selector = '#app') {
114
112
 
115
113
  let delay = delayBase;
116
114
  while (gameState.players.some((pl) => pl.isAI && pl.availableMoves)) {
117
- gameState = moveAI(
115
+ gameState = wrapperAI(
118
116
  gameState,
119
117
  gameState.players.findIndex((pl) => pl.isAI && pl.availableMoves)
120
118
  );
@@ -0,0 +1,12 @@
1
+ import { MoveName, Phase } from 'powergrid-engine';
2
+ import type { RoundPlan } from 'powergrid-engine/src/planning';
3
+ import type { PhasePlan } from 'powergrid-engine/src/premoves';
4
+
5
+ export function completedPhases(plan: RoundPlan): PhasePlan[] {
6
+ return [Phase.Building, Phase.Bureaucracy].flatMap((phase) => {
7
+ const moves = plan.entries.filter((e) => e.phase === phase).map((e) => e.move);
8
+ return moves.length && moves[moves.length - 1].name === MoveName.Pass
9
+ ? [{ phase: phase as Phase.Building | Phase.Bureaucracy, moves }]
10
+ : [];
11
+ });
12
+ }