Source code for pettingzoo.classic.rlcard_envs.texas_holdem

# noqa: D212, D415
"""
# Texas Hold'em

```{figure} classic_texas_holdem.gif
:width: 140px
:name: texas_holdem
```

This environment is part of the <a href='..'>classic environments</a>. Please read that page first for general information.

| Import             | `from pettingzoo.classic import texas_holdem_v4` |
|--------------------|--------------------------------------------------|
| Actions            | Discrete                                         |
| Parallel API       | Yes                                              |
| Manual Control     | No                                               |
| Agents             | `agents= ['player_0', 'player_1']`               |
| Agents             | 2                                                |
| Action Shape       | Discrete(4)                                      |
| Action Values      | Discrete(4)                                      |
| Observation Shape  | (72,)                                            |
| Observation Values | [0, 1]                                           |


## Arguments

``` python
texas_holdem_v4.env(num_players=2)
```

`num_players`: Sets the number of players in the game. Minimum is 2.

### Observation Space

The observation is a dictionary which contains an `'observation'` element which is the usual RL observation described below, and an  `'action_mask'` which holds the legal moves, described in the Legal Actions Mask section.

The main observation space is a vector of 72 boolean integers. The first 52 entries depict the current player's hand plus any community cards as follows

|  Index  | Description                                                 |
|:-------:|-------------------------------------------------------------|
|  0 - 12 | Spades<br>_`0`: A, `1`: 2, ..., `12`: K_                    |
| 13 - 25 | Hearts<br>_`13`: A, `14`: 2, ..., `25`: K_                  |
| 26 - 38 | Diamonds<br>_`26`: A, `27`: 2, ..., `38`: K_                |
| 39 - 51 | Clubs<br>_`39`: A, `40`: 2, ..., `51`: K_                   |
| 52 - 56 | Chips raised in Round 1<br>_`52`: 0, `53`: 1, ..., `56`: 4_ |
| 57 - 61 | Chips raised in Round 2<br>_`57`: 0, `58`: 1, ..., `61`: 4_ |
| 62 - 66 | Chips raised in Round 3<br>_`62`: 0, `63`: 1, ..., `66`: 4_ |
| 67 - 71 | Chips raised in Round 4<br>_`67`: 0, `68`: 1, ..., `71`: 4_ |

#### Legal Actions Mask

The legal moves available to the current agent are found in the `action_mask` element of the dictionary observation. The `action_mask` is a binary vector where each index of the vector represents whether the action is legal or not. The `action_mask` will be all zeros for any agent except the one
whose turn it is. Taking an illegal move ends the game with a reward of -1 for the illegally moving agent and a reward of 0 for all other agents.

### Action Space

| Action ID | Action |
|:---------:|--------|
|     0     | Call   |
|     1     | Raise  |
|     2     | Fold   |
|     3     | Check  |

### Rewards

| Winner          | Loser           |
| :-------------: | :-------------: |
| +raised chips/2 | -raised chips/2 |

### Version History

* v4: Upgrade to RLCard 1.0.3 (1.11.0)
* v3: Fixed bug in arbitrary calls to observe() (1.8.0)
* v2: Bumped RLCard version, bug fixes, legal action mask in observation replaced illegal move list in infos (1.5.0)
* v1: Bumped RLCard version, fixed observation space, adopted new agent iteration scheme where all agents are iterated over after they are done (1.4.0)
* v0: Initial versions release (1.0.0)

"""
from __future__ import annotations

import os

import gymnasium
import numpy as np
import pygame
from gymnasium.utils import EzPickle

from pettingzoo.classic.rlcard_envs.rlcard_base import RLCardBase
from pettingzoo.utils import wrappers

# Pixel art from Mariia Khmelnytska (https://www.123rf.com/photo_104453049_stock-vector-pixel-art-playing-cards-standart-deck-vector-set.html)


def get_image(path):
    from os import path as os_path

    cwd = os_path.dirname(__file__)
    image = pygame.image.load(cwd + "/" + path)
    return image


def get_font(path, size):
    from os import path as os_path

    cwd = os_path.dirname(__file__)
    font = pygame.font.Font((cwd + "/" + path), size)
    return font


[docs] def env(**kwargs): env = raw_env(**kwargs) env = wrappers.TerminateIllegalWrapper(env, illegal_reward=-1) env = wrappers.AssertOutOfBoundsWrapper(env) env = wrappers.OrderEnforcingWrapper(env) return env
[docs] class raw_env(RLCardBase, EzPickle): metadata = { "render_modes": ["human", "rgb_array"], "name": "texas_holdem_v4", "is_parallelizable": False, "render_fps": 1, } def __init__( self, num_players: int = 2, render_mode: str | None = None, screen_height: int | None = 1000, ): EzPickle.__init__(self, num_players, render_mode, screen_height) super().__init__("limit-holdem", num_players, (72,)) self.render_mode = render_mode self.screen_height = screen_height if self.render_mode == "human": self.clock = pygame.time.Clock()
[docs] def step(self, action): super().step(action) if self.render_mode == "human": self.render()
[docs] def render(self): if self.render_mode is None: gymnasium.logger.warn( "You are calling render method without specifying any render mode." ) return def calculate_width(self, screen_width, i): return int( ( screen_width / (np.ceil(len(self.possible_agents) / 2) + 1) * np.ceil((i + 1) / 2) ) + (tile_size * 33 / 616) ) def calculate_offset(hand, j, tile_size): return int( (len(hand) * (tile_size * 23 / 56)) - ((j) * (tile_size * 23 / 28)) ) def calculate_height(screen_height, divisor, multiplier, tile_size, offset): return int(multiplier * screen_height / divisor + tile_size * offset) screen_height = self.screen_height screen_width = int( screen_height * (1 / 20) + np.ceil(len(self.possible_agents) / 2) * (screen_height * 12 / 20) ) if self.screen is None: pygame.init() if self.render_mode == "human": self.screen = pygame.display.set_mode((screen_width, screen_height)) pygame.display.set_caption("Texas Hold'em") else: self.screen = pygame.Surface((screen_width, screen_height)) # Setup dimensions for card size and setup for colors tile_size = screen_height * 2 / 10 bg_color = (7, 99, 36) white = (255, 255, 255) self.screen.fill(bg_color) chips = { 0: {"value": 10000, "img": "ChipOrange.png", "number": 0}, 1: {"value": 5000, "img": "ChipPink.png", "number": 0}, 2: {"value": 1000, "img": "ChipYellow.png", "number": 0}, 3: {"value": 100, "img": "ChipBlack.png", "number": 0}, 4: {"value": 50, "img": "ChipBlue.png", "number": 0}, 5: {"value": 25, "img": "ChipGreen.png", "number": 0}, 6: {"value": 10, "img": "ChipLightBlue.png", "number": 0}, 7: {"value": 5, "img": "ChipRed.png", "number": 0}, 8: {"value": 1, "img": "ChipWhite.png", "number": 0}, } # Load and blit all images for each card in each player's hand for i, player in enumerate(self.possible_agents): state = self.env.game.get_state(self._name_to_int(player)) for j, card in enumerate(state["hand"]): # Load specified card card_img = get_image(os.path.join("img", card + ".png")) card_img = pygame.transform.scale( card_img, (int(tile_size * (142 / 197)), int(tile_size)) ) # Players with even id go above public cards if i % 2 == 0: self.screen.blit( card_img, ( ( calculate_width(self, screen_width, i) - calculate_offset(state["hand"], j, tile_size) - tile_size * (8 / 10) * (1 - np.ceil(i / 2)) * (0 if len(self.possible_agents) == 2 else 1) ), calculate_height(screen_height, 4, 1, tile_size, -1), ), ) # Players with odd id go below public cards else: self.screen.blit( card_img, ( ( calculate_width(self, screen_width, i) - calculate_offset(state["hand"], j, tile_size) - tile_size * (8 / 10) * (1 - np.ceil((i - 1) / 2)) * (0 if len(self.possible_agents) == 2 else 1) ), calculate_height(screen_height, 4, 3, tile_size, 0), ), ) # Load and blit text for player name font = get_font(os.path.join("font", "Minecraft.ttf"), 36) text = font.render("Player " + str(i + 1), True, white) textRect = text.get_rect() if i % 2 == 0: textRect.center = ( ( screen_width / (np.ceil(len(self.possible_agents) / 2) + 1) * np.ceil((i + 1) / 2) - tile_size * (8 / 10) * (1 - np.ceil(i / 2)) * (0 if len(self.possible_agents) == 2 else 1) ), calculate_height(screen_height, 4, 1, tile_size, -(22 / 20)), ) else: textRect.center = ( ( screen_width / (np.ceil(len(self.possible_agents) / 2) + 1) * np.ceil((i + 1) / 2) - tile_size * (8 / 10) * (1 - np.ceil((i - 1) / 2)) * (0 if len(self.possible_agents) == 2 else 1) ), calculate_height(screen_height, 4, 3, tile_size, (23 / 20)), ) self.screen.blit(text, textRect) # Load and blit number of poker chips for each player font = get_font(os.path.join("font", "Minecraft.ttf"), 24) text = font.render(str(state["my_chips"]), True, white) textRect = text.get_rect() # Calculate number of each chip total = state["my_chips"] height = 0 for key in chips: num = total / chips[key]["value"] chips[key]["number"] = int(num) total %= chips[key]["value"] chip_img = get_image(os.path.join("img", chips[key]["img"])) chip_img = pygame.transform.scale( chip_img, (int(tile_size / 2), int(tile_size * 16 / 45)) ) # Blit poker chip img for j in range(0, int(chips[key]["number"])): if i % 2 == 0: self.screen.blit( chip_img, ( ( calculate_width(self, screen_width, i) + tile_size * (8 / 10) * ( 1 if len(self.possible_agents) == 2 else np.ceil(i / 2) ) ), calculate_height(screen_height, 4, 1, tile_size, -1 / 2) - ((j + height) * tile_size / 15), ), ) else: self.screen.blit( chip_img, ( ( calculate_width(self, screen_width, i) + tile_size * (8 / 10) * ( 1 if len(self.possible_agents) == 2 else np.ceil((i - 1) / 2) ) ), calculate_height(screen_height, 4, 3, tile_size, 1 / 2) - ((j + height) * tile_size / 15), ), ) height += chips[key]["number"] # Blit text number if i % 2 == 0: textRect.center = ( ( calculate_width(self, screen_width, i) + (tile_size * (5 / 20)) + tile_size * (8 / 10) * (1 if len(self.possible_agents) == 2 else np.ceil(i / 2)) ), calculate_height(screen_height, 4, 1, tile_size, -1 / 2) - ((height + 1) * tile_size / 15), ) else: textRect.center = ( ( calculate_width(self, screen_width, i) + (tile_size * (5 / 20)) + tile_size * (8 / 10) * ( 1 if len(self.possible_agents) == 2 else np.ceil((i - 1) / 2) ) ), calculate_height(screen_height, 4, 3, tile_size, 1 / 2) - ((height + 1) * tile_size / 15), ) self.screen.blit(text, textRect) # Load and blit public cards for i, card in enumerate(state["public_cards"]): card_img = get_image(os.path.join("img", card + ".png")) card_img = pygame.transform.scale( card_img, (int(tile_size * (142 / 197)), int(tile_size)) ) if len(state["public_cards"]) <= 3: self.screen.blit( card_img, ( ( ( ((screen_width / 2) + (tile_size * 31 / 616)) - calculate_offset(state["public_cards"], i, tile_size) ), calculate_height(screen_height, 2, 1, tile_size, -(1 / 2)), ) ), ) else: if i <= 2: self.screen.blit( card_img, ( ( ( ((screen_width / 2) + (tile_size * 31 / 616)) - calculate_offset( state["public_cards"][:3], i, tile_size ) ), calculate_height( screen_height, 2, 1, tile_size, -21 / 20 ), ) ), ) else: self.screen.blit( card_img, ( ( ( ((screen_width / 2) + (tile_size * 31 / 616)) - calculate_offset( state["public_cards"][3:], i - 3, tile_size ) ), calculate_height( screen_height, 2, 1, tile_size, 1 / 20 ), ) ), ) if self.render_mode == "human": pygame.display.update() self.clock.tick(self.metadata["render_fps"]) observation = np.array(pygame.surfarray.pixels3d(self.screen)) return ( np.transpose(observation, axes=(1, 0, 2)) if self.render_mode == "rgb_array" else None )