Links:

simple Durian

The code for playing the durian game with an AI you can implement yourself.

"""
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

logging.getLogger().setLevel(logging.INFO)

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, players:list[Player]):
    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))
    players = [HumanPlayer("Pieter"), FlipPlayer("Filip"), BellPlayer("Bella"), ThreeTurnPlayer('Trien')]
    run_games(args.number_of_games, players)

license

Disclaimer

simple_durian.py is an independent, simplified Python
implementation inspired by Durian. It is not affiliated
with, endorsed by, or associated with Oink Games or any of
its subsidiaries. All trademarks, service marks, trade
names, trade dress, product names, and logos appearing in
this software are the property of their respective owners.
The use of these names, trademarks, and brands does not
imply endorsement.

License

This software is licensed under the MIT License. You may
obtain a copy of the License at:
https://opensource.org/license/mit

Fair Use

This software is created under the principles of fair use.
It is a transformative work that builds upon the original
board game concept for educational and non-commercial
purposes. The implementation does not replicate the
original game in its entirety and is intended to provide a
simplified version for learning and experimentation. The
use of any copyrighted material is limited and does not
impact the market value of the original work.