Skip to content

Commit a43d15b

Browse files
committed
Add level switching, fix bugs
- New /settings command to change which conventions the bot plays with. - The focused card is no longer considered to be bad touch. - No longer gives fix clues before level 3. - Fixed finding unknown playable connecting cards. - Fixed identification of forward finesses. - No longer interprets 5cms in early game. - Fixed stall identification when giver's hand is locked. - Fixed console on Windows terminals. - Cleaned up some logging.
1 parent a8e220d commit a43d15b

11 files changed

+108
-48
lines changed

src/command-handler.js

+48-3
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,15 @@ let self;
1414
const tables = {};
1515

1616
/** @type {State} */
17-
let state;
17+
let state = {};
18+
19+
/** @type {boolean} */
20+
let gameStarted = false;
21+
22+
const settings = {
23+
convention: 'HGroup',
24+
level: 1
25+
};
1826

1927
export const handle = {
2028
// Received when any message in chat is sent
@@ -72,6 +80,35 @@ export const handle = {
7280
else if (data.msg.startsWith('/start')) {
7381
Utils.sendCmd('tableStart', { tableID: Number(data.msg.slice(data.msg.indexOf(' ') + 1)) });
7482
}
83+
// Displays or modifies the current settings (format: /settings [convention = 'HGroup'] [level = 1])
84+
else if (data.msg.startsWith('/settings')) {
85+
const parts = data.msg.split(' ');
86+
87+
if (parts.length > 1) {
88+
if (gameStarted) {
89+
Utils.sendChat(data.who, 'Settings cannot be modified in the middle of a game.');
90+
}
91+
else {
92+
if (conventions[parts[1]]) {
93+
settings.convention = parts[1];
94+
95+
if (settings.convention === 'HGroup' && (parts.length === 2 || !isNaN(Number(parts[2])))) {
96+
const level = Number(parts[2]) || 1;
97+
98+
if (level <= 0 || level > 4) {
99+
Utils.sendChat(data.who, 'This bot can currently only play between levels 1 and 4.');
100+
}
101+
settings.level = Math.max(Math.min(level, 4), 1);
102+
}
103+
}
104+
else {
105+
Utils.sendChat(data.who, `Correct format is /settings [convention = 'HGroup'] [level = 1].`);
106+
}
107+
}
108+
}
109+
const settingsString = (settings.convention === 'HGroup') ? `H-Group level ${settings.level}` : 'Referential Sieve';
110+
Utils.sendChat(data.who, `Currently playing with ${settingsString} conventions.`);
111+
}
75112
}
76113
},
77114
// Received when an action is taken in the current active game
@@ -97,19 +134,27 @@ export const handle = {
97134
setTimeout(() => state.take_action(state), 3000);
98135
}
99136
},
137+
joined: (data) => {
138+
const { tableID } = data;
139+
state.tableID = tableID;
140+
},
100141
// Received at the beginning of the game, with information about the game
101142
init: async (data) => {
102143
const { tableID, playerNames, ourPlayerIndex, options } = data;
103144
const variant = await getVariant(options.variantName);
104145

105146
// Initialize game state using convention set
106-
const convention = process.env.MODE || 'HGroup';
107-
state = new conventions[convention](tableID, playerNames, ourPlayerIndex, variant.suits);
147+
state = new conventions[settings.convention](tableID, playerNames, ourPlayerIndex, variant.suits, settings.level);
108148

109149
Utils.globalModify({state});
110150

111151
// Ask the server for more info
112152
Utils.sendCmd('getGameInfo2', { tableID: data.tableID });
153+
gameStarted = true;
154+
},
155+
left: () => {
156+
state.tableID = undefined;
157+
gameStarted = false;
113158
},
114159
// Received when a table updates its information
115160
table: (data) => {

src/conventions/h-group.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,6 @@ export default class HGroup extends State {
2424
}
2525

2626
createBlank() {
27-
return new HGroup(this.tableID, this.playerNames, this.ourPlayerIndex, this.suits);
27+
return new HGroup(this.tableID, this.playerNames, this.ourPlayerIndex, this.suits, this.level);
2828
}
2929
}

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

+11-17
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ function find_save(state, target, card) {
8181
* @returns {Clue | undefined} The TCM if valid, otherwise undefined.
8282
*/
8383
function find_tcm(state, target, saved_cards, trash_card) {
84-
logger.info(`saved cards ${saved_cards.map(c => Utils.logCard(c)).join(',')}, trash card ${Utils.logCard(trash_card)}`);
84+
logger.info(`attempting tcm with trash card ${Utils.logCard(trash_card)}, saved cards ${saved_cards.map(c => Utils.logCard(c)).join(',')}`);
8585
const chop = saved_cards.at(-1);
8686

8787
// Colour or rank save (if possible) is preferred over trash chop move
@@ -101,22 +101,17 @@ function find_tcm(state, target, saved_cards, trash_card) {
101101
return;
102102
}
103103

104-
let saved_trash = 0;
105-
// At most 1 trash card should be saved
106-
for (const card of saved_cards) {
107-
const { suitIndex, rank, order } = card;
108-
109-
// Saving a trash card or two of the same card
110-
if (isTrash(state, state.ourPlayerIndex, suitIndex, rank, order) ||
111-
saved_cards.some(c => card.matches(c.suitIndex, c.rank) && card.order > c.order)
112-
) {
113-
saved_trash++;
114-
logger.info(`would save trash ${Utils.logCard(card)}`);
115-
}
116-
}
104+
const saved_trash = saved_cards.filter(card => {
105+
const {suitIndex, rank, order} = card;
106+
107+
return isTrash(state, state.ourPlayerIndex, suitIndex, rank, order) || // Saving a trash card
108+
saved_cards.some(c => card.matches(c.suitIndex, c.rank) && card.order > c.order); // Saving 2 of the same card
109+
}).map(c => Utils.logCard(c));
110+
111+
logger.info(`would save ${saved_trash.length === 0 ? 'no' : saved_trash.join()} trash`);
117112

118-
// There has to be more useful cards saved than trash cards, and a trash card should not be on chop (otherwise can wait)
119-
if (saved_trash <= 1 && (saved_cards.length - saved_trash) > saved_trash) {
113+
// There has to be more useful cards saved than trash cards
114+
if (saved_trash.length <= 1 && (saved_cards.length - saved_trash.length) > saved_trash.length) {
120115
const possible_clues = direct_clues(state, target, trash_card);
121116

122117
const tcm = possible_clues.find(clue => {
@@ -228,7 +223,6 @@ export function find_clues(state, options = {}) {
228223
if (!options.ignoreCM && isBasicTrash(state, suitIndex, rank)) {
229224
// Trash chop move (we only want to find the rightmost tcm)
230225
if (!(card.clued || card.chop_moved) && cardIndex !== chopIndex && !found_tcm) {
231-
logger.info('looking for tcm on', Utils.logCard(card));
232226
const saved_cards = hand.slice(cardIndex + 1).filter(c => !(c.clued || c.chop_moved));
233227
// Use original save clue if tcm not found
234228
save_clues[target] = find_tcm(state, target, saved_cards, card) ?? save_clues[target];

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

+7-9
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ export function determine_clue(state, target, target_card, options) {
6969
const touch = hand.clueTouched(state.suits, clue);
7070
const list = touch.map(c => c.order);
7171

72-
const bad_touch_cards = find_bad_touch(state, touch.filter(c => !c.clued)); // Ignore cards that were already clued
7372
const { focused_card } = determine_focus(hand, list, { beforeClue: true });
73+
const bad_touch_cards = find_bad_touch(state, touch.filter(c => !c.clued), focused_card.order); // Ignore cards that were already clued
7474

7575
if (focused_card.order !== target_card.order) {
7676
logger.info(`${Utils.logClue(clue)} doesn't focus, ignoring`);
@@ -83,9 +83,9 @@ export function determine_clue(state, target, target_card, options) {
8383
// Prevent outputting logs until we know that the result is correct
8484
logger.collect();
8585

86-
logger.info('------- ENTERING HYPO --------');
86+
logger.info(`------- ENTERING HYPO ${Utils.logClue(clue)} --------`);
8787

88-
let hypo_state = state.simulate_clue(action, { enableLogs: true });
88+
const hypo_state = state.simulate_clue(action, { enableLogs: true });
8989

9090
logger.info('------- EXITING HYPO --------');
9191

@@ -153,23 +153,21 @@ export function determine_clue(state, target, target_card, options) {
153153
}
154154
else {
155155
// Don't double count bad touch when cluing two of the same card
156-
if (bad_touch_cards.some(c => c.matches(card.suitIndex, card.rank) && c.order > card.order)) {
156+
// Focused card should not be bad touched?
157+
if (bad_touch_cards.some(c => c.matches(card.suitIndex, card.rank) && c.order > card.order) || focused_card.order === card.order) {
157158
continue;
158159
}
159160
bad_touch++;
160161
}
161162
}
162163
}
163164

164-
// Re-simulate clue, but from our perspective so we can count the playable cards and finesses correctly
165-
hypo_state = state.simulate_clue(action);
166-
167165
let finesses = 0;
168166
const playables = [];
169167

170168
// Count the number of finesses and newly known playable cards
171-
logger.info(`hypo stacks before clue: ${state.hypo_stacks}`);
172-
logger.info(`hypo stacks after clue: ${hypo_state.hypo_stacks}`);
169+
logger.debug(`hypo stacks before clue: ${state.hypo_stacks}`);
170+
logger.debug(`hypo stacks after clue: ${hypo_state.hypo_stacks}`);
173171
for (let suitIndex = 0; suitIndex < state.suits.length; suitIndex++) {
174172
for (let rank = state.hypo_stacks[suitIndex] + 1; rank <= hypo_state.hypo_stacks[suitIndex]; rank++) {
175173
// Find the card

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

+6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { LEVEL } from '../h-constants.js';
12
import { direct_clues } from './determine-clue.js';
23
import { isBasicTrash, isSaved, isTrash, playableAway } from '../../../basics/hanabi-util.js';
34
import logger from '../../../logger.js';
@@ -24,6 +25,11 @@ export function find_fix_clues(state, play_clues, save_clues, options = {}) {
2425

2526
for (let target = 0; target < state.numPlayers; target++) {
2627
fix_clues[target] = [];
28+
29+
if (state.level <= LEVEL.FIX) {
30+
continue;
31+
}
32+
2733
// Ignore our own hand
2834
if (target === state.ourPlayerIndex || target === options.ignorePlayerIndex) {
2935
continue;

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

+18-7
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ function find_playable(state, playerIndex, suitIndex, rank, ignoreOrders) {
4242
return state.hands[playerIndex].find(card =>
4343
!ignoreOrders.includes(card.order) &&
4444
(card.inferred.every(c => playableAway(state, c.suitIndex, c.rank) === 0) || card.finessed) && // Card must be playable
45-
playerIndex !== state.ourPlayerIndex ?
46-
card.matches(suitIndex, rank) : // If not in our hand, the card must match
47-
card.inferred.some(c => c.matches(suitIndex, rank)) // If in our hand, at least one inference must match
45+
(playerIndex !== state.ourPlayerIndex ?
46+
card.matches(suitIndex, rank) : // If not in our hand, the card must match
47+
card.inferred.some(c => c.matches(suitIndex, rank))) // If in our hand, at least one inference must match
4848
);
4949
}
5050

@@ -59,10 +59,8 @@ function find_playable(state, playerIndex, suitIndex, rank, ignoreOrders) {
5959
* @returns {Connection}
6060
*/
6161
export function find_connecting(state, giver, target, suitIndex, rank, ignoreOrders = []) {
62-
logger.info('looking for connecting', Utils.logCard({suitIndex, rank}));
63-
6462
if (state.discard_stacks[suitIndex][rank - 1] === cardCount(state.suits[suitIndex], rank)) {
65-
logger.info('all cards in trash');
63+
logger.info(`all ${Utils.logCard({suitIndex, rank})} in trash`);
6664
return;
6765
}
6866

@@ -117,7 +115,7 @@ export function find_connecting(state, giver, target, suitIndex, rank, ignoreOrd
117115
}
118116
else if (finesse?.matches(suitIndex, rank)) {
119117
// If target is after giver, then finesse must be in between. Otherwise, finesse must be outside.
120-
if (state.level === 1 && !((target > giver) ? (giver < i && i < target) : (i < giver || i > target))) {
118+
if (state.level === 1 && !inBetween(state.numPlayers, i, giver, target)) {
121119
logger.warn(`found finesse ${Utils.logCard(finesse)} in ${state.playerNames[i]}'s hand, but not between giver and target`);
122120
continue;
123121
}
@@ -126,6 +124,19 @@ export function find_connecting(state, giver, target, suitIndex, rank, ignoreOrd
126124
}
127125
}
128126
}
127+
128+
logger.info(`couldn't find connecting ${Utils.logCard({suitIndex, rank})}`);
129+
}
130+
131+
function inBetween(numPlayers, playerIndex, giver, target) {
132+
let i = (giver + 1) % numPlayers;
133+
while(i !== target) {
134+
if (i === playerIndex) {
135+
return true;
136+
}
137+
i = (i + 1) % numPlayers;
138+
}
139+
return false;
129140
}
130141

131142
/**

src/conventions/h-group/clue-interpretation/interpret-clue.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,8 @@ export function interpret_clue(state, action) {
119119
interpret_tcm(state, target);
120120
return;
121121
}
122-
// 5's chop move
123-
else if (clue.type === CLUE.RANK && clue.value === 5 && focused_card.newly_clued) {
122+
// 5's chop move - for now, 5cm cannot be done in early game.
123+
else if (clue.type === CLUE.RANK && clue.value === 5 && focused_card.newly_clued && !state.early_game) {
124124
if (interpret_5cm(state, target)) {
125125
return;
126126
}

src/conventions/h-group/clue-interpretation/interpret-cm.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,18 @@ export function interpret_tcm(state, target) {
2727

2828
logger.info(`oldest trash card is ${Utils.logCard(state.hands[target][oldest_trash_index])}`);
2929

30+
const cm_cards = [];
31+
3032
// Chop move every unclued card to the right of this
3133
for (let i = oldest_trash_index + 1; i < state.hands[target].length; i++) {
3234
const card = state.hands[target][i];
3335

3436
if (!card.clued) {
3537
card.chop_moved = true;
36-
logger.info(`trash chop move on ${Utils.logCard(card)}`);
38+
cm_cards.push(Utils.logCard(card));
3739
}
3840
}
41+
logger.info(`trash chop move on ${cm_cards.join(',')}`);
3942
}
4043

4144
/**

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

+7-5
Original file line numberDiff line numberDiff line change
@@ -133,13 +133,18 @@ export function determine_focus(hand, list, options = {}) {
133133
* @param {State} state
134134
* @param {Card[]} cards
135135
*/
136-
export function find_bad_touch(state, cards) {
136+
export function find_bad_touch(state, cards, focusedCardOrder = -1) {
137137
/** @type {Card[]} */
138138
const bad_touch_cards = [];
139139

140140
for (const card of cards) {
141141
let bad_touch = false;
142142

143+
// Assume focused card cannot be bad touched
144+
if (card.order === focusedCardOrder) {
145+
continue;
146+
}
147+
143148
const { suitIndex, rank } = card;
144149
// Card has already been played or can never be played
145150
// Or someone else has the card finessed, clued or chop moved already
@@ -178,10 +183,7 @@ export function stall_severity(state, giver) {
178183
if (state.clue_tokens === 7 && state.turn_count !== 0) {
179184
return 4;
180185
}
181-
if (state.hands[giver].isLocked()) {
182-
if (handLoaded(state, giver)) {
183-
return 0;
184-
}
186+
if (state.hands[giver].isLocked() && !handLoaded(state, giver)) {
185187
return 3;
186188
}
187189
if (state.early_game) {

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,8 @@ export function take_action(state) {
118118
return;
119119
}
120120

121-
// Discard known trash at high pace
122-
if (trash_cards.length > 0 && getPace(state) > state.numPlayers * 2) {
121+
// Discard known trash at high pace, low clues
122+
if (trash_cards.length > 0 && getPace(state) > state.numPlayers * 2 && state.clue_tokens <= 3) {
123123
Utils.sendCmd('action', { tableID, type: ACTION.DISCARD, target: trash_cards[0].order });
124124
return;
125125
}

src/util.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ export function initConsole() {
3838

3939
process.stdout.write(key.sequence);
4040
switch(key.sequence) {
41-
case '\r': {
41+
case '\r':
42+
case '\n': {
4243
console.log();
4344
const parts = command.join('').split(' ');
4445
const { state } = globals;

0 commit comments

Comments
 (0)