/**
* Parameter validation for solver
*/
import { isValidBoard, isValidCard, POSITIONS, POST_FLOP_STREETS, ACTIONS } from '../utils/constants.js';
export class SolverValidator {
/**
* Validate pot size
* @param {number} pot
* @throws {Error}
*/
static validatePot(pot) {
if (!Number.isFinite(pot) || pot <= 0) {
throw new Error('Pot must be a positive number');
}
}
/**
* Validate effective stack
* @param {number} stack
* @throws {Error}
*/
static validateStack(stack) {
if (!Number.isFinite(stack) || stack <= 0) {
throw new Error('Effective stack must be a positive number');
}
}
/**
* Validate pot and stack relationship
* @param {number} pot
* @param {number} stack
* @throws {Error}
*/
static validatePotStackRelationship(pot, stack) {
if (pot > stack * 2) {
throw new Error('Pot cannot be more than 2x the effective stack');
}
}
/**
* Validate board cards
* @param {string} board - Board string (e.g., "As,Kh,Qd")
* @throws {Error}
*/
static validateBoard(board) {
if (board && !isValidBoard(board)) {
throw new Error(`Invalid board format: "${board}". Use format like "As,Kh,Qd"`);
}
}
/**
* Validate hand range format
* @param {string} range - Range string (e.g., "AA-22,AK-AJ")
* @throws {Error}
*/
static validateRange(range) {
if (!range || typeof range !== 'string' || range.trim() === '') {
throw new Error('Range must be a non-empty string');
}
// Basic validation - just check that it has some hand notation
const hasHandNotation = /[A-Z]/.test(range);
if (!hasHandNotation) {
throw new Error(`Invalid range format: "${range}". Expected poker hand notation like "AA", "AKs", "22-99"`);
}
}
/**
* Validate bet size array
* @param {Array<number>} sizes - Array of bet sizes as percentages
* @throws {Error}
*/
static validateBetSizes(sizes) {
if (!Array.isArray(sizes)) {
throw new Error('Bet sizes must be an array');
}
for (const size of sizes) {
if (!Number.isFinite(size) || size <= 0 || size > 200) {
throw new Error(`Invalid bet size: ${size}. Must be a number between 0 and 200`);
}
}
}
/**
* Validate bet sizes object structure
* @param {Object} betSizes - Bet sizes configuration object
* @throws {Error}
*/
static validateBetSizesObject(betSizes) {
if (!betSizes || typeof betSizes !== 'object') {
return; // Optional parameter
}
for (const position of Object.keys(betSizes)) {
if (!['ip', 'oop'].includes(position)) {
throw new Error(`Invalid position: ${position}. Must be "ip" or "oop"`);
}
const positionBets = betSizes[position];
if (!positionBets || typeof positionBets !== 'object') {
throw new Error(`Position ${position} must have bet size configuration`);
}
for (const street of POST_FLOP_STREETS) {
if (positionBets[street]) {
const streetBets = positionBets[street];
if (typeof streetBets !== 'object') {
throw new Error(`Invalid bet sizes for ${position} ${street}`);
}
for (const action of Object.keys(streetBets)) {
if (!['bet', 'raise', 'donk', 'allin', 'check'].includes(action)) {
throw new Error(`Invalid action: ${action}`);
}
const sizeValue = streetBets[action];
if (sizeValue === 'allin') {
continue; // Valid
}
if (Array.isArray(sizeValue)) {
this.validateBetSizes(sizeValue);
}
}
}
}
}
}
/**
* Validate solver configuration
* @param {Object} config - Solver config
* @throws {Error}
*/
static validateSolverConfig(config) {
if (!config || typeof config !== 'object') {
return; // Optional parameter
}
if ('thread_num' in config) {
if (!Number.isInteger(config.thread_num) || config.thread_num < 1 || config.thread_num > 64) {
throw new Error('thread_num must be an integer between 1 and 64');
}
}
if ('accuracy' in config) {
if (!Number.isFinite(config.accuracy) || config.accuracy < 0.1 || config.accuracy > 10) {
throw new Error('accuracy must be a number between 0.1 and 10');
}
}
if ('max_iteration' in config) {
if (!Number.isInteger(config.max_iteration) || config.max_iteration < 1) {
throw new Error('max_iteration must be a positive integer');
}
}
if ('print_interval' in config) {
if (!Number.isInteger(config.print_interval) || config.print_interval < 1) {
throw new Error('print_interval must be a positive integer');
}
}
if ('use_isomorphism' in config) {
if (typeof config.use_isomorphism !== 'boolean' && ![0, 1].includes(config.use_isomorphism)) {
throw new Error('use_isomorphism must be a boolean or 0/1');
}
}
if ('allin_threshold' in config) {
if (!Number.isFinite(config.allin_threshold) || config.allin_threshold < 0 || config.allin_threshold > 1) {
throw new Error('allin_threshold must be a number between 0 and 1');
}
}
if ('dump_rounds' in config) {
if (!Number.isInteger(config.dump_rounds) || ![0, 1, 2].includes(config.dump_rounds)) {
throw new Error('dump_rounds must be 0 (flop), 1 (turn), or 2 (river)');
}
}
}
/**
* Validate all solver parameters
* @param {Object} params
* @throws {Error}
*/
static validateAll(params) {
const { pot, effective_stack, board, range_ip, range_oop, bet_sizes, solver_config } = params;
// Required parameters
this.validatePot(pot);
this.validateStack(effective_stack);
this.validatePotStackRelationship(pot, effective_stack);
this.validateBoard(board);
this.validateRange(range_ip);
this.validateRange(range_oop);
// Optional parameters
this.validateBetSizesObject(bet_sizes);
this.validateSolverConfig(solver_config);
return true;
}
}
export default SolverValidator;