/**
* Parse poker range strings
*/
export class RangeParser {
/**
* Parse a range string into individual hands with weights
* Format: "AA:1.0,KK:0.75,QQ,AKs:0.5" where weight defaults to 1.0
* @param {string} rangeString
* @returns {Array<Object>} Array of { hand, weight }
*/
static parseRange(rangeString) {
if (!rangeString || typeof rangeString !== 'string') {
throw new Error('Range must be a non-empty string');
}
const hands = [];
const entries = rangeString.split(',');
for (const entry of entries) {
const trimmed = entry.trim();
if (!trimmed) continue;
const [hand, weightStr] = trimmed.split(':');
const cleanHand = hand.trim();
const weight = weightStr ? parseFloat(weightStr) : 1.0;
if (!cleanHand) {
throw new Error(`Invalid range entry: "${entry}"`);
}
if (isNaN(weight) || weight < 0 || weight > 1) {
throw new Error(`Invalid weight for ${cleanHand}: ${weightStr}. Weight must be between 0 and 1`);
}
hands.push({
hand: cleanHand,
weight
});
}
if (hands.length === 0) {
throw new Error('Range is empty');
}
return hands;
}
/**
* Expand a range notation to individual hands
* "AA-KK" becomes ["AA", "KK"]
* "AKs-AJs" becomes ["AKs", "AQs", "AJs"]
* @param {string} notation - e.g., "AA-KK", "AKs-AJs", "22-99"
* @returns {Array<string>}
*/
static expandRange(notation) {
// Simple expansion for basic sequences
const pairs = ['AA', 'KK', 'QQ', 'JJ', 'TT', '99', '88', '77', '66', '55', '44', '33', '22'];
const ranks = ['A', 'K', 'Q', 'J', 'T', '9', '8', '7', '6', '5', '4', '3', '2'];
if (notation.includes('-')) {
const [start, end] = notation.split('-');
const expanded = [];
// Check if it's a pair range like "AA-KK"
if (pairs.includes(start) && pairs.includes(end)) {
const startIdx = pairs.indexOf(start);
const endIdx = pairs.indexOf(end);
const [minIdx, maxIdx] = startIdx < endIdx ? [startIdx, endIdx] : [endIdx, startIdx];
return pairs.slice(minIdx, maxIdx + 1);
}
// Otherwise return as is
return [notation];
}
return [notation];
}
/**
* Calculate total hand combinations in a range
* Assumes full deck (no blockers)
* @param {Array<Object>} hands - Array of { hand, weight }
* @returns {number}
*/
static calculateCombinations(hands) {
let total = 0;
for (const { hand, weight } of hands) {
const combos = this.getHandCombinations(hand);
total += combos * weight;
}
return total;
}
/**
* Get number of combinations for a hand
* @param {string} hand - e.g., "AA", "AKs", "AKo", "AK"
* @returns {number}
*/
static getHandCombinations(hand) {
if (hand.length < 2) {
throw new Error(`Invalid hand notation: "${hand}"`);
}
const rank1 = hand[0];
const rank2 = hand[1];
const suit = hand[2];
// Pair
if (rank1 === rank2) {
return 6; // C(4,2) = 6 combinations
}
// Suited
if (suit === 's') {
return 4; // 4 suit combinations (each suit)
}
// Offsuit
if (suit === 'o') {
return 12; // 4 * 3 = 12 combinations
}
// Unspecified suit (assume all combinations)
return 16; // 4 + 12 for any AK
}
/**
* Validate hand notation
* @param {string} hand
* @returns {boolean}
*/
static isValidHand(hand) {
if (!hand || hand.length < 2) return false;
const ranks = ['A', 'K', 'Q', 'J', 'T', '9', '8', '7', '6', '5', '4', '3', '2'];
const rank1 = hand[0];
const rank2 = hand[1];
const suit = hand[2];
const validRank1 = ranks.includes(rank1);
const validRank2 = ranks.includes(rank2);
if (!validRank1 || !validRank2) return false;
// If there's a suit specifier, it must be 's' or 'o'
if (suit && !['s', 'o'].includes(suit)) return false;
// Pairs must be exactly 2 characters
if (rank1 === rank2 && hand.length !== 2) return false;
return true;
}
/**
* Convert range to human-readable string
* @param {Array<Object>} hands
* @returns {string}
*/
static formatRange(hands) {
return hands
.map(({ hand, weight }) => weight < 1 ? `${hand}:${weight.toFixed(2)}` : hand)
.join(',');
}
/**
* Parse range file content
* Range files can be single-line or multi-line format
* @param {string} content - File content
* @returns {Array<Object>}
*/
static parseRangeFile(content) {
if (!content) {
throw new Error('Empty range file');
}
// Try to parse as comma-separated on single or multiple lines
const cleanContent = content.trim().replace(/\n/g, '');
return this.parseRange(cleanContent);
}
}
export default RangeParser;