import heapq

# Exercice 1 : Affichage de l'état du jeu
def afficher_etat(etat_jeu):
    print("État actuel du jeu :")
    for i, tas in enumerate(etat_jeu):
        print(f"Tas {i+1}: {tas} objets")
    print()

# Exercice 2 : Génération des mouvements légaux avec un maximum de 3 objets retirés
def generer_mouvements(etat_jeu):
    mouvements = []
    for i, tas in enumerate(etat_jeu):
        # Limiter à un maximum de 3 objets retirés
        for j in range(1, min(4, tas + 1)):  # max 3 objets peuvent être retirés
            nouvel_etat = etat_jeu.copy()
            nouvel_etat[i] -= j
            mouvements.append(nouvel_etat)
    return mouvements

# Exercice 3 : Appliquer un mouvement
def appliquer_mouvement(etat_jeu, tas_index, nb_objets):
    if 0 <= tas_index < len(etat_jeu) and 1 <= nb_objets <= etat_jeu[tas_index] and nb_objets <= 3:
        nouvel_etat = etat_jeu.copy()
        nouvel_etat[tas_index] -= nb_objets
        return nouvel_etat
    else:
        print("Mouvement invalide")
        return None

# Exercice 4 : Fonction heuristique
def heuristique(etat_jeu):
    return sum(etat_jeu)

# Exercice 5 : Définir l'état final et le coût
def est_etat_final(etat_jeu):
    return all(tas == 0 for tas in etat_jeu)

def cout_mouvement(etat_precedent, etat_suivant):
    return 1  # Chaque mouvement a un coût de 1

# Exercice 6 : Classe Noeud et algorithme A*
class Noeud:
    def __init__(self, etat, parent=None, g=0, h=0):
        self.etat = etat
        self.parent = parent
        self.g = g  # Coût du chemin depuis le départ
        self.h = h  # Heuristique
        self.f = g + h  # Coût total (g + h)

    def __lt__(self, autre):
        return self.f < autre.f

def algorithme_a_star(etat_initial):
    file_priorite = []
    etat_initial_heuristique = heuristique(etat_initial)
    noeud_initial = Noeud(etat_initial, g=0, h=etat_initial_heuristique)
    heapq.heappush(file_priorite, noeud_initial)
    
    visites = set()
    
    while file_priorite:
        noeud_courant = heapq.heappop(file_priorite)
        
        if est_etat_final(noeud_courant.etat):
            return reconstruire_chemin(noeud_courant)
        
        visites.add(tuple(noeud_courant.etat))
        
        for successeur in generer_mouvements(noeud_courant.etat):
            if tuple(successeur) in visites:
                continue
            
            g_successeur = noeud_courant.g + cout_mouvement(noeud_courant.etat, successeur)
            h_successeur = heuristique(successeur)
            noeud_successeur = Noeud(successeur, parent=noeud_courant, g=g_successeur, h=h_successeur)
            heapq.heappush(file_priorite, noeud_successeur)
    
    return None  # Aucun chemin trouvé

# Exercice 8 : Reconstruction du chemin
def reconstruire_chemin(noeud_final):
    chemin = []
    noeud_courant = noeud_final
    while noeud_courant:
        chemin.append(noeud_courant.etat)
        noeud_courant = noeud_courant.parent
    return chemin[::-1]

# Fonction pour calculer le XOR de tous les tas
def calculer_xor(etat_jeu):
    xor_total = 0
    for tas in etat_jeu:
        xor_total ^= tas
    return xor_total

# Stratégie gagnante pour l'IA
def strategie_gagnante(etat_jeu):
    xor_total = calculer_xor(etat_jeu)
    
    if xor_total == 0:
        # Pas de stratégie gagnante, faire un mouvement quelconque
        for i, tas in enumerate(etat_jeu):
            if tas > 0:
                return appliquer_mouvement(etat_jeu, i, min(3, tas))  # Retire le maximum autorisé
    else:
        # Stratégie gagnante : trouver le tas à modifier
        for i, tas in enumerate(etat_jeu):
            target = tas ^ xor_total
            if target < tas:
                return appliquer_mouvement(etat_jeu, i, tas - target)
    
    return None  # Aucun mouvement possible

# Exercice 9 : Interface utilisateur et IA
def jouer_nim():
    # Initialiser l'état du jeu
    etat_jeu = [3, 4, 5]
    
    while True:
        # Afficher l'état actuel du jeu
        afficher_etat(etat_jeu)
        
        # Vérifier si l'état actuel est une victoire
        if est_etat_final(etat_jeu):
            print("Le jeu est terminé !")
            break
        
        # Tour de l'utilisateur
        print("C'est à votre tour !")
        tas_index = int(input("Choisissez le tas (1, 2, 3) : ")) - 1
        nb_objets = int(input(f"Combien d'objets retirer du tas {tas_index + 1} (max 3) ? "))
        
        nouvel_etat = appliquer_mouvement(etat_jeu, tas_index, nb_objets)
        if nouvel_etat:
            etat_jeu = nouvel_etat
        else:
            continue  # Demande de nouveau si le mouvement est invalide
        
        # Vérifier si l'état actuel est une victoire
        if est_etat_final(etat_jeu):
            afficher_etat(etat_jeu)
            print("Vous avez gagné !")
            break
        
        # Tour de l'IA avec la stratégie gagnante
        print("C'est au tour de l'IA...")
        etat_jeu = strategie_gagnante(etat_jeu)
        
        # Vérifier si l'IA a gagné
        if est_etat_final(etat_jeu):
            afficher_etat(etat_jeu)
            print("L'IA a gagné !")
            break

# Lancer le jeu
jouer_nim()


import unittest

class TestJeuNim(unittest.TestCase):

    # Test pour l'affichage de l'état du jeu (Exercice 1)
    def test_afficher_etat(self):
        etat_jeu = [3, 4, 5]
        # Ici on ne peut pas tester directement la sortie console, mais on peut vérifier si la fonction retourne les bons tas
        self.assertEqual(afficher_etat(etat_jeu), None)  # Il n'y a pas de retour attendu, juste un affichage

    # Test pour la génération des mouvements légaux (Exercice 2)
    def test_generer_mouvements(self):
        etat_jeu = [1, 2]
        mouvements_attendus = [[0, 2], [1, 1], [1, 0]]  # Il y a 3 mouvements possibles
        self.assertEqual(generer_mouvements(etat_jeu), mouvements_attendus)

    # Test pour appliquer un mouvement valide (Exercice 3)
    def test_appliquer_mouvement_valide(self):
        etat_jeu = [3, 4, 5]
        nouvel_etat = appliquer_mouvement(etat_jeu, 1, 3)
        self.assertEqual(nouvel_etat, [3, 1, 5])

    # Test pour appliquer un mouvement invalide (Exercice 3)
    def test_appliquer_mouvement_invalide(self):
        etat_jeu = [3, 4, 5]
        nouvel_etat = appliquer_mouvement(etat_jeu, 1, 5)  # Trop d'objets retirés
        self.assertIsNone(nouvel_etat)

    # Test pour l'heuristique (Exercice 4)
    def test_heuristique(self):
        etat_jeu = [3, 4, 5]
        self.assertEqual(heuristique(etat_jeu), 12)

    # Test pour la vérification de l'état final (Exercice 5)
    def test_est_etat_final(self):
        self.assertTrue(est_etat_final([0, 0, 0]))
        self.assertFalse(est_etat_final([0, 1, 0]))

    # Test pour le coût des mouvements (Exercice 5)
    def test_cout_mouvement(self):
        self.assertEqual(cout_mouvement([3, 4, 5], [3, 4, 4]), 1)

    # Test pour l'algorithme A* (Exercice 6) avec validation de l'état final seulement
    def test_algorithme_a_star(self):
        etat_initial = [1, 2]
        chemin_trouve = algorithme_a_star(etat_initial)
        etat_final_attendu = [0, 0]
        # Vérifier que le chemin atteint bien l'état final attendu
        self.assertEqual(chemin_trouve[-1], etat_final_attendu)

    # Test pour la reconstruction du chemin (Exercice 8)
    def test_reconstruire_chemin(self):
        etat_initial = [3, 4, 5]
        noeud_initial = Noeud(etat_initial)
        noeud_final = Noeud([0, 0, 0], parent=noeud_initial)
        chemin_attendu = [[3, 4, 5], [0, 0, 0]]
        self.assertEqual(reconstruire_chemin(noeud_final), chemin_attendu)

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