/**
* MCP Tool: Configure Game Tree
*/
import { SolverValidator } from '../solver/validator.js';
import { buildSolverCommand, CommandBuilder } from '../solver/command-builder.js';
import { StorageManager } from '../storage/manager.js';
import { RangeLoader } from '../ranges/loader.js';
export class ConfigureTreeTool {
constructor(solverPath, storageManager) {
this.solverPath = solverPath;
this.storageManager = storageManager;
this.rangeLoader = new RangeLoader(solverPath);
}
/**
* Build game tree configuration without running solver
* @param {Object} params
* @returns {Promise<Object>}
*/
async execute(params) {
try {
// Validate parameters (everything except solver_config since we're not running)
const validationParams = {
pot: params.pot,
effective_stack: params.effective_stack,
board: params.board || '',
range_ip: params.range_ip,
range_oop: params.range_oop
};
SolverValidator.validateAll(validationParams);
SolverValidator.validateBetSizesObject(params.bet_sizes);
// Resolve range paths (if they're file paths)
let resolvedRangeIP = params.range_ip;
let resolvedRangeOOP = params.range_oop;
if (params.range_ip.includes('/') || params.range_ip.endsWith('.txt')) {
try {
const loaded = await this.rangeLoader.loadRange(params.range_ip);
resolvedRangeIP = loaded.range_string;
} catch {
// Not a file path, use as-is
}
}
if (params.range_oop.includes('/') || params.range_oop.endsWith('.txt')) {
try {
const loaded = await this.rangeLoader.loadRange(params.range_oop);
resolvedRangeOOP = loaded.range_string;
} catch {
// Not a file path, use as-is
}
}
// Build command (without executing or saving)
const builder = new CommandBuilder();
builder
.setPot(params.pot)
.setEffectiveStack(params.effective_stack)
.setBoard(params.board || '')
.setRange('ip', resolvedRangeIP)
.setRange('oop', resolvedRangeOOP);
// Add bet sizes if provided
if (params.bet_sizes) {
for (const position of ['ip', 'oop']) {
if (params.bet_sizes[position]) {
const positionBets = params.bet_sizes[position];
for (const street of ['flop', 'turn', 'river']) {
if (positionBets[street]) {
const streetBets = positionBets[street];
for (const [action, sizes] of Object.entries(streetBets)) {
if (sizes) {
builder.setBetSizes(position, street, action, sizes);
}
}
}
}
}
}
}
builder.buildTree();
// Generate temporary filename for reference
const tempFilename = this.storageManager.generateFilename('config', 'txt');
const commandPath = this.storageManager.getCommandPath(tempFilename);
const commands = builder.getCommands();
const commandContent = builder.build();
// Save to storage for reference (optional)
await this.storageManager.saveCommand(commandContent, tempFilename);
return {
success: true,
command_file: commandPath,
commands: commands,
preview: commandContent,
board: params.board || 'preflop',
pot: params.pot,
effective_stack: params.effective_stack
};
} catch (error) {
return {
success: false,
error: error.message,
error_type: 'VALIDATION_ERROR'
};
}
}
}
/**
* Create the MCP tool definition
* @returns {Object}
*/
export function createConfigureTreeTool() {
return {
name: 'build_game_tree_config',
description: 'Generate a game tree configuration without running the solver. Useful for inspecting or editing the configuration before execution.',
inputSchema: {
type: 'object',
properties: {
pot: {
type: 'number',
description: 'Starting pot size (e.g., 4 for 4bb)'
},
effective_stack: {
type: 'number',
description: 'Effective stack size for both players (e.g., 100 for 100bb)'
},
board: {
type: 'string',
description: 'Board cards in format "As,Kh,Qd" or empty string for preflop (optional)'
},
range_ip: {
type: 'string',
description: 'In-position player range (e.g., "AA-22,AK-AJ")'
},
range_oop: {
type: 'string',
description: 'Out-of-position player range (e.g., "AA-TT")'
},
bet_sizes: {
type: 'object',
description: 'Custom bet sizes by position and street (optional)'
}
},
required: ['pot', 'effective_stack', 'range_ip', 'range_oop']
}
};
}
export default ConfigureTreeTool;