# -*- coding: utf-8 -*-
from __future__ import unicode_literals, absolute_import, division, print_function
import re
import random
import itertools
import functools
from decimal import Decimal
from pathlib import Path
from cached_property import cached_property
from ._common import PokerEnum, _ReprMixin
from .card import Rank, Card, BROADWAY_RANKS
__all__ = ['Shape', 'Hand', 'Combo', 'Range', 'PAIR_HANDS', 'OFFSUIT_HANDS', 'SUITED_HANDS']
# pregenerated all the possible suit combinations, so we don't have to count them all the time
_PAIR_SUIT_COMBINATIONS = ('cd', 'ch', 'cs', 'dh', 'ds', 'hs')
_OFFSUIT_SUIT_COMBINATIONS = ('cd', 'ch', 'cs', 'dc', 'dh', 'ds',
'hc', 'hd', 'hs', 'sc', 'sd', 'sh')
_SUITED_SUIT_COMBINATIONS = ('cc', 'dd', 'hh', 'ss')
[docs]class Shape(PokerEnum):
OFFSUIT = 'o', 'offsuit', 'off'
SUITED = 's', 'suited'
PAIR = '',
class _HandMeta(type):
"""Makes Hand class iterable. __iter__ goes through all hands in ascending order."""
def __new__(metacls, clsname, bases, classdict):
"""Cache all possible Hand instances on the class itself."""
cls = super(_HandMeta, metacls).__new__(metacls, clsname, bases, classdict)
cls._all_hands = tuple(cls._get_non_pairs()) + tuple(cls._get_pairs())
return cls
def _get_non_pairs(cls):
for rank1 in Rank:
for rank2 in (r for r in Rank if r < rank1):
yield cls('{}{}o'.format(rank1, rank2))
yield cls('{}{}s'.format(rank1, rank2))
def _get_pairs(cls):
for rank in Rank:
yield cls(rank.val * 2)
def __iter__(cls):
return iter(cls._all_hands)
def make_random(cls):
obj = object.__new__(cls)
first = Rank.make_random()
second = Rank.make_random()
obj._set_ranks_in_order(first, second)
if first == second:
obj._shape = ''
else:
obj._shape = random.choice(['s', 'o'])
return obj
[docs]@functools.total_ordering
class Hand(_ReprMixin):
"""General hand without a precise suit. Only knows about two ranks and shape."""
__metaclass__ = _HandMeta
__slots__ = ('first', 'second', '_shape')
def __new__(cls, hand):
if isinstance(hand, cls):
return hand
if len(hand) not in (2, 3):
raise ValueError('Length should be 2 (pair) or 3 (hand)')
first, second = hand[:2]
self = object.__new__(cls)
if len(hand) == 2:
if first != second:
raise ValueError('%r, Not a pair! Maybe you need to specify a suit?' % hand)
self._shape = ''
elif len(hand) == 3:
shape = hand[2].lower()
if first == second:
raise ValueError("{!r}; pairs can't have a suit: {!r}".format(hand, shape))
if shape not in ('s', 'o'):
raise ValueError('{!r}; Invalid shape: {!r}'.format(hand, shape))
self._shape = shape
self._set_ranks_in_order(first, second)
return self
def __unicode__(self):
return '{}{}{}'.format(self.first, self.second, self.shape)
def __hash__(self):
return hash(self.first) + hash(self.second) + hash(self.shape)
def __getstate__(self):
return {'first': self.first, 'second': self.second, '_shape': self._shape}
def __setstate__(self, state):
self.first, self.second, self._shape = state['first'], state['second'], state['_shape']
def __eq__(self, other):
if self.__class__ is not other.__class__:
return NotImplemented
# AKs != AKo, because AKs is better
return (self.first == other.first and
self.second == other.second and
self.shape.val == other.shape.val)
def __lt__(self, other):
if self.__class__ is not other.__class__:
return NotImplemented
# pairs are better than non-pairs
if not self.is_pair and other.is_pair:
return True
elif self.is_pair and not other.is_pair:
return False
elif (not self.is_pair and not other.is_pair and
self.first == other.first and self.second == other.second and
self._shape != other._shape):
# when Rank match, only suit is the deciding factor
# so, offsuit hand is 'less' than suited
return self._shape == 'o'
elif self.first == other.first:
return self.second < other.second
else:
return self.first < other.first
def _set_ranks_in_order(self, first, second):
# set as Rank objects.
self.first, self.second = Rank(first), Rank(second)
if self.first < self.second:
self.first, self.second = self.second, self.first
[docs] def to_combos(self):
first, second = self.first.val, self.second.val
if self.is_pair:
return tuple(Combo(first + s1 + first + s2) for s1, s2 in _PAIR_SUIT_COMBINATIONS)
elif self.is_offsuit:
return tuple(Combo(first + s1 + second + s2) for s1, s2 in _OFFSUIT_SUIT_COMBINATIONS)
else:
return tuple(Combo(first + s1 + second + s2) for s1, s2 in _SUITED_SUIT_COMBINATIONS)
@property
def is_suited_connector(self):
return self.is_suited and self.is_connector
@property
def is_suited(self):
return self._shape == 's'
@property
def is_offsuit(self):
return self._shape == 'o'
@property
def is_connector(self):
return self.rank_difference == 1
@property
def is_one_gapper(self):
return self.rank_difference == 2
@property
def is_two_gapper(self):
return self.rank_difference == 3
@property
def rank_difference(self):
"""The difference between the first and second rank of the Hand."""
# self.first >= self.second
return Rank.difference(self.first, self.second)
@property
def is_broadway(self):
return (self.first in BROADWAY_RANKS and self.second in BROADWAY_RANKS)
@property
def is_pair(self):
return self.first == self.second
@property
def shape(self):
return Shape(self._shape)
@shape.setter
def shape(self, value):
self._shape = Shape(value).val
PAIR_HANDS = tuple(hand for hand in Hand if hand.is_pair)
"""Tuple of all pair hands in ascending order."""
OFFSUIT_HANDS = tuple(hand for hand in Hand if hand.is_offsuit)
"""Tuple of offsuit hands in ascending order."""
SUITED_HANDS = tuple(hand for hand in Hand if hand.is_suited)
"""Tuple of suited hands in ascending order."""
[docs]@functools.total_ordering
class Combo(_ReprMixin):
"""Hand combination."""
__slots__ = ('first', 'second')
def __new__(cls, combo):
if isinstance(combo, Combo):
return combo
if len(combo) != 4:
raise ValueError('%r, should have a length of 4' % combo)
elif (combo[0] == combo[2] and combo[1] == combo[3]):
raise ValueError("{!r}, Pair can't have the same suit: {!r}".format(combo, combo[1]))
self = super(Combo, cls).__new__(cls)
self._set_cards_in_order(combo[:2], combo[2:])
return self
[docs] @classmethod
def from_cards(cls, first, second):
self = super(Combo, cls).__new__(cls)
first = first.rank.val + first.suit.val
second = second.rank.val + second.suit.val
self._set_cards_in_order(first, second)
return self
def __unicode__(self):
return '{}{}'.format(self.first, self.second)
def __hash__(self):
return hash(self.first) + hash(self.second)
def __getstate__(self):
return {'first': self.first, 'second': self.second}
def __setstate__(self, state):
self.first, self.second = state['first'], state['second']
def __eq__(self, other):
if self.__class__ is other.__class__:
return self.first == other.first and self.second == other.second
return NotImplemented
def __lt__(self, other):
if self.__class__ is not other.__class__:
return NotImplemented
# lookup optimization
self_is_pair, other_is_pair = self.is_pair, other.is_pair
self_first, other_first = self.first, other.first
if self_is_pair and other_is_pair:
if self_first == other_first:
return self.second < other.second
return self_first < other_first
elif self_is_pair or other_is_pair:
# Pairs are better than non-pairs
return self_is_pair < other_is_pair
else:
if self_first.rank == other_first.rank:
if self.second.rank == other.second.rank:
# same ranks, suited go first in order by Suit rank
if self.is_suited or other.is_suited:
return self.is_suited < other.is_suited
# both are suited
return self_first.suit < other_first.suit
return self.second < other.second
return self_first < other_first
def _set_cards_in_order(self, first, second):
self.first, self.second = Card(first), Card(second)
if self.first < self.second:
self.first, self.second = self.second, self.first
[docs] def to_hand(self):
"""Convert combo to :class:`Hand` object, losing suit information."""
return Hand('{}{}{}'.format(self.first.rank, self.second.rank, self.shape))
@property
def is_suited_connector(self):
return self.is_suited and self.is_connector
@property
def is_suited(self):
return self.first.suit == self.second.suit
@property
def is_offsuit(self):
return not self.is_suited and not self.is_pair
@property
def is_connector(self):
return self.rank_difference == 1
@property
def is_one_gapper(self):
return self.rank_difference == 2
@property
def is_two_gapper(self):
return self.rank_difference == 3
@property
def rank_difference(self):
"""The difference between the first and second rank of the Combo."""
# self.first >= self.second
return Rank.difference(self.first.rank, self.second.rank)
@property
def is_pair(self):
return self.first.rank == self.second.rank
@property
def is_broadway(self):
return self.first.is_broadway and self.second.is_broadway
@property
def shape(self):
if self.is_pair:
return Shape.PAIR
elif self.is_suited:
return Shape.SUITED
else:
return Shape.OFFSUIT
@shape.setter
def shape(self, value):
self._shape = Shape(value).val
class _RegexRangeLexer(object):
_separator_re = re.compile(r"[,;\s]*")
_rank = r"([2-9TJQKA])"
_suit = r"[cdhs♣♦♥♠]"
# the second card is not the same as the first
# (negative lookahead for the first matching group)
# this will not match pairs, but will match e.g. 86 or AK
_nonpair1 = r"{0}(?!\1){0}".format(_rank)
_nonpair2 = r"{0}(?!\2){0}".format(_rank)
rules = (
# NAME, REGEX, value extractor METHOD NAME
('ALL', r"XX", '_get_value'),
('PAIR', r"{}\1$".format(_rank), '_get_first'),
('PAIR_PLUS', r"{}\1\+$".format(_rank), '_get_first'),
('PAIR_MINUS', r"{}\1-$".format(_rank), '_get_first'),
('PAIR_DASH', r"{0}\1-{0}\2$".format(_rank), '_get_for_pair_dash'),
('BOTH', _nonpair1 + r"$", '_get_first_two'),
('BOTH_PLUS', r"{}\+$".format(_nonpair1), '_get_first_two'),
('BOTH_MINUS', r"{}-$".format(_nonpair1), '_get_first_two'),
('BOTH_DASH', r"{}-{}$".format(_nonpair1, _nonpair2), '_get_for_both_dash'),
('SUITED', r"{}s$".format(_nonpair1), '_get_first_two'),
('SUITED_PLUS', r"{}s\+$".format(_nonpair1), '_get_first_two'),
('SUITED_MINUS', r"{}s-$".format(_nonpair1), '_get_first_two'),
('SUITED_DASH', r"{}s-{}s$".format(_nonpair1, _nonpair2), '_get_for_shaped_dash'),
('OFFSUIT', r"{}o$".format(_nonpair1), '_get_first_two'),
('OFFSUIT_PLUS', r"{}o\+$".format(_nonpair1), '_get_first_two'),
('OFFSUIT_MINUS', r"{}o-$".format(_nonpair1), '_get_first_two'),
('OFFSUIT_DASH', r"{}o-{}o$".format(_nonpair1, _nonpair2), '_get_for_shaped_dash'),
('X_SUITED', r"{0}Xs$|X{0}s$".format(_rank), '_get_rank'),
('X_SUITED_PLUS', r"{0}Xs\+$|X{0}s\+$".format(_rank), '_get_rank'),
('X_SUITED_MINUS', r"{0}Xs-$|X{0}s-$".format(_rank), '_get_rank'),
('X_OFFSUIT', r"{0}Xo$|X{0}o$".format(_rank), '_get_rank'),
('X_OFFSUIT_PLUS', r"{0}Xo\+$|X{0}o\+$".format(_rank), '_get_rank'),
('X_OFFSUIT_MINUS', r"{0}Xo-$|X{0}o-$".format(_rank), '_get_rank'),
('X_PLUS', r"{0}X\+$|X{0}\+$".format(_rank), '_get_rank'),
('X_MINUS', r"{0}X-$|X{0}-$".format(_rank), '_get_rank'),
('X_BOTH', r"{0}X$|X{0}$".format(_rank), '_get_rank'),
# might be anything, even pair
# FIXME: 5s5s accepted
('COMBO', r"{0}{1}{0}{1}$".format(_rank, _suit), '_get_value'),
)
# compile regexes when initializing class, so every instance will have them precompiled
rules = [(name, re.compile(regex, re.IGNORECASE), method) for (name, regex, method) in rules]
def __init__(self, range=''):
# filter out empty matches
self.tokens = [token for token in self._separator_re.split(range) if token]
def __iter__(self):
"""Goes through all the tokens and compare them with the regex rules. If it finds a match,
makes an appropriate value for the token and yields them.
"""
for token in self.tokens:
for name, regex, method_name in self.rules:
if regex.match(token):
val_method = getattr(self, method_name)
yield name, val_method(token)
break
else:
raise ValueError('Invalid token: %s' % token)
@staticmethod
def _get_value(token):
return token
@staticmethod
def _get_first(token):
return token[0]
@staticmethod
def _get_rank(token):
return token[0] if token[1].upper() == 'X' else token[1]
@classmethod
def _get_in_order(cls, first_part, second_part, token):
smaller, bigger = cls._get_rank_in_order(token, first_part, second_part)
return smaller.val, bigger.val
@classmethod
def _get_first_two(cls, token):
return cls._get_in_order(0, 1, token)
@classmethod
def _get_for_pair_dash(cls, token):
return cls._get_in_order(0, 3, token)
@classmethod
def _get_first_smaller_bigger(cls, first_part, second_part, token):
smaller1, bigger1 = cls._get_rank_in_order(token[first_part], 0, 1)
smaller2, bigger2 = cls._get_rank_in_order(token[second_part], 0, 1)
if bigger1 != bigger2:
raise ValueError('Invalid token: %s' % token)
smaller, bigger = min(smaller1, smaller2), max(smaller1, smaller2)
return bigger1.val, smaller.val, bigger.val
@staticmethod
def _get_rank_in_order(token, first_part, second_part):
first, second = Rank(token[first_part]), Rank(token[second_part])
smaller, bigger = min(first, second), max(first, second)
return smaller, bigger
@classmethod
# for 'A5-AT'
def _get_for_both_dash(cls, token):
return cls._get_first_smaller_bigger(slice(0, 2), slice(3, 5), token)
@classmethod
# for 'A5o-ATo' and 'A5s-ATs'
def _get_for_shaped_dash(cls, token):
return cls._get_first_smaller_bigger(slice(0, 2), slice(4, 6), token)
[docs]@functools.total_ordering
class Range(object):
"""Parses a str range into tuple of Combos (or Hands)."""
slots = ('_hands', '_combos')
def __init__(self, range=''):
self._hands = set()
self._combos = set()
for name, value in _RegexRangeLexer(range):
if name == 'ALL':
for card in itertools.combinations('AKQJT98765432', 2):
self._add_offsuit(card)
self._add_suited(card)
for rank in 'AKQJT98765432':
self._add_pair(rank)
# full range, no need to parse any more name
break
elif name == 'PAIR':
self._add_pair(value)
elif name == 'PAIR_PLUS':
smallest = Rank(value)
for rank in (rank.val for rank in Rank if rank >= smallest):
self._add_pair(rank)
elif name == 'PAIR_MINUS':
biggest = Rank(value)
for rank in (rank.val for rank in Rank if rank <= biggest):
self._add_pair(rank)
elif name == 'PAIR_DASH':
first, second = Rank(value[0]), Rank(value[1])
ranks = (rank.val for rank in Rank if first <= rank <= second)
for rank in ranks:
self._add_pair(rank)
elif name == 'BOTH':
self._add_offsuit(value[0] + value[1])
self._add_suited(value[0] + value[1])
elif name == 'X_BOTH':
for rank in (r.val for r in Rank if r < Rank(value)):
self._add_suited(value + rank)
self._add_offsuit(value + rank)
elif name == 'OFFSUIT':
self._add_offsuit(value[0] + value[1])
elif name == 'SUITED':
self._add_suited(value[0] + value[1])
elif name == 'X_OFFSUIT':
biggest = Rank(value)
for rank in (rank.val for rank in Rank if rank < biggest):
self._add_offsuit(value + rank)
elif name == 'X_SUITED':
biggest = Rank(value)
for rank in (rank.val for rank in Rank if rank < biggest):
self._add_suited(value + rank)
elif name == 'BOTH_PLUS':
smaller, bigger = Rank(value[0]), Rank(value[1])
for rank in (rank.val for rank in Rank if smaller <= rank < bigger):
self._add_suited(value[1] + rank)
self._add_offsuit(value[1] + rank)
elif name == 'BOTH_MINUS':
smaller, bigger = Rank(value[0]), Rank(value[1])
for rank in (rank.val for rank in Rank if rank <= smaller):
self._add_suited(value[1] + rank)
self._add_offsuit(value[1] + rank)
elif name in ('X_PLUS', 'X_SUITED_PLUS', 'X_OFFSUIT_PLUS'):
smallest = Rank(value)
first_ranks = (rank for rank in Rank if rank >= smallest)
for rank1 in first_ranks:
second_ranks = (rank for rank in Rank if rank < rank1)
for rank2 in second_ranks:
if name != 'X_OFFSUIT_PLUS':
self._add_suited(rank1.val + rank2.val)
if name != 'X_SUITED_PLUS':
self._add_offsuit(rank1.val + rank2.val)
elif name in ('X_MINUS', 'X_SUITED_MINUS', 'X_OFFSUIT_MINUS'):
biggest = Rank(value)
first_ranks = (rank for rank in Rank if rank <= biggest)
for rank1 in first_ranks:
second_ranks = (rank for rank in Rank if rank < rank1)
for rank2 in second_ranks:
if name != 'X_OFFSUIT_MINUS':
self._add_suited(rank1.val + rank2.val)
if name != 'X_SUITED_MINUS':
self._add_offsuit(rank1.val + rank2.val)
elif name == 'COMBO':
self._combos.add(Combo(value))
elif name == 'OFFSUIT_PLUS':
smaller, bigger = Rank(value[0]), Rank(value[1])
for rank in (rank.val for rank in Rank if smaller <= rank < bigger):
self._add_offsuit(value[1] + rank)
elif name == 'OFFSUIT_MINUS':
smaller, bigger = Rank(value[0]), Rank(value[1])
for rank in (rank.val for rank in Rank if rank <= smaller):
self._add_offsuit(value[1] + rank)
elif name == 'SUITED_PLUS':
smaller, bigger = Rank(value[0]), Rank(value[1])
for rank in (rank.val for rank in Rank if smaller <= rank < bigger):
self._add_suited(value[1] + rank)
elif name == 'SUITED_MINUS':
smaller, bigger = Rank(value[0]), Rank(value[1])
for rank in (rank.val for rank in Rank if rank <= smaller):
self._add_suited(value[1] + rank)
elif name == 'BOTH_DASH':
smaller, bigger = Rank(value[1]), Rank(value[2])
for rank in (rank.val for rank in Rank if smaller <= rank <= bigger):
self._add_offsuit(value[0] + rank)
self._add_suited(value[0] + rank)
elif name == 'OFFSUIT_DASH':
smaller, bigger = Rank(value[1]), Rank(value[2])
for rank in (rank.val for rank in Rank if smaller <= rank <= bigger):
self._add_offsuit(value[0] + rank)
elif name == 'SUITED_DASH':
smaller, bigger = Rank(value[1]), Rank(value[2])
for rank in (rank.val for rank in Rank if smaller <= rank <= bigger):
self._add_suited(value[0] + rank)
[docs] @classmethod
def from_file(cls, filename):
"""Creates an instance from a given file, containing a range.
It can handle the PokerCruncher (.rng extension) format.
"""
range_string = Path(filename).open().read()
return cls(range_string)
[docs] @classmethod
def from_objects(cls, iterable):
"""Make an instance from an iterable of Combos, Hands or both."""
range_string = ' '.join(unicode(obj) for obj in iterable)
return cls(range_string)
def __eq__(self, other):
if self.__class__ is other.__class__:
return self._all_combos == other._all_combos
return NotImplemented
def __lt__(self, other):
if self.__class__ is other.__class__:
return len(self._all_combos) < len(other._all_combos)
return NotImplemented
def __contains__(self, item):
if isinstance(item, Combo):
return item in self._combos or item.to_hand() in self._hands
elif isinstance(item, Hand):
return item in self._all_hands
elif isinstance(item, unicode):
if len(item) == 4:
combo = Combo(item)
return combo in self._combos or combo.to_hand() in self._hands
else:
return Hand(item) in self._all_hands
def __len__(self):
return self._count_combos()
def __unicode__(self):
return ', '.join(self.rep_pieces)
def __str__(self):
return unicode(self).encode('utf-8')
def __repr__(self):
range = ' '.join(self.rep_pieces)
return "{}('{}')".format(self.__class__.__name__, range).encode('utf-8')
def __getstate__(self):
return {'_hands': self._hands, '_combos': self._combos}
def __setstate__(self, state):
self._hands, self._combos = state['_hands'], state['_combos']
def __hash__(self):
return hash(self.combos)
[docs] def to_html(self):
"""Returns a 13x13 HTML table representing the range.
The table's CSS class is ``range``, pair cells (td element) are ``pair``, offsuit hands are
``offsuit`` and suited hand cells has ``suited`` css class.
The HTML contains no extra whitespace at all.
Calculating it should not take more than 30ms (which takes calculating a 100% range).
"""
# note about speed: I tried with functools.lru_cache, and the initial call was 3-4x slower
# than without it, and the need for calling this will usually be once, so no need to cache
html = ['<table class="range">']
for row in reversed(Rank):
html.append('<tr>')
for col in reversed(Rank):
if row > col:
suit, cssclass = 's', 'suited'
elif row < col:
suit, cssclass = 'o', 'offsuit'
else:
suit, cssclass = '', 'pair'
html.append('<td class="%s">' % cssclass)
hand = Hand(row.val + col.val + suit)
if hand in self.hands:
html.append(unicode(hand))
html.append('</td>')
html.append('</tr>')
html.append('</table>')
return ''.join(html)
[docs] def to_ascii(self, border=False):
"""Returns a nicely formatted ASCII table with optional borders."""
table = []
if border:
table.append('┌' + '─────┬' * 12 + '─────┐\n')
line = '├' + '─────┼' * 12 + '─────┤\n'
border = '│ '
lastline = '\n└' + '─────┴' * 12 + '─────┘'
else:
line = border = lastline = ''
for row in reversed(Rank):
for col in reversed(Rank):
if row > col:
suit = 's'
elif row < col:
suit = 'o'
else:
suit = ''
hand = Hand(row.val + col.val + suit)
hand = unicode(hand) if hand in self.hands else ''
table.append(border)
table.append(hand.ljust(4))
if row.val != '2':
table.append(border)
table.append('\n')
table.append(line)
table.append(border)
table.append(lastline)
return ''.join(table)
@property
def rep_pieces(self):
"""List of str pieces how the Range is represented."""
if self._count_combos() == 1326:
return ['XX']
all_combos = self._all_combos
pairs = list(filter(lambda c: c.is_pair, all_combos))
pair_pieces = self._get_pieces(pairs, 6)
suiteds = list(filter(lambda c: c.is_suited, all_combos))
suited_pieces = self._get_pieces(suiteds, 4)
offsuits = list(filter(lambda c: c.is_offsuit, all_combos))
offsuit_pieces = self._get_pieces(offsuits, 12)
pair_strs = self._shorten_pieces(pair_pieces)
suited_strs = self._shorten_pieces(suited_pieces)
offsuit_strs = self._shorten_pieces(offsuit_pieces)
return pair_strs + suited_strs + offsuit_strs
def _get_pieces(self, combos, combos_in_hand):
if not combos:
return []
sorted_combos = sorted(combos, reverse=True)
hands_and_combos = []
current_combos = []
last_combo = sorted_combos[0]
for combo in sorted_combos:
if (last_combo.first.rank == combo.first.rank and
last_combo.second.rank == combo.second.rank):
current_combos.append(combo)
length = len(current_combos)
if length == combos_in_hand:
hands_and_combos.append(combo.to_hand())
current_combos = []
else:
hands_and_combos.extend(current_combos)
current_combos = [combo]
last_combo = combo
# add the remainder if any, current_combos might be empty
hands_and_combos.extend(current_combos)
return hands_and_combos
def _shorten_pieces(self, pieces):
if not pieces:
return []
str_pieces = []
first = last = pieces[0]
for current in pieces[1:]:
if isinstance(last, Combo):
str_pieces.append(unicode(last))
first = last = current
elif isinstance(current, Combo):
str_pieces.append(self._get_format(first, last))
first = last = current
elif ((current.is_pair and Rank.difference(last.first, current.first) == 1) or
(last.first == current.first and
Rank.difference(last.second, current.second) == 1)):
last = current
else:
str_pieces.append(self._get_format(first, last))
first = last = current
# write out any remaining pieces
str_pieces.append(self._get_format(first, last))
return str_pieces
def _get_format(self, first, last):
if first == last:
return unicode(first)
elif (first.is_pair and first.first.val == 'A' or
Rank.difference(first.first, first.second) == 1):
return '%s+' % last
elif last.second.val == '2':
return '%s-' % first
else:
return '{}-{}'.format(first, last)
def _add_pair(self, rank):
self._hands.add(Hand(rank * 2))
def _add_offsuit(self, tok):
self._hands.add(Hand(tok[0] + tok[1] + 'o'))
def _add_suited(self, tok):
self._hands.add(Hand(tok[0] + tok[1] + 's'))
@cached_property
def hands(self):
"""Tuple of hands contained in this range. If only one combo of the same hand is present,
it will be shown here. e.g. ``Range('2s2c').hands == (Hand('22'),)``
"""
return tuple(sorted(self._all_hands))
@cached_property
def combos(self):
return tuple(sorted(self._all_combos))
@cached_property
def percent(self):
"""What percent of combos does this range have compared to all the possible combos.
There are 1326 total combos in Hold'em: 52 * 51 / 2 (because order doesn't matter)
Precision: 2 decimal point
"""
dec_percent = (Decimal(self._count_combos()) / 1326 * 100)
# round to two decimal point
return float(dec_percent.quantize(Decimal('1.00')))
def _count_combos(self):
combo_count = len(self._combos)
for hand in self._hands:
if hand.is_pair:
combo_count += 6
elif hand.is_offsuit:
combo_count += 12
elif hand.is_suited:
combo_count += 4
return combo_count
@cached_property
def _all_combos(self):
hand_combos = {combo for hand in self._hands for combo in hand.to_combos()}
return hand_combos | self._combos
@cached_property
def _all_hands(self):
combo_hands = {combo.to_hand() for combo in self._combos}
return combo_hands | self._hands
if __name__ == '__main__':
import cProfile
print('_all_COMBOS')
cProfile.run("Range('XX')._all_combos", sort='tottime')
print('COMBOS')
cProfile.run("Range('XX').combos", sort='tottime')
print('HANDS')
cProfile.run("Range('XX').hands", sort='tottime')
r = ('KK-QQ, 88-77, A5s, A3s, K8s+, K3s, Q7s+, Q5s, Q3s, J9s-J5s, T4s+, 97s, 95s-93s, 87s, '
'85s-84s, 75s, 64s-63s, 53s, ATo+, K5o+, Q7o-Q5o, J9o-J7o, J4o-J3o, T8o-T3o, 96o+, '
'94o-93o, 86o+, 84o-83o, 76o, 74o, 63o, 54o, 22')
print('R _all_COMBOS')
cProfile.run("Range('%s')._all_combos" % r, sort='tottime')
print('R COMBOS')
cProfile.run("Range('%s').combos" % r, sort='tottime')
print('R HANDS')
cProfile.run("Range('%s').hands" % r, sort='tottime')