Skip to content

Commit 5a23717

Browse files
committed
More performance improvements (v1.7.1)
- Elimination (empathy) is now significantly faster. - Removed some unnecessary transformations. - The bot now announces when its settings have been changed if it is currently in a table (fixes #440).
1 parent 63e1539 commit 5a23717

File tree

8 files changed

+94
-75
lines changed

8 files changed

+94
-75
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ node_modules/
22
seeds/
33
.env
44
.DS_Store
5+
*.0x/

src/basics.js

+6-3
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,15 @@ export function onClue(game, action) {
4242

4343
for (const player of game.allPlayers) {
4444
const { possible, inferred } = player.thoughts[order];
45+
46+
const operation = list.includes(order) ? 'intersect' : 'subtract';
47+
const new_inferred = inferred[operation](new_possible);
48+
4549
player.updateThoughts(order, (draft) => {
46-
const operation = list.includes(order) ? 'intersect' : 'subtract';
4750
draft.possible = possible[operation](new_possible);
48-
draft.inferred = inferred[operation](new_possible);
51+
draft.inferred = new_inferred;
4952

50-
if (list.includes(order) && draft.inferred.length < inferred.length) {
53+
if (list.includes(order) && new_inferred.length < inferred.length) {
5154
draft.reasoning.push(state.actionList.length - 1);
5255
draft.reasoning_turn.push(state.turn_count);
5356
}

src/basics/Player.js

+14-9
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { produce } from '../StateProxy.js';
1515
* @typedef {import('../types.js').Identity} Identity
1616
* @typedef {import('../types.js').Link} Link
1717
* @typedef {import('../types.js').WaitingConnection} WaitingConnection
18+
* @typedef {import('../StateProxy.js').Patch} Patch
1819
*/
1920

2021
export class Player {
@@ -34,6 +35,9 @@ export class Player {
3435
/** @type {Set<number>} */
3536
hypo_plays;
3637

38+
/** @type {Map<number, Patch[]>} */
39+
patches = new Map();
40+
3741
/**
3842
* @param {number} playerIndex
3943
* @param {IdentitySet} all_possible
@@ -99,10 +103,13 @@ export class Player {
99103
/**
100104
* @param {number} order
101105
* @param {(draft: import('../types.js').Writable<Card>) => void} func
102-
* @param {(patches: import('../StateProxy.js').Patch[]) => void} [patchListener]
106+
* @param {boolean} [listenPatches]
103107
*/
104-
updateThoughts(order, func, patchListener) {
105-
this.thoughts = this.thoughts.with(order, produce(this.thoughts[order], func, patchListener));
108+
updateThoughts(order, func, listenPatches = this.playerIndex === -1) {
109+
this.thoughts = this.thoughts.with(order, produce(this.thoughts[order], func, listenPatches ? (patches) => {
110+
if (patches.length > 0)
111+
this.patches.set(order, (this.patches.get(order) ?? []).concat(patches));
112+
} : undefined));
106113
}
107114

108115
/**
@@ -309,12 +316,10 @@ export class Player {
309316
wc.focus === order && !state.deck[wc.focus].matches(wc.inference, { assume: true }));
310317

311318
// Ignore all waiting connections that will be proven wrong
312-
const diff = produce(card, (draft) => { draft.inferred = card.inferred.subtract(fake_wcs.flatMap(wc => wc.inference)); });
313-
314-
const playable = state.hasConsistentInferences(diff) &&
315-
(delayed_playable(diff.possible.array) ||
316-
delayed_playable(diff.inferred.array) ||
317-
(diff.finessed && delayed_playable([card])) ||
319+
const playable = state.hasConsistentInferences(card) &&
320+
(delayed_playable(card.possible.array) ||
321+
delayed_playable(card.inferred.subtract(fake_wcs.flatMap(wc => wc.inference)).array) ||
322+
(card.finessed && delayed_playable([card])) ||
318323
this.play_links.some(pl => pl.connected === order && pl.orders.every(o => unknown_plays.has(o))));
319324

320325
if (!playable)

src/basics/helper.js

+12-25
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { visibleFind } from './hanabi-util.js';
22

33
import logger from '../tools/logger.js';
44
import { logCard } from '../tools/log.js';
5+
import { applyPatches } from '../StateProxy.js';
56

67
/**
78
* @typedef {import('./Game.js').Game} Game
@@ -21,39 +22,25 @@ import { logCard } from '../tools/log.js';
2122
* @param {Game} game
2223
*/
2324
export function team_elim(game) {
24-
const { state } = game;
25+
const { common, state } = game;
2526

2627
for (const player of game.players) {
27-
for (let i = 0; i < game.common.thoughts.length; i++) {
28-
const ccard = game.common.thoughts[i];
29-
const card = player.thoughts[i];
30-
31-
const new_possible = ccard.possible.intersect(card.possible);
32-
let new_inferred = ccard.inferred.intersect(card.possible);
33-
34-
// Reset to GTP if common interpretation doesn't make sense
35-
if (new_inferred.length === 0 && !card.chop_moved)
36-
new_inferred = new_possible;
37-
38-
player.updateThoughts(i, (draft) => {
39-
draft.possible = new_possible;
40-
draft.inferred = new_inferred;
41-
42-
for (const property of Object.getOwnPropertyNames(ccard)) {
43-
if (!['suitIndex', 'rank', 'possible', 'inferred', 'reasoning', 'reasoning_turn'].includes(property))
44-
draft[property] = ccard[property];
45-
}
46-
47-
draft.reasoning = ccard.reasoning.slice();
48-
draft.reasoning_turn = ccard.reasoning_turn.slice();
49-
});
28+
for (const [order, patches] of common.patches) {
29+
const { possible, inferred } = common.thoughts[order];
30+
player.updateThoughts(order, (draft) => {
31+
draft.possible = possible.intersect(player.thoughts[order].possible);
32+
draft.inferred = inferred.intersect(player.thoughts[order].inferred);
33+
applyPatches(draft, patches.filter(p => !(p.path[0] === 'inferred' || p.path[0] === 'possible')));
34+
}, false);
5035
}
5136

52-
player.waiting_connections = game.common.waiting_connections.slice();
37+
player.waiting_connections = common.waiting_connections.slice();
5338
player.good_touch_elim(state, state.numPlayers === 2);
5439
player.refresh_links(state);
5540
player.update_hypo_stacks(state);
5641
}
42+
43+
common.patches = new Map();
5744
}
5845

5946
/**

src/basics/player-elim.js

+45-29
Original file line numberDiff line numberDiff line change
@@ -28,34 +28,40 @@ export function card_elim(state) {
2828

2929
const candidates = state.hands.flatMap((hand, playerIndex) => hand.map(order => ({ playerIndex, order })));
3030

31-
const all_ids = state.hands.flatMap(hand => hand.flatMap(o => this.thoughts[o].possible.array));
32-
const identities = state.base_ids.union(all_ids).array;
31+
let identities = state.base_ids;
32+
for (const order of state.hands.flat()) {
33+
identities = identities.union(this.thoughts[order].possible);
34+
35+
if (identities.length === this.all_possible.length)
36+
break;
37+
}
38+
39+
/** @type {(order: number) => void} */
40+
const addToMap = (order) => {
41+
const card = this.thoughts[order];
42+
const id = card.identity({ symmetric: this.playerIndex === -1 });
43+
44+
if (id !== undefined) {
45+
const id_hash = logCard(id);
46+
certain_map.set(id_hash, (certain_map.get(id_hash) ?? new Set()).add(order));
47+
candidates.splice(candidates.findIndex(c => c.order === order), 1);
48+
}
49+
};
50+
51+
for (const order of state.hands.flat())
52+
addToMap(order);
3353

3454
/**
3555
* The "typical" empathy operation. If there are enough known instances of an identity, it is removed from every card (including future cards).
3656
* Returns true if at least one card was modified.
3757
*/
3858
const basic_elim = () => {
3959
let changed = false;
60+
const curr_identities = identities.array;
61+
let new_identities = identities;
4062

41-
/** @type {(index: number) => void} */
42-
const addToMap = (index) => {
43-
const { order } = candidates[index];
44-
const card = this.thoughts[order];
45-
const id = card.identity({ symmetric: this.playerIndex === -1 });
46-
47-
if (id !== undefined) {
48-
const id_hash = logCard(id);
49-
certain_map.set(id_hash, (certain_map.get(id_hash) ?? new Set()).add(order));
50-
candidates.splice(index, 1);
51-
}
52-
};
53-
54-
for (let i = candidates.length - 1; i >= 0; i--)
55-
addToMap(i);
56-
57-
for (let i = 0; i < identities.length; i++) {
58-
const identity = identities[i];
63+
for (let i = 0; i < curr_identities.length; i++) {
64+
const identity = curr_identities[i];
5965
const id_hash = logCard(identity);
6066

6167
const known_count = state.baseCount(identity) + (certain_map.get(id_hash)?.size ?? 0) + (uncertain_ids.has(identity) ? 1 : 0);
@@ -67,9 +73,9 @@ export function card_elim(state) {
6773
// Remove it from the list of future possibilities
6874
this.all_possible = this.all_possible.subtract(identity);
6975
this.all_inferred = this.all_inferred.subtract(identity);
76+
new_identities = new_identities.subtract(identity);
7077

71-
for (let i = candidates.length - 1; i >= 0; i--) {
72-
const { order } = candidates[i];
78+
for (const { order } of candidates) {
7379
const { possible, inferred } = this.thoughts[order];
7480

7581
if (!possible.has(identity) || certain_map.get(id_hash)?.has(order) || uncertain_map.get(order)?.has(identity))
@@ -89,12 +95,13 @@ export function card_elim(state) {
8995
}
9096
// Card can be further eliminated
9197
else if (updated_card.possible.length === 1) {
92-
identities.push(updated_card.identity());
93-
addToMap(i);
98+
curr_identities.push(updated_card.identity());
99+
addToMap(order);
94100
}
95101
}
96-
logger.debug(`removing ${id_hash} from ${state.playerNames[this.playerIndex]} possibilities, now ${this.all_possible.map(logCard)}`);
102+
// logger.debug(`removing ${id_hash} from ${state.playerNames[this.playerIndex]} possibilities, now ${this.all_possible.map(logCard)}`);
97103
}
104+
identities = new_identities;
98105
return changed;
99106
};
100107

@@ -155,13 +162,22 @@ export function card_elim(state) {
155162
const total_multiplicity = (identities) => identities.reduce((acc, id) => acc += cardCount(state.variant, id) - state.baseCount(id), 0);
156163

157164
for (let i = 2; i <= cross_elim_candidates.length; i++) {
158-
const subsets = Utils.allSubsetsOfSize(cross_elim_candidates, i);
165+
const subsets = Utils.allSubsetsOfSize(cross_elim_candidates.filter(({ order }) => this.thoughts[order].possible.length <= i), i);
159166

160167
for (const subset of subsets) {
161-
const identities = subset.reduce((acc, { order }) => acc.union(this.thoughts[order].possible), state.base_ids);
168+
let failed = false;
169+
let acc_ids = state.base_ids;
170+
for (const { order } of subset) {
171+
acc_ids = acc_ids.union(this.thoughts[order].possible);
172+
173+
if (total_multiplicity(acc_ids) > subset.length) {
174+
failed = true;
175+
break;
176+
}
177+
}
162178

163-
if (subset.length === total_multiplicity(identities))
164-
perform_elim(subset, identities);
179+
if (!failed && subset.length === total_multiplicity(acc_ids))
180+
perform_elim(subset, acc_ids);
165181
}
166182
}
167183

src/command-handler.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ export const handle = {
136136
}
137137
// Displays or modifies the current settings (format: /settings [convention = 'HGroup'] [level = 1])
138138
if (data.msg.startsWith('/settings')) {
139-
assignSettings(data, true);
139+
assignSettings(data, game.tableID === undefined);
140140
return;
141141
}
142142
if (data.msg.startsWith('/terminate')) {

src/constants.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export const MAX_H_LEVEL = 11;
2-
export const BOT_VERSION = '1.7.0';
2+
export const BOT_VERSION = '1.7.1';
33

44
export const ACTION = /** @type {const} */ ({
55
PLAY: 0,

test/playful-sieve/safe-actions.js

+14-7
Original file line numberDiff line numberDiff line change
@@ -109,13 +109,20 @@ describe('connecting cards', () => {
109109
const game = setup(PlayfulSieve, [
110110
['xx', 'xx', 'xx', 'xx', 'xx'],
111111
['g2', 'y4', 'g3', 'r4', 'r4']
112-
]);
113-
114-
// Alice has a fully known g1 in slot 2
115-
const card = game.common.thoughts[game.state.hands[PLAYER.ALICE][1]];
116-
card.clued = true;
117-
for (const poss of /** @type {const} */ (['possible', 'inferred']))
118-
card[poss] = card[poss].intersect([{ suitIndex: 2, rank: 1 }]);
112+
], {
113+
init: (game) => {
114+
const { common, state } = game;
115+
116+
// Alice has a fully known g1 in slot 2
117+
const order = state.hands[PLAYER.ALICE][1];
118+
const { possible, inferred } = common.thoughts[order];
119+
common.updateThoughts(order, (draft) => {
120+
draft.clued = true;
121+
draft.possible = possible.intersect([{ suitIndex: 2, rank: 1 }]);
122+
draft.inferred = inferred.intersect([{ suitIndex: 2, rank: 1 }]);
123+
});
124+
}
125+
});
119126

120127
team_elim(game);
121128
game.common.update_hypo_stacks(game.state);

0 commit comments

Comments
 (0)