import random

class die:
    """
    Represents a single die. Construct with the current value and sides of the dice.
    """
    rollVal = 0
    sides = 0

    def __init__(self,val:int,sideCount:int):
        self.rollVal = val
        self.sides = sideCount

    def __repr__(self) -> str:
            return(f"{type(self).__name__}(rollVal={self.rollVal})(sides={self.sides})")

    def getSides(self) -> int:
        return(self.sides)
    
    def getVal(self) -> int:
        return(self.rollVal)
    
    def setVal(self,valIn:int):
        self.rollVal=valIn

    def setSides(self,sidesIn:int):
        self.sides=sidesIn
    
    def __eq__(self, value):
         if self.rollVal == value.rollVal and self.sides == value.sides: return True
         else: return False


class gamestate:
    """
    Class represents the state of a game of competitive yahtzee lite. 

    """

    currentHand = []
    playedHands = []
    scoreOptions=["aces","twos","threes","fours","fives","sixes","three of a kind","full house","yahtzee"]
    turnsPlayed = 0    
    seed = 0
    score = 0

    def __init__(self, seedIn):
        self.turnsPlayed = 0
        self.seed = seedIn

    def getSRandom(self, numIn:int) -> int:
        """
        Takes an 8 long int and gives a predicatble random 8 long int back using middle-square method
        """
        if self.seed==0: 
            print("Seed not intialized")
            return(0)
        doubled = str(numIn*numIn)
        self.seed = int(doubled[4:12])
        return(self.seed)

    def getScore(self) -> int:
        return(self.score)
    
    def setCurrentHand(self, chIn):
        self.currentHand=chIn
        
    def setSeed(self, seedIn):
        """
        Setter for the seed, should not need to ever use this as __init__ is set to take in the seed
        """
        self.seed=seedIn

    def getCurrentHand(self):
        """
        Returns an array of strings representing the current hand
        """
        returnData = []
        for dice in self.currentHand:
             returnData.append(dice.getVal())
        return returnData
    
    def getPlayedHands(self):
        return(self.playedHands)

    def getTurnsPlayed(self) -> int:
        return(self.turnsPlayed)

    def incTurn(self):
        """
        Increments turnsPlayed by one
        """
        self.turnsPlayed+=1
    
    def incScore(self, scoreIn:int):
        """
        Adds scoreIn to the stored score value and reports current score to user
        """
        self.score+=scoreIn
        print(f"You scored {scoreIn} now totalling {self.score}")        

    def genCurrentHand(self, numSides):
        """
        Generates a hand of six dice of numSides number of sides
        """
        self.currentHand.clear()
        for i in range(1,6):
            self.currentHand.append(self.rollDice(6))
        print("generated new hand", self.currentHand)

    def commitHand(self):
        self.playedHands.append(self.currentHand)

    def rollDice(self, sides:int) -> die:
        """
        Returns an dice object representing a roll seeded with 1st param with side numbers 2nd parameter           
        """
        diceout = die((1+self.getSRandom(self.seed)%sides),sides)
        return(diceout)
    
    def modifyHand(self):
        """
        Asks user for input to reroll up to 3 die in the currentHand. 
        """
        validated = False
        rerollNum = []
        
        
        while not validated:
            userinput=input("Would you like to reroll? Enter up to 3 numbers with spaces between, enter nothing to skip: ")

            if (len(userinput)==0):
                        return
            
            rerollNum = userinput.split(" ")
        
        
            if (len(rerollNum)>3) : 
                    print("Sorry, only up to 3 choices")
            else:
                try:
                    for num in rerollNum:
                        int(num)
                    try:
                        testHand = self.getCurrentHand()
                        for num in rerollNum:
                            testHand.remove(int(num))

                            
                        for num in rerollNum:
                            searchDie = die(int(num),6)
                            self.currentHand.remove(searchDie)
                            self.currentHand.append(self.rollDice(6))
                            print(f"{self.getCurrentHand()} is current hand")

                        validated = True

                    except ValueError: 
                        print("Couldn't find all those die in your current hand")
                except ValueError: 
                    print("Invalid input, please try again")
        # end of while loop to get valid input
        
        self.commitHand()

    def scoreHand(self):
        """
        Asks user for input on how to score their hand
        """
        validated = False
        while not validated:

            for option in self.scoreOptions:
                print(option)

            testCase = input("How would you like to score this hand? ")
            #  Dupes (Aces through Sixes), Three of a Kind, Full House, Yahtzee? ")
            
            try:
                self.scoreOptions.index(testCase.lower())
            except:
                print("I didn't catch that one, try again")
            
            match testCase:
                case "aces": 
                    validated = self.scoreDupes(1)
                    self.scoreOptions.remove("aces")
                case "twos":
                    validated = self.scoreDupes(2)
                    self.scoreOptions.remove("twos")
                case "threes":
                    validated = self.scoreDupes(3)
                    self.scoreOptions.remove("threes")
                case "fours":
                    validated = self.scoreDupes(4)
                    self.scoreOptions.remove("fours")
                case "fives":
                    validated = self.scoreDupes(5)
                    self.scoreOptions.remove("fives")
                case "sixes":
                    validated = self.scoreDupes(6)
                    self.scoreOptions.remove("sixes")
                case "three of a Kind":
                    validated = self.scoreThreeKind()
                    self.scoreOptions.remove("three of a kind")
                case "full house":
                    validated = self.scoreFullHouse()
                    self.scoreOptions.remove("full house")
                case "yahtzee":
                    validated = self.scoreYahtzee()
                    self.scoreOptions.remove("yahtzee")
                case _:
                    print("Invalid Option, try again")
                    validated = False
            
    def scoreDupes(self, toCount):
        """ 
        Scoring logic for counting duplicates 
        """
        try:
            self.incScore(self.getCurrentHand().count(toCount)*toCount)
            return(True)
        except:
            print("Couldn't find any ", toCount, "no points scored this hand")
            return(False)
        
    def scoreThreeKind(self):        
        """
        Scoring logic for three of a kind
        """
        ordered = sorted(self.getCurrentHand())
        count = []
        
        for i in range(1,7):
            count.append(ordered.count(i))
        toScore=0
        try:
            toScore = count.index(3)
            self.incScore(sum(ordered))
            return(True)
        except:
            print("Couldn't find any three of a kind, try again")
            return(False)
    
    def scoreFullHouse(self) -> int:
        """
        Scoring logic for a full house, aka three of one kind and a pair of another kind
        """
        ordered = sorted(self.getCurrentHand())
        count = []
        
        for i in range(1,7):
            count.append(ordered.count(i))
        
        try:
            count.index(3)
            count.index(2)
            self.incScore(25)
            return(True)
        except:
            print("Couldn't find any full house try again")
            return(False)
        
    
    def scoreYahtzee(self):
        """
        Scoring logic for a Yahtzee or 5 die all the same value
        """
        ordered = sorted(self.getCurrentHand())
        count = []
        
        for i in range(1,7):
            count.append(ordered.count(i))
        
        try:
            count.index(5)
            self.incScore(50)
            return(True)
        except:
            print("Couldn't find any yahtzee, try again")
            return(False)