Last active
January 13, 2020 14:30
-
-
Save cessor/7dddfec51dca5744d1d8250e41c046cf to your computer and use it in GitHub Desktop.
poker_dice.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import time | |
| import itertools | |
| class DieRollAnimation: | |
| _CYCLE = itertools.cycle(range(1, 7)) | |
| def __init__(self, roll_duration_s): | |
| self._roll_duration_s = roll_duration_s | |
| def _cycle_seconds(self, seconds): | |
| start = time.time() | |
| now = time.time() | |
| while now - start < seconds: | |
| yield next(self._CYCLE) | |
| now = time.time() | |
| def _typewrite(self, pre, boundary): | |
| for current in self._cycle_seconds(self._roll_duration_s): | |
| buffer_ = f'{pre}{current}' | |
| print(f'\r{buffer_}', end='') | |
| return f'{pre}{boundary}' | |
| def animate(self, dice): | |
| pre = '🎲 ' | |
| for die in dice: | |
| pre = self._typewrite(pre, int(die)) + ' ' | |
| print(f'\r{pre}') | |
| if __name__ == '__main__': | |
| DieRollAnimation([1, 2, 3, 4, 5, 6], 0.5).animate() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import secrets | |
| from collections import Counter | |
| from animation import DieRollAnimation | |
| class Die: | |
| def __init__(self, value): | |
| self._value = value | |
| def __repr__(self): | |
| return f'<Die: {self._value}>' | |
| def __str__(self): | |
| return str(self._value) | |
| def __hash__(self): | |
| return self._value | |
| def __eq__(self, other): | |
| return self._value == other._value | |
| def __lt__(self, other): | |
| return self._value < other._value | |
| def __int__(self): | |
| '''PIPS''' | |
| return self._value | |
| @classmethod | |
| def roll(cls): | |
| return Die(secrets.choice((1, 2, 3, 4, 5, 6))) | |
| class _Hand: | |
| _RANK = 0 | |
| _ICON = ' ' | |
| def __init__(self, dice): | |
| self._dice = dice | |
| def __lt__(self, other): | |
| """ | |
| Compare this hand to another. | |
| Hands outrank each other, e.g. "Five of a Kind" outranks a "Full House" | |
| When the ranks are tied, the higher sum wins. | |
| """ | |
| #if isinstance(self, type(other)): | |
| return (self._RANK, self.sum()) < (other._RANK, other.sum()) | |
| def __iter__(self): | |
| yield from sorted(self._dice or []) | |
| def loose_dice(self): | |
| """ | |
| Returns the dice that do not contribute to the hand. | |
| E.g. A in a hand of "One Pair" 5,5,1,2,3 the dice 1,2,3 are loose | |
| """ | |
| raise NotImplemented | |
| def reroll_loose_dice(self): | |
| loose = self.loose_dice() | |
| for die in self: | |
| if die not in loose: | |
| yield die | |
| else: | |
| yield Die.roll() | |
| def sum(self): | |
| return sum(int(die) for die in self) | |
| def __str__(self): | |
| icon = self._ICON | |
| name = self.__class__.__name__ | |
| dice = ','.join([str(die) for die in sorted(self._dice)]) | |
| return f'{icon} {name}: {dice}' | |
| class FiveOfAKind(_Hand): | |
| """ | |
| E.g. 5,5,5,5,5 | |
| """ | |
| _RANK = 9 | |
| _ICON = '🖐️' | |
| @staticmethod | |
| def test(dice): | |
| return len(set(dice)) == 1 | |
| def loose_dice(self): | |
| return [] | |
| class FourOfAKind(_Hand): | |
| """ | |
| E.g. 5,5,5,5,1 | |
| """ | |
| _RANK = 8 | |
| _ICON = '🍀' | |
| @staticmethod | |
| def test(dice): | |
| head, *_ = Counter(dice).most_common() | |
| die, count = head | |
| return count == 4 | |
| def loose_dice(self): | |
| dice = Counter(self._dice).most_common() | |
| return [die for die, count in dice if count != 4] | |
| class FullHouse(_Hand): | |
| """ | |
| One Triplet, One Pair, e.g. 5,5,5,1,1 | |
| """ | |
| _RANK = 7 | |
| _ICON = '🏠' | |
| @staticmethod | |
| def test(dice): | |
| dist = Counter(dice).most_common() | |
| if len(dist) != 2: | |
| return False | |
| first, second = dist | |
| die_, count_first = first | |
| die_, count_second = second | |
| return count_first == 3 and count_second == 2 | |
| def loose_dice(self): | |
| return [] | |
| class HighStraight(_Hand): | |
| """ | |
| i.e. 6,5,4,3,2 | |
| """ | |
| _RANK = 6 | |
| _ICON = '🏎️' | |
| @staticmethod | |
| def test(dice): | |
| return set(dice) == {Die(6), Die(5), Die(4), Die(3), Die(2)} | |
| def loose_dice(self): | |
| return [] | |
| class LowStraight(_Hand): | |
| """ | |
| i.e. 5,4,3,2,1 | |
| """ | |
| _RANK = 5 | |
| _ICON = '🚗' | |
| @staticmethod | |
| def test(dice): | |
| return set(dice) == {Die(5), Die(4), Die(3), Die(2), Die(1)} | |
| def loose_dice(self): | |
| return [] | |
| class ThreeOfAKind(_Hand): | |
| """ | |
| e.g. 5,5,5,1,2 | |
| """ | |
| _RANK = 4 | |
| _ICON = '☘️' | |
| @staticmethod | |
| def test(dice): | |
| head, *tail = Counter(dice).most_common() | |
| die, count = head | |
| return count == 3 and len(tail) == 2 | |
| def loose_dice(self): | |
| dist = Counter(self._dice).most_common() | |
| return [die for die, count in dist if count != 3] | |
| class TwoPairs(_Hand): | |
| """ | |
| e.g. 5,5,3,3,3 | |
| """ | |
| _RANK = 3 | |
| _ICON = '🌟' | |
| @staticmethod | |
| def test(dice): | |
| dist = Counter(dice).most_common() | |
| return len(dist) == 3 and dist[0][1] == 2 and dist[1][1] == 2 | |
| def loose_dice(self): | |
| dist = Counter(self._dice).most_common() | |
| return [die for die, count in dist if count == 1] | |
| class OnePair(_Hand): | |
| """ | |
| e.g. 5,5,3,2,1 | |
| """ | |
| _RANK = 2 | |
| _ICON = '⭐' | |
| @staticmethod | |
| def test(dice): | |
| dist = Counter(dice).most_common() | |
| return len(dist) == 4 and dist[0][1] == 2 | |
| def loose_dice(self): | |
| dist = Counter(self._dice).most_common() | |
| return [die for die, count in dist if count != 2] | |
| class Runt(_Hand): | |
| """ | |
| e.g. 6,5,3,2,1 | |
| """ | |
| _RANK = 1 | |
| _ICON = '💩' | |
| @staticmethod | |
| def test(dice): | |
| raise NotImplemented() | |
| def loose_dice(self): | |
| return self._dice | |
| class Dice: | |
| _hands = [ | |
| FiveOfAKind, | |
| FourOfAKind, | |
| FullHouse, | |
| HighStraight, | |
| LowStraight, | |
| ThreeOfAKind, | |
| TwoPairs, | |
| OnePair | |
| ] | |
| def __init__(self, dice): | |
| self._dice = dice | |
| def hand(self): | |
| for Hand in self._hands: | |
| if Hand.test(self._dice): | |
| return Hand(self._dice) | |
| return Runt(self._dice) | |
| @classmethod | |
| def roll(cls): | |
| dice = [Die.roll() for _ in range(5)] | |
| return Dice(dice).hand() | |
| class Player: | |
| """ | |
| A human player who gets to choose what to do. | |
| """ | |
| def first_hand(self): | |
| return Dice.roll() | |
| def _reroll_dice(self, dice_indices, first_hand): | |
| for i, die in enumerate(first_hand, start=1): | |
| if i in dice_indices: | |
| yield Die.roll() | |
| else: | |
| yield die | |
| def _choice(self, first_hand): | |
| print("You've got:", first_hand) | |
| s = input("> ").strip() | |
| if not s: | |
| return first_hand | |
| if 'all' in s: | |
| return Dice.roll() | |
| dice_indices = {int(d) for d in s.split(' ')} | |
| dice = list(self._reroll_dice(dice_indices, first_hand)) | |
| return Dice(dice).hand() | |
| def second_hand(self, first_hand): | |
| return self._choice(first_hand) | |
| # while True: | |
| # try: | |
| # except: | |
| # pass | |
| class Novice: | |
| """ | |
| Rerolls every time. | |
| The novice doesn't know any combinations | |
| and simply playes by chance. | |
| """ | |
| def __init__(self): | |
| pass | |
| def first_hand(self): | |
| return Dice.roll() | |
| def second_hand(self, first_hand): | |
| # Better: Rerolling all each time: | |
| # Improves 4_871_709 times out of 10_000_000 | |
| # Worse: Rerolling all when there are loose dice: | |
| # Improves 4_836_385 times out of 10_000_000 | |
| return Dice.roll() | |
| class Adept(Novice): | |
| """ | |
| Rerolls only dice that don't contribute to actual hand, | |
| in hopes to improve their hand. | |
| """ | |
| def second_hand(self, first_hand): | |
| # Improves 6_981_030 out of 10_000_000 | |
| if first_hand.loose_dice(): | |
| dice = list(first_hand.reroll_loose_dice()) | |
| return Dice(dice).hand() | |
| return first_hand | |
| class SmartPlayer(Novice): | |
| """ | |
| Smart Player also checks | |
| if a runt can be made into a Straight | |
| """ | |
| pass | |
| class VigilantPlayer(Novice): | |
| """ | |
| Vigilant Player considers your hand, too | |
| """ | |
| pass | |
| ANIMATION_DURATION_S = 0.5 | |
| def animate(label, dice): | |
| print(label) | |
| DieRollAnimation(ANIMATION_DURATION_S).animate(dice) | |
| if __name__ == '__main__': | |
| player, opponent = Player(), Adept() | |
| p1 = player.first_hand() | |
| e1 = opponent.first_hand() | |
| animate('🐮 You:', p1) | |
| animate('🤖 Opponent:', e1) | |
| p2 = player.second_hand(p1) | |
| e2 = opponent.second_hand(e1) | |
| if p1 is not p2: | |
| animate('🐮 You:', p2) | |
| print(f"🐮 You: {p2}") | |
| if e1 is not e2: | |
| animate('🤖 Opponent:', e2) | |
| print(f"🤖 Opponent: {e2}") | |
| if p2 > e2: | |
| print('👑👑👑 YOU WIN 👑👑👑') | |
| exit() | |
| if p2 < e2: | |
| print('💩💩💩 YOU LOSE 💩💩💩') | |
| exit() | |
| print('👉👉👉 TIE 👈👈👈') | |
| ''' | |
| Suggestions for Player icons | |
| https://emojipedia.org/nature/ | |
| ''' | |
| ''' | |
| Suggestions for Opponent Icons | |
| ⚔️ Crossed Swords | |
| 👻 Ghost | |
| 👽 Alien | |
| 👾 Alien Monster | |
| 🤖 Robot | |
| ''' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| from poker import * | |
| import unittest | |
| class Hands(unittest.TestCase): | |
| # https://de.wikipedia.org/wiki/Poker_Dice | |
| def test_five_of_a_kind(self): | |
| # '4,4,4,4,4' | |
| dice = [Die(1), Die(1), Die(1), Die(1), Die(1)] | |
| self.assertTrue(FiveOfAKind.test(dice)) | |
| dice = [Die(2), Die(1), Die(1), Die(1), Die(1)] | |
| self.assertFalse(FiveOfAKind.test(dice)) | |
| def test_four_of_a_kind(self): | |
| # '4,4,4,4,1' | |
| dice = [Die(1), Die(1), Die(1), Die(1), Die(1)] | |
| self.assertFalse(FourOfAKind.test(dice)) | |
| dice = [Die(2), Die(1), Die(1), Die(1), Die(1)] | |
| self.assertTrue(FourOfAKind.test(dice)) | |
| dice = [Die(2), Die(2), Die(1), Die(1), Die(1)] | |
| self.assertFalse(FourOfAKind.test(dice)) | |
| def test_full_house(self): | |
| # 4,4,4,2,2 | |
| dice = [Die(2), Die(2), Die(1), Die(1), Die(1)] | |
| self.assertTrue(FullHouse.test(dice)) | |
| dice = [Die(2), Die(2), Die(2), Die(1), Die(1)] | |
| self.assertTrue(FullHouse.test(dice)) | |
| dice = [Die(2), Die(2), Die(2), Die(2), Die(1)] | |
| self.assertFalse(FullHouse.test(dice)) | |
| def test_high_straight(self): | |
| # 6,5,4,3,2 | |
| dice = [Die(6), Die(5), Die(4), Die(3), Die(2)] | |
| self.assertTrue(HighStraight.test(dice)) | |
| dice = [Die(5), Die(5), Die(4), Die(2), Die(2)] | |
| self.assertFalse(HighStraight.test(dice)) | |
| def test_low_straight(self): | |
| # 5,4,3,2,1 | |
| dice = [Die(5), Die(4), Die(3), Die(2), Die(1)] | |
| self.assertTrue(LowStraight.test(dice)) | |
| dice = [Die(5), Die(5), Die(4), Die(2), Die(2)] | |
| self.assertFalse(LowStraight.test(dice)) | |
| def test_three_of_a_kind(self): | |
| # 5,5,5,2,1 | |
| dice = [Die(5), Die(5), Die(5), Die(2), Die(1)] | |
| self.assertTrue(ThreeOfAKind.test(dice)) | |
| dice = [Die(5), Die(5), Die(5), Die(1), Die(1)] | |
| self.assertFalse(ThreeOfAKind.test(dice)) | |
| dice = [Die(1), Die(2), Die(3), Die(4), Die(5)] | |
| self.assertFalse(ThreeOfAKind.test(dice)) | |
| def test_two_pairs(self): | |
| # e.g. 5,5,2,2,1 | |
| dice = [Die(5), Die(5), Die(2), Die(2), Die(1)] | |
| self.assertTrue(TwoPairs.test(dice)) | |
| dice = [Die(5), Die(5), Die(5), Die(1), Die(1)] | |
| self.assertFalse(TwoPairs.test(dice)) | |
| dice = [Die(1), Die(2), Die(3), Die(4), Die(5)] | |
| self.assertFalse(TwoPairs.test(dice)) | |
| def test_one_pair(self): | |
| # e.g. 5,5,3,2,1 | |
| dice = [Die(5), Die(5), Die(3), Die(2), Die(1)] | |
| self.assertTrue(OnePair.test(dice)) | |
| dice = [Die(5), Die(5), Die(2), Die(2), Die(1)] | |
| self.assertFalse(OnePair.test(dice)) | |
| dice = [Die(1), Die(2), Die(3), Die(4), Die(5)] | |
| self.assertFalse(OnePair.test(dice)) | |
| def test_one_pair_loose_dice(self): | |
| # Returns the dice that can be rerolled | |
| # e.g. 5,5,3,2,1 --> 3,2,1 | |
| a, b, c, d, e = Die(5), Die(5), Die(3), Die(2), Die(1) | |
| dice = (a, b, c, d, e) | |
| loose_dice = OnePair(dice).loose_dice() | |
| self.assertFalse(a in loose_dice) | |
| self.assertFalse(b in loose_dice) | |
| self.assertTrue(c in loose_dice) | |
| self.assertTrue(d in loose_dice) | |
| self.assertTrue(e in loose_dice) | |
| def test_two_pairs_loose_dice(self): | |
| # Returns the dice that can be rerolled | |
| # e.g. 5,5,3,3,1 --> 1 | |
| a, b, c, d, e = Die(5), Die(5), Die(3), Die(3), Die(1) | |
| dice = (a, b, c, d, e) | |
| loose_dice = TwoPairs(dice).loose_dice() | |
| self.assertFalse(a in loose_dice) | |
| self.assertFalse(b in loose_dice) | |
| self.assertFalse(c in loose_dice) | |
| self.assertFalse(d in loose_dice) | |
| self.assertTrue(e in loose_dice) | |
| def test_three_of_a_kind_loose_dice(self): | |
| # Returns the dice that can be rerolled | |
| # e.g. 5,5,5,3,1 --> 3,1 | |
| a, b, c, d, e = Die(5), Die(5), Die(5), Die(3), Die(1) | |
| dice = (a, b, c, d, e) | |
| loose_dice = ThreeOfAKind(dice).loose_dice() | |
| self.assertFalse(a in loose_dice) | |
| self.assertFalse(b in loose_dice) | |
| self.assertFalse(c in loose_dice) | |
| self.assertTrue(d in loose_dice) | |
| self.assertTrue(e in loose_dice) | |
| def test_low_straight_loose_dice(self): | |
| # Returns the dice that can be rerolled | |
| # e.g. 5,4,3,2,1 --> {} | |
| a, b, c, d, e = Die(5), Die(5), Die(3), Die(2), Die(1) | |
| dice = (a, b, c, d, e) | |
| loose_dice = LowStraight(dice).loose_dice() | |
| self.assertEqual(loose_dice, []) | |
| def test_high_straight_loose_dice(self): | |
| # Returns the dice that can be rerolled | |
| # e.g. 6,5,4,3,2 --> {} | |
| a, b, c, d, e = Die(5), Die(5), Die(3), Die(2), Die(1) | |
| dice = (a, b, c, d, e) | |
| loose_dice = HighStraight(dice).loose_dice() | |
| self.assertEqual(loose_dice, []) | |
| def test_full_house_loose_dice(self): | |
| # Returns the dice that can be rerolled | |
| # e.g. 5,5,5,3,3 --> {} | |
| a, b, c, d, e = Die(5), Die(5), Die(5), Die(3), Die(3) | |
| dice = (a, b, c, d, e) | |
| loose_dice = FullHouse(dice).loose_dice() | |
| self.assertEqual(loose_dice, []) | |
| def test_four_of_a_kind_loose_dice(self): | |
| # Returns the dice that can be rerolled | |
| # e.g. 5,5,5,5,3 --> 3 | |
| a, b, c, d, e = Die(5), Die(5), Die(5), Die(5), Die(3) | |
| dice = (a, b, c, d, e) | |
| loose_dice = FourOfAKind(dice).loose_dice() | |
| self.assertFalse(a in loose_dice) | |
| self.assertFalse(b in loose_dice) | |
| self.assertFalse(c in loose_dice) | |
| self.assertFalse(d in loose_dice) | |
| self.assertTrue(e in loose_dice) | |
| def test_five_of_a_kind_loose_dice(self): | |
| # Returns the dice that can be rerolled | |
| # e.g. 5,5,5,5,5 --> {} | |
| a, b, c, d, e = Die(5), Die(5), Die(5), Die(5), Die(5) | |
| dice = (a, b, c, d, e) | |
| loose_dice = FiveOfAKind(dice).loose_dice() | |
| self.assertEqual(loose_dice, []) | |
| def test_runt_loose_dice(self): | |
| # Returns the dice that can be rerolled | |
| # e.g. 5,5,5,5,5 --> {} | |
| a, b, c, d, e = Die(5), Die(5), Die(5), Die(5), Die(5) | |
| dice = [a, b, c, d, e] | |
| loose_dice = Runt(dice).loose_dice() | |
| self.assertEqual(loose_dice, dice) | |
| def test_hand_ranks(self): | |
| dice = None | |
| self.assertTrue( | |
| FiveOfAKind(dice) > | |
| FourOfAKind(dice) > | |
| FullHouse(dice) > | |
| HighStraight(dice) > | |
| LowStraight(dice) > | |
| ThreeOfAKind(dice) > | |
| TwoPairs(dice) > | |
| OnePair(dice) > | |
| Runt(dice) | |
| ) | |
| def test_tied_ranks(self): | |
| ''' | |
| If it is a tie, the higher sum wins | |
| ''' | |
| self.assertTrue( | |
| FiveOfAKind((Die(5), Die(5), Die(5), Die(5), Die(5))) > | |
| FiveOfAKind((Die(3), Die(3), Die(3), Die(3), Die(3))) | |
| ) | |
| self.assertTrue( | |
| FourOfAKind((Die(5), Die(5), Die(5), Die(5), Die(1))) > | |
| FourOfAKind((Die(3), Die(3), Die(3), Die(3), Die(1))) | |
| ) | |
| self.assertTrue( | |
| FullHouse((Die(5), Die(5), Die(5), Die(2), Die(2))) > | |
| FullHouse((Die(3), Die(3), Die(3), Die(1), Die(1))) | |
| ) | |
| # Can't decide on sum for high straight | |
| self.assertFalse( | |
| HighStraight((Die(6), Die(5), Die(4), Die(3), Die(2))) > | |
| HighStraight((Die(6), Die(5), Die(4), Die(3), Die(2))) | |
| ) | |
| # Can't decide on sum for low straight | |
| self.assertFalse( | |
| LowStraight((Die(5), Die(4), Die(3), Die(2), Die(1))) > | |
| LowStraight((Die(5), Die(4), Die(3), Die(2), Die(1))) | |
| ) | |
| self.assertTrue( | |
| TwoPairs((Die(5), Die(5), Die(3), Die(3), Die(1))) > | |
| TwoPairs((Die(3), Die(3), Die(2), Die(2), Die(1))) | |
| ) | |
| self.assertTrue( | |
| OnePair((Die(5), Die(5), Die(3), Die(2), Die(1))) > | |
| OnePair((Die(4), Die(4), Die(3), Die(2), Die(1))) | |
| ) | |
| self.assertFalse( | |
| Runt((Die(6), Die(4), Die(3), Die(2), Die(1))) > | |
| Runt((Die(6), Die(4), Die(3), Die(2), Die(1))) | |
| ) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment