Skip to content

Commit 4f20a58

Browse files
committedJan 19, 2025·
Add prompts, finesses to RS (v1.9.2)
- Implement simple (unambiguous) prompts and finesses in RS. - Loaded fix clues in RS are no longer referential. - No longer fixes chop moved cards in RS. - No longer tries to solve the endgame if there are more than 2*(# of suits) cards left to play (fixes #1032). - Fixed endgame solving occasionally not timing out after 10s (fixes #1034). - Fixed certain cases where playables were counted incorrectly. - Fixed several more elim situations.
1 parent 3194f43 commit 4f20a58

31 files changed

+1255
-287
lines changed
 

‎src/StateProxy.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ export function original(value) {
140140

141141
/**
142142
* @template T
143-
* @param {(draft: { -readonly [P in keyof T]: T[P] }) => void} func
143+
* @param {(draft: { -readonly [P in keyof T]: T[P] }, ...args: unknown[]) => void} func
144144
*/
145145
export function produceC(func) {
146146
/** @type {(state: T, ...args: unknown[]) => T} */

‎src/basics.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,12 @@ export function onClue(game, action) {
4646
const new_inferred = inferred[operation](new_possible);
4747

4848
player.updateThoughts(order, (draft) => {
49-
if (list.includes(order))
49+
if (list.includes(order)) {
50+
if (!player.thoughts[order].clued)
51+
draft.firstTouch = { giver, turn: state.turn_count };
52+
5053
update_func(draft);
54+
}
5155

5256
draft.possible = possible[operation](new_possible);
5357
draft.inferred = new_inferred;

‎src/basics/Card.js

+7-2
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ export class Card extends ActualCard {
150150
// Boolean flags about the state of the card
151151
focused = false;
152152
finessed = false;
153+
possibly_finessed = false;
153154
bluffed = false;
154155
possibly_bluffed = false;
155156
chop_moved = false;
@@ -158,13 +159,17 @@ export class Card extends ActualCard {
158159
was_cm = false;
159160
superposition = false; // Whether the card is currently in a superposition
160161
hidden = false;
162+
called_to_play = false;
161163
called_to_discard = false;
162164
permission_to_discard = false;
163165
certain_finessed = false;
164166
trash = false;
165167
uncertain = false;
166168
known = false;
167169

170+
/** @type {{ giver: number, turn: number }} */
171+
firstTouch;
172+
168173
finesse_index = -1; // Action index of when the card was finessed
169174
reasoning = /** @type {number[]} */ ([]); // The action indexes of when the card's possibilities/inferences were updated
170175
reasoning_turn = /** @type {number[]} */ ([]); // The game turns of when the card's possibilities/inferences were updated
@@ -244,12 +249,12 @@ export class Card extends ActualCard {
244249

245250
/** Returns whether the card has been "touched" (i.e. clued or finessed). */
246251
get touched() {
247-
return this.clued || this.finessed || this.known;
252+
return this.clued || this.finessed || this.known || this.called_to_play;
248253
}
249254

250255
/** Returns whether the card has been "saved" (i.e. clued, finessed or chop moved). */
251256
get saved() {
252-
return this.clued || this.finessed || this.chop_moved || this.known;
257+
return this.clued || this.finessed || this.chop_moved || this.known || this.called_to_play;
253258
}
254259

255260
/**

‎src/basics/Player.js

+65-63
Original file line numberDiff line numberDiff line change
@@ -370,85 +370,87 @@ export class Player {
370370
while (found_new_playable) {
371371
found_new_playable = false;
372372

373-
for (const order of state.hands.flat()) {
374-
if (ignoreOrders?.has(order))
375-
continue;
376-
377-
const card = this.thoughts[order];
378-
379-
if (!card.saved || good_touch_elim.has(card) || linked_orders.has(order) || unknown_plays.has(order) || already_played.has(order))
380-
continue;
381-
382-
const fake_wcs = this.waiting_connections.filter(wc =>
383-
wc.focus === order && !state.deck[wc.focus].matches(wc.inference, { assume: true }));
384-
385-
// Ignore all waiting connections that will be proven wrong
386-
const playable = state.hasConsistentInferences(card) &&
387-
(delayed_playable(card.possible.array) ||
388-
delayed_playable(card.inferred.subtract(fake_wcs.flatMap(wc => wc.inference)).array) ||
389-
(card.finessed && delayed_playable([card])) ||
390-
this.play_links.some(pl => pl.connected === order && pl.orders.every(o => unknown_plays.has(o))));
373+
for (let i = 0; i < state.numPlayers; i++) {
374+
for (const order of state.hands[i]) {
375+
if (ignoreOrders?.has(order))
376+
continue;
391377

392-
if (!playable)
393-
continue;
378+
const card = this.thoughts[order];
394379

395-
const id = card.identity({ infer: true });
396-
const actual_id = state.deck[order].identity();
380+
if (!card.saved || good_touch_elim.has(card) || linked_orders.has(order) || unknown_plays.has(order) || already_played.has(order))
381+
continue;
397382

398-
// Do not allow false updating of hypo stacks
399-
if (this.playerIndex === -1 && (
400-
(id && state.deck.filter(c => c?.matches(id) && c.order !== order).length === cardCount(state.variant, id)) ||
401-
(actual_id && !card.inferred.has(actual_id)) // None of the inferences match
402-
))
403-
continue;
383+
const fake_wcs = this.waiting_connections.filter(wc =>
384+
wc.focus === order && !state.deck[wc.focus].matches(wc.inference, { assume: true }));
404385

405-
if (this.playerIndex === -1 && actual_id) {
406-
const existing = Array.from(unknown_plays).find(o => state.deck[o].matches(actual_id));
386+
// Ignore all waiting connections that will be proven wrong
387+
const playable = state.hasConsistentInferences(card) &&
388+
(delayed_playable(card.possible.array) ||
389+
delayed_playable(card.inferred.subtract(fake_wcs.flatMap(wc => wc.inference)).array) ||
390+
(card.finessed && delayed_playable([card])) ||
391+
this.play_links.some(pl => pl.connected === order && pl.orders.every(o => unknown_plays.has(o))));
407392

408-
// An unknown play matches this identity, try swapping it out later
409-
if (existing !== undefined) {
410-
const hash = logCard(actual_id);
411-
duplicated_plays.set(hash, (duplicated_plays.get(hash) ?? [existing]).concat(order));
393+
if (!playable)
412394
continue;
413-
}
414-
}
415395

416-
if (id === undefined) {
417-
// Playable, but the player doesn't know what card it is
418-
unknown_plays.add(order);
419-
already_played.add(order);
420-
found_new_playable = true;
396+
const id = card.identity({ infer: true, symmetric: this.playerIndex === i });
397+
const actual_id = state.deck[order].identity();
421398

422-
const fulfilled_links = this.links.filter(link =>
423-
link.promised && link.orders.includes(order) && link.orders.every(o => unknown_plays.has(o)));
399+
// Do not allow false updating of hypo stacks
400+
if (this.playerIndex === -1 && (
401+
(id && state.deck.filter(c => c?.matches(id) && c.order !== order).length === cardCount(state.variant, id)) ||
402+
(actual_id && !card.inferred.has(actual_id)) // None of the inferences match
403+
))
404+
continue;
424405

425-
// All cards in a promised link will be played
426-
for (const link of fulfilled_links) {
427-
const id2 = link.identities[0];
406+
if (this.playerIndex === -1 && actual_id) {
407+
const existing = Array.from(unknown_plays).find(o => state.deck[o].matches(actual_id));
428408

429-
if (id2.rank !== hypo_stacks[id2.suitIndex] + 1) {
430-
logger.warn(`tried to add ${logCard(id2)} onto hypo stacks, but they were at ${hypo_stacks[id2.suitIndex]}??`);
409+
// An unknown play matches this identity, try swapping it out later
410+
if (existing !== undefined) {
411+
const hash = logCard(actual_id);
412+
duplicated_plays.set(hash, (duplicated_plays.get(hash) ?? [existing]).concat(order));
413+
continue;
431414
}
432-
else {
433-
hypo_stacks[id2.suitIndex] = id2.rank;
434-
good_touch_elim = good_touch_elim.union(id2);
415+
}
416+
417+
if (id === undefined) {
418+
// Playable, but the player doesn't know what card it is
419+
unknown_plays.add(order);
420+
already_played.add(order);
421+
found_new_playable = true;
422+
423+
const fulfilled_links = this.links.filter(link =>
424+
link.promised && link.orders.includes(order) && link.orders.every(o => unknown_plays.has(o)));
425+
426+
// All cards in a promised link will be played
427+
for (const link of fulfilled_links) {
428+
const id2 = link.identities[0];
429+
430+
if (id2.rank !== hypo_stacks[id2.suitIndex] + 1) {
431+
logger.warn(`tried to add ${logCard(id2)} onto hypo stacks, but they were at ${hypo_stacks[id2.suitIndex]}??`);
432+
}
433+
else {
434+
hypo_stacks[id2.suitIndex] = id2.rank;
435+
good_touch_elim = good_touch_elim.union(id2);
436+
}
435437
}
438+
continue;
436439
}
437-
continue;
438-
}
439440

440-
const { suitIndex, rank } = id;
441+
const { suitIndex, rank } = id;
441442

442-
if (rank !== hypo_stacks[suitIndex] + 1) {
443-
// e.g. a duplicated 1 before any 1s have played will have all bad possibilities eliminated by good touch
444-
logger.warn(`tried to add new playable card ${logCard(id)} ${order}, hypo stacks at ${hypo_stacks[suitIndex]}`);
445-
continue;
446-
}
443+
if (rank !== hypo_stacks[suitIndex] + 1) {
444+
// e.g. a duplicated 1 before any 1s have played will have all bad possibilities eliminated by good touch
445+
logger.warn(`tried to add new playable card ${logCard(id)} ${order}, hypo stacks at ${hypo_stacks[suitIndex]}`);
446+
continue;
447+
}
447448

448-
hypo_stacks[suitIndex] = rank;
449-
good_touch_elim = good_touch_elim.union(id);
450-
found_new_playable = true;
451-
already_played.add(order);
449+
hypo_stacks[suitIndex] = rank;
450+
good_touch_elim = good_touch_elim.union(id);
451+
found_new_playable = true;
452+
already_played.add(order);
453+
}
452454
}
453455
}
454456

‎src/basics/State.js

+8
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,14 @@ export class State {
150150
return (playerIndex + 1) % this.numPlayers;
151151
}
152152

153+
/**
154+
* Returns the player index of the next player, in turn order.
155+
* @param {number} playerIndex
156+
*/
157+
lastPlayerIndex(playerIndex) {
158+
return (playerIndex + this.numPlayers - 1) % this.numPlayers;
159+
}
160+
153161
/**
154162
* Returns whether the state is in the endgame.
155163
*/

‎src/basics/hanabi-util.js

+16
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ export function cardValue(state, player, identity, order = -1) {
110110
// Unknown card in our hand, return average of possibilities
111111
if (suitIndex === -1 && rank === -1 && order !== -1) {
112112
const card = player.thoughts[order];
113+
114+
// Possible in rare circumstances when simulating endgames and trash cards are drawn but not clued.
115+
if (card.possible.length === 0)
116+
return 0;
117+
113118
return card.possible.reduce((sum, curr) => sum += cardValue(state, player, curr), 0) / card.possible.length;
114119
}
115120

@@ -136,3 +141,14 @@ export function cardValue(state, player, identity, order = -1) {
136141
export function knownAs(game, order, regex) {
137142
return game.common.thoughts[order].possible.every(i => regex.test(game.state.variant.suits[i.suitIndex]));
138143
}
144+
145+
/**
146+
* @param {Game} game
147+
* @param {number} index
148+
* @param {number} suitIndex
149+
*/
150+
export function getIgnoreOrders(game, index, suitIndex) {
151+
return (game.next_ignore[index] ?? [])
152+
.filter(i => i.inference === undefined || i.inference.suitIndex === suitIndex)
153+
.map(i => i.order);
154+
}

0 commit comments

Comments
 (0)
Please sign in to comment.