powergrid-viewer 2.0.7 → 2.0.8
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
package/src/components/Game.vue
CHANGED
|
@@ -727,6 +727,7 @@ import type { GameState, LogItem, Move, Player } from 'powergrid-engine';
|
|
|
727
727
|
import { EventEmitter } from 'events';
|
|
728
728
|
import {
|
|
729
729
|
bufferedBuyCounts,
|
|
730
|
+
engineRefusedIt,
|
|
730
731
|
lastBuyIndex,
|
|
731
732
|
matchesTurnBuffer,
|
|
732
733
|
rebaseTurnBuffer,
|
|
@@ -980,6 +981,9 @@ export default class Game extends Vue {
|
|
|
980
981
|
// server-side from the last committed state; undo simply shortens it.
|
|
981
982
|
turnMoves: Move[] = [];
|
|
982
983
|
|
|
984
|
+
/** Why the last local replay stopped, if it did — see `engineRefusedIt`. */
|
|
985
|
+
private lastReplayFailure: unknown;
|
|
986
|
+
|
|
983
987
|
// Last committed state received from the platform. Undo replays the shortened
|
|
984
988
|
// turn buffer from this state; when the buffer empties, the preview resets to it
|
|
985
989
|
// without any server call (the platform's saved state IS the turn start).
|
|
@@ -1039,8 +1043,9 @@ export default class Game extends Vue {
|
|
|
1039
1043
|
* engine now rejects — possible after a rebase) and returns the preview.
|
|
1040
1044
|
*/
|
|
1041
1045
|
replayTurnBuffer(): GameState {
|
|
1042
|
-
const { state, applied } = replayBuffer(this.committedState!, this.turnMoves, this.player!);
|
|
1046
|
+
const { state, applied, failure } = replayBuffer(this.committedState!, this.turnMoves, this.player!);
|
|
1043
1047
|
this.turnMoves = applied;
|
|
1048
|
+
this.lastReplayFailure = failure;
|
|
1044
1049
|
return state;
|
|
1045
1050
|
}
|
|
1046
1051
|
|
|
@@ -1529,11 +1534,20 @@ export default class Game extends Vue {
|
|
|
1529
1534
|
return;
|
|
1530
1535
|
}
|
|
1531
1536
|
|
|
1537
|
+
// What the board offered when this click was judged — the same list the
|
|
1538
|
+
// button that produced it was enabled from.
|
|
1539
|
+
const offeredWhenClicked =
|
|
1540
|
+
this.player != undefined && this.G && this.G.players[this.player]
|
|
1541
|
+
? this.G.players[this.player].availableMoves
|
|
1542
|
+
: undefined;
|
|
1543
|
+
const theServerOfferedThisMove = !!offeredWhenClicked && !!offeredWhenClicked[move.name];
|
|
1544
|
+
|
|
1532
1545
|
// Stamp the move ONCE, when it enters the turn buffer, so the engine can
|
|
1533
1546
|
// advance the per-player clocks. The engine never reads the system clock
|
|
1534
1547
|
// itself — the stamp travels with the move on every resend of the buffer, so
|
|
1535
1548
|
// replays (and the eventual committed log) reproduce the same times.
|
|
1536
|
-
|
|
1549
|
+
const stamped = { ...move, time: Date.now() };
|
|
1550
|
+
this.turnMoves.push(stamped);
|
|
1537
1551
|
|
|
1538
1552
|
// Preview the move locally BEFORE sending it. Without this the board — and with
|
|
1539
1553
|
// it `availableMoves` — stays frozen on the last server reply, so every click
|
|
@@ -1542,9 +1556,6 @@ export default class Game extends Vue {
|
|
|
1542
1556
|
// replayed buffer with "Wrong argument for the command BuyResource"; the
|
|
1543
1557
|
// rejected move then stays in the buffer, so every later action resends it and
|
|
1544
1558
|
// fails again until the page is refreshed (#131).
|
|
1545
|
-
//
|
|
1546
|
-
// Replaying the buffer on the committed state is exact: any move with hidden
|
|
1547
|
-
// side effects commits, which ends the buffer (see util/turn-buffer).
|
|
1548
1559
|
let preview: GameState | null = null;
|
|
1549
1560
|
|
|
1550
1561
|
if (this.committedState && this.player != undefined) {
|
|
@@ -1552,9 +1563,31 @@ export default class Game extends Vue {
|
|
|
1552
1563
|
preview = this.replayTurnBuffer();
|
|
1553
1564
|
|
|
1554
1565
|
if (this.turnMoves.length < buffered) {
|
|
1555
|
-
// The
|
|
1556
|
-
//
|
|
1557
|
-
|
|
1566
|
+
// The replay could not carry the move — which is NOT the same thing as
|
|
1567
|
+
// the move being illegal, and telling them apart is the whole job here.
|
|
1568
|
+
//
|
|
1569
|
+
// The state a player holds is stripped of everything they may not see.
|
|
1570
|
+
// A fastBid auction keeps the other players' sealed bids hidden, so the
|
|
1571
|
+
// response that RESOLVES that auction cannot be replayed on this copy at
|
|
1572
|
+
// all: the engine reaches for a bid that was stripped out and throws.
|
|
1573
|
+
// A human is almost always the last to answer — bots reply instantly —
|
|
1574
|
+
// so treating that as a refusal silently swallowed nearly every Pass in
|
|
1575
|
+
// a fastBid auction, with an enabled button and no way to tell.
|
|
1576
|
+
//
|
|
1577
|
+
// So defer to the only authority on legality that this viewer has: the
|
|
1578
|
+
// `availableMoves` the server sent. If it offered the move, send it and
|
|
1579
|
+
// let the server replay it from the full state; just do not pretend to
|
|
1580
|
+
// preview what could not be previewed.
|
|
1581
|
+
const onlyThisMoveWasDropped = this.turnMoves.length === buffered - 1;
|
|
1582
|
+
|
|
1583
|
+
if (engineRefusedIt(this.lastReplayFailure) || !theServerOfferedThisMove || !onlyThisMoveWasDropped) {
|
|
1584
|
+
// A genuinely stale click (#131): the engine asserted against it on
|
|
1585
|
+
// a state it could read in full. The board already shows the truth.
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
this.turnMoves.push(stamped);
|
|
1590
|
+
preview = null;
|
|
1558
1591
|
}
|
|
1559
1592
|
}
|
|
1560
1593
|
|
package/src/util/turn-buffer.ts
CHANGED
|
@@ -131,21 +131,43 @@ export function replayTurnBuffer(
|
|
|
131
131
|
committedState: GameState,
|
|
132
132
|
turnMoves: Move[],
|
|
133
133
|
player: number
|
|
134
|
-
): { state: GameState; applied: Move[] } {
|
|
134
|
+
): { state: GameState; applied: Move[]; failure?: unknown } {
|
|
135
135
|
let state: GameState = JSON.parse(JSON.stringify(committedState));
|
|
136
136
|
const applied: Move[] = [];
|
|
137
|
+
let failure: unknown;
|
|
137
138
|
|
|
138
139
|
for (const move of turnMoves) {
|
|
139
140
|
try {
|
|
140
141
|
state = engineMove(state, move, player);
|
|
141
142
|
} catch (err) {
|
|
142
143
|
console.error('dropping turn-buffer tail no longer legal on the committed state', move, err);
|
|
144
|
+
failure = err;
|
|
143
145
|
break;
|
|
144
146
|
}
|
|
145
147
|
applied.push(move);
|
|
146
148
|
}
|
|
147
149
|
|
|
148
|
-
return { state, applied };
|
|
150
|
+
return { state, applied, failure };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Did the engine REFUSE this move, or could this copy of the state simply not carry it?
|
|
155
|
+
*
|
|
156
|
+
* The engine refuses with an assertion. A state stripped of what a player may not see —
|
|
157
|
+
* a fastBid auction's sealed bids — instead fails while reaching for something that was
|
|
158
|
+
* stripped out, which says nothing at all about legality.
|
|
159
|
+
*/
|
|
160
|
+
export function engineRefusedIt(failure: unknown): boolean {
|
|
161
|
+
if (!failure) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Node stamps `code: 'ERR_ASSERTION'`; the browser build of `assert` does not, and
|
|
166
|
+
// checking only for the code let every illegal move through in the bundle while
|
|
167
|
+
// passing every test in Node.
|
|
168
|
+
const err = failure as { name?: string; code?: string };
|
|
169
|
+
|
|
170
|
+
return err.name === 'AssertionError' || err.code === 'ERR_ASSERTION';
|
|
149
171
|
}
|
|
150
172
|
|
|
151
173
|
/**
|