"""
 28.09.2019
 Schnapsen Simulation

 Rules:
    4 Farben:
        H Herz 0
        K Karo 1
        P Pik 2
        K Kreuz 3
    
    Werte:
        A Ass 11
        Z Zehner 10
        K König 4
        D Dame 3
        B Bube 2

    Players:
        G Geber
        V Vorhand

    Geben:
        5 cards each (= 10)
        1 card A Atout (= Trumpf)
        9 cards T Talon

    Gameplay:
        Stichzwang {T, F}, Farbzwang {T, F}:
            Spieler muss:
            1) mit höherer Karte der angespielten Farbe stechen, sonst
            2) eine niedrigere Karte der angespielten Farbe zurückgeben, sonst
            3) mit Trumpf stechen, sonst
            4) beliebige Karte abwerfen

            Note: Farbzwang vor Stichzwang! Nur mit Trumpf stechen, wenn man Farbe nicht hat!        

        Stechen:
            gleiche Farbe und höherer Wert
            Trumpf

        Talon:
            Talonsperre {T, F}
            Nach Runde bekommt Stecher höchste Karte, anderer nächsthöhere
            wenn aufgebraucht => Farb- und Stichzwang = True
            wenn zugedreht => Farb- und Stichzwang = True, Talonsperre = True

        Ansagen:
            König + Dame (Trumpf) = 40
            König + Dame (nicht Trumpf) = 20
            König oder Dame muss gespielt werden

            Note: Ansagen zählen erst ab ersten Stich

        Austausch:
            Spieler mit Bube der Trumpffarbe darf gegen Trumpfkarte austauschen

        Gewinnen:
            66 Punkte nach Stich (braucht Ausmeldung!)
            letzter Stich (nicht bei Talonsperre)

        Punkte bei Sieg:
            Hat der Gegner keinen Stich erzielt, gewinnt der Spieler drei Punkte.
            hat der Gegner 32 oder weniger Augen erhalten, gewinnt der Spieler zwei Punkte.
            hat der Gegner 33 oder mehr Augen erhalten, gewinnt der Spieler einen Punkt.

        Weird Rules:
            Liegt nur mehr eine Karte als Talon auf der offenen Trumpfkarte, darf man austauschen, jedoch nicht zudrehen.

            Kann der Spieler, der den Talon gesperrt hat, keine 66 Augen erzielen, bzw. kann sich sein Gegner zuvor ausmelden, so gewinnt der Gegner
                drei Punkte, falls er zum Zeitpunkt des Zudrehens noch stichlos war, ansonsten
                zwei Punkte

    Programm Syntax:
        
        Command: [Ansage, Stich, Wechsel, Sperre]

        
"""
import random
import numpy as np

decode_table = {
                    "colors": {
                        0: "Herz",
                        1: "Karo",
                        2: "Pik",
                        3: "Kreuz",
                    },
                    "values": {
                        2: "Bube",
                        3: "Dame",
                        4: "König",
                        10: "Zehner",
                        11: "Ass",
                    }
                }

#%%
# util functions
def decodeCards(cards):
    """
    converts numerical cards array to string cards array
    """

    return [decode_table["colors"][i] + " "+ decode_table["values"][j] for i,j in cards]

def encodeCards(cards_str):
    """
    converts string cards array to numerical cards array
    """

    reverse_decode_table = getReverseDecodeTable()

    cards = []
    for card in cards_str:
        card = card.strip().split(" ")
        cards.append(card)

    return [(reverse_decode_table["colors"][i], reverse_decode_table["values"][j]) for i,j in cards]

def generateNewDeck():
    """
    generates a new random deck
    """

    cards = []
    for color in range(4):
        for value in [2,3,4,10,11]:
            cards.append( (color, value) )
    
    random.shuffle(cards) 
    return cards

def getInput(player, action):
    """
    handles all user input
    returns the numeric encoding of the card
    """

    ret = None
    # get input from command line
    if action == "card":
        while True:
            card = input(player.name+", which card to play? ")
            
            try:
                card = encodeCards([card])[0]
            except:
                print("card is written as: [color] [value]!")
                continue

            ret = card
            break

    elif action == "call":
        while True:
            first_card = input(player.name+", first card to call? ")
            second_card = input(player.name+", second card to call? ")
            cards = [first_card, second_card]
            try:
                cards = encodeCards(cards)
            except:
                print("card is written as: [color] [value]!")
                continue

            ret = cards
            break

    elif action == None:
        ret = input(player.name+", what do you want to do?").strip()

    # print(ret) # to see piped input
    return ret

def getReverseDecodeTable():
    """
    reverses the decoding table allow quick access with string values
    """

    reverse_colors = {v:k for k, v in decode_table["colors"].items()}
    reverse_values = {v:k for k, v in decode_table["values"].items()}

    return {"colors": reverse_colors, "values":reverse_values}

#%%
# Game Class
class Game:

    def __init__(self, cards=None):
        if cards == None:
            self.cards = generateNewDeck()
        else:
            self.cards = cards

        self.playerA = Player(self.cards[:5], "Alice")
        self.playerB = Player(self.cards[5:10], "Bob")
        self.players = [self.playerA, self.playerB]

        self.talon = self.cards[10:19]
        self.trump = [self.cards[19]]
        self.trump_color = self.trump[0][0]

        self.talon_closed = False
        self.who_closed = None


    def cardsClosedValid(self, cardA, cardB, cardsB):
        """
        checks whether Player B played a valid card when the talon is closed
        """

        cardA_color, cardA_value = cardA
        cardB_color, cardB_value = cardB

        other_cards_with_A_color = [card for card in cardsB if card[0] == cardA_color]

        if cardA_color == cardB_color:
            if cardB_value > cardA_value:
                return True 
            else:
                # check if there was a card that could have beat A's
                if len(other_cards_with_A_color) == 0:
                    return True
                bigger_cards = np.array([value for color, value in other_cards_with_A_color]) > cardA_value
                return not np.cumsum(bigger_cards)[-1]

        else:
            # check if player B had other cards with cardA's color
            if cardA_color in [color for color, value in other_cards_with_A_color]:
                return False
            else:
                return True


    def playersAction(self, players):
        """
        handles the players input and checks if the input is correct
        """

        cards = []
        for player in players:
            called = False
            called_cards = None

            while True:
                user_input = getInput(player, None)

                if user_input == "quit":
                    exit()

                if user_input == "Stechen":
                    card = getInput(player, "card")

                    if called and card not in called_cards:
                        print("Not allowed to play that! Play one of the called cards")
                    elif card not in player.cards:
                        print("Not allowed to play that! Card not in possession")
                    else:
                        if card in player.cards:
                            cards.append(card)
                            break
                        else:
                            print("Player does not have this card")

                elif user_input == "Ansage":
                    if not called:
                        called_cards = getInput(player, "call")

                        if player.processCall(called_cards, self.trump_color):
                            called = True
                    else:
                        print("Only 1 call per round!")

                elif user_input == "Tausch":
                    self.trade(player)

                elif user_input == "Zudrehen":
                    self.talon_closed = True
                    self.must_color = True
                    self.must_sting = True
                    self.who_closed = player
                    print("Closed talon")

                else:
                    print("Commands are: [Stechen, Ansage, Tausch, Zudrehen]")
        
        cardA, cardB = cards
        playerA, playerB = players

        playerA.cards.remove(cardA)
        playerB.cards.remove(cardB)

        # check if correct cards have been played when talon is closed
        if self.talon_closed and not self.cardsClosedValid(cardA, cardB, playerB.cards):
            print(playerA.name)
            print(playerB.name)
            print(playerB.cards)
            print("Wrong Card from {}!".format(playerB.name))
            print("{} wins the game!".format(playerA.name))
            exit()

        return cards



    def print(self):
        """
        prints the round
        """

        playerA, playerB = self.players

        cardsA_str = decodeCards(playerA.cards)
        cardsB_str = decodeCards(playerB.cards)

        talon_str = decodeCards(self.talon)
        trump_str = decodeCards(self.trump)
        trump_color_str = decode_table["colors"][self.trump_color]

        print(2* ('#'*80 + '\n'))
        print("{}'s cards are: {}".format(playerA.name, ", ".join(cardsA_str)))
        print("{}'s points {} + {}".format(playerA.name, playerA.points, playerA.called_points))
        print("{}'s cards are: {}".format(playerB.name, ", ".join(cardsB_str)))
        print("{}'s points {} + {}".format(playerB.name, playerB.points, playerB.called_points))

        print("Talon cards are: {}".format(", ".join(talon_str)))
        print("Trump card is: {}".format(", ".join(trump_str)))
        print("Trump color is: {}".format(trump_color_str))
        print(self.players[0].name + " is starting\n")


    def round(self):
        """
        handles the steps of each round
        """

        # print standings
        self.print()

        # check which player is first
        playerA, playerB = self.players

        # players make moves and play their cards
        cardA, cardB = self.playersAction([playerA, playerB])
        print("{} vs {}".format(decodeCards([cardA])[0], decodeCards([cardB])[0]))

        # check which card won
        if self.sting(cardA, cardB) == 0:
            winner = playerA
            loser = playerB
        else:
            winner = playerB
            loser = playerA

        # winner gets points
        winner.points += cardA[1] + cardB[1]
        print(winner.name+" won "+str(cardA[1] + cardB[1])+" points in this round!\n")

        if winner.getTotalPoints() >= 66:
            print("{} wins the game with {} points!".format(winner.name, str(winner.getTotalPoints())))
            exit()
        if loser.getTotalPoints() >= 66:
            # it is possible to win the game while losing a round
            print("{} wins the game with {} points!".format(loser.name, str(loser.getTotalPoints())))
            exit()

        # running out of cards: last sting wins
        if len(playerA.cards) == 0:
            if self.who_closed != None:
                # last sting does not win, closing player has to have enough, otherwise he loses
                # if he won, this if would not be reached
                print("{} lost the game with {} points, even with closing it!".format(self.who_closed.name, str(self.who_closed.getTotalPoints())))

                if playerA == self.who_closed:
                    print("{} wins the game with {} points!".format(playerB.name, str(playerB.getTotalPoints())))
                else:
                    print("{} wins the game with {} points!".format(playerA.name, str(playerA.getTotalPoints())))
                
            else:
                # last sting wins
                print("{} wins the game with {} points!".format(winner.name, str(winner.getTotalPoints())))

            exit()

        # take new cards from talon
        if not self.talon_closed:
            if len(self.talon) > 1:
                winner.cards.append(self.talon.pop())
                loser.cards.append(self.talon.pop())
            else:
                winner.cards.append(self.talon.pop())
                loser.cards.append(self.trump[0])
                self.trump = []
                self.talon_closed = True

        # set starting order for next round
        self.players = [winner, loser]


    def sting(self,cardA, cardB):
        """
        returns index of winner, 0 = cardA, 1 = cardB
        """

        cA_color, cA_value = cardA
        cB_color, cB_value = cardB

        if cA_color == cB_color: 
            return cB_value > cA_value # same color, value decides

        else:
            if cA_color == self.trump_color: # trump vs non-trump
                return 0
            elif cB_color == self.trump_color: # non-trump vs trump
                return 1

            return 0 # non-trump vs non-trump


    def trade(self, player):
        """
        trade boy of trump color with trump card
        """
        
        trump_boy = (self.trump_color, 2)
        if not trump_boy in player.cards:
            print("no trade possible! Card not in possession")
        else:
            index = player.cards.index(trump_boy)
            player.cards[index] = self.trump[0] 
            self.trump[0] = trump_boy
            print("Trade successfull! New Trump is "+ decodeCards(self.trump)[0])


class Player:
    def __init__(self, cards, name):
        self.cards = cards
        self.points = 0
        self.name = name
        self.called_points = 0

    def processCall(self, called_cards, trump_color):
        """
        checks players cards if call is possible and adds points
        """

        # check if in possesion
        if called_cards[0] not in self.cards or called_cards[1] not in self.cards:
            print("cards not in possesion")
            return False
        
        # len 2 and same color
        if len(called_cards) != 2 or called_cards[0][0] != called_cards[1][0]:
            print("not same color")
            return False

        # check if Dame and König
        values = [called_cards[0][1], called_cards[1][1]]
        if 3 not in values or 4 not  in values:
            print("not Dame or Bube")
            return False

        # check if trump:
        if called_cards[0][0] == trump_color:
            self.called_points += 40
        else:
            self.called_points += 20
        
        return True
    
    def getTotalPoints(self):
        """
        returns players total points, check if called points are valid
        """

        if self.points > 0:
            return self.points + self.called_points
        else:
            return self.points


cards = [(2, 10), (2, 4), (0, 10), (3, 4), (2, 3), (3, 3), (3, 11), (3, 2), (0, 2), (1, 2), (0, 3), (0, 11), (0, 4), (1, 11), (2, 2), (1, 10), (2, 11), (3, 10), (1, 4), (1, 3)] #generateNewDeck()

# g = Game(cards)
if __name__ == '__main__':
    g = Game()
    while True:
        g.round()