Skip to content

Commit 5f9847b

Browse files
committed
Play clue fixes (v1.10.0)
- Corrected some OCM logic (fixes #1084). - Now plays non-connecting cards to force the next player into anxiety (fixes #1065). - Recognizes more known finesses when the focus cannot be a finessed card (fixes #1046). - Allows self-prompts with a colour clue when the focus cannot be the next direct identity (fixes #1026). - Fixed some instances of being too cautious about whether someone has a playable (fixes #1140). - Fixed false positives of out-of-order play clues (fixes #1021).
1 parent 7b9a693 commit 5f9847b

File tree

11 files changed

+141
-26
lines changed

11 files changed

+141
-26
lines changed

src/basics/Player.js

+35-12
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as Utils from '../tools/util.js';
66
import * as Elim from './player-elim.js';
77

88
import logger from '../tools/logger.js';
9-
import { logCard } from '../tools/log.js';
9+
import { logCard, logConnection } from '../tools/log.js';
1010
import { produce } from '../StateProxy.js';
1111

1212
/**
@@ -228,18 +228,41 @@ export class Player {
228228
card.possible.every(p => state.isBasicTrash(p) || state.isPlayable(p)) && card.possible.some(p => state.isPlayable(p));
229229

230230
const conflicting_conn = () => {
231-
const wc = this.waiting_connections.find((wc, i1) =>
232-
(playerIndex !== state.ourPlayerIndex || !wc.symmetric) &&
231+
/** @type {WaitingConnection[]} */
232+
const dependents = [];
233+
234+
for (const wc of this.waiting_connections) {
235+
// Ignore symmetric connections when looking at our own playables, since no one else will consider them
236+
if (playerIndex === state.ourPlayerIndex && wc.symmetric)
237+
continue;
238+
233239
// Unplayable target of possible waiting connection
234-
(wc.focus === o && !state.isPlayable(wc.inference) && card.possible.has(wc.inference)) ||
235-
wc.connections.some((conn, ci) => ci >= wc.conn_index && conn.order === o && (
236-
// Unplayable connecting card
237-
conn.identities.some(i => !state.isPlayable(i) && card.possible.has(i)) ||
238-
// A different connection on the same focus doesn't use this connecting card
239-
this.waiting_connections.some((wc2, i2) =>
240-
i1 !== i2 && wc2.focus === wc.focus && wc2.connections.every(conn2 => conn2.order !== o))))
241-
);
242-
return wc !== undefined;
240+
if (wc.focus === o && !state.isPlayable(wc.inference) && card.possible.has(wc.inference)) {
241+
logger.debug(`order ${o} has conflicting connection ${wc.connections.map(logConnection).join(' -> ')} (unplayable target)`);
242+
return true;
243+
}
244+
245+
if (wc.connections.some((conn, ci) => ci >= wc.conn_index && conn.order === o))
246+
dependents.push(wc);
247+
}
248+
249+
for (const wc of dependents) {
250+
const depending_conn = wc.connections.find((conn, ci) => ci >= wc.conn_index && conn.order === o);
251+
const unplayable_ids = depending_conn.identities.filter(i => !state.isPlayable(i) && card.possible.has(i));
252+
if (unplayable_ids.length > 0) {
253+
logger.debug(`order ${o} has conflicting connection ${wc.connections.map(logConnection).join(' -> ')} with unplayable ids ${unplayable_ids.map(logCard)})`);
254+
return true;
255+
}
256+
}
257+
258+
// Every connection using this card has another connection with the same focus that doesn't use it
259+
const replaceable = dependents.map(wc =>
260+
this.waiting_connections.find(wc2 => wc !== wc2 && wc2.focus === wc.focus && wc2.connections.every(conn2 => conn2.order !== o)));
261+
262+
if (replaceable.every(r => r !== undefined))
263+
logger.debug(`order ${o} has connections replaceable with ${replaceable.map(wc => wc.connections.map(logConnection).join(' -> '))}`);
264+
265+
return replaceable.every(r => r !== undefined);
243266
};
244267

245268
return card.possibilities.every(p => (card.chop_moved ? state.isBasicTrash(p) : false) || state.isPlayable(p)) && // cm cards can ignore trash ids

src/basics/helper.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ export function checkFix(game, oldThoughts, clueAction) {
166166
const card = newCommon.thoughts[order];
167167

168168
// No new eliminations
169-
if (card.possible.length === common.thoughts[order].possible.length)
169+
if (card.possible.length === oldThoughts[order].possible.length)
170170
return false;
171171

172172
if (newCommon.thoughts[order].identity() === undefined || card.clues.filter(clue => clue.type === card.clues.at(-1).type && clue.value === card.clues.at(-1).value ).length > 1)

src/constants.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { find_all_clues as h_find, find_all_discards as h_dc } from './conventio
22
import { find_all_clues as rs_find, find_all_discards as rs_dc } from './conventions/ref-sieve/take-action.js';
33

44
export const MAX_H_LEVEL = 11;
5-
export const BOT_VERSION = '1.9.9';
5+
export const BOT_VERSION = '1.10.0';
66

77
export const ACTION = /** @type {const} */ ({
88
PLAY: 0,

src/conventions/h-group/action-helper.js

+6
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,12 @@ export function order_1s(state, player, orders, options = { no_filter: false })
7070
const [c1_start, c2_start] = [order1, order2].map(o => state.inStartingHand(o));
7171
const [c1, c2] = [order1, order2].map(o => player.thoughts[o]);
7272

73+
/** @param {number} o */
74+
const first_clued_turn = (o) => (state.deck[o].clues[0] ?? { turn: state.turn_count }).turn;
75+
76+
if (first_clued_turn(order1) !== first_clued_turn(order2))
77+
return first_clued_turn(order2) - first_clued_turn(order1);
78+
7379
if (c1.finessed && c2.finessed)
7480
return c1.finesse_index - c2.finesse_index;
7581

src/conventions/h-group/clue-finder/clue-finder.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ export function get_clue_interp(game, clue, giver, options) {
282282
return;
283283
}
284284

285-
if (finesses.length > 0 && list.some(o => ((id = state.deck[o]) => id.playedBefore(focused_card) && common.hypo_stacks[id.suitIndex] < id.rank)())) {
285+
if (finesses.some(f => list.some(o => f.card.order !== o && state.deck[f.card.order].matches(state.deck[o])))) {
286286
logger.warn('looks like out-of-order play clue, not giving');
287287
return;
288288
}

src/conventions/h-group/clue-interpretation/connecting-cards.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ function find_unknown_connecting(game, action, reacting, identity, connected = [
196196
if (game.level >= LEVEL.INTERMEDIATE_FINESSES && state.play_stacks[prompt_c.suitIndex] + 1 === prompt_c.rank) {
197197
// Could be duplicated in giver's hand - disallow hidden prompt
198198
if (giver === state.ourPlayerIndex && state.hands[giver].some(o => state.deck[o].clued && game.players[giver].thoughts[o].inferred.has(identity))) {
199-
logger.warn(`disallowed hidden prompt on ${logCard(prompt_c)} ${prompt_order}, true ${logCard(identity)} could be duplicated in giver's hand`);
199+
logger.warn(`disallowed hidden prompt on ${logCard(prompt_c)} ${prompt_order}, true ${logCard(identity)} could be duplicated in giver's hand`);
200200
return { tried: true };
201201
}
202202
return { tried: true, conn: { type: 'prompt', reacting, order: prompt_order, hidden: true, identities: [prompt_c.raw()] } };

src/conventions/h-group/clue-interpretation/connection-helper.js

+6-3
Original file line numberDiff line numberDiff line change
@@ -301,15 +301,18 @@ export function assign_all_connections(game, simplest_poss, all_poss, action, fo
301301
if (card.uncertain || giver === state.ourPlayerIndex || card.rewinded)
302302
return false;
303303

304-
if (reacting === state.ourPlayerIndex)
304+
if (reacting === state.ourPlayerIndex) {
305305
// We are uncertain if the card isn't known and there's some other card in our hand that allows for a swap
306-
return type !== 'known' && identities.some(i => state.ourHand.some(o => o !== order && me.thoughts[o].possible.has(i)));
306+
return type !== 'known' && identities.some(i => state.ourHand.some(o => o !== order && me.thoughts[o].possible.has(i))) &&
307+
// The card also needs to be playable in some other suit
308+
card.possible.some(i => i.suitIndex !== identities[0].suitIndex && i.rank <= common.hypo_stacks[i.suitIndex] + 1);
309+
}
307310

308311
// We are uncertain if the connection is a finesse that could be ambiguous
309312
const uncertain_conn = (type === 'finesse' && all_poss.length > 1) ||
310313
(type === 'prompt' && me.thoughts[focus].possible.some(p => p.suitIndex !== identities[0].suitIndex));
311314

312-
return uncertain_conn && (!(identities.every(i => state.isCritical(i)) && focused_card.matches(inference)) ||
315+
return uncertain_conn && !((identities.every(i => state.isCritical(i)) && focused_card.matches(inference)) ||
313316
// Colour finesses are guaranteed if the focus cannot be a finessed identity
314317
(clue.type === CLUE.COLOUR && identities.every(i => !me.thoughts[focused_card.order].possible.has(i))));
315318
})();

src/conventions/h-group/clue-interpretation/own-finesses.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ function connect(game, action, identity, looksDirect, connected, ignoreOrders, i
8787

8888
const focus = connected[0];
8989

90-
const self_allowed = giver !== state.ourPlayerIndex && !(target === state.ourPlayerIndex && looksDirect);
90+
const self_allowed = giver !== state.ourPlayerIndex && !(target === state.ourPlayerIndex && looksDirect && me.thoughts[connected[0]].possible.has(identity));
9191

9292
if (self_allowed) {
9393
if (options.bluffed) {

src/conventions/h-group/urgent-actions.js

+24-6
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,22 @@ import { produce } from '../../StateProxy.js';
2424
* @typedef {import('../../types.js').PerformAction} PerformAction
2525
*/
2626

27+
/**
28+
* Determines whether the order can be placed into anxiety.
29+
* @param {Game} game
30+
* @param {number} target
31+
* @param {number} order
32+
* @returns {boolean}
33+
*/
34+
export function anxiety_targetable(game, target, order) {
35+
const { common, state } = game;
36+
37+
return game.level >= LEVEL.STALLING &&
38+
common.thinksLocked(state, target) &&
39+
state.clue_tokens === 0 &&
40+
game.players[target].anxietyPlay(state, state.hands[target]) === order;
41+
}
42+
2743
/**
2844
* Determines whether we can play a connecting card into the target's hand.
2945
* @param {Game} game
@@ -45,12 +61,8 @@ export function find_unlock(game, target) {
4561
if (our_connecting === undefined)
4662
continue;
4763

48-
// The card must become playable
49-
const known = game.players[target].thoughts[order].inferred.every(c => state.isPlayable(c) || c.matches(card)) ||
50-
(game.level >= LEVEL.STALLING &&
51-
common.thinksLocked(state, target) &&
52-
state.clue_tokens === 0 &&
53-
game.players[target].anxietyPlay(state, state.hands[target]) === order);
64+
// The card must become playable (TODO: maybe anxiety should only be on next player?)
65+
const known = game.players[target].thoughts[order].inferred.every(c => state.isPlayable(c) || c.matches(card)) || anxiety_targetable(game, target, order);
5466

5567
if (known) {
5668
// Reorder if unknown 1 (e.g. we have a good touch link for the last remaining 1)
@@ -217,6 +229,12 @@ export function find_urgent_actions(game, play_clues, save_clues, fix_clues, sta
217229

218230
// They are locked (or will be locked), we should try to unlock
219231
if (locked) {
232+
const playable = playable_priorities.flat()[0];
233+
if (playable !== undefined && state.hands[target].some(order => anxiety_targetable(game, target, order))) {
234+
urgent_actions[PRIORITY.UNLOCK + nextPriority].push({ tableID, type: ACTION.PLAY, target: playable });
235+
continue;
236+
}
237+
220238
const unlock_order = find_unlock(game, target);
221239
if (unlock_order !== undefined && (finessed_order === -1 || finessed_order == unlock_order)) {
222240
urgent_actions[PRIORITY.UNLOCK + nextPriority].push({ tableID, type: ACTION.PLAY, target: unlock_order });

test/h-group/level-2.js

+44
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,50 @@ describe('reverse finesse', () => {
115115
// Alice's slot 1 should be i4, not [i4,i5].
116116
ExAsserts.cardHasInferences(game.common.thoughts[game.state.hands[PLAYER.ALICE][0]], ['i4']);
117117
});
118+
119+
it(`self-prompts if impossible to be direct`, () => {
120+
const game = setup(HGroup, [
121+
['xx', 'xx', 'xx', 'xx', 'xx'],
122+
['r4', 'g1', 'y2', 'g4', 'b4'],
123+
['r1', 'g3', 'b3', 'r5', 'm3']
124+
], {
125+
level: { min: 2 },
126+
play_stacks: [0, 0, 1, 0, 0],
127+
starting: PLAYER.CATHY,
128+
variant: VARIANTS.RAINBOW
129+
});
130+
131+
takeTurn(game, 'Cathy clues 2 to Alice (slots 1,2)');
132+
takeTurn(game, 'Alice plays g2 (slot 1)');
133+
takeTurn(game, 'Bob clues red to Alice (slots 2,5)'); // slot 5 is !2, so this is either r1, m1 or r3
134+
135+
ExAsserts.cardHasInferences(game.common.thoughts[game.state.hands[PLAYER.ALICE][4]], ['r1', 'r3', 'm1']);
136+
137+
takeTurn(game, 'Cathy plays r1', 'b1');
138+
139+
// Alice knows she has r3.
140+
ExAsserts.cardHasInferences(game.common.thoughts[game.state.hands[PLAYER.ALICE][4]], ['r3']);
141+
});
142+
143+
it(`recognizes known finesses`, () => {
144+
const game = setup(HGroup, [
145+
['xx', 'xx', 'xx', 'xx', 'xx'],
146+
['r1', 'r4', 'r4', 'g4', 'g4'],
147+
['y4', 'y4', 'b4', 'b4', 'r3']
148+
], {
149+
level: { min: 2 },
150+
starting: PLAYER.CATHY,
151+
discarded: ['r1', 'r1']
152+
});
153+
154+
takeTurn(game, 'Cathy clues red to Alice (slot 1)');
155+
156+
// Bob's r1 is definitely finessed.
157+
assert.equal(game.common.thoughts[game.state.hands[PLAYER.ALICE][0]].uncertain, false);
158+
159+
// Alice knows that her card is r2.
160+
ExAsserts.cardHasInferences(game.players[PLAYER.ALICE].thoughts[game.state.hands[PLAYER.ALICE][0]], ['r2']);
161+
});
118162
});
119163

120164
describe('self-finesse', () => {

test/h-group/level-9.js

+21
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,27 @@ describe('anxiety plays', () => {
387387
ExAsserts.objHasProperties(action, { type: ACTION.PLAY, target: game.state.hands[PLAYER.ALICE][0] });
388388
});
389389

390+
it('forces the next player into anxiety by playing an unrelated card', async () => {
391+
const game = setup(HGroup, [
392+
['xx', 'xx', 'xx', 'xx'],
393+
['r5', 'y5', 'b5', 'g5'],
394+
['b3', 'g4', 'b4', 'b2'],
395+
['y4', 'y4', 'r4', 'r3']
396+
], {
397+
level: { min: 9 },
398+
play_stacks: [4, 0, 0, 0, 0],
399+
clue_tokens: 2,
400+
starting: PLAYER.CATHY
401+
});
402+
403+
takeTurn(game, 'Cathy clues 5 to Bob');
404+
takeTurn(game, 'Donald clues blue to Alice (slot 1)');
405+
406+
// Alice should play slot 1 as b1.
407+
const action = await game.take_action();
408+
ExAsserts.objHasProperties(action, { type: ACTION.PLAY, target: game.state.hands[PLAYER.ALICE][0] });
409+
});
410+
390411
/*it('gives an anxiety clue to the next player', async () => {
391412
const game = setup(HGroup, [
392413
['xx', 'xx', 'xx', 'xx'],

0 commit comments

Comments
 (0)