Skip to content

Commit 0f56985

Browse files
committed
Improve endgames (v1.10.1)
- Endgames should be slightly faster. - Now attempts to solve the endgame, even on the final round (fixes #1189). - Fixed a bug in rainbow mismatch (fixes #1186).
1 parent 84cea91 commit 0f56985

File tree

6 files changed

+140
-97
lines changed

6 files changed

+140
-97
lines changed

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.10.0';
5+
export const BOT_VERSION = '1.10.1';
66

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

src/conventions/h-group/hanabi-logic.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ export function rainbowMismatch(game, action, identity, prompt) {
313313
if (knownAs(game, prompt, variantRegexes.rainbowish))
314314
return false;
315315

316-
const free_choice_clues = state.allValidClues(target).filter(clue => Utils.objEquals(state.clueTouched(state.hands[target], clue), list));
316+
const free_choice_clues = state.allValidClues(target).filter(clue => Utils.objEquals(state.clueTouched(state.hands[target], clue).toSorted(), list.toSorted()));
317317
const matching_clues = free_choice_clues.filter(cl => state.deck[prompt].clues.some(clu =>
318318
cl.type === CLUE.COLOUR && clu.type === CLUE.COLOUR && cl.value === clu.value));
319319

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

+34-34
Original file line numberDiff line numberDiff line change
@@ -254,40 +254,6 @@ export async function take_action(game) {
254254
if (discards.length > 0)
255255
logger.info('discards', logHand(discards));
256256

257-
if (playable_orders.length > 0 && state.endgameTurns > 0) {
258-
const best_connector = Utils.maxOn(playable_orders, order => {
259-
const card_id = me.thoughts[order].identity({ infer: true });
260-
261-
if (card_id === undefined)
262-
return -Infinity;
263-
264-
const play_stacks = state.play_stacks.with(card_id.suitIndex, card_id.rank);
265-
let connectables = 0;
266-
267-
for (let i = 1; i < state.endgameTurns; i++) {
268-
const playerIndex = (state.ourPlayerIndex + i) % state.numPlayers;
269-
const connectable = state.hands[playerIndex].some(o => {
270-
const id = game.players[playerIndex].thoughts[o].identity({ infer: true });
271-
return id !== undefined && id.rank === play_stacks[id.suitIndex] + 1;
272-
});
273-
274-
if (connectable) {
275-
connectables++;
276-
play_stacks[card_id.suitIndex]++;
277-
}
278-
}
279-
280-
return connectables;
281-
}, 0);
282-
283-
const best_playable = best_connector ??
284-
playable_orders.find(o => me.thoughts[o].inferred.every(i => i.rank === 5)) ??
285-
playable_orders.find(o => me.thoughts[o].inferred.every(i => state.isCritical(i))) ??
286-
playable_orders[0];
287-
288-
return { tableID, type: ACTION.PLAY, target: best_playable };
289-
}
290-
291257
const playable_priorities = determine_playable_card(game, playable_orders);
292258

293259
const actionPrioritySize = Object.keys(ACTION_PRIORITY).length;
@@ -494,6 +460,40 @@ export async function take_action(game) {
494460
}
495461
}
496462

463+
if (playable_orders.length > 0 && state.endgameTurns > 0) {
464+
const best_connector = Utils.maxOn(playable_orders, order => {
465+
const card_id = me.thoughts[order].identity({ infer: true });
466+
467+
if (card_id === undefined)
468+
return -Infinity;
469+
470+
const play_stacks = state.play_stacks.with(card_id.suitIndex, card_id.rank);
471+
let connectables = 0;
472+
473+
for (let i = 1; i < state.endgameTurns; i++) {
474+
const playerIndex = (state.ourPlayerIndex + i) % state.numPlayers;
475+
const connectable = state.hands[playerIndex].some(o => {
476+
const id = game.players[playerIndex].thoughts[o].identity({ infer: true });
477+
return id !== undefined && id.rank === play_stacks[id.suitIndex] + 1;
478+
});
479+
480+
if (connectable) {
481+
connectables++;
482+
play_stacks[card_id.suitIndex]++;
483+
}
484+
}
485+
486+
return connectables;
487+
}, 0);
488+
489+
const best_playable = best_connector ??
490+
playable_orders.find(o => me.thoughts[o].inferred.every(i => i.rank === 5)) ??
491+
playable_orders.find(o => me.thoughts[o].inferred.every(i => state.isCritical(i))) ??
492+
playable_orders[0];
493+
494+
return { tableID, type: ACTION.PLAY, target: best_playable };
495+
}
496+
497497
// Consider finesses while finessed if we are only waited on to play one card,
498498
// it's not a selfish finesse, doesn't require more than one play from our own hand,
499499
// and we're not in the end-game.

src/conventions/ref-sieve/take-action.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export function find_all_clues(game, giver) {
3737

3838
/** @type {(clue: Clue) => ClueAction} */
3939
const clueToAction = (clue) => ({ type: 'clue', clue, giver, target: clue.target, list: state.clueTouched(state.hands[clue.target], clue) });
40-
const all_clue_values = all_clues.map(clue => ({ clue, value: predict_value(game, clueToAction(clue)) }));
40+
const all_clue_values = all_clues.map(clue => ({ clue, value: predict_value(game, clueToAction(clue)) })).filter(cv => cv.value > -100);
4141

4242
logger.on();
4343

src/conventions/shared/endgame-helper.js

+70-32
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { logCard } from '../../tools/log.js';
1515
* @typedef {import('../../types.js').Action} Action
1616
* @typedef {import('../../types.js').PerformAction} PerformAction
1717
* @typedef {Omit<PerformAction, 'tableID'> & {playerIndex: number}} ModPerformAction
18+
* @typedef {{ id: Identity, missing: number, all: boolean }[]} RemainingSet
1819
*/
1920

2021
export const simpler_cache = new Map();
@@ -42,6 +43,7 @@ function find_must_plays(state, hand) {
4243
if (id.suitIndex === -1 || state.isBasicTrash(id))
4344
return acc;
4445

46+
// All remaining copies of this identity are in the hand
4547
if (cardCount(state.variant, id) - state.baseCount(id) === group.length)
4648
acc.push(id);
4749

@@ -201,70 +203,106 @@ function get_playables(state, playerTurn) {
201203
*
202204
* @param {State} state
203205
* @param {number} playerTurn
206+
* @param {RemainingSet} remaining_ids
207+
* @param {number} depth
204208
* @returns {boolean}
205209
*/
206-
export function winnable_simpler(state, playerTurn) {
207-
if (state.score === state.maxScore)
210+
export function winnable_simpler(state, playerTurn, remaining_ids, depth = 0) {
211+
if (state.score === state.maxScore) {
212+
// logger.info(`${Array.from({ length: depth }, _ => ' ').join('')}won!!`);
208213
return true;
214+
}
209215

210-
if (unwinnable_state(state, playerTurn))
216+
if (unwinnable_state(state, playerTurn)) {
217+
// logger.info(`${Array.from({ length: depth }, _ => ' ').join('')}unwinnable state`);
211218
return false;
219+
}
212220

213-
const cached_result = simpler_cache.get(hash_state(state) + `,${playerTurn}`);
221+
const hash = `${hash_state(state)},${playerTurn},${JSON.stringify(remaining_ids.filter(r => logCard(r.id) !== 'xx'))}`;
222+
223+
const cached_result = simpler_cache.get(hash);
214224
if (cached_result !== undefined)
215225
return cached_result;
216226

217-
for (const order of get_playables(state, playerTurn)) {
218-
const action = { type: ACTION.PLAY, target: order, playerIndex: playerTurn };
227+
/** @type {ModPerformAction[]} */
228+
const possible_actions = [];
219229

220-
if (predict_winnable2(state, playerTurn, action))
221-
return true;
222-
}
230+
for (const order of get_playables(state, playerTurn))
231+
possible_actions.push({ type: ACTION.PLAY, target: order, playerIndex: playerTurn });
223232

224-
if (state.clue_tokens > 0) {
225-
const action = { type: ACTION.RANK, target: -1, value: -1, playerIndex: playerTurn };
226-
227-
if (predict_winnable2(state, playerTurn, action))
228-
return true;
229-
}
233+
if (state.clue_tokens > 0)
234+
possible_actions.push({ type: ACTION.RANK, target: -1, value: -1, playerIndex: playerTurn });
230235

231236
const discardable = state.hands[playerTurn].find(o => ((c = state.deck[o]) => c.identity() === undefined || state.isBasicTrash(c))());
232237

233-
if (state.pace >= 0 && discardable !== undefined) {
234-
const action = { type: ACTION.DISCARD, target: discardable, playerIndex: playerTurn };
238+
if (state.pace >= 0 && discardable !== undefined)
239+
possible_actions.push({ type: ACTION.DISCARD, target: discardable, playerIndex: playerTurn });
235240

236-
if (predict_winnable2(state, playerTurn, action))
237-
return true;
238-
}
241+
const winnable = possible_actions.some(action => winnable_if(state, playerTurn, action, remaining_ids, depth).winnable);
242+
simpler_cache.set(hash, winnable);
239243

240-
simpler_cache.set(hash_state(state) + `,${playerTurn}`, false);
241-
return false;
244+
return winnable;
242245
}
243246

244247
/**
245-
* @param {State} _state
246-
* @param {number} _playerTurn
247-
* @param {ModPerformAction} _action
248+
* @param {RemainingSet} remaining
249+
* @param {Identity} id
248250
*/
249-
export function predict_winnable(_state, _playerTurn, _action) {
250-
return true;
251-
// return winnable_simpler(advance_state(state, action), state.nextPlayerIndex(playerTurn));
251+
export function remove_remaining(remaining, id) {
252+
const index = remaining.findIndex(r => r.id.suitIndex === id.suitIndex && r.id.rank === id.rank);
253+
const { missing, all } = remaining[index];
254+
255+
if (missing === 1)
256+
return remaining.toSpliced(index, 1);
257+
else
258+
return remaining.with(index, { id, missing: missing - 1, all });
252259
}
253260

254261
/**
255262
* @param {State} state
256263
* @param {number} playerTurn
257264
* @param {ModPerformAction} action
265+
* @param {RemainingSet} remaining_ids
266+
* @param {number} [depth]
258267
*/
259-
export function predict_winnable2(state, playerTurn, action) {
260-
return winnable_simpler(advance_state(state, action), state.nextPlayerIndex(playerTurn));
268+
export function winnable_if(state, playerTurn, action, remaining_ids, depth = 0) {
269+
if (action.type === ACTION.RANK || action.type === ACTION.COLOUR || state.cardsLeft === 0) {
270+
const newState = advance_state(state, action, undefined);
271+
// logger.info(`${Array.from({ length: depth }, _ => ' ').join('')}checking if winnable after ${logObjectiveAction(state, action)} {`);
272+
const winnable = winnable_simpler(newState, state.nextPlayerIndex(playerTurn), remaining_ids, depth + 1);
273+
274+
// logger.info(`${Array.from({ length: depth }, _ => ' ').join('')}} ${winnable}`);
275+
return { winnable };
276+
}
277+
278+
/** @type {Identity[]} */
279+
const winnable_draws = [];
280+
281+
// logger.info(`${Array.from({ length: depth }, _ => ' ').join('')}remaining ids ${JSON.stringify(remaining_ids.map(r => ({...r, id: logCard(r.id) })))}`);
282+
283+
for (const { id } of remaining_ids) {
284+
const draw = Object.freeze(new ActualCard(id.suitIndex, id.rank, state.cardOrder + 1, state.turn_count));
285+
const newState = advance_state(state, action, draw);
286+
const new_remaining = remove_remaining(remaining_ids, id);
287+
288+
// logger.info(`${Array.from({ length: depth }, _ => ' ').join('')}checking if winnable after ${logObjectiveAction(state, action)} drawing ${logCard(id)} {`);
289+
const winnable = winnable_simpler(newState, state.nextPlayerIndex(playerTurn), new_remaining, depth + 1);
290+
291+
if (winnable)
292+
winnable_draws.push(id);
293+
294+
// logger.info(`${Array.from({ length: depth }, _ => ' ').join('')}} ${winnable}`);
295+
}
296+
297+
return { winnable: winnable_draws.length > 0, winnable_draws };
261298
}
262299

263300
/**
264301
* @param {State} state
265302
* @param {ModPerformAction} action
303+
* @param {ActualCard} draw
266304
*/
267-
function advance_state(state, action) {
305+
function advance_state(state, action, draw) {
268306
const new_state = state.shallowCopy();
269307
new_state.hands = state.hands.slice();
270308
new_state.turn_count++;
@@ -293,7 +331,7 @@ function advance_state(state, action) {
293331

294332
if (state.deck[newCardOrder] === undefined) {
295333
new_state.deck = new_state.deck.slice();
296-
new_state.deck[newCardOrder] = Object.freeze(new ActualCard(-1, -1, newCardOrder, state.turn_count));
334+
new_state.deck[newCardOrder] = draw ?? Object.freeze(new ActualCard(-1, -1, newCardOrder, state.turn_count));
297335
}
298336
};
299337

0 commit comments

Comments
 (0)