Fix AI at Winner
This commit is contained in:
parent
dfce3aa2f4
commit
c6bb6c0ad5
@ -20,7 +20,14 @@ const Winner: React.FC<WinnerProps> = ({ winnerDismissed, setWinnerDismissed })
|
|||||||
const { lastJsonMessage, sendJsonMessage } = useContext(GlobalContext);
|
const { lastJsonMessage, sendJsonMessage } = useContext(GlobalContext);
|
||||||
const [winner, setWinner] = useState<any>(undefined);
|
const [winner, setWinner] = useState<any>(undefined);
|
||||||
const [state, setState] = useState<string | undefined>(undefined);
|
const [state, setState] = useState<string | undefined>(undefined);
|
||||||
const fields = useMemo(() => ["winner", "state"], []);
|
// Track the client's color so we can tell if the current user is an observer
|
||||||
|
// (observers don't have a color assigned). Observers should remain on the
|
||||||
|
// winner screen even if the server advances the global state to `lobby`.
|
||||||
|
const [color, setColor] = useState<string | undefined>(undefined);
|
||||||
|
// Include color/private so this component can determine whether the local
|
||||||
|
// session is an observer. We purposely request these minimal fields here
|
||||||
|
// to avoid coupling Winner to higher-level RoomView state handling.
|
||||||
|
const fields = useMemo(() => ["winner", "state", "color", "private"], []);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!lastJsonMessage) {
|
if (!lastJsonMessage) {
|
||||||
return;
|
return;
|
||||||
@ -33,11 +40,39 @@ const Winner: React.FC<WinnerProps> = ({ winnerDismissed, setWinnerDismissed })
|
|||||||
if ("winner" in data.update && !equal(data.update.winner, winner)) {
|
if ("winner" in data.update && !equal(data.update.winner, winner)) {
|
||||||
setWinner(data.update.winner);
|
setWinner(data.update.winner);
|
||||||
}
|
}
|
||||||
|
// Keep track of our client color so we can special-case observer behavior
|
||||||
|
// (observers have no color). This allows an observer to stay on the
|
||||||
|
// winner screen even when the server moves the game back to the lobby.
|
||||||
|
if ("color" in data.update && data.update.color !== color) {
|
||||||
|
setColor(data.update.color);
|
||||||
|
}
|
||||||
if ("state" in data.update && data.update.state !== state) {
|
if ("state" in data.update && data.update.state !== state) {
|
||||||
|
// If the server leaves the winner state we normally clear the
|
||||||
|
// winner data. However, if the local user is an observer (no
|
||||||
|
// `color`) and they were viewing the winner screen, we keep the
|
||||||
|
// winner visible until they explicitly click "Go back to Lobby".
|
||||||
|
// This lets observers continue to view final game stats and the
|
||||||
|
// chat even as the server advances global state to `lobby`.
|
||||||
|
const isObserver = typeof color === "undefined" || color === null;
|
||||||
|
const leavingWinner = state === "winner" && data.update.state !== "winner";
|
||||||
|
|
||||||
if (data.update.state !== "winner") {
|
if (data.update.state !== "winner") {
|
||||||
setWinner(undefined);
|
if (isObserver && leavingWinner) {
|
||||||
|
// OBSERVER SPECIAL CASE: don't clear `winner` here. Still update
|
||||||
|
// the internal `state` so other parts of the UI know the server
|
||||||
|
// state changed, but keep rendering the winner panel for the
|
||||||
|
// observer until they click the button.
|
||||||
|
} else {
|
||||||
|
setWinner(undefined);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setWinnerDismissed(false);
|
|
||||||
|
// Only reset the dismissed flag when the server re-enters winner
|
||||||
|
// (otherwise preserve user's dismissal choice).
|
||||||
|
if (data.update.state === "winner") {
|
||||||
|
setWinnerDismissed(false);
|
||||||
|
}
|
||||||
|
|
||||||
setState(data.update.state);
|
setState(data.update.state);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -58,8 +93,17 @@ const Winner: React.FC<WinnerProps> = ({ winnerDismissed, setWinnerDismissed })
|
|||||||
const quitClicked = useCallback(() => {
|
const quitClicked = useCallback(() => {
|
||||||
if (!winnerDismissed) {
|
if (!winnerDismissed) {
|
||||||
setWinnerDismissed(true);
|
setWinnerDismissed(true);
|
||||||
|
// Tell server we want to go back to the lobby. Also immediately
|
||||||
|
// request the latest room/game fields so the client receives the
|
||||||
|
// updated lobby/game state and can continue observing the current
|
||||||
|
// game id. This helps observers remain connected to the same game
|
||||||
|
// record after the server transitions them.
|
||||||
|
sendJsonMessage({ type: "goto-lobby" });
|
||||||
sendJsonMessage({
|
sendJsonMessage({
|
||||||
type: "goto-lobby",
|
type: "get",
|
||||||
|
// Request a conservative set of fields required to continue
|
||||||
|
// observing the current game/lobby state.
|
||||||
|
fields: ["id", "state", "color", "private", "players"],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [sendJsonMessage, winnerDismissed, setWinnerDismissed]);
|
}, [sendJsonMessage, winnerDismissed, setWinnerDismissed]);
|
||||||
|
335
server/ai/app.ts
335
server/ai/app.ts
@ -229,6 +229,7 @@ const tryProgress = (_received?: any): any => {
|
|||||||
|
|
||||||
let sleeping = false;
|
let sleeping = false;
|
||||||
let paused = false;
|
let paused = false;
|
||||||
|
let winnerAnnounced = false;
|
||||||
|
|
||||||
const sleep = async (delay: number): Promise<void> => {
|
const sleep = async (delay: number): Promise<void> => {
|
||||||
if (sleeping) {
|
if (sleeping) {
|
||||||
@ -346,6 +347,78 @@ const isMatch = (input: any, received: any): boolean => {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper: deep clone/compare simple JSON-able values
|
||||||
|
const deepClone = (v: any) => {
|
||||||
|
try { return JSON.parse(JSON.stringify(v)); } catch (e) { return v; }
|
||||||
|
};
|
||||||
|
// Stable stringify: recursively sort object keys so output is deterministic
|
||||||
|
const stableStringify = (v: any): string => {
|
||||||
|
if (v === null) return 'null';
|
||||||
|
const t = typeof v;
|
||||||
|
if (t === 'number' || t === 'boolean' || t === 'string') return JSON.stringify(v);
|
||||||
|
if (Array.isArray(v)) {
|
||||||
|
return '[' + v.map((el) => stableStringify(el)).join(',') + ']';
|
||||||
|
}
|
||||||
|
if (t === 'object') {
|
||||||
|
const keys = Object.keys(v).sort();
|
||||||
|
return '{' + keys.map(k => JSON.stringify(k) + ':' + stableStringify((v as any)[k])).join(',') + '}';
|
||||||
|
}
|
||||||
|
// fallback
|
||||||
|
try { return JSON.stringify(v); } catch (e) { return String(v); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const deepEqual = (a: any, b: any) => {
|
||||||
|
try { return stableStringify(a) === stableStringify(b); } catch (e) { return a === b; }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper: get value at dot-separated path (path may be array or string)
|
||||||
|
const getAtPath = (obj: any, path: string): any => {
|
||||||
|
if (!path) return undefined;
|
||||||
|
const parts = path.split('.');
|
||||||
|
let cur: any = obj;
|
||||||
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
if (cur === undefined || cur === null) return undefined;
|
||||||
|
const p = parts[i];
|
||||||
|
// @ts-ignore - dynamic index; we've checked cur for null/undefined
|
||||||
|
cur = (cur as any)[p];
|
||||||
|
}
|
||||||
|
return cur;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper: collect all leaf paths from waitingFor object (returns dot-separated)
|
||||||
|
const collectLeafPaths = (obj: any, prefix = ''): string[] => {
|
||||||
|
const paths: string[] = [];
|
||||||
|
if (obj === anyValue || obj === undefined) {
|
||||||
|
// leaf
|
||||||
|
if (prefix) paths.push(prefix);
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
if (typeof obj !== 'object' || obj === null) {
|
||||||
|
if (prefix) paths.push(prefix);
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
for (const k of Object.keys(obj)) {
|
||||||
|
const next = prefix ? `${prefix}.${k}` : k;
|
||||||
|
paths.push(...collectLeafPaths(obj[k], next));
|
||||||
|
}
|
||||||
|
return paths;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper: check if msg.update explicitly contains the full path (leaf present in the incremental update)
|
||||||
|
const msgUpdateContainsPath = (update: any, path: string): boolean => {
|
||||||
|
if (!update) return false;
|
||||||
|
const parts = path.split('.');
|
||||||
|
let cur: any = update;
|
||||||
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
if (cur === undefined || cur === null || typeof cur !== 'object') return false;
|
||||||
|
const key: PropertyKey = parts[i] as PropertyKey;
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(cur, key)) return false;
|
||||||
|
// @ts-ignore - dynamic index guarded by previous checks
|
||||||
|
cur = (cur as any)[key];
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
const processLobby = (received: any): any => {
|
const processLobby = (received: any): any => {
|
||||||
/*
|
/*
|
||||||
* Lobby flow notes:
|
* Lobby flow notes:
|
||||||
@ -723,22 +796,120 @@ const processTrade = async (received?: any): Promise<any> => {
|
|||||||
gives: [give],
|
gives: [give],
|
||||||
gets: [get]
|
gets: [get]
|
||||||
};
|
};
|
||||||
|
// Helper: can our private resources satisfy a list of required give items?
|
||||||
|
const privateCanGive = (items: Array<any>): boolean => {
|
||||||
|
if (!game.private) return false;
|
||||||
|
for (let i = 0; i < (items || []).length; i++) {
|
||||||
|
const it = items[i];
|
||||||
|
if (!it) continue;
|
||||||
|
if (it.type === 'bank') {
|
||||||
|
// bank entries refer to a give type in our own player.gives; treat as not satisfiable here
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const have = Number(game.private[it.type] || 0);
|
||||||
|
if (have < (it.count || 0)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper: do two offers represent matching terms (p.gives == myGets && p.gets == myGives)
|
||||||
|
const offersMatch = (p: any, myOffer: any): boolean => {
|
||||||
|
if (!p || !myOffer) return false;
|
||||||
|
// compare lengths first
|
||||||
|
if ((p.gives || []).length !== (myOffer.gets || []).length) return false;
|
||||||
|
if ((p.gets || []).length !== (myOffer.gives || []).length) return false;
|
||||||
|
// each give in p must match a get in myOffer
|
||||||
|
for (let i = 0; i < (p.gives || []).length; i++) {
|
||||||
|
const g = p.gives[i];
|
||||||
|
let found = false;
|
||||||
|
for (let j = 0; j < (myOffer.gets || []).length; j++) {
|
||||||
|
const mg = myOffer.gets[j];
|
||||||
|
if (g.type === mg.type && g.count === mg.count) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) return false;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < (p.gets || []).length; i++) {
|
||||||
|
const g = p.gets[i];
|
||||||
|
let found = false;
|
||||||
|
for (let j = 0; j < (myOffer.gives || []).length; j++) {
|
||||||
|
const mg = myOffer.gives[j];
|
||||||
|
if (g.type === mg.type && g.count === mg.count) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// If there's a current turn-level offer, decide how to respond.
|
||||||
if (received && received.turn && received.turn.offer) {
|
if (received && received.turn && received.turn.offer) {
|
||||||
send({
|
const turnOffer = received.turn.offer;
|
||||||
type: 'trade',
|
|
||||||
action: 'accept',
|
// If the offer originates from another player (not the bank), then we (as the active player)
|
||||||
offer: {
|
// should accept if we can satisfy the terms, otherwise explicitly reject.
|
||||||
name: 'The bank',
|
if (turnOffer.name && turnOffer.name !== 'The bank') {
|
||||||
gets: [{ type: get.type, count: 1 }],
|
// Active player accepting other player's offer must be able to give what that player requests (offer.gets)
|
||||||
gives: [{ type: give.type, count: give.count }]
|
if (privateCanGive(turnOffer.gets || [])) {
|
||||||
|
send({ type: 'trade', action: 'accept', offer: turnOffer });
|
||||||
|
return { turn: { actions: anyValue } };
|
||||||
|
} else {
|
||||||
|
send({ type: 'trade', action: 'reject', offer: turnOffer });
|
||||||
|
return { turn: anyValue };
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
return {
|
|
||||||
turn: {
|
// If the current offer is a bank offer, wait to allow other players to respond
|
||||||
actions: anyValue
|
// and prefer player trades over the bank.
|
||||||
|
const TRADE_RESPONSE_DELAY_MS = 15000;
|
||||||
|
await new Promise(r => setTimeout(r, TRADE_RESPONSE_DELAY_MS));
|
||||||
|
|
||||||
|
// Find our own player entry in the received players (our proposed offer)
|
||||||
|
const myEntry = (received.players && received.players[game.color]) || null;
|
||||||
|
const myOffer = myEntry && (myEntry.gives || myEntry.gets) ? { gives: myEntry.gives, gets: myEntry.gets, name: myEntry.name, color: myEntry.color } : null;
|
||||||
|
|
||||||
|
if (myOffer && myOffer.gives && myOffer.gives.length && myOffer.gets && myOffer.gets.length) {
|
||||||
|
// Scan other players to see if any submitted a matching counter-offer. If found and they can meet it,
|
||||||
|
// accept that player trade. If they can't meet it, explicitly reject their offer.
|
||||||
|
for (const color in (received.players || {})) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(received.players, color)) continue;
|
||||||
|
const p = received.players[color];
|
||||||
|
if (!p || p.color === game.color) continue;
|
||||||
|
if (p.status !== 'Active') continue;
|
||||||
|
if (!p.gives || !p.gets) continue;
|
||||||
|
if (!offersMatch(p, myOffer)) continue;
|
||||||
|
|
||||||
|
// If the candidate player requests resources that we can provide, accept their offer.
|
||||||
|
if (privateCanGive(p.gets || [])) {
|
||||||
|
send({ type: 'trade', action: 'accept', offer: { name: p.name, color: p.color, gives: p.gives, gets: p.gets } });
|
||||||
|
return { turn: { actions: anyValue } };
|
||||||
|
} else {
|
||||||
|
// Cannot meet their terms — explicitly reject so the server will know.
|
||||||
|
send({ type: 'trade', action: 'reject', offer: { name: p.name, color: p.color, gives: p.gives, gets: p.gets } });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
// If no player trades were accepted, fall back to the bank trade (if we can still give the required resources).
|
||||||
|
if (privateCanGive([{ type: give.type, count: give.count }])) {
|
||||||
|
send({
|
||||||
|
type: 'trade',
|
||||||
|
action: 'accept',
|
||||||
|
offer: {
|
||||||
|
name: 'The bank',
|
||||||
|
gets: [{ type: get.type, count: 1 }],
|
||||||
|
gives: [{ type: give.type, count: give.count }]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { turn: { actions: anyValue } };
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we cannot satisfy the bank trade, do nothing this cycle.
|
||||||
|
return { turn: anyValue };
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Initiate offer... */
|
/* Initiate offer... */
|
||||||
@ -984,7 +1155,96 @@ const message = async (data: WebSocket.Data): Promise<void> => {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'game-update':
|
case 'game-update':
|
||||||
|
// Preserve previous snapshot to detect whether waited-for fields actually changed
|
||||||
|
const oldGameSnapshot = deepClone(game);
|
||||||
Object.assign(game, msg.update || {});
|
Object.assign(game, msg.update || {});
|
||||||
|
// If the server just announced a winner, send a congratulatory chat
|
||||||
|
// and then dismiss the winner dialog by navigating back to the lobby.
|
||||||
|
if (msg.update && (msg.update.winner || msg.update.state === 'winner')) {
|
||||||
|
// If we got the full winner payload, use it. Otherwise request it from the server.
|
||||||
|
if (msg.update.winner) {
|
||||||
|
const w = msg.update.winner as any;
|
||||||
|
if (!winnerAnnounced) {
|
||||||
|
let message = '';
|
||||||
|
try {
|
||||||
|
// If we won, be witty. Use turns/points if available.
|
||||||
|
if (w.name === name) {
|
||||||
|
const pts = w.points !== undefined ? w.points : 'many';
|
||||||
|
const turns = w.turns !== undefined ? w.turns : 'a few';
|
||||||
|
message = `Victory! I won with ${pts} points after ${turns} turns. Beep boop — humility.exe not found.`;
|
||||||
|
} else {
|
||||||
|
const pts = w.points !== undefined ? ` with ${w.points} points` : '';
|
||||||
|
message = `Congratulations ${w.name}${pts}! Well played.`;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
message = `Congratulations ${w.name || 'winner'}!`;
|
||||||
|
}
|
||||||
|
send({ type: 'chat', message });
|
||||||
|
// After a short pause show the lobby (dismiss winner dialog)
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
send({ type: 'goto-lobby' });
|
||||||
|
// Immediately request lobby information so we can decide whether to
|
||||||
|
// auto-start (startOnFull) as soon as the server transitions back
|
||||||
|
// to the lobby. This avoids waiting for an unrelated update cycle.
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
send({ type: 'get', fields: ['players', 'participants', 'unselected', 'state'] });
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Failed to request lobby fields', err);
|
||||||
|
}
|
||||||
|
}, 250);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Failed to send goto-lobby', err);
|
||||||
|
}
|
||||||
|
}, 1200);
|
||||||
|
winnerAnnounced = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We were notified the state is 'winner' but didn't receive the winner object.
|
||||||
|
// Request the winner field from the server. If it arrives shortly, the
|
||||||
|
// handler above will process it. Otherwise fall back to a generic message
|
||||||
|
// and return to the lobby.
|
||||||
|
try {
|
||||||
|
send({ type: 'get', fields: ['winner'] });
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Failed to request winner field', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback after a short wait if full winner info doesn't arrive.
|
||||||
|
setTimeout(() => {
|
||||||
|
if (winnerAnnounced) return;
|
||||||
|
let msgText = 'Congratulations to the winner!';
|
||||||
|
try {
|
||||||
|
// If we have some partial info in game, try to include it
|
||||||
|
if (game && game.turn && game.turn.name) {
|
||||||
|
msgText = `Congratulations ${game.turn.name}!`;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
send({ type: 'chat', message: msgText });
|
||||||
|
try {
|
||||||
|
send({ type: 'goto-lobby' });
|
||||||
|
// Ask for lobby state so we can react quickly (claim slot / auto-start)
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
send({ type: 'get', fields: ['players', 'participants', 'unselected', 'state'] });
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Failed to request lobby fields (fallback)', err);
|
||||||
|
}
|
||||||
|
}, 250);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Failed to send goto-lobby (fallback)', err);
|
||||||
|
}
|
||||||
|
winnerAnnounced = true;
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If the server clears the winner/state, reset our announced flag so we can handle future games
|
||||||
|
if (msg.update && typeof msg.update.state !== 'undefined' && msg.update.state !== 'winner') {
|
||||||
|
winnerAnnounced = false;
|
||||||
|
}
|
||||||
if (msg.update && msg.update.chat) {
|
if (msg.update && msg.update.chat) {
|
||||||
let newState = paused;
|
let newState = paused;
|
||||||
const rePause = new RegExp(`${name}: pause`, 'i');
|
const rePause = new RegExp(`${name}: pause`, 'i');
|
||||||
@ -999,16 +1259,53 @@ const message = async (data: WebSocket.Data): Promise<void> => {
|
|||||||
if (sleeping) { if (waitingFor) Object.assign(received, msg.update); console.log(`${name} - sleeping`); return; }
|
if (sleeping) { if (waitingFor) Object.assign(received, msg.update); console.log(`${name} - sleeping`); return; }
|
||||||
|
|
||||||
if (waitingFor) {
|
if (waitingFor) {
|
||||||
|
// Accumulate recent update fields for debugging
|
||||||
Object.assign(received, msg.update);
|
Object.assign(received, msg.update);
|
||||||
if (!isMatch(waitingFor, received)) {
|
|
||||||
|
// First ensure presence in the merged snapshot
|
||||||
|
if (!isMatch(waitingFor, game)) {
|
||||||
if (game.turn && game.turn.robberInAction) { console.log(`${name} - robber in action! Must check discards...`); } else { return; }
|
if (game.turn && game.turn.robberInAction) { console.log(`${name} - robber in action! Must check discards...`); } else { return; }
|
||||||
} else {
|
|
||||||
console.log(`${name} - received match - received: `, msg.update);
|
|
||||||
console.log(`${name} - going to sleep`);
|
|
||||||
await sleep(1000 + Math.random() * 500);
|
|
||||||
console.log(`${name} - waking up`);
|
|
||||||
waitingFor = undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Now require that at least one of the waited-for leaf paths actually
|
||||||
|
// changed in this update (or was explicitly provided in msg.update).
|
||||||
|
// For leaves equal to `anyValue` we accept any change; for concrete
|
||||||
|
// values we accept only if the new value equals the concrete value.
|
||||||
|
const paths = collectLeafPaths(waitingFor);
|
||||||
|
let changeDetected = false;
|
||||||
|
for (const p of paths) {
|
||||||
|
const oldVal = getAtPath(oldGameSnapshot, p);
|
||||||
|
const newVal = getAtPath(game, p);
|
||||||
|
|
||||||
|
// If the incremental update explicitly contained the path, treat that as a change.
|
||||||
|
if (msg.update && msgUpdateContainsPath(msg.update, p)) {
|
||||||
|
changeDetected = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If old and new differ, that's a change
|
||||||
|
if (!deepEqual(oldVal, newVal)) {
|
||||||
|
changeDetected = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the waiting leaf expects a concrete value (not anyValue) and the
|
||||||
|
// value already equals that concrete value, consider that satisfied
|
||||||
|
// only if it changed to that value in this update (above checks cover it).
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!changeDetected) {
|
||||||
|
// No relevant change occurred in this update; continue waiting.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We found a change and presence is satisfied
|
||||||
|
const delay = 1000 + Math.random() * 500;
|
||||||
|
console.log(`${name} - received match - received: `, msg.update);
|
||||||
|
console.log(`${name} - going to sleep for ${Math.round(delay)}ms`);
|
||||||
|
await sleep(delay);
|
||||||
|
console.log(`${name} - waking up to wait for new socket messages...`);
|
||||||
|
waitingFor = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (game.state) {
|
switch (game.state) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user