Skip to content

Commit

Permalink
Merge pull request #1 from smessie/setup-game
Browse files Browse the repository at this point in the history
Implement initial game logic
  • Loading branch information
smessie authored Nov 10, 2020
2 parents 24035d9 + ad3a4bf commit 955e250
Show file tree
Hide file tree
Showing 16 changed files with 466 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,6 @@ dmypy.json

# Pyre type checker
.pyre/

# Jetbrains
.idea
Empty file added agents/__init__.py
Empty file.
11 changes: 11 additions & 0 deletions agents/simple_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from game.agent import Agent
from game.table import Table
from game.player import Player


class SimpleAgent(Agent):
def __init__(self):
super().__init__(Player())

def make_move(self, table: Table) -> None:
table.try_move(self, [sorted(self.player.hand)[0]])
Empty file added game/__init__.py
Empty file.
31 changes: 31 additions & 0 deletions game/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from __future__ import annotations

from typing import List, TYPE_CHECKING

from game.player import Player

if TYPE_CHECKING:
from game.card import Card
from game.table import Table


class Agent:
def __init__(self, player: Player):
self.player: Player = player

def make_move(self, table: Table) -> None:
"""
This function is called when the agent should make a move.
table.make_move() should be called, this makes the move and returns a reward.
"""
pass

def receive_event(self, move, event) -> None:
pass

def get_preferred_card_order(self, table: Table) -> List[Card]:
"""
Function used by President to exchange cards at the beginning of a round. Most wanted card should be in front.
"""
return sorted(table.deck.card_stack, reverse=True)
64 changes: 64 additions & 0 deletions game/card.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import enum


class Color(enum.Enum):
Black = 1
Red = 2

def __lt__(self, other):
return self.value < other.value

def __gt__(self, other):
return self.value > other.value


class Suit(enum.Enum):
Clubs = 1
Diamonds = 2
Spades = 3
Hearts = 4

def get_color(self):
"""Suits have fixed colors"""
return Color.Red if self.value % 2 == 0 else Color.Black


class Card:
""""A card in the deck"""

def __init__(self, value: int, suit: Suit, name: str):
self.value = value
self.suit = suit
self.name = name

def get_value(self) -> int:
return self.value

def get_color(self) -> Color:
return self.suit.get_color()

def get_suit(self) -> Suit:
return self.suit

def get_name(self) -> str:
return self.name

def __eq__(self, other):
return self.value == other.value and self.suit.get_color() == other.suit.get_color()

def __ne__(self, other):
return not self == other

def __lt__(self, other):
return self.value < other.value or (
self.value == other.value and self.suit.get_color() < other.suit.get_color())

def __le__(self, other):
return self < other or self == other

def __gt__(self, other):
return self.value > other.value or (
self.value == other.value and self.suit.get_color() > other.suit.get_color())

def __ge__(self, other):
return self > other or self == other
24 changes: 24 additions & 0 deletions game/deck.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from random import shuffle
from typing import List

from game.card import Card, Suit
from game.settings import CARD_VALUES


class Deck:
"""Just a collection of cards, with some functions to ease things. No game specific implementations."""

def __init__(self):
self.card_stack: List[Card] = []
self.reset_cards_stack()

def reset_cards_stack(self) -> None:
"""
Reset the card stack. Clear all cards and add a fresh (shuffled) set.
"""
self.card_stack.clear()
for symbol in CARD_VALUES:
for suit in Suit:
self.card_stack.append(
Card(CARD_VALUES[symbol], suit, str(suit.get_color()) + " " + symbol + " of " + str(suit)))
shuffle(self.card_stack)
25 changes: 25 additions & 0 deletions game/player.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from __future__ import annotations

from itertools import combinations
from typing import List, TYPE_CHECKING

if TYPE_CHECKING:
from game.card import Card
from game.table import Table


class Player:
_player_id: int = 0

def __init__(self):
self.hand: List[Card] = []
self.player_id: int = Player._player_id
Player._player_id += 1

def get_all_possible_moves(self, table: Table) -> [[Card]]:
possible_moves = []
for amount_of_cards in range(len(table.last_move()[0]) if table.last_move() else 1, len(self.hand) + 1):
for potential_move in combinations(self.hand, amount_of_cards):
if table.game.valid_move(list(potential_move)):
possible_moves.append(list(potential_move))
return possible_moves
157 changes: 157 additions & 0 deletions game/president.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
from __future__ import annotations

from typing import List, Iterator, Tuple, Optional, Dict, TYPE_CHECKING

from tqdm import tqdm

from game.table import Table
from util.cards import get_played_value
from util.iterator import CustomIterator

if TYPE_CHECKING:
from game.agent import Agent
from game.card import Card


class President:
"""
A class containing the game logic.
"""

def __init__(self, agents: List[Agent]):
self.agents: List[Agent] = agents
self.passed_agents: Dict[Agent, bool] = {
agent: False for agent in self.agents
}
self.agent_finish_order: List[Agent] = []
self.agent_iterator: CustomIterator = CustomIterator(agents)
self.table = Table(self)

def play(self, games: int, rounds: int) -> None:
"""
Start the game. Play a certain amount of games each consisting of a certain amount of rounds.
"""
progress = tqdm(total=games * rounds)

for g in range(games):
for r in range(rounds):
# Update the progress bar
progress.set_description(f"Running round {r} of game {g}")
progress.update()

# Reset from the previous round
self._reset()

# If this is not the first round exchange cards
if r != 0:
self._exchange_cards()
self.agent_finish_order = []

# Play the round
for agent in self._get_play_order():
agent.make_move(self.table)

# If the player finished this round award it by giving it its position.
if len(agent.player.hand) == 0:
self.agent_finish_order.append(agent)

progress.close()

def on_move(self, agent: Agent, cards: List[Card]) -> Tuple[int, bool]:
"""
Handle move from Agent, We can be sure the agent can actually play the card.
return (reward, is_final).
"""
if not cards:
# A Pass, disable the player for this round
self.passed_agents[agent] = True
return -5, False # TODO fix reward

# Previous value should be lower
if self.valid_move(cards):
self.table.do_move(agent, cards)
return 10, False # TODO fix reward
else:
return -10, False # TODO fix reward

def valid_move(self, cards: List[Card]) -> bool:
last_move: Tuple[List[Card], Agent] = self.table.last_move()

# If multiple cards are played length should be at least the same.
if cards and last_move and len(cards) < len(last_move[0]):
return False

# Check that each played card in the trick has the same rank, or if not, it is a 2.
played_value: Optional[int] = get_played_value(cards)
if not played_value or played_value < 0:
return False
last_move_value: int = get_played_value(last_move[0]) if last_move else None

# Previous value should be lower
return not last_move or last_move_value <= played_value

def _reset(self) -> None:
"""
- (Re)divide cards
- reset the finish order
- reset the playing table
"""
for i, hand in enumerate(self.table.divide(len(self.agents))):
self.agents[i].player.hand = hand
self.table.reset()

def _exchange_cards(self) -> None:
# Todo discuss this, but for now only the first and last player trade cards
first: Agent = self.agent_finish_order[0]
last: Agent = self.agent_finish_order[-1]
preferred_cards: List[Card] = first.get_preferred_card_order(self.table)

# Hand best card from loser to winner
card_index = 0
while preferred_cards[card_index] not in last.player.hand:
card_index += 1

exchange_card: Card = last.player.hand[card_index]
first.player.hand.append(exchange_card)
last.player.hand.remove(exchange_card)

# Hand lowest card from winner to loser
exchange_card = sorted(first.player.hand)[0]
first.player.hand.remove(exchange_card)
last.player.hand.append(exchange_card)

def _get_play_order(self) -> Iterator[Agent]:
"""
Return the player order, this is an iterator so this allows for cleaner code in the President class.
"""
# As long as there are 2 unfinished players
while [len(agent.player.hand) > 0 for agent in self.agents].count(True) >= 2:
self.agent_iterator.next()
nr_skips: int = 0

while nr_skips <= len(self.agents) and (
len(self.agent_iterator.get().player.hand) == 0 or self.passed_agents[self.agent_iterator.get()]):
self.agent_iterator.next()
nr_skips += 1

if nr_skips > len(self.agents):
# All agents have no cards left
if all(len(agent.player.hand) == 0 for agent in self.agents):
return
# Some player still has a card. Start a new trick
last_agent = self.table.last_move()[1]

self.table.new_trick()
self.passed_agents = {
agent: False for agent in self.agents
}

# The player that has made the last move can start in the new trick
while self.agent_iterator.get() != last_agent:
self.agent_iterator.next()
# We found the player, but the loop will call next, so we have to call previous to neutralize this.
self.agent_iterator.previous()

yield self.agent_iterator.get()
# The unfinished player comes last, add it to the last_played lis
self.agent_finish_order.append(list(filter(lambda x: len(x.player.hand) > 0, self.agents))[0])
16 changes: 16 additions & 0 deletions game/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Some settings for easy adjustment later on
CARD_VALUES = {
"Ace": 14,
"2": 15,
"3": 3,
"4": 4,
"5": 5,
"6": 6,
"7": 7,
"8": 8,
"9": 9,
"10": 10,
"Jack": 11,
"Queen": 12,
"King": 13,
}
Loading

0 comments on commit 955e250

Please sign in to comment.