# Test de Majin


import unittest

#from majin import Ficha
#from majin import FichaAS
#from majin import Jugador
#from majin import JugadorHumano
#from majin import JugadorOrdenador


# Majin

"""Para un juego de dominó basado en el Mahjong, el Majín,
es necesario crear objetos Ficha con doble valor facial (iz izquierdo y dr derecho)
del 1 al 9.
Internamente se guardan los dos valores como una tupla,
accesible mediante las propiedades valor (devuelve la tupla entera),
iz (el primer valor) y dr (el segundo valor).
El método __str_() devuelve los valores con el formato “[<iz>|<dr>]”.
El método encaje_con(par: tuple[Ficha]) devuelve a) ENCAJE_NULO,
b) ENCAJE_PARCIAL o c) ENCAJE_TOTAL según la ficha
a) no contenga ninguno de los valores de las fichas en el par,
b) contenga uno de los valores de las fichas en el par,
o c) contenga valores de las dos fichas en el par.
Por ejemplo, dadas las fichas f0 = (1, 1), f1 = (4, 5), f2 = (9, 4), f3 = (5, 9)
y f4 = (4, 4), la ficha f0 no encaja con ninguna otra tomadas en pares,
la ficha f4 encaja parcialmente con f2 y f3,
y la ficha f1 encaja totalmente con f2 y f3.
"""
class Ficha:
    ENCAJE_NULO = 1111
    ENCAJE_PARCIAL = 1112
    ENCAJE_TOTAL = 1113
    
    def __init__(self, iz: int, dr: int):
        if iz < 0 or iz > 9:
            raise ValueError("iz fuera de rango (0-9): " + str(iz))
        
        if dr < 0 or dr > 9:
            raise ValueError("dr fuera de rango (0-9): " + str(dr))
        
        self.__valor = (iz, dr)
        
    @property
    def valor(self):
        return self.__valor
    
    @property
    def iz(self):
        return self.valor[0]
    
    @property
    def dr(self):
        return self.valor[1]
    
    def encaje_con(self, par: tuple["Ficha"]) -> int:
        toret = Ficha.ENCAJE_NULO
        
        for ficha in par:
            if (self.iz == ficha.iz
                or self.dr == ficha.dr):
                toret = Ficha.ENCAJE_PARCIAL if toret == Ficha.ENCAJE_NULO else Ficha.ENCAJE_TOTAL
        
        return toret
    
    def __str__(self):
        return f"[{self.iz}|{self.dr}]"
    

"""La clase FichaAS representa a una ficha que puede encajar con cualquier otra,
cuya representación textual es “[*|*]”.
Internamente, los valores de esta ficha son (0, 0).
"""
class FichaAS(Ficha):
    def __init__(self):
        super().__init__(0, 0)
        
    def encaje_con(self, par: tuple["Ficha"]) -> int:
        return Ficha.ENCAJE_TOTAL
    
    def __str__(self):
        return "[*|*]"
    
    
"""Escriba (al menos), las clases JugadorHumano y JugadorOrdenador.
Ambas clases guardan un nombre
y una colección doble de fichas
(una lista y un diccionario sobre las mismas fichas),
que son las que el jugador posee.
Ambas tienen un método admite(fichas: lista[Ficha])
que añade las fichas dadas a las colecciones de fichas del jugador,
una propiedad de solo lectura para devolver nombre y un método __str_()
que devuelve “<nombre>: <ficha1>, <ficha2> [, … <fichan>]”.
Ambas tienen un método fichas_con(i: int) - > tuple[Ficha],
que simplifica el mostrar agrupadas al jugador las fichas
que tuvieran un determinado valor i en alguno de sus dos extremos.
El jugador ordenador siempre se llama Ralph^,
y si por ejemplo tuviese las fichas (1, 3), (4, 2) y (9, 6),
su método __str_() devolvería “Ralph^: [1|3] [4|2] [9|6]”.
Además, ambas clases tienen el método
haz_jugada(self, par: tuple[Ficha]) -> Ficha”.
El jugador humano simplemente devuelve la jugada almacenada mediante
la propiedad de lectura y escritura jugada, que acepta
y devuelve un objeto Ficha.
En la clase JugadorOrdenador, el método haz_jugada()
busca una ficha que haga encaje total,
en caso de no encontrarla devuelve una ficha que haga encaje parcial,
o devuelve None."""
class Jugador:
    def __init__(self, nombre: str):
        self.__nombre = nombre
        self.__fichas = []
        self.__dict_fichas = {0: [], 1: [], 2:[], 3:[], 4:[],
                              5:[], 6:[], 7:[], 8:[], 9:[]}
        
    @property
    def nombre(self):
        return self.__nombre
    
    @property
    def fichas(self):
        return list(self.__fichas)
    
    def admite(self, fichas: list[Ficha]):
        self.__fichas += fichas
        for f in fichas:
            self.__dict_fichas[f.iz].append(f)
            self.__dict_fichas[f.dr].append(f)
            
    def fichas_con(self, i: int) -> tuple[Ficha]:
        return tuple(self.__dict_fichas[i])
    
    def __str__(self):
        return f"{self.nombre}: {str.join(' ', [str(x) for x in self.fichas])}"
    

"""Al jugador humano se le pasa la jugada."""
class JugadorHumano(Jugador):
    def __init__(self, nombre: str):
        super().__init__(nombre)
        self.__jugada: Ficha| None = None
        
    @property
    def jugada(self) -> Ficha|None:
        return self.__jugada
    
    @jugada.setter
    def jugada(self, f:Ficha):
        self.__jugada = f
        
    def haz_jugada(self, otro_par: tuple[Ficha]) -> Ficha|None:
        return self.__jugada

    
"""El jugador ordenador deduce la jugada."""
class JugadorOrdenador(Jugador):
    def __init__(self):
        super().__init__("Ralph^")
        
    def haz_jugada(self, otro_par: tuple[Ficha]) -> Ficha|None:
        toret = None
        
        for ficha in self.fichas:
            if ficha.encaje_con(otro_par) == Ficha.ENCAJE_TOTAL:
                toret = ficha
                break
            elif ficha.encaje_con(otro_par) == Ficha.ENCAJE_PARCIAL:
                toret = ficha
            
        return toret


class TestFicha(unittest.TestCase):
    def test_crea_ficha(self):
        f = Ficha(1, 5)
        self.assertEqual((1, 5), f.valor)
        self.assertEqual(1, f.iz)
        self.assertEqual(5, f.dr)
        self.assertEqual("[1|5]", str(f))
        self.assertNotEqual(5, f.iz)
        self.assertNotEqual(1, f.dr)
        self.assertNotEqual("[5|1]", str(f))
        
        #self.assertRaises(ValueError, lambda: Ficha(-1, 5))
        #self.assertRaises(ValueError, lambda: Ficha(9, -5))
        #self.assertRaises(ValueError, lambda: Ficha(9, 11))
        #self.assertRaises(ValueError, lambda: Ficha(11, 5))
        
    def test_encaje(self):
        f1 = Ficha(1, 6)
        f2 = Ficha(9, 9)
        f3 = Ficha(5, 6)
        f4 = Ficha(1, 6)
        
        self.assertEqual(Ficha.ENCAJE_NULO, f2.encaje_con((f1, f3)))
        self.assertEqual(Ficha.ENCAJE_PARCIAL, f1.encaje_con((f2, f3)))
        self.assertEqual(Ficha.ENCAJE_TOTAL, f1.encaje_con((f4, f3)))
        
    def test_crea_as(self):
        f1 = Ficha(1, 5)
        f2 = Ficha(9, 9)
        f3 = Ficha(5, 6)
        fas = FichaAS()
        
        self.assertEqual("[*|*]", str(fas))
        self.assertEqual(Ficha.ENCAJE_TOTAL, fas.encaje_con((f1, f2)))
        self.assertEqual(Ficha.ENCAJE_TOTAL, fas.encaje_con((f3, f1)))
        

class TestJugador(unittest.TestCase):
    def setUp(self):
        self.__fichas = [Ficha(1, 2), Ficha(3, 4), Ficha(5, 6)]
        
    def test_crea_jugador(self):
        j = Jugador("abstract")
        j.admite(self.__fichas)
        self.assertEqual("abstract", j.nombre)
        self.assertEqual(self.__fichas, j.fichas)
        self.assertEqual("abstract: [1|2] [3|4] [5|6]", str(j))
        
    def test_jugador_fichas_con(self):
        j = Jugador("abstract")
        j.admite(self.__fichas)
        
        self.assertEqual("[1|2]", str(j.fichas_con(1)[0]))
        self.assertEqual("[1|2]", str(j.fichas_con(2)[0]))
        self.assertEqual("[3|4]", str(j.fichas_con(3)[0]))
        self.assertEqual("[3|4]", str(j.fichas_con(4)[0]))
        self.assertEqual("[5|6]", str(j.fichas_con(5)[0]))
        self.assertEqual("[5|6]", str(j.fichas_con(6)[0]))
        
        
    def test_jugada_jugador_humano(self):
        jh1 = JugadorHumano("jh1")
        jh1.admite(self.__fichas)
        self.assertEqual("jh1", jh1.nombre)
        self.assertEqual(self.__fichas, jh1.fichas)
        self.assertEqual("jh1: [1|2] [3|4] [5|6]", str(jh1))
        
        f = Ficha(9, 9)        
        jh1.jugada = f
        self.assertEqual(f, jh1.jugada)
        self.assertEqual(f, jh1.haz_jugada((None, None)))
        
    def test_jugada_jugador_ordenador(self):
        jo1 = JugadorOrdenador()
        jo1.admite(self.__fichas)
        self.assertEqual(self.__fichas, jo1.fichas)
        self.assertEqual("Ralph^: [1|2] [3|4] [5|6]", str(jo1))
        
        # No match
        other_pieces = (Ficha(8, 9), Ficha(7, 8))
        f = jo1.haz_jugada(other_pieces)
        self.assertEqual(None, f)
        
        # Partial match
        other_pieces = (Ficha(3, 9), Ficha(5, 7))
        f = jo1.haz_jugada(other_pieces)
        self.assertNotEqual("[1|2]", str(f))
        
        # Total match
        other_pieces = (Ficha(3, 9), Ficha(4, 7))
        f = jo1.haz_jugada(other_pieces)
        self.assertEqual("[3|4]", str(f))


if __name__ == "__main__":
    unittest.main()

