Valeurs de Sprague-Grundy pour le jeu de Wythoff

De Wiki du LAMA (UMR 5127)
Aller à la navigation Aller à la recherche

Étudiant : Nolann SANMARTI

Tuteur : Stéphane TAVENAS

Jeu de Nim (Jeu simple à 1 ligne)

Description du jeu

Le jeu de Nim est un jeu à deux joueurs qui se joue en tour par tour.

Des allumettes sont disposées sur une table, en ligne, et chaque joueur retire entre 1 et 3 allumettes de cette ligne.

La personne qui retire la dernière allumette est perdante.

  • Exemple avec 12 allumettes :
    1. Le joueur A retire 2 allumettes. Il en reste 10.
    2. Le joueur B retire 3 allumettes. Il en reste 7.
    3. Le joueur A retire 1 allumette. Il en reste 6.
    4. Le joueur B retire 2 allumettes. Il en reste 4.
    5. Le joueur A retire 3 allumettes. Il en reste 1.
    6. Le joueur B perd car il est forcé de prendre la dernière allumette.

Wythoff 01.png

Stratégie de jeu

Ce jeu est un jeu avantageant le premier joueur par le fait qu'une stratégie gagnante est présente.

En effet, on sait que chaque joueur ne peut enlever qu'entre 1 et 3 allumettes, donc en ayant 5 allumettes sur le plateau :

  1. Si le joueur A enlève 1 allumette, on aura alors 4 allumettes sur le plateau et le joueur B enlèvera 3 allumettes ;
  2. Si le joueur A enlève 2 allumettes, on aura alors 3 allumettes sur le plateau et le joueur B enlèvera 2 allumettes ;
  3. Si le joueur A enlève 3 allumettes, on aura alors 2 allumettes sur le plateau et le joueur B enlèvera 1 allumette.

On remarque que cette stratégie peut être appliquée pour passer de 9 à 5, de 13 à 9, et ainsi de suite.

Wythoff 02.png

En gardant cet écart de 4, on aura une suite de positions perdantes permettant ainsi en se positionnant dessus de faire perdre l'adversaire.

Suite : 1,5,9,13,17,...

Jeu de Wythoff (Jeu à 2 lignes)

Description du jeu

Le jeu de Wythoff est une variante du jeu de Nim où l'on va rajouter un jeu d'allumettes.

Les règles sont également un peu complexifiés pour s'adapter à 2 jeux :

  • On peut retirer le nombre d'allumettes qu'on veut dans 1 seul des jeux ;
  • On peut retirer le nombre d'allumettes qu'on veut dans les 2 jeux.

À l'inverse du jeu d'avant, le but est de retirer la dernière allumette des jeux (Les deux jeux vides).

Pour simplifier la lecture, on va écrire ces valeurs comme des coordonnées de tableau.

Le nombre d'allumettes dans le 1er paquet sera x // Le nombre d'allumettes dans le 2e paquet sera y // On aura donc (x,y) le nombres d'allumettes dans chacun des paquets.

Exemple avec (8,5) :

  1. Le joueur A enlève 4 à x. On se retrouve en (4,5).
  2. Le joueur B enlève 1 à x et y. On se retrouve en (3,4).
  3. Le joueur A enlève 3 à y. On se retrouve en (3,1).
  4. Le joueur B enlève 1 à x. On se retrouve en (2,1).
  5. Le joueur A enlève 1 à x et y. On se retrouve en (1,0).
  6. Le joueur B gagne en retirant 1 à x. On se retrouve en (0,0).

Wythoff 03.png

Stratégie de jeu

Trouver de façon expérimentale

Ce jeu avantage également le premier joueur avec une stratégie gagnante.

Cette stratégie gagnante est un peu plus difficile à cerner aux premiers abords, mais rapidement applicable après avoir l'astuce :

On va commencer par colorier en vert la case (0,0) qui est l'objectif, puis colorier en noir les cases alignés verticalement, horizontalement et en diagonale.

Les cases avec la valeur de x ou y la plus petite seront en vert, puis on colorie également en noir les cases alignés verticalement, horizontalement et en diagonale.

On répète ce processus jusqu'à atteindre les bords du plateau sans cases libres.

Wythoff 04.png

Trouver par le calcul (Algorithme)

On va prendre un emplacement qui stockera des nombres déjà utilisés => Poubelle [n1,n2,n3,...]

On prend un autre emplacement qui stockera les positions gagnantes => Positions [(X1,Y1),(X2,Y2),(X3,Y3),...]

On va définir une variable qui est égale à la n-ième position gagnante => Occurence (K)

On va commencer à compter de 1 en 1 à partir de 1 => I. (On sait déjà que la position (0,0) est gagnante du fait que c'est le but)

  • Si le nombre est déjà dans la poubelle, on continue de compter... => I+1
  • Sinon :
    1. On entre I dans les positions gagnantes en X et on entre le nombre I+K en Y => Positions : Add >> (I,I+K)
    2. Les valeurs de I et I+K sont envoyés en poubelle
    3. On ajoute 1 à K => K : K+1
    4. I retourne à 1. => I : 1.

/!\ Si I ou I+K est plus grand que la taille de grille, on finit la boucle sans rien ajouter

Quand on ressort les valeurs de Positions, il faut les ressortir en deux fois : Une fois à l'endroit et une fois en permutant X et Y ( Symétrie : (X1,Y1),(Y1,X1),(X2,Y2),(Y2,X2),... )

Robot jouant parfaitement

Principe

Le robot doit pouvoir jouer contre un humain sans le laisser gagner.

Dans cette version, le robot laisse une chance au joueur de pouvoir gagner s'il se déplace tout le temps sur une position perdante.

Algorithme brut

from matplotlib import pyplot as plt
import random as rnd

#longueur sert de taille de grille de jeu (ex: grille 10x10 => longueur = 10)
        

#Vérifie si le pion n'est pas directement aligné à (0,0) faisant gagner systématiquement le joueur 1 sans jeu.
def conditionPion(xPos,yPos):
    if(xPos == 0 or yPos == 0):
        return True
    elif(xPos == yPos):
        return True
    else:
        return False
def Pion(longueur):
    xposPion = rnd.randint(0, longueur)
    yposPion = rnd.randint(0, longueur)
    return (xposPion,yposPion)
def grille(longueur,On):
    ## DESSIN DE LA GRILLE ##
    valeursX = [0,longueur,longueur]
    valeursY = [longueur,longueur,0]
    
    for i in range(longueur-1):
        valeursX += [longueur-1-i]*3
        valeursY += [0,longueur,0]
    valeursX += [0]
    valeursY += [0]
    
    for i in range(longueur-1):
        valeursX += [0,longueur,0]
        valeursY += [longueur-1-i]*3
    valeursX += [0]
    valeursY += [longueur]

    plt.plot(valeursX,valeursY)
    plt.plot(0,0,'ro')
    #########################

    ## AFFICHAGE DES POSITIONS GAGNANTES ##
    if(On):
        positionsGagnantes = posGagnantes(longueur)
        for i in range(len(positionsGagnantes)):
            plt.plot(positionsGagnantes[i][0],positionsGagnantes[i][1], 'yo')
    #######################################

    ## AJOUT DU PION ##
    xposPion,yposPion = Pion(longueur)
    while(conditionPion(xposPion,yposPion)):
        xposPion,yposPion = Pion(longueur)
    plt.plot(xposPion, yposPion, 'bo')
    ###################
    
    ## AFFICHAGE DE LA GRILLE ##
    plt.title('Jeu de Whytoff')
    plt.show()
    ############################

    return (xposPion,yposPion)
def Jouer(opt,longueur,On):
		
		## Récupération de la position des pions ##
    xposPion,yposPion = grille(longueur,On)
    Joueur = 0
    MoveAutorise = ["Gauche","Bas","Diagonale"]
    PositionX = xposPion
    PositionY = yposPion
		###########################################
		
		## Partie ##
    while(PositionX > 0 or PositionY > 0):
        print("Position du pion : (" + str(PositionX) + "," + str(PositionY) + ")")
        deplAutorise = ""
        for i in range(len(MoveAutorise)):
            deplAutorise += MoveAutorise[i]
            if(i != len(MoveAutorise)-1):
                deplAutorise += ", "
        
        typeMove=input("Déplacement dans quel sens ?\r\n*Déplacements autorisés : " + deplAutorise + "\r\n")
        while(typeMove not in MoveAutorise):
            typeMove=input("Mouvement non autorisé.\r\nDéplacement dans quel sens ?\r\n*Déplacements autorisés : " + deplAutorise + "\r\n")
				
				## Déplacement à gauche ##
        if(typeMove == "Gauche"):
            valMove=int(input("De combien voulez-vous vous déplacer à gauche ?\r\n*Vous pouvez vous déplacer de 1 à " + str(PositionX) + " cases.\r\n"))
            while(valMove < 0 or valMove > PositionX):
                valMove=int(input("Valeur incorrecte.\r\nDe combien voulez-vous vous déplacer à gauche ?\r\n*Vous pouvez vous déplacer de 1 à " + str(PositionX) + " cases.\r\n"))
            PositionX -= valMove
            
        ## Déplacement en bas ##
        elif(typeMove == "Bas"):
            valMove=int(input("De combien voulez-vous vous déplacer en bas ?\r\n*Vous pouvez vous déplacer de 1 à " + str(PositionY) + " cases.\r\n"))
            while(valMove < 0 or valMove > PositionY):
                valMove=int(input("Valeur incorrecte.\r\nDe combien voulez-vous vous déplacer en bas ?\r\n*Vous pouvez vous déplacer de 1 à " + str(PositionY) + " cases.\r\n"))
            PositionY -= valMove
        
				## Déplacement en diagonale ##
        else:
            valMove=int(input("De combien voulez-vous vous déplacer en diagonale ?\r\n*Vous pouvez vous déplacer de 1 à " + str(min(PositionX,PositionY)) + " cases.\r\n"))
            while(valMove < 0 or valMove > min(PositionX,PositionY)):
                valMove=int(input("Valeur incorrecte.\r\nDe combien voulez-vous vous déplacer en diagonale ?\r\n*Vous pouvez vous déplacer de 1 à " + str(min(PositionX,PositionY)) + " cases.\r\n"))
            PositionX -= valMove
            PositionY -= valMove
        
				## Déplacements autorisés ##
        if(PositionX != 0 or PositionY != 0):
            if(PositionX == 0):
                MoveAutorise.remove("Gauche")
                if("Diagonale" in MoveAutorise):
                    MoveAutorise.remove("Diagonale")
            if(PositionY == 0):
                MoveAutorise.remove("Bas")
                if("Diagonale" in MoveAutorise):
                    MoveAutorise.remove("Diagonale")
        
        print()
        print("Type de mouvement :", typeMove, "\r\nValeur de mouvement :", valMove)
        print()
				
				## Deux humains ##
        if(opt == 1):
            if(PositionX != 0 or PositionY != 0):
                print("Au tour de l'adversaire !")
                print()
                Joueur += 1
                Joueur = Joueur%2
            else:
                print("Fin de partie ! Le joueur " + str(Joueur+1) + "remporte la partie.")
				
				## Un humain et un Robot ##
        else:
            if([PositionX,PositionY] in posGagnantes(longueur)):
                typeMove = rnd.choice(MoveAutorise)
                
                if(typeMove == "Gauche"):
                    valMove = rnd.randint(1, PositionX)
                    print('Le Bot s\'est déplacé de ' + str(valMove) + ' vers la gauche.')
                    PositionX -= valMove

                elif(typeMove == "Bas"):
                    valMove = rnd.randint(1, PositionY)
                    print('Le Bot s\'est déplacé de ' + str(valMove) + ' vers le bas.')
                    PositionY -= valMove

                else:
                    valMove = rnd.randint(1, min(PositionX,PositionY))
                    print('Le Bot s\'est déplacé de ' + str(valMove) + ' en diagonale.')
                    PositionX -= valMove
                    PositionY -= valMove

            else:
                if not(PositionX == 0 and PositionY == 0):
                    if(PositionX == 0 or PositionY == 0 or PositionX == PositionY):
                        PositionX = 0
                        PositionY = 0
                        print("J'ai gagné en allant vers (0,0) !! Dommage D:")
                    else:
                        positionsGagnantes = posGagnantes(max(PositionX,PositionY))
                        print(posGagnantes(max(PositionX,PositionY)))
                        i=0
                        state = 0
                        if(positionsGagnantes != None):
                            while(i<len(positionsGagnantes)):
                                if(PositionY == positionsGagnantes[i][1] and PositionX > positionsGagnantes[i][0]):
                                    PositionX = positionsGagnantes[i][0]
                                    state = 1
                                    print("J'ai joué vers la gauche à la position (" + str(PositionX) + "," + str(PositionY) + ")")
                                    break
                                elif(PositionX == positionsGagnantes[i][0] and PositionY > positionsGagnantes[i][1]):
                                    PositionY = positionsGagnantes[i][1]
                                    state = 1
                                    print("J'ai joué vers le bas à la position (" + str(PositionX) + "," + str(PositionY) + ")")
                                    break
                                else:
                                    i += 1
                        if(state == 0):
                            while not([PositionX,PositionY] in positionsGagnantes and [PositionY,PositionX] in positionsGagnantes):
                                PositionX -= 1  
                                PositionY -= 1
                            print("J'ai joué en diagonale à la position (" + str(PositionX) + "," + str(PositionY) + ")")
                else:
                    print("Bravo pour avoir gagné contre moi :D")
        
				## Déplacements autorisés ##
        if(PositionX != 0 or PositionY != 0):
            if(PositionX == 0):
                MoveAutorise.remove("Gauche")
                if("Diagonale" in MoveAutorise):
                    MoveAutorise.remove("Diagonale")
            if(PositionY == 0):
                MoveAutorise.remove("Bas")
                if("Diagonale" in MoveAutorise):
                    MoveAutorise.remove("Diagonale")
#Calcul des positions gagnantes
def posGagnantes(longueur):
    positionsGagnantes = [] #Positions qui font que le joueur gagnera systématiquement en se déplaçant de l'une à l'autre
    nombresUtilises = [] #Coordonnée déjà utilisée dans l'une des positions gagnantes
    i = 1
    while(i < longueur): #Strictement inférieur car on ne peut pas avoir une position (longueur,longueur) gagnante.
        if i not in nombresUtilises:
            ordonne = i+(len(positionsGagnantes)//2)+1
            if(ordonne > longueur):
                return positionsGagnantes
            else:
                nombresUtilises += [i,ordonne]
                positionsGagnantes += [[i,ordonne]]
                positionsGagnantes += [[ordonne,i]]
                i = 1
        else:
            i+=1
    return positionsGagnantes

Valeurs de Sprague-Grundy appliquées au jeu de Wythoff

On va maintenant jouer avec 2 plateaux de 2 lignes (4 lignes) fonctionnant comme un jeu de Wythoff.

Wythoff 05.png

On pourra jouer que sur un seul des deux par tour.

Le gagnant est celui qui retire la toute dernière allumette ( Arrive en ((0,0)(0,0)) )

Principe

La valeur de Sprague-Grundy d'une case est la plus petite valeur qui n'apparait pas à gauche, en bas et en diagonale de cette case.

Exemple pour une grille de 9x9 :

Wythoff 06.png

Calcul

Pour les calculer automatiquement, il faut entrer dans 3 tableaux différents les nombres déjà présents sur les cases du bas, gauche et diagonale puis prendre la valeur entière la plus basse qui n'est dans aucune des listes.

On les calcule (et attribue) de gauche à droite et de bas en haut.

Exemple sur un tableau de 3x3 :

Wythoff 07.png

VISI201 CMI : visite de laboratoire (Retour à la page des projets)