275 lines
8.1 KiB
Plaintext
275 lines
8.1 KiB
Plaintext
{
|
|
"cells": [
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "a6b94ebb",
|
|
"metadata": {},
|
|
"source": [
|
|
"# [Poker Hands](https://projecteuler.net/problem=54)\n",
|
|
"\n",
|
|
"No real interesting mathematical concepts to apply in this one, more just a straight programming problem. Here's a (slightly overengineered) solution.\n",
|
|
"\n",
|
|
"First we'll define enums for the rank and suit of a card."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 1,
|
|
"id": "86e737fd",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"from enum import Enum, IntEnum\n",
|
|
"\n",
|
|
"class Rank(IntEnum):\n",
|
|
" TWO = 2\n",
|
|
" THREE = 3\n",
|
|
" FOUR = 4\n",
|
|
" FIVE = 5\n",
|
|
" SIX = 6\n",
|
|
" SEVEN = 7\n",
|
|
" EIGHT = 8\n",
|
|
" NINE = 9\n",
|
|
" TEN = 10\n",
|
|
" JACK = 11\n",
|
|
" QUEEN = 12\n",
|
|
" KING = 13\n",
|
|
" ACE = 14\n",
|
|
" \n",
|
|
"class Suit(Enum):\n",
|
|
" CLUBS = \"C\"\n",
|
|
" DIAMONDS = \"D\"\n",
|
|
" HEARTS = \"H\"\n",
|
|
" SPADES = \"S\""
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "b772617e",
|
|
"metadata": {},
|
|
"source": [
|
|
"We'll use these enums to implement a card [dataclass](https://docs.python.org/3/library/dataclasses.html). We define a few comparison methods so we can say what cards rank above others in certain hands. The [total_ordering decorator](https://docs.python.org/3/library/functools.html) defines the comparison methods that are left out. These other methods aren't strictly necessary, but it's a [PEP 8](https://peps.python.org/pep-0008/) recommendation."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 2,
|
|
"id": "80ce84b6",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"from dataclasses import dataclass\n",
|
|
"from functools import total_ordering\n",
|
|
"\n",
|
|
"@dataclass\n",
|
|
"@total_ordering\n",
|
|
"class Card:\n",
|
|
" rank: Rank\n",
|
|
" suit: Suit\n",
|
|
" \n",
|
|
" def __init__(self, s):\n",
|
|
" x, y = s\n",
|
|
" \n",
|
|
" lookup = {\n",
|
|
" \"2\": Rank.TWO,\n",
|
|
" \"3\": Rank.THREE,\n",
|
|
" \"4\": Rank.FOUR,\n",
|
|
" \"5\": Rank.FIVE,\n",
|
|
" \"6\": Rank.SIX,\n",
|
|
" \"7\": Rank.SEVEN,\n",
|
|
" \"8\": Rank.EIGHT,\n",
|
|
" \"9\": Rank.NINE,\n",
|
|
" \"T\": Rank.TEN,\n",
|
|
" \"J\": Rank.JACK,\n",
|
|
" \"Q\": Rank.QUEEN,\n",
|
|
" \"K\": Rank.KING,\n",
|
|
" \"A\": Rank.ACE,\n",
|
|
" }\n",
|
|
" self.rank = lookup[x]\n",
|
|
" self.suit = Suit(y)\n",
|
|
" \n",
|
|
" def __eq__(self, other):\n",
|
|
" return self.rank == other.rank\n",
|
|
" \n",
|
|
" def __lt__(self, other):\n",
|
|
" return self.rank < other.rank"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "440a49cc",
|
|
"metadata": {},
|
|
"source": [
|
|
"Now we'll make an enum for comparing hand ranks."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 3,
|
|
"id": "25fef2a4",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"class HandRank(IntEnum):\n",
|
|
" HIGHCARD = 1\n",
|
|
" ONEPAIR = 2\n",
|
|
" TWOPAIR = 3\n",
|
|
" THREEOFAKIND = 4\n",
|
|
" STRAIGHT = 5\n",
|
|
" FLUSH = 6\n",
|
|
" FULLHOUSE = 7\n",
|
|
" FOUROFAKIND = 8\n",
|
|
" STRAIGHTFLUSH = 9"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "5a1dbf39",
|
|
"metadata": {},
|
|
"source": [
|
|
"Most of the magic happens in the Hand class. We determine a hand's rank with the `rank` method. Then we define comparison methods to determine whether one hand beats another. If two hands have the same rank, we group the cards in each hand by each card's rank, and sort the groups primarily by frequency and secondarily by rank. We then compare these special group orderings to determine which hand wins. This allows for proper handling of situations like example 4 in the problem statement, since it will compare the ranks of the largest groups of cards first - for example, we don't want a pair of kings to lose to a pair of nines just because the latter hand also has an ace."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 4,
|
|
"id": "0d0de002",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"from collections import Counter\n",
|
|
"\n",
|
|
"@total_ordering\n",
|
|
"class Hand:\n",
|
|
" def __init__(self, cards):\n",
|
|
" self.cards = cards\n",
|
|
" \n",
|
|
" def __eq__(self, other):\n",
|
|
" return sorted(self.cards) == sorted(other.cards)\n",
|
|
" \n",
|
|
" def __lt__(self, other):\n",
|
|
" if self.rank == other.rank:\n",
|
|
" return self.rank_groups() < other.rank_groups()\n",
|
|
" \n",
|
|
" return self.rank < other.rank\n",
|
|
" \n",
|
|
" def rank_groups(self):\n",
|
|
" ranks = Counter(card.rank for card in self.cards)\n",
|
|
" groups = ((v, k) for (k, v) in ranks.most_common())\n",
|
|
" return sorted(groups, reverse=True)\n",
|
|
" \n",
|
|
" @property\n",
|
|
" def is_straight(self):\n",
|
|
" ranks = {card.rank for card in self.cards}\n",
|
|
" return len(ranks) == 5 and max(ranks) - min(ranks) == 4\n",
|
|
" \n",
|
|
" @property\n",
|
|
" def is_flush(self):\n",
|
|
" return len({card.suit for card in self.cards}) == 1\n",
|
|
" \n",
|
|
" @property\n",
|
|
" def rank(self):\n",
|
|
" is_straight = self.is_straight\n",
|
|
" is_flush = self.is_flush\n",
|
|
" \n",
|
|
" if is_straight and is_flush:\n",
|
|
" return HandRank.STRAIGHTFLUSH\n",
|
|
" \n",
|
|
" ranks = Counter(card.rank for card in self.cards)\n",
|
|
" if 4 in ranks.values():\n",
|
|
" return HandRank.FOUROFAKIND\n",
|
|
" elif 3 in ranks.values() and 2 in ranks.values():\n",
|
|
" return HandRank.FULLHOUSE\n",
|
|
" elif is_flush:\n",
|
|
" return HandRank.FLUSH\n",
|
|
" elif is_straight:\n",
|
|
" return HandRank.STRAIGHT\n",
|
|
" elif 3 in ranks.values():\n",
|
|
" return HandRank.THREEOFAKIND\n",
|
|
" elif list(ranks.values()).count(2) == 2:\n",
|
|
" return HandRank.TWOPAIR\n",
|
|
" elif 2 in ranks.values():\n",
|
|
" return HandRank.ONEPAIR\n",
|
|
" else:\n",
|
|
" return HandRank.HIGHCARD"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "18e2e2fb",
|
|
"metadata": {},
|
|
"source": [
|
|
"With all these classes, all that's left is to read the file and compare each hand."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 5,
|
|
"id": "909e853a",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"hands = []\n",
|
|
"with open(\"txt/0054_poker.txt\") as f:\n",
|
|
" for line in f:\n",
|
|
" cards = tuple(Card(c) for c in line.split())\n",
|
|
" hand1, hand2 = Hand(cards[:5]), Hand(cards[5:])\n",
|
|
" hands.append((hand1, hand2))"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 6,
|
|
"id": "4605d126",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"data": {
|
|
"text/plain": [
|
|
"376"
|
|
]
|
|
},
|
|
"execution_count": 6,
|
|
"metadata": {},
|
|
"output_type": "execute_result"
|
|
}
|
|
],
|
|
"source": [
|
|
"wins = [i for (i, (x, y)) in enumerate(hands) if x > y]\n",
|
|
"len(wins)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "dc2baa5b",
|
|
"metadata": {},
|
|
"source": [
|
|
"#### Copyright (C) 2025 filifa\n",
|
|
"\n",
|
|
"This work is licensed under the [Creative Commons Attribution-ShareAlike 4.0 International license](https://creativecommons.org/licenses/by-sa/4.0/) and the [BSD Zero Clause license](https://spdx.org/licenses/0BSD.html)."
|
|
]
|
|
}
|
|
],
|
|
"metadata": {
|
|
"kernelspec": {
|
|
"display_name": "SageMath 9.5",
|
|
"language": "sage",
|
|
"name": "sagemath"
|
|
},
|
|
"language_info": {
|
|
"codemirror_mode": {
|
|
"name": "ipython",
|
|
"version": 3
|
|
},
|
|
"file_extension": ".py",
|
|
"mimetype": "text/x-python",
|
|
"name": "python",
|
|
"nbconvert_exporter": "python",
|
|
"pygments_lexer": "ipython3",
|
|
"version": "3.11.2"
|
|
}
|
|
},
|
|
"nbformat": 4,
|
|
"nbformat_minor": 5
|
|
}
|