/**
* Load and manage poker range files
*/
import fs from 'fs/promises';
import path from 'path';
import { RangeParser } from './parser.js';
export class RangeLoader {
constructor(solverPath) {
// Assume ranges/ is a sibling directory to console_solver
this.solverDir = path.dirname(solverPath);
this.rangesDir = path.join(this.solverDir, 'ranges');
}
/**
* Load and parse a range file
* @param {string} rangePath - Path relative to ranges/ or absolute path
* @returns {Promise<Object>}
*/
async loadRange(rangePath) {
if (!rangePath || typeof rangePath !== 'string') {
throw new Error('Range path must be a non-empty string');
}
const fullPath = path.isAbsolute(rangePath)
? rangePath
: path.join(this.rangesDir, rangePath);
try {
const content = await fs.readFile(fullPath, 'utf8');
const hands = RangeParser.parseRangeFile(content);
return {
range_string: content.trim(),
parsed_hands: hands,
hand_count: hands.length,
total_combos: RangeParser.calculateCombinations(hands),
file_path: fullPath
};
} catch (error) {
if (error.code === 'ENOENT') {
throw new Error(`Range file not found: ${rangePath}`);
}
throw new Error(`Failed to load range file: ${error.message}`);
}
}
/**
* Check if range file exists
* @param {string} rangePath
* @returns {Promise<boolean>}
*/
async rangeExists(rangePath) {
const fullPath = path.isAbsolute(rangePath)
? rangePath
: path.join(this.rangesDir, rangePath);
try {
await fs.access(fullPath);
return true;
} catch {
return false;
}
}
/**
* Get full path for a range file
* @param {string} rangePath
* @returns {string}
*/
getFullPath(rangePath) {
return path.isAbsolute(rangePath)
? rangePath
: path.join(this.rangesDir, rangePath);
}
/**
* List all range files in the ranges directory
* @returns {Promise<Array>}
*/
async listRangeFiles() {
try {
const ranges = [];
// Recursively walk the ranges directory
const walk = async (dir, prefix = '') => {
const files = await fs.readdir(dir, { withFileTypes: true });
for (const file of files) {
const fullPath = path.join(dir, file.name);
const relativePath = prefix ? `${prefix}/${file.name}` : file.name;
if (file.isDirectory()) {
await walk(fullPath, relativePath);
} else if (file.isFile() && (file.name.endsWith('.txt') || !file.name.includes('.'))) {
// Include .txt files and files without extension
ranges.push({
path: relativePath,
full_path: fullPath,
name: file.name,
size: (await fs.stat(fullPath)).size
});
}
}
};
try {
await walk(this.rangesDir);
} catch (error) {
// Ranges directory might not exist
if (error.code !== 'ENOENT') {
throw error;
}
}
return ranges.sort((a, b) => a.path.localeCompare(b.path));
} catch (error) {
throw new Error(`Failed to list range files: ${error.message}`);
}
}
/**
* Filter range files by pattern
* @param {string} pattern - Search pattern (e.g., "BTN", "6max")
* @returns {Promise<Array>}
*/
async filterRanges(pattern) {
const allRanges = await this.listRangeFiles();
const lowerPattern = pattern.toLowerCase();
return allRanges.filter(range =>
range.path.toLowerCase().includes(lowerPattern) ||
range.name.toLowerCase().includes(lowerPattern)
);
}
/**
* Get ranges by set (e.g., "6max", "qb")
* @param {string} rangeSet
* @returns {Promise<Array>}
*/
async getRangesBySet(rangeSet) {
const allRanges = await this.listRangeFiles();
const setName = rangeSet.toLowerCase();
return allRanges.filter(range =>
range.path.toLowerCase().includes(setName)
);
}
/**
* Get ranges by position
* @param {string} position - e.g., "BTN", "SB", "BB"
* @returns {Promise<Array>}
*/
async getRangesByPosition(position) {
const allRanges = await this.listRangeFiles();
const posName = position.toUpperCase();
return allRanges.filter(range =>
range.path.includes(posName) || range.name.includes(posName)
);
}
/**
* Load multiple ranges at once
* @param {Array<string>} paths
* @returns {Promise<Object>}
*/
async loadMultipleRanges(paths) {
const results = {};
for (const rangePath of paths) {
try {
results[rangePath] = await this.loadRange(rangePath);
} catch (error) {
results[rangePath] = { error: error.message };
}
}
return results;
}
/**
* Get ranges directory info
* @returns {Promise<Object>}
*/
async getRangesInfo() {
try {
const stat = await fs.stat(this.rangesDir);
const ranges = await this.listRangeFiles();
return {
path: this.rangesDir,
exists: true,
total_files: ranges.length,
ranges: ranges
};
} catch (error) {
return {
path: this.rangesDir,
exists: false,
error: error.message
};
}
}
}
export default RangeLoader;