Un jeu de Nim

Version 1: humain vs humain

Règles du jeu

On dispose un tas d’allumettes sur la table.
Les deux joueurs ramassent, à leur tour, 2 ou 3 allumettes.
Celui qui prend la dernière allumette a gagné. S’il reste une seule allumette, il y a égalité.

Déroulement du projet

Comme ce projet ainsi que les suivants qui seront à réaliser dans une certaine autonomie demande de gérer beaucoup d’éléments, il est difficile de savoir par où débuter. Je vous conseille dans ce cas de vous appuyer sur la méthode de Zelle exposée au tout début d’année dans le (1.1).

  1. Analyser le problème : bien comprendre les règles du jeu quitte à réaliser quelques parties.

  2. Spécifier le programme :

    Problème: déroulé du jeu de Nim
    |---Entrées/paramètres : pions/allumettes (représentation ?, nombre initial ?), plateau de jeu (représentation ?)
    |---Sortie : déterminer l'état final du jeu (vainqueur ? égalité ?)
    |---Effet de bord : afficher l'état intermédiaire du jeu (à chaque tour, donner le nom du joueur et le nombre d'allumettes, afficher le plateau)
    
  3. Concevoir le programme

    Ce qui suit est un choix personnel (il peut différer sur certains points notamment sur l’affichage de certains éléments, l’ordre d’affichage, …).

  • Prédéfinir les éléments du jeu : pions, …
  • Préparer le plateau de jeu :
    • demander le nombre d’allumettes initialement,
    • créer le plateau de jeu,
    • [cosmétique] afficher le plateau de jeu initial.
  • Définir le critère d’arrêt du jeu
  • Éxecuter :
    • Tant que le jeu n’est pas fini
      • définir le joueur en cours,
      • afficher/rappeler l’état du jeu en cours,
      • demander le choix du joueur en cours,
      • définir le nouveau plateau de jeu,
      • afficher le l’état du jeu suite à ce choix.
    • Quand le jeu est fini, déterminer l’éventuel vainqueur.

Réalisation du jeu

Les deux joueurs seront numérotés : 1 et 2.

Définition des pions:

C’est un choix personnel qui peut évoluer en fonction du développement du projet.

PION = {0: (" ", 0), 1: ("|", 1)}

Choisir un dictionnaire est très souvent judicieux car cela permet de gérer plusieurs paramètres dans le même temps. Pensez au jeu où votre personnage dispose de points de vie, de points de magie et éventuellement d’autres caractéristiques.

Remarque : les personnages partagent souvent un même mode de fonctionnement. Les chanceux qui continueront NSI l’an prochain aborderont la programmation objet. Elle permettra entre autre de gagner un temps phénoménal lors du développement de tels personnages.

Préparation du plateau de jeu

a) Réalisez une fonction preparer() qui prend en paramètre le nombre d’allumettes (25 par défaut). Cette fonction demande à l’utilisateur le nombre d’allumettes qu’il veut sur la table en début de partie. Elle affiche ensuite un message de confirmation. Elle retourne enfin ce nombre d’allumettes.

>>> preparer()
Veuillez indiquer le nombre d allumettes présentes au départ (25 par défaut).
15
Sur le plateau, sont donc diposées 15 allumettes.

b) Écrire une fonction creer_plateau() qui prend en paramètre le nombre d’allumettes, qui retourne le plateau de jeu sous la forme d’une liste composée uniquement de l’élément PION[1] et qui a pour effet de bord de demander le nombre d’allumettes désiré.

c) Proposez une fonction afficher_plateau() qui prend en paramètre le plateau de jeu, qui ne retourne rien, mais qui a pour effet de bord d’afficher ce plateau de jeu en cours.

>>> grille = [PION[1] for _ in range(13)]+[PION[0] for _ in range(12)]
>>> afficher_plateau(grille)
||||| ||||| |||

La partie est-elle terminée ?

d) Réalisez une fonction jeu_fini() qui prend en paramètre le nombre d’allumettes restant sur la table. Elle retourne True si la partie continue ou False si la partie est terminée.

>>> jeu_fini(10)
False
>>> jeu_fini(1)
True

Changement de joueur

e) Réalisez une fonction changement_joueur() qui prend en paramètre le numéro du joueur (1 ou 2). Cette fonction retourne le numéro du joueur suivant.

>>> changement_joueur(1)
2

Déterminer le nombre d’allumettes sur le plateau de jeu

f) Proposer une fonction nb_allumettes() qui prend en paramètres le plateau de jeu et retourne le nombre d’allumettes dans ce plateau de jeu.

>>> grille = [PION[1] for _ in range(13)]+[PION[0] for _ in range(12)]
>>> nb_allumettes(grille)
13

Choix effectué par le joueur

g) Réalisez une fonction possibilites() qui prend en paramètre le nombre d’allumettes restant sur la table. Cette fonction retourne la liste de tous les coups possibles.

>>> possibilites(21)
[2, 3]
>>> possibilites(2)
[2]

h) Réalisez une fonction choix_humain() qui prend en paramètre le plateau de jeu. Cette fonction demande au joueur le nombre d’allumettes qu’il veut retirer de la table tout en contrôlant que ce choix est possible, puis finalement retourne le nombre effectivement choisi par le joueur.

Etat suivant du plateau de jeu

i) Réalisez une fonction etat_suivant() qui prend en paramètre le plateau de jeu, le choix d’allumettes à enlever effectué par un joueur. Cette fonction retourne le nombre d’allumettes restant sur la table après avoir enlevé celles choisies par le joueur.

>>> grille1 = [PION[1] for _ in range(17)]+[PION[0] for _ in range(8)]
>>> grille2 = etat_suivant(grille1, 3)
>>> grille2 == [PION[1] for _ in range(14)]+[PION[0] for _ in range(11)]
True

Bilan du jeu

j) Réalisez une fonction etat_bilan() qui prend en paramètre le nombre d’allumettes restant sur la table et le numéro du joueur en cours (1 ou 2). Cette fonction a pour effet de bord d’afficher le nombre d’allumettes restant si la partie continue, “Egalité” en cas de match nul, le nom du joueur gagnant si c’est la cas.

>>> etat_bilan(1,1)
Egalité
>>> etat_bilan(17, 1)
Il reste 17 allumette(s) sur la table.
>>> etat_bilan(0, 2)
La partie est remportée par le joueur 2.

Exécution du jeu

k) Réalisez enfin une fonction executer() qui prend en paramètre le nombre d’allumettes. Cette fonction fera fonctionner le jeu en utilisant toutes les fonctions précédentes.

Version 2: humain vs ordinateur

Seulement lorsque la version 1 a été finie et qu’elle a été vérifiée par moi-même !

Voici les fonctions à modifier (preparer() et executer()) et nouvelles à ajouter:

def preparer(allumettes=25):
    """
    Retourne le mode de jeu, la profondeur (ici imposée) et la grille dont les 
    éléments sont les pions (allumettes) au début de la partie
    :param allumettes :(int)
    :return : (list) la liste donnant le mode, la profondeur et la grille
    :CU : allumettes doit être un entier positif
    """
    mode = mode_jeu()
    prof = profondeur(mode)
    grille = creer_plateau(allumettes)
    return [mode, prof, grille]

def mode_jeu():
    """
    Choix du mode de jeu : seul contre l'ordinateur, ou à deux joueurs
    :param : None
    :return : (tuple) couple (nb joueur, ordre des joueurs)
    """
    # PvP ou PvE ?
    print("Voulez-vous jouer à deux joueurs, ou seul contre l'ordinateur ?")
    try:
        nb_joueur = int(input("Pour jouer à deux, tapez 2 ; pour jouer seul, taper 1 : "))
    except ValueError:
        return mode_jeu()
    if not( nb_joueur in {1, 2} ):
        return mode_jeu()
    # ordre joueur/ordinateur
    if nb_joueur == 1:
        ordre = input("Souhaitez-vous commencer ? o/n ?")
        while not( ordre in {"o", "n"} ):
            ordre = input("Souhaitez-vous commencer ? o/n ?")
    else:
        ordre = None
    return nb_joueur, ordre

def profondeur(mode):
    """
    Retourne la profondeur fournie par l'utilisateur

    :param mode: (tuple (int, str)) (nb joueur, ordre des joueurs)
    :return: (int) la profondeur est fixé à 1
    """
    return 1
    
def evaluation(grille, prof, joueur):
    """
    fonction d'évaluation du plateau de jeu pour la méthode minMax
    :param etat_actuel : (int) nb d'allumettes sur le plateau de jeu au moment 
    où doit l'ordinateur va jouer
    :param joueur: (int) numéro du joueur (1 ou 2)
    :return : (int)

    >>> grille = [PION[1] for _ in range(10)]
    >>> evaluation(grille, 1, 1)
    -1
    >>> evaluation(grille, 1, 2)
    1

    >>> grille = [PION[1] for _ in range(9)]
    >>> evaluation(grille, 1, 1)
    0
    >>> evaluation(grille, 1, 2)
    0

    >>> grille = [PION[1] for _ in range(8)]
    >>> evaluation(grille, 1, 1)
    1
    >>> evaluation(grille, 1, 2)
    -1
    """
    allumettes = nb_allumettes(grille)
    if allumettes % 5 == 2 or allumettes % 5 == 3:
        return (-1)**(joueur+1) #1 pour Max et -1 pour Min
    elif allumettes % 5 == 1 or allumettes % 5 == 4:
        return 0
    else:
        return (-1)**joueur #-1 pour Max et 1 pour Min    

def min_max(etat_actuel, prof, joueur):
    """
    Définit la méthode min_max utilisée par l'ordinateur pour choisir le coup 
    suivant le plus fort de son point de vue
    :param etat_actuel: (list) état du plateau actuel de jeu avant toute action
    :param prof: (int) profondeur pour min_max
    :param joueur: (int) numéro du joueur (1 ou 2)
    :return: (tuple) valeur du plateau de jeu, choix de l'ordinateur associé
    """
    autre_joueur = 3 - joueur  #a voir sur Nim

    if jeu_fini(etat_actuel) or prof == 0:
        return evaluation(etat_actuel, prof, joueur) * (-1)**(joueur + 1), 0
    else:
        if joueur == 1: #Max
            valeur, choix = max( [ (min_max(suivant(etat_actuel, choix, joueur), 
                                            prof - 1, autre_joueur)[0], choix)
                                   for choix in possibilites(etat_actuel) ] )
        else: #Min
            valeur, choix = min( [ (min_max(suivant(etat_actuel, choix, joueur), 
                                            prof - 1, autre_joueur)[0], choix)
                                   for choix in possibilites(etat_actuel) ] )
        return valeur, choix

def executer(liste): #liste=[mode, prof, grille]
    """
    Fait tourner le jeu de Nim
    :param liste : (list) contient le nombre d'allumettes au début du jeu,
                   le mode de jeu (seul contre l'ordinateur ou à 2 joueurs),
                   la profondeur pour la méthode minMax.
    :return : jeu
    """
    mode, prof, grille = liste[0], liste[1], liste[2]
    joueur = 2
    afficher_plateau(grille)
    while not( jeu_fini(grille) ):
        joueur = changement_joueur(joueur)
        choix = choisir(mode, prof, grille, joueur)
        grille = suivant(grille, choix, joueur)
        afficher_choix(choix, joueur)
        bilan(grille, joueur)

Ne reste plus qu’à jouer !

objets_initiaux = preparer()
executer(objets_initiaux)