Skip to content
This repository has been archived by the owner on Jul 16, 2024. It is now read-only.

Commit

Permalink
Merge pull request #89 from fedden/feature/rewrite-ascii-game-engine
Browse files Browse the repository at this point in the history
MVP of terminal game completed; we can play against an offline strategy
  • Loading branch information
fedden authored May 17, 2020
2 parents def7f7b + a850a58 commit 5f52288
Show file tree
Hide file tree
Showing 19 changed files with 468 additions and 263 deletions.
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Below is a rough structure of the repository.
│   ├── games # Implementations of poker games as node based objects that
│ │ # can be traversed in a depth-first recursive manner.
│   ├── poker # WIP general code for managing a hand of poker.
│   ├── terminal # Code to play against the AI from your console.
│   └── utils # Utility code like seed setting.
├── research # A directory for research/development scripts
│ # to help formulate understanding and ideas.
Expand Down Expand Up @@ -79,7 +80,28 @@ for action in state.legal_actions:
new_state: ShortDeckPokerState = state.apply_action(action)
```

### Visualisation code
### Playing against AI in your terminal

We also have some code to play a round of poker against the AI agents, inside your terminal.

The characters are a little broken when captured in `asciinema`, but you'll get the idea by watching this video below. Results should be better in your actual terminal!

<p align="center">
<a href="https://asciinema.org/a/331234" target="_blank">
<img src="https://asciinema.org/a/331234.svg" width="500" />
</a>
</p>
To invoke the code, either call the `run_terminal_app` method directly from the `pluribus.terminal.runner` module, or call from python like so:

```bash
cd /path/to/pluribus/dir
python -m pluribus.terminal.runner \
--agent offline \
--pickle_dir ./research/blueprint_algo \
--strategy_path ./research/blueprint_algo/offline_strategy_285800.gz
```

### Web visualisation code

We are also working on code to visualise a given instance of the `ShortDeckPokerState`, which looks like this:
<p align="center">
Expand Down
1 change: 1 addition & 0 deletions pluribus/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@
from . import ai
from . import games
from . import poker
from . import terminal
from . import utils
4 changes: 2 additions & 2 deletions pluribus/games/short_deck/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,9 @@ def apply_action(self, action_str: Optional[str]) -> ShortDeckPokerState:
# Distribute winnings.
new_state._poker_engine.compute_winners()
break
for player in self.players:
for player in new_state.players:
player.is_turn = False
self.current_player.is_turn = True
new_state.current_player.is_turn = True
return new_state

@staticmethod
Expand Down
33 changes: 28 additions & 5 deletions pluribus/poker/card.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import List, Set, Union
from typing import Dict, List, Set, Union

from pluribus.poker.evaluation.eval_card import EvaluationCard

Expand Down Expand Up @@ -48,6 +48,11 @@ def __init__(self, rank: Union[str, int], suit: str):
suit_char = self.suit.lower()[0]
self._eval_card = EvaluationCard.new(f"{rank_char}{suit_char}")

def __repr__(self):
"""Pretty printing the object."""
icon = self._suit_to_icon(self.suit)
return f"<Card card=[{self.rank} of {self.suit} {icon}]>"

def __int__(self):
return self._eval_card

Expand Down Expand Up @@ -152,7 +157,25 @@ def _suit_to_icon(self, suit: str) -> str:
"""Icons for pretty printing."""
return {"hearts": "♥", "diamonds": "♦", "clubs": "♣", "spades": "♠"}[suit]

def __repr__(self):
"""Pretty printing the object."""
icon = self._suit_to_icon(self.suit)
return f"<Card card=[{self.rank} of {self.suit} {icon}]>"
def to_dict(self) -> Dict[str, Union[int, str]]:
"""Turn into dict."""
return dict(rank=self._rank, suit=self._suit)

@staticmethod
def from_dict(x: Dict[str, Union[int, str]]):
"""From dict turn into class."""
if set(x) != {"rank", "suit"}:
raise NotImplementedError(f"Unrecognised dict {x}")
return Card(rank=x["rank"], suit=x["suit"])

def to_dict(self) -> Dict[str, Union[int, str]]:
"""Turn into dict."""
return dict(rank=self._rank, suit=self._suit)

@staticmethod
def from_dict(x: Dict[str, Union[int, str]]):
"""From dict turn into class."""
if set(x) != {"rank", "suit"}:
raise NotImplementedError(f"Unrecognised dict {x}")
return Card(rank=x["rank"], suit=x["suit"])

2 changes: 1 addition & 1 deletion pluribus/poker/evaluation/evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def evaluate(self, cards, board):
Supports empty board, etc very flexible. No input validation
because that's cycles!
"""
all_cards = cards + board
all_cards = [int(c) for c in cards + board]
return self.hand_size_map[len(all_cards)](all_cards)

def _five(self, cards):
Expand Down
6 changes: 6 additions & 0 deletions pluribus/terminal/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Terminal Application

Here lies the code to play a round of poker against the AI agents, inside the terminal.

The characters are a little broken when captured in `asciinema`, but you'll get the idea by watching this video below. Results should be better in your actual terminal!
[![asciicast](https://asciinema.org/a/331234.png)](https://asciinema.org/a/331234)
4 changes: 4 additions & 0 deletions pluribus/terminal/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from . import ascii_objects
from . import render
from . import results
from . import runner
3 changes: 3 additions & 0 deletions pluribus/terminal/ascii_objects/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import card_collection
from . import logger
from . import player
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@


class AsciiCardCollection:
def __init__(self, *cards, hide_cards: bool = False, term: Terminal = None):
def __init__(
self,
*cards,
hide_cards: bool = False,
term: Terminal = None,
):
""""""
self.term = term
self.cards = cards
Expand Down
37 changes: 37 additions & 0 deletions pluribus/terminal/ascii_objects/logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from collections import deque
from datetime import datetime

from blessed import Terminal


class AsciiLogger:
""""""

def __init__(self, term: Terminal):
""""""
self._log_queue: deque = deque()
self._term = term
self.height = None

def clear(self):
""""""
self._log_queue: deque = deque()

def info(self, *args):
""""""
if self.height is None:
raise ValueError("Logger.height must be set before logging.")
x: str = " ".join(map(str, args))
str_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self._log_queue.append(f"{self._term.skyblue1(str_time)} {x}")
if len(self._log_queue) > self.height:
self._log_queue.popleft()

def __str__(self) -> str:
""""""
if self.height is None:
raise ValueError("Logger.height must be set before logging.")
n_logs = len(self._log_queue)
start = max(n_logs - self.height, 0)
lines = [self._log_queue[i] for i in range(start, n_logs)]
return "\n".join(lines)
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
from blessed import Terminal

from card_collection import AsciiCardCollection
from pluribus.terminal.ascii_objects.card_collection import AsciiCardCollection


class AsciiPlayer:
def __init__(
self,
*cards,
player,
term: Terminal,
name: str = "",
og_name: str = "",
chips_in_pot: int = 0,
chips_in_bank: int = 0,
info_position: str = "right",
folded: bool = False,
is_turn: bool = False,
is_small_blind: bool = False,
Expand All @@ -24,29 +23,21 @@ def __init__(
self.card_collection_kwargs = card_collection_kwargs
self.chips_in_pot = chips_in_pot
self.chips_in_bank = chips_in_bank
self.player = player
self.name = name
self.og_name = og_name
self.term = term
self.folded = folded
self.is_turn = is_turn
self.info_position = info_position
self.is_small_blind = is_small_blind
self.is_big_blind = is_big_blind
self.is_dealer = is_dealer

@property
def name(self):
return self._name

@name.setter
def name(self, n):
self._name = self.player.name = n
self.update()

def stylise_name(self, name: str, extra: str) -> str:
if self.folded:
name = f"{name} (folded)"
if self.is_turn:
name = self.term.orangered(f"{name} {self.term.blink_bold('turn')}")
name = f"**{name}**"
if extra:
name = f"{name} ({extra})"
return name
Expand All @@ -69,21 +60,8 @@ def update(self):
f"bet chips: {self.chips_in_pot}",
f"bank roll: {self.chips_in_bank}",
]
if self.info_position == "right":
max_len = max(len(i) for i in info)
for line_i, line in enumerate(info):
self.lines[1 + line_i] += f" {line}"
max_len = max(len(l) for l in self.lines)
for line_i, line in enumerate(self.lines):
n_spaces = max_len - len(line)
self.lines[line_i] += f" {n_spaces * ' '}"
elif self.info_position == "top":
self.lines = info + self.lines
elif self.info_position == "bottom":
self.lines = self.lines + info
else:
raise NotImplementedError(
f"info position {self.info_position} not supported")
card_width = len(self.lines[0])
self.lines = [i + (card_width - len(i)) * " " for i in info] + self.lines
self.width = max(len(line) for line in self.lines)
self.height = len(self.lines)

Expand Down
86 changes: 86 additions & 0 deletions pluribus/terminal/render.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import copy
from operator import itemgetter
from typing import Dict, List

from blessed import Terminal

from pluribus.games.short_deck.state import ShortDeckPokerState
from pluribus.terminal.ascii_objects.card_collection import AsciiCardCollection
from pluribus.terminal.ascii_objects.logger import AsciiLogger
from pluribus.terminal.ascii_objects.player import AsciiPlayer


def _compute_header_lines(
state: ShortDeckPokerState, og_name_to_name: Dict[str, str]
) -> List[str]:
if state.is_terminal:
player_winnings = []
for player_i, chips_delta in state.payout.items():
p = state.players[player_i]
player_winnings.append((p, chips_delta))
player_winnings.sort(key=itemgetter(1), reverse=True)
winnings_desc_strings = [
f"{og_name_to_name[p.name]} {'wins' if x > 0 else 'loses'} {x} chips"
for p, x in player_winnings
]
winnings_desc: str = ", ".join(winnings_desc_strings)
winning_player = player_winnings[0][0]
winning_rank: int = state._poker_engine.evaluator.evaluate(
state.community_cards, winning_player.cards
)
winning_hand_class: int = state._poker_engine.evaluator.get_rank_class(
winning_rank
)
winning_hand_desc: str = state._poker_engine.evaluator.class_to_string(
winning_hand_class
).lower()
return [
f"{og_name_to_name[winning_player.name]} won with a {winning_hand_desc}",
winnings_desc,
]
return ["", state.betting_stage]


def print_header(term: Terminal, state: ShortDeckPokerState, og_name_to_name: Dict[str, str]):
for line in _compute_header_lines(state, og_name_to_name):
print(term.center(term.yellow(line)))
print(f"\n{term.width * '-'}\n")


def print_footer(term: Terminal, selected_action_i: int, legal_actions: List[str]):
print(f"\n{term.width * '-'}\n")
actions = []
for action_i in range(len(legal_actions)):
action = copy.deepcopy(legal_actions[action_i])
if action_i == selected_action_i:
action = term.blink_bold_orangered(action)
actions.append(action)
print(term.center(" ".join(actions)))


def print_table(
term: Terminal,
players: Dict[str, AsciiPlayer],
public_cards: AsciiCardCollection,
n_table_rotations: int,
n_spaces_between_cards: int = 4,
n_chips_in_pot: int = 0,
):
left_player = players["left"]
middle_player = players["middle"]
right_player = players["right"]
for line in public_cards.lines:
print(term.center(line))
print(term.center(f"chips in pot: {n_chips_in_pot}"))
print("\n\n")
spacing = " " * n_spaces_between_cards
for l, m, r in zip(left_player.lines, middle_player.lines, right_player.lines):
print(term.center(f"{l}{spacing}{m}{spacing}{r}"))


def print_log(term: Terminal, log: AsciiLogger):
print(f"\n{term.width * '-'}\n")
y, _ = term.get_location()
# Tell the log how far it can print before logging any more.
log.height = term.height - y - 1
print(log)
Loading

0 comments on commit 5f52288

Please sign in to comment.