simple Durian

"""
Durian game is made by Oink Games https://oinkgames.com/
See LICENSE.txt
"""

import argparse
import random
import itertools
from collections import UserList
import logging

FRUITS = "🍈🍓🍌🍇"


class Card:
    def __init__(self, left_count, left_fruit, right_count, right_fruit):
        self.left_count = left_count
        self.left_fruit = left_fruit
        self.right_count = right_count
        self.right_fruit = right_fruit

    def __repr__(self):
        return (
            f"[{self.left_count}{self.left_fruit}|{self.right_count}{self.right_fruit}]"
        )
    def flip(self):
        return Card(
            self.right_count,
            self.right_fruit,
            self.left_count,
            self.left_fruit,
        )

class DeckEmptyException(Exception):
    pass

class Deck:
    def __init__(self):
        fruit_counts = list(itertools.product([1, 2, 3], FRUITS))
        #TODO: there can be cards with the same fruit twice
        deck = list(
            Card(*left, *right) for left, right in itertools.combinations(fruit_counts, 2)
        )
        deck = [card for card in deck if card.left_fruit != card.right_fruit]
        random.shuffle(deck)
        self.cards = deck
    
    def pick(self, amount=1) -> list[Card]:
        picked = self.cards[:amount]
        del(self.cards[:amount])
        if not picked:
            raise DeckEmptyException()
        return picked
    
class Orders(UserList):
    def __init__(self, cards:list[Card]=None):
        self.data = cards if cards else []
    
    def __repr__(self):
        return (
            "\n   🦍   " +
            "\n ✅ | ❌ \n" +
            "\n".join([str(x) for x in self.data])
        )
    
    def accepted(self):
        order_counts = {f:0 for f in FRUITS}
        for card in self.data:
            order_counts[card.left_fruit] += card.left_count
        return order_counts

class Player:
    visible_cards: list[Card] = None

    def __init__(self, name: str = None):
        self.name = name if name else self.__class__.__name__ + hex(id(self))
        self.anger_chips = []
    
    def __repr__(self):
        anger = " ".join(["🦍"+"💢"*anger_chip for anger_chip in self.anger_chips])
        return f"{self.name} {anger}"

    def pick_or_bell(self, orders):
        raise NotImplementedError("write this function yourself!")
    
    def flip_or_not(self, card:Card):
        return card

class HumanPlayer(Player):
    def pick_or_bell(self, orders):
        # player that always picks
        action = "?"
        while action not in "PB":
            action = input(f"{self.name} (P)ick or (B)ell?")
            action = action.upper()
        return action
    
    def flip_or_not(self, card):
        action = "?"
        while action not in "FN":
            action = input(f"{self.name} (F)lip or (N)ot flip?")
            action = action.upper()
        if action == "F":
            return card.flip()
        else:
            return card

class PickPlayer(Player):
    def pick_or_bell(self, orders):
        return "P"

class FlipPlayer(Player):
    def pick_or_bell(self, orders):
        return "P"
    
    def flip_or_not(self, card):
        return card.flip()

class BellPlayer(Player):
    def pick_or_bell(self, orders):
        return "B"

class RandomPlayer(Player):
    def pick_or_bell(self, orders):
        logging.debug("player that does random action")

class ThreeTurnPlayer(Player):
    def pick_or_bell(self, orders):
        if len(orders) >= 3:
            return "B"
        else:
            return "P"
class DurianGame:
    def __init__(
        self,
        players: list[Player],
        deck:Deck,
        anger_level = 0,
        round = 0
    ):
        self.players = players
        self.deck = deck
        self.anger_level = anger_level
        self.orders = Orders()
        self.round = round
        self.inventory=self.deck.pick(len(self.players))
        for player, card in zip(players, self.inventory):
            player.visible_cards = [x for x in self.inventory if x != card]

    def orders_possible(self):
        inventory_counts = {f:0 for f in FRUITS}
        order_counts = self.orders.accepted()
        for card in self.inventory:
            inventory_counts[card.left_fruit] += card.left_count
            inventory_counts[card.right_fruit] += card.right_count
        for fruit, count in order_counts.items():
            if inventory_counts[fruit] < count:
                return False
        return True
    
    def new_round(self):
        self.__init__(self.players, Deck(), self.anger_level + 1, self.round + 1)
        logging.debug(f"--- ROUND {self.round} ---")
        logging.debug(f"inventory: {self.inventory}")
    
    def is_game_over(self):
        if max([sum(player.anger_chips) for player in self.players]) >= 7:
            return True
        return False

    def determine_winners(self):
        lowest_anger =  sum(sorted(self.players, key=lambda p:sum(p.anger_chips))[0].anger_chips)
        lowest_count =  len(sorted(self.players, key=lambda p:len(p.anger_chips))[0].anger_chips)
        return [p for p in self.players if len(p.anger_chips) == lowest_count and sum(p.anger_chips) == lowest_anger]

    def run(self):
        random.shuffle(self.players)
        self.new_round()
        previous_player = self.players[0]
        while True:
            for player in self.players:
                action = player.pick_or_bell(self.orders)
                if action == "P":
                    try:
                        picked_card = self.deck.pick()[0]
                        logging.debug(f"{player.name} picks {picked_card}.")
                    except DeckEmptyException:
                        logging.debug("deck empty, nobody wins")
                        return []
                    picked_card = player.flip_or_not(picked_card)
                    self.orders += [picked_card]
                    logging.debug(f"Orders:{self.orders}")
                elif action == "B":
                    if self.orders_possible():
                        #manager gets angry at current player
                        player.anger_chips.append(self.anger_level)
                        logging.debug(f"{player.name} 🔔🔔🔔, but orders are possible. {player}")
                    else:
                        #manager gets angry at previous player
                        previous_player.anger_chips.append(self.anger_level)
                        logging.debug(f"{player.name} 🔔🔔🔔, and was right. {previous_player}")
                    if self.is_game_over():
                        winners = self.determine_winners()
                        for player in self.players:
                            player.__init__(player.name)
                        return winners
                    self.new_round()
                    break
                previous_player = player

def run_games(number_of_games, verbose=False):
    players = [PickPlayer("Pieter"), FlipPlayer("Filip"), BellPlayer("Bella"), ThreeTurnPlayer('Trien')]
    wins = {p.name:0 for p in players}
    logging.debug(wins)
    for game_number in range(number_of_games):
        logging.debug(f"start game {game_number}")
        deck = Deck()
        game = DurianGame(players, deck=deck)
        winners = game.run()
        logging.debug(f"winners for game{game_number}: {winners}")
        for winner in winners:
            wins[winner.name] += 1
    logging.info("result:")
    logging.info(wins)

if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        prog='Durian',
        description='play a game of Durian by oink games'
    )
    parser.add_argument('-n', '--number_of_games', default=1, type=int)
    parser.add_argument('-l', '--log_level', default="INFO", choices=list(logging.getLevelNamesMapping()))
    args = parser.parse_args()
    logging.getLogger().setLevel(logging.getLevelName(args.log_level))
    run_games(args.number_of_games)