coucou
This commit is contained in:
parent
8a9bb79119
commit
5e54410555
498
app.py
498
app.py
@ -1,247 +1,251 @@
|
||||
import heapq
|
||||
import csv
|
||||
from datetime import datetime
|
||||
|
||||
# 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)), i + 1, 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), i + 1, tas - target
|
||||
|
||||
return None, None, None # Aucun mouvement possible
|
||||
|
||||
# Fonction pour sauvegarder l'historique dans un fichier CSV, format sur une seule ligne
|
||||
def sauvegarder_historique(date_partie, vainqueur, historique, fichier="historique_parties.csv"):
|
||||
# Construire la séquence des coups
|
||||
coups = []
|
||||
for tas, nb_objets, joueur in historique:
|
||||
coups.append(f"{joueur}({tas},{nb_objets})")
|
||||
|
||||
with open(fichier, mode='a', newline='') as file:
|
||||
writer = csv.writer(file)
|
||||
# Une seule ligne avec la date, le vainqueur et tous les coups
|
||||
writer.writerow([date_partie, vainqueur] + coups)
|
||||
|
||||
# Exercice 9 : Interface utilisateur et IA
|
||||
def jouer_nim():
|
||||
# Initialiser l'état du jeu
|
||||
etat_jeu = [3, 4, 5]
|
||||
historique = [] # Historique des coups (tas, nb_objets, joueur)
|
||||
|
||||
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:
|
||||
historique.append((tas_index + 1, nb_objets, "Joueur"))
|
||||
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é !")
|
||||
date_partie = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
sauvegarder_historique(date_partie, "Joueur", historique)
|
||||
break
|
||||
|
||||
# Tour de l'IA avec la stratégie gagnante
|
||||
print("C'est au tour de l'IA...")
|
||||
nouvel_etat, tas_ia, nb_objets_ia = strategie_gagnante(etat_jeu)
|
||||
|
||||
# Gérer les mouvements invalides pour l'IA
|
||||
if nouvel_etat is None:
|
||||
print("IA n'a pas trouvé de mouvement valide.")
|
||||
continue
|
||||
|
||||
historique.append((tas_ia, nb_objets_ia, "IA"))
|
||||
etat_jeu = nouvel_etat
|
||||
|
||||
# Vérifier si l'IA a gagné
|
||||
if est_etat_final(etat_jeu):
|
||||
afficher_etat(etat_jeu)
|
||||
print("L'IA a gagné !")
|
||||
date_partie = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
sauvegarder_historique(date_partie, "IA", historique)
|
||||
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()
|
||||
import heapq
|
||||
import csv
|
||||
from datetime import datetime
|
||||
|
||||
# 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 (corrigée)
|
||||
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)), i + 1, min(3, tas) # Retire le maximum autorisé
|
||||
else:
|
||||
# Stratégie gagnante : trouver le tas à modifier
|
||||
for i, tas in enumerate(etat_jeu):
|
||||
# Calculer ce qu'il reste si on change ce tas pour atteindre un XOR de 0
|
||||
target = tas ^ xor_total
|
||||
if target < tas:
|
||||
nb_a_retirer = tas - target
|
||||
if nb_a_retirer > 0 and nb_a_retirer <= 3: # Vérifier qu'on retire entre 1 et 3 objets
|
||||
return appliquer_mouvement(etat_jeu, i, nb_a_retirer), i + 1, nb_a_retirer
|
||||
|
||||
# Si aucun mouvement n'est possible (ce qui ne devrait jamais arriver), renvoyer None
|
||||
return None, None, None
|
||||
|
||||
# Fonction pour sauvegarder l'historique dans un fichier CSV, format sur une seule ligne
|
||||
def sauvegarder_historique(date_partie, vainqueur, historique, fichier="historique_parties.csv"):
|
||||
# Construire la séquence des coups
|
||||
coups = []
|
||||
for tas, nb_objets, joueur in historique:
|
||||
coups.append(f"{joueur}({tas},{nb_objets})")
|
||||
|
||||
with open(fichier, mode='a', newline='') as file:
|
||||
writer = csv.writer(file)
|
||||
# Une seule ligne avec la date, le vainqueur et tous les coups
|
||||
writer.writerow([date_partie, vainqueur] + coups)
|
||||
|
||||
# Exercice 9 : Interface utilisateur et IA
|
||||
def jouer_nim():
|
||||
# Initialiser l'état du jeu
|
||||
etat_jeu = [3, 4, 5]
|
||||
historique = [] # Historique des coups (tas, nb_objets, joueur)
|
||||
|
||||
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:
|
||||
historique.append((tas_index + 1, nb_objets, "Joueur"))
|
||||
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é !")
|
||||
date_partie = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
sauvegarder_historique(date_partie, "Joueur", historique)
|
||||
break
|
||||
|
||||
# Tour de l'IA avec la stratégie gagnante
|
||||
print("C'est au tour de l'IA...")
|
||||
nouvel_etat, tas_ia, nb_objets_ia = strategie_gagnante(etat_jeu)
|
||||
|
||||
# Gérer les mouvements invalides pour l'IA
|
||||
if nouvel_etat is None:
|
||||
print("IA n'a pas trouvé de mouvement valide.")
|
||||
continue
|
||||
|
||||
historique.append((tas_ia, nb_objets_ia, "IA"))
|
||||
etat_jeu = nouvel_etat
|
||||
|
||||
# Vérifier si l'IA a gagné
|
||||
if est_etat_final(etat_jeu):
|
||||
afficher_etat(etat_jeu)
|
||||
print("L'IA a gagné !")
|
||||
date_partie = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
sauvegarder_historique(date_partie, "IA", historique)
|
||||
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()
|
||||
|
Loading…
Reference in New Issue
Block a user