Create a Simple 5-Card Draw Poker Game in Python with Asyncio
Tags: python, asyncio, programming
Published: 2023-04-01 11:29:49

In this blog post, we will create a simple 5-card draw poker game in Python using the asyncio library. The game will allow 2 to 4 players to play without a betting aspect, but it will determine a winner based on their poker hands. The main purpose of using the asyncio library in this example is to familiarize ourselves with asynchronous programming concepts, even though our game does not require concurrent execution.

Asynchronous programming is a programming paradigm that allows multiple tasks to be performed concurrently without waiting for one task to finish before starting the next one. This is particularly useful in situations where tasks involve I/O-bound operations, such as reading from a file or making network requests, which can take a significant amount of time. The asyncio library is an essential component of asynchronous programming in Python. It provides an event loop, coroutines, and other utilities that enable developers to write efficient, non-blocking code, significantly improving the performance and responsiveness of applications, particularly in networking and web-based contexts. 

In this tutorial, we will leverage the asyncio library to create a simple poker game, demonstrating the power and flexibility of asynchronous programming in Python.

Requirements:

Python 3.7+
Step 1: Importing necessary libraries and defining the dataclasses
First, let's import the required libraries and define our dataclasses for Card, Rank, Suit, and GameState:

```python
import asyncio
import random
from collections import Counter
from dataclasses import dataclass
from enum import Enum, auto
from typing import List, Tuple

class Suit(Enum):
    SPADES = "♠"
    HEARTS = "♥"
    DIAMONDS = "♦"
    CLUBS = "♣"

class Rank(Enum):
    TWO = 2
    THREE = 3
    FOUR = 4
    FIVE = 5
    SIX = 6
    SEVEN = 7
    EIGHT = 8
    NINE = 9
    TEN = 10
    JACK = 11
    QUEEN = 12
    KING = 13
    ACE = 14

@dataclass
class Card:
    suit: Suit
    rank: Rank

    def __str__(self):
        return f"{self.rank.name.capitalize()}{self.suit.value}"

@dataclass
class GameState:
    deck: List[Card]
    players: List[List[Card]]
```

Step 2: Implementing the deck creation and shuffling functions
Now, let's create a function to create a deck of cards and another function to shuffle the deck using asyncio:

```python
def create_deck() -> List[Card]:
    return [Card(suit, rank) for suit in Suit for rank in Rank]

async def shuffle_deck(deck: List[Card]) -> List[Card]:
    await asyncio.sleep(0)  # simulating asynchronous behavior
    random.shuffle(deck)
    return deck
```

Step 3: Dealing and ranking hands
Next, we need to implement a function to deal cards from the deck and a function to rank the hands of the players:

```python
async def deal_cards(game_state: GameState, num_cards: int) -> List[Card]:
    new_cards = []
    for _ in range(num_cards):
        card = game_state.deck.pop()
        new_cards.append(card)
    return new_cards

def rank_hand(hand: List[Card]) -> Tuple[int, List[int]]:
    ranks = sorted([card.rank.value for card in hand], reverse=True)
    suits = [card.suit for card in hand]
    rank_counts = Counter(ranks)
    is_flush = len(set(suits)) == 1
    is_straight = len(set(ranks)) == 5 and max(ranks) - min(ranks) == 4

    # Determine hand rank based on poker hand rankings
    # ... (refer to the previous code snippets for the full rank_hand function)
```

Step 4: Drawing cards and playing the game
Now, let's add the functionality to draw new cards after discarding and the main game loop:

```python
async def draw_cards(game_state: GameState, player_idx: int, discard_indices: List[int]) -> None:
    player_hand = game_state.players[player_idx]
    for index in sorted(discard_indices, reverse=True):
        del player_hand[index]
    new_cards = await deal_cards(game_state, len(discard_indices))
    game_state.players[player_idx] = player_hand + new_cards

async def play_game(num_players: int) -> None:
    deck = await shuffle_deck(create_deck())
    game_state = GameState(deck=deck, players=[[] for _ in range(num_players)])

    for i in range(num_players):
        game_state.players[i] = await deal_cards(game_state, 5)

    for i, player_hand in enumerate(game_state.players):
        print(f"Player {i + 1}'s hand: {', '.join(str(card) for card in player_hand)}")

    for i in range(num_players):
        discard_indices = input(f"Player {i + 1}, enter the indices of the cards to discard (0-4, separated by spaces): ")
        discard_indices = [int(index) for index in discard_indices.split()]
        await draw_cards(game_state, i, discard_indices)

    for i, player_hand in enumerate(game_state.players):
        print(f"Player {i + 1}'s final hand: {', '.join(str(card) for card in player_hand)}")

    hand_ranks = [rank_hand(hand) for hand in game_state.players]
    max_rank = max(hand_ranks)
    winner_idx = hand_ranks.index(max_rank)
    print(f"Player {winner_idx + 1} wins with a {', '.join(str(card) for card in game_state.players[winner_idx])}!")
```

Step 5: Running the game
Finally, let's add the main function to run the game:

```python
if __name__ == "__main__":
    num_players = int(input("Enter the number of players (2-4): "))
    while not (2 <= num_players <= 4):
        num_players = int(input("Enter a valid number of players (2-4): "))
    asyncio.run(play_game(num_players))
```

Now, save the code in a file named poker.py and run it using the following command:

```
python3 poker.py
```

Congratulations! You've created a simple 5-card draw poker game in Python using the asyncio library. Although the primary purpose was to familiarize ourselves with asynchronous programming, the game can be further extended and improved with additional features, such as betting, improved user interface, or even networked multiplayer capabilities. 

Happy coding and enjoy playing poker!