-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from smessie/setup-game
Implement initial game logic
- Loading branch information
Showing
16 changed files
with
466 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -127,3 +127,6 @@ dmypy.json | |
|
||
# Pyre type checker | ||
.pyre/ | ||
|
||
# Jetbrains | ||
.idea |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} |
Oops, something went wrong.