Analyse mathématique du jeu alquerkonane

lundi 6 mars 2023
par  Alain BUSSER , Sébastien HOARAU

Le jeu alquerkonane est un jeu en apparence simple (la règle du jeu s’apprend en moins d’une minute, une partie dure quelques minutes) mais dont l’analyse mathématique peut s’avérer surprenamment complexe.

Licence Creative Commons
Cet article est mis à disposition selon les termes de la Licence Creative Commons Attribution - Partage dans les Mêmes Conditions 2.0 France.

Présentation au séminaire IREMI

Le 30 novembre 2022, cette présentation a été faite au séminaire IREMI :

On y a évoqué les points suivants :

  • des entiers relatifs, construits à la Conway, apparaissent dans les fins de parties, parfois comme sommes d’entiers relatifs plus simples,
  • la finitude du jeu est prouvable grâce à l’existence d’un variant : le nombre de degrés de liberté des pions, plus le triple du nombre de pions,
  • en changeant certains signes dans le variant, on obtient une expression qui n’est plus un variant mais qui peut servir d’heuristique pour un algorithme minimax voire une illustration de l’élagage alpha-bêta...

La contribution de Julien Lemoine

Julien Lemoine, qui a déjà publié d’autres analyses de jeux, a programmé un algorithme de recherche de stratégie gagnante pour l’ancienne version d’alquerkonane (où on avait le droit de reculer un pion).

Le script

Voici son script :

# (C) 2022 julien.lemoine@gmail.com  (WTFPL)
# Résolution forte d'Alquerkonane 3x3
# Le plateau est supposé non orienté :
# les pions peuvent avancer dans n'importe quel sens


# Une position est une chaîne de caractères décrivant l'état du plateau, 
# du type 'NXO·OXOX··' où le premier caractère indique le joueur dont c'est le tour (N noir ou B blanc),
# les X sont les pions noirs et les O les pions blancs, · pour une case vide

# Numérotation des cases du plateau :
# 123
# 456
# 789
# Noir a ses pions sur des cases impaires et blanc sur des cases paires

# décrit les cases qu'on peut atteindre en un coup sans prise
dico_voisins = {1: [5], 3: [5], 5: [1, 3, 7, 9], 7: [5], 9: [5], 2: [4, 6], 4: [2, 8], 6: [2, 8], 8: [4, 6]}

# décrit les prises possibles par des couples : (case du pion pris, case d'arrivée)
dico_sauts = {1: [(2, 3), (4, 7)], 3:[(2, 1), (6, 9)], 5: [], 7: [(4, 1), (8, 9)], 9: [(8, 7), (6, 3)], 2: [(5, 8)], 4: [(5, 6)], 6: [(5, 4)], 8: [(5, 2)]}

# dictionnaire pour stocker les positions du jeu
# la clé est la position, la valeur est le résultat de la position :
# n > 0 signifie que Noir peut gagner en n coups
# n < 0 signifie que Blanc peut gagner en -n coups
# n = 0 signifie que le résultat n'a pas encore été calculé, ou que c'est Nul
dico_pos = {}


#####
# Fonctions de canonisation des positions (pour identifier les positions équivalentes)
#####

def rotation(position):
    '''
    effectue une rotation du plateau d'un quart de tour vers la droite
    et renvoie la chaine de caracteres correspondante
    '''
    return position[0] + position[7] + position[4] + position[1] + position[8] + position[5] + position[2] + position[9] + position[6] + position[3]


def symetrie(position):
    '''
    effectue une symétrie axiale d'axe vertical du plateau
    et renvoie la chaine de caracteres correspondante
    '''
    return position[0] + position[3] + position[2] + position[1] + position[6] + position[5] + position[4] + position[9] + position[8] + position[7]


def isometries(position):
    '''
    renvoie la liste des 8 positions obtenues à partir de la position
    en appliquant une symétrie ou une rotation
    '''
    liste_iso = [position]
    for i in range(3):
        liste_iso.append( rotation(liste_iso[-1]) )
    for i in range(4):
        liste_iso.append( symetrie(liste_iso[i]) )
    return liste_iso


def canonise(position):
    '''
    renvoie l'isométrie minimale de la position
    pour l'ordre lexicographique
    '''
    return min(isometries(position))


#####
# Création des positions du jeu
#####

def dec_to_bin(n):
    '''
    renvoie (dans l'ordre inverse) les 9 premiers bits de la représentation binaire de l'entier n (en base 10)
    '''
    resultat = ''
    reste = n
    for i in range(9):
        resultat += str(reste % 2)
        reste = reste // 2
    return resultat

assert dec_to_bin(19) == '110010000'


# Remplissage du dictionnaire des positions
for couleur in ['N', 'B']:
    # il y a 2**9 façons d'occuper ou non les 9 cases
    for n in range(2**9):
        # création de la position
        position = couleur
        mot_binaire = dec_to_bin(n)
        for i in range(9):
            if mot_binaire[i] == '1':
                if i % 2 == 0:
                    position += 'X'
                else:
                    position += 'O'
            else:
                position += '·'
        # après canonisation, on ne stocke que les positions non terminales
        # et pour lesquelles aucun joueur n'a toutes ses cases occupées (sinon il serait bloqué)
        if 1 <= position.count('X') <= 4 and 1 <= position.count('O') <= 3:
            position = canonise(position)
            if position not in dico_pos:
                dico_pos[position] = 0  # au départ, on ne connaît pas le résultat de la position


#####
# Calcul des fils (canonisés) d'une position
#####
    
def fils(position):
    '''
    renvoie la liste des positions (canonisées : à isométrie près)
    obtenues en un coup à partir de la position
    '''
    if position[0] == 'N':
        nv_tour = 'B'
    else:
        nv_tour = 'N'
    
    liste_fils = []
    
    ### coups sans prise ###
    if position[0] == 'N':
        départ = 1
        pion = 'X'
    else:
        départ = 2
        pion = 'O'
    # pour chaque case de la bonne couleur
    for case in range(départ, 10, 2):
        # s'il y a un pion sur la case
        if position[case] == pion:
            # pour chaque case voisine
            for voisin in dico_voisins[case]:
                # si la case voisine est libre
                if position[voisin] == '·':
                    # on fabrique la nouvelle position
                    nv_pos = nv_tour
                    for i in range(1, 10):
                        if i == case:       # la case se vide
                            nv_pos += '·'
                        elif i == voisin:   # la case voisine se remplit
                            nv_pos += pion
                        else:               # les autres cases ne changent pas
                            nv_pos += position[i]
                    # on canonise la position et on l'ajoute si elle est nouvelle
                    nv_pos = canonise( nv_pos )
                    if nv_pos not in liste_fils:
                        liste_fils.append( nv_pos )

    ### coups avec prise ###
    if position[0] == 'N':
        départ = 1
        pion = 'X'
        pion_opp = 'O'
    else:
        départ = 2
        pion = 'O'
        pion_opp = 'X'
    # pour chaque case de la bonne couleur
    for case in range(départ, 10, 2):
        # s'il y a un pion sur la case
        if position[case] == pion:
            # pour chaque saut envisageable
            for saut in dico_sauts[case]:
                # si la case voisine contient un pion de l'opposant, et la case d'arrivée est vide
                if position[saut[0]] == pion_opp and position[saut[1]] == '·':
                    # on fabrique la nouvelle position
                    nv_pos = nv_tour
                    for i in range(1, 10):
                        if i == case:       # la case se vide
                            nv_pos += '·'
                        elif i == saut[0]:  # la case voisine se vide
                            nv_pos += '·'
                        elif i == saut[1]:  # la case d'après se remplit
                            nv_pos += pion
                        else:               # les autres cases ne changent pas
                            nv_pos += position[i]
                    # on canonise la position et on l'ajoute si elle est nouvelle
                    nv_pos = canonise( nv_pos )
                    if nv_pos not in liste_fils:
                        liste_fils.append( nv_pos )

    return liste_fils


#####
# Calcul des résultats des positions du jeu
#####

# initialisation : on calcule le résultat des positions pour lesquelles on peut gagner au coup suivant
for position in dico_pos:
    for f in fils(position):
        # si c'est le tour de Noir et qu'il existe un fils sans pion blanc
        if position[0] == 'N' and f.count('O') == 0:
            # Noir gagne au tour suivant
            dico_pos[position] = 1
        # si c'est le tour de Blanc et qu'il existe un fils sans pion noir
        elif position[0] == 'B' and f.count('X') == 0:
            # Blanc gagne au tour suivant
            dico_pos[position] = -1


# détermination de proche en proche des positions gagnantes pour le joueur Noir
changement = True
prochain_nb = 2

# on procède par passes où on regarde toutes les positions du jeu
# si, pour une passe, on ne trouve pas de nouvelle position gagnante,
# c'est qu'on les a déjà toutes trouvées, on peut alors arrêter
while changement:
    changement = False

    if prochain_nb % 2 == 0:
    # c'est au tour de Blanc
        for position in dico_pos:
            # on s'intéresse aux positions de résultat inconnu, où c'est à Blanc de jouer
            if dico_pos[position] == 0 and position[0] == 'B':
                # ***si tous les fils*** conduisent à une victoire de Noir, alors Noir est gagnant
                tous_fils_N = True
                for f in fils(position):
                    if dico_pos[f] <= 0:
                        tous_fils_N = False
                        break
                if tous_fils_N:
                    dico_pos[position] = prochain_nb
                    changement = True
    else:
    # c'est au tour de Noir
        for position in dico_pos:
            # on s'intéresse aux positions de résultat inconnu, où c'est à Noir de jouer
            if dico_pos[position] == 0 and position[0] == 'N':
                for f in fils(position):
                    # ***s'il existe un fils*** qui conduit à une victoire de Noir, alors Noir est gagnant
                    if dico_pos[f] == prochain_nb - 1:
                        dico_pos[position] = prochain_nb
                        changement = True
                        break
          
    prochain_nb += 1


# détermination de proche en proche des positions gagnantes pour le joueur Blanc
# (même code que précédemment, non factorisé par souci de clarté du code)
changement = True
prochain_nb = -2

while changement:
    changement = False

    if prochain_nb % 2 == 0:
        for position in dico_pos:
            if dico_pos[position] == 0 and position[0] == 'N':
                tous_fils_B = True
                for f in fils(position):
                    if dico_pos[f] >= 0:
                        tous_fils_B = False
                        break
                if tous_fils_B:
                    dico_pos[position] = prochain_nb
                    changement = True
    else:
        for position in dico_pos:
            if dico_pos[position] == 0 and position[0] == 'B':
                for f in fils(position):
                    if dico_pos[f] == prochain_nb + 1:
                        dico_pos[position] = prochain_nb
                        changement = True
                        break
          
    prochain_nb -= 1


#####
# Export au format pour Graphviz
#####

hexa = '0123456789abcdef'


def export_noeud(position):
    '''
    renvoie une chaîne de caractères au format Graphviz
    décrivant le noeud correspondant à la position
    '''
    # identifiant du noeud
    chaine = position
    
    # contenu et forme du noeud
    chaine += ' [label=\"' + position[1:4] + '\\n' + position[4:7] + '\\n' + position[7:] + '\", shape=square, '
    
    # si le noeud est gagnant pour le joueur Noir, le noeud est d'autant plus rouge
    # que le nombre de coups nécessaire pour gagner est important
    if dico_pos[position] > 0:
        chaine += 'style=filled, fillcolor="#ff' + hexa[ 12 - dico_pos[position] ] * 4 +'", '
    # idem avec le joueur Blanc (noeud bleu)
    elif dico_pos[position] < 0:
        chaine += 'style=filled, fillcolor="#' + hexa[ 12 + dico_pos[position] ] * 4 + 'ff", '

    # si c'est le tour de Noir, le tour du noeud est noir et épais
    if position[0] == 'N':
        chaine += 'penwidth=8];'
    # si c'est le tour de Blanc, le tour du noeud est composé de deux traits fins
    else:
        chaine += 'peripheries=2];'

    return chaine


def export_aretes(position):
    '''
    renvoie une chaîne de caractères décrivant les arêtes
    entre un noeud et ses noeuds fils au format Graphviz
    '''
    # suppression des fils terminaux
    liste_fils = []
    for f in fils(position):
        if f.count('X') > 0 and f.count('O') > 0:
            liste_fils.append(f)

    # création de la chaîne à renvoyer
    chaine = ''
    for f in liste_fils:
        chaine += position + ' -> ' + f + ';\n'
    return chaine


def export_fichier_graphviz():
    '''
    renvoie une chaîne de caractères correspondant à un fichier Graphviz
    décrivant l'arbre de jeu d'Alquerkonane 3x3
    '''
    fichier = 'digraph G {\n'
    fichier += 'graph [fontname = \"courier\"];\n'
    fichier += 'node [fontname = \"courier\"];\n'
    fichier += 'edge [fontname = \"courier\"];\n\n'
    for position in dico_pos:
        fichier += export_noeud(position) + '\n'
        fichier += export_aretes(position) + '\n'
    fichier += '}'
    return fichier


print(export_fichier_graphviz())


#####
# Tests
#####

"""
p1 = 'NXO··XOXO·'
print(p1)

print('---test fils---')
print(fils(p1))

print('---test isométries---')
print(isometries(p1))

print('---position canonisée---')
p1 = canonise(p1)
print(p1)

#décommenter pour connaître le résultat des positions
print('---dico---')
for position in dico_pos:
    print(position, dico_pos[position])
print('nombre de positions :', len(dico_pos))

print('---export Graphviz---')
print(export_noeud(p1))
print(export_aretes(p1))
"""

Le script crée un graphe au format graphviz des positions du jeu (sommets du graphe) et de la règle du jeu (arêtes du graphe orienté). Si c’est aux noirs de jouer, le damier est encerclé de noir, si c’est aux blancs de jouer, le damier est encerclé de blanc. Les pions noirs sont représentés par des « X » et les pions blancs sont représentés par des « O ».

Si la position est gagnante pour les noirs, elle est coloriée en rouge (d’autant plus foncé qu’il faut du temps pour gagner) et si elle est gagnante pour les blancs, elle est coloriée en bleu (clair parce que dans ce cas les blancs gagnent vite).

Le graphe

Le rendu du graphe, effectué par graphviz (et plus précisément la commande dot), est dans ce pdf :

Le graphe est grand parce qu’il contient toutes les positions possibles, sans tenir compte de celle de départ. Il n’est pas si grand que cela parce que tous les sommets identiques à rotation ou symétrie près, sont représentés par un seul d’entre eux.

En cherchant la position de départ NXO··XOXO· (en haut au milieu) on trouve que les noirs ont une stratégie gagnante puisque le sommet est rouge :

En cherchant la position de départ BXO··XOXO· par contre on découvre que si les blancs commencent, ils peuvent arriver à une partie nulle (un sommet blanc fait partie d’un cycle ou peut y mener) :

C’est ce constat qui a mené à la restriction des mouvements des pions : comme au jeu de dames, les pions n’ont le droit de reculer que pour prendre.

La contribution de Fabrice Nativel

Elle est ici. On y apprend que

  • le jeu alquerkonane 3×3 est positif
  • le jeu alquerkonane 4×4 n’est pas un nombre
  • le jeu alquerkonane 4×4 à une seule rangée de pions par joueur, est égal à zéro
  • le jeu alquerkonane 5×5 est positif
  • le jeu alquerkonane 6×6 à une rangée de pions, n’est pas un nombre.

Durée des calculs

Lorsqu’on teste une position de départ, avant l’affichage de celle-ci dans le GUI, on a l’affichage de la durée du calcul dans la console. Pour estimer le temps (extrêmement long) que cela prendrait pour calculer le jeu alquerkonane 7×7 à une rangée de pions par joueur, on commence par mesurer les temps de calcul obtenus pour des tailles de damiers plus petites. Les mesures, effectuées sur un Raspberry Pi 3, donnent ceci :

taille,temps
1,8.010400051716715e-05
2,0.00046687400026712567
3,0.0009988510000766837
4,0.09224246400026459
5,3.8309030609998445
6,313.57927399100026

Remarquer le temps mis à faire le calcul pour un damier 6×6. On constate qu’une ligne de descripteurs a été ajoutée. Cela permet d’ouvrir le fichier avec le module pandas de Python. Le fichier s’appelant temps1ligne.csv, on utilise le script Python suivant pour effectuer une régression :

import os
import pandas as pd
import numpy as np
import datetime as dt

chemin = '~/documents'
fichier_csv = os.path.join(chemin,'temps1ligne.csv')
df = pd.read_csv(fichier_csv)
dnp = (df.iloc[:]).to_numpy()
x_train = dnp[:,0]
y_train = dnp[:,1]

fit = np.polyfit(x_train, np.log(y_train), 3)
print(fit)

def predict(x):
    return np.exp(fit[0]*x**3+fit[1]*x**2+fit[2]*x+fit[3])


print(predict(x_train))
print(y_train)
print(7,dt.timedelta(seconds=int(predict(7))))

À la ligne 8, pandas crée une data frame à partir des données csv. Il s’agit d’un tenseur dont les colonnes sont des couches. Ces couches sont extraites de la data frame aux lignes 9 à 11, pour les convertir en listes de numpy. La liste des tailles x des damiers (première colonne du tableau csv) donne les abscisses d’entraînement, et la liste des durées y donne les ordonnées d’entraînement.

L’entraînement consiste à chercher un modèle de régression à partir des données d’entraînement. Il s’agit donc d’un entraînement supervisé. On cherche un polynôme, non pas sur la liste des y, mais sur celle de leurs logarithmes (la complexité semblant exponentielle et peut-être même plus qu’exponentielle). Et on se limite au degré 3.

Le script prédit que pour un damier de taille n×n, il faut e-0,04897183 n3 + 0,9619658 n2 -1,61632623 n -8.55036353 secondes. Pour un damier de taille 7×7 cela ferait 9 h 49 min 55 sec (et rien que pour une seule rangée de pions !).

Conjecture : la recherche d’une stratégie gagnante à alquerkonane sur un damier n×n est un problème PSPACE-complet.

Parcours de graphes

Chercher une stratégie gagnante à alquerkonane, revient à chercher un chemin allant de la position de départ jusqu’à une position gagnante. En fait plusieurs chemins sont possibles, et ils forment un arbre couvrant d’une partie du graphe. Pour chercher cet arbre, on peut opter pour un parcours en profondeur d’abord, ou un parcours en largeur d’abord.

En profondeur d’abord

Pour un parcours en profondeur d’abord, les sommets à étudier sont rangés dans une pile (informatique). La hauteur de cette pile évolue de manière assez chaotique au cours du temps :

Mais elle reste de dimension raisonnable (il n’y a jamais plus de 50 sommets à visiter). La hauteur de la pile varie tout de même 33200 fois.

En largeur d’abord

Pour effectuer un parcours du graphe en largeur d’abord, on utilise une file (structure de données). Celle-ci évolue de manière moins chaotique que la pile vue plus haut, mais la taille de la file atteint des valeurs impressionnantes (plus de 18000 sommets à étudier, à certains moments) :

En plus, le nombre de modifications de la file est plus important que pour la pile : environ 5 fois plus, d’après le graphique ci-dessus.

Théorie de Conway

La modélisation par les surréels de Conway permet d’en savoir plus. En particulier, le fait que le jeu alquerkonane 3×3 est positif, ne signifie pas nécessairement que ce soit un nombre positif (des jeux non numériques peuvent également être positifs). Mais on découvre qu’il s’agit du nombre 1 en l’occurrence, ce qui signifie que les noirs peuvent se garantir un minimum d’un mouvement d’avance sur les blancs.

alquerkonane 2×2 vaut 0 alquerkonane 3×3 vaut 1 la fraction -1/2 dans alquerkonane stratégie gagnante 3×3

On note ci-dessus qu’à alquerkonane 3×3, si les noirs jouent mal, les blancs peuvent avoir un demi-coup d’avance sur les noirs ! Pour en savoir plus sur le formalisme de Conway, voir la carte des cartes.

Conjectures

  1. La valeur initiale de alquerkonane 4×4 est le jeu que Conway notait ±2 (celui qui commence assure au moins 2 points d’avance sur son adversaire).
  2. Le jeu alquerkonane sur un damier n×n est PSPACE-difficile.

IA avec ludii

Le logiciel AlphaGo Zero, comme son nom l’indique, est spécialisé dans le jeu de Go. Il a été généralisé en AlphaZero qui gagne aussi aux échecs, et pas seulement au Go. Ces logiciels utilisent des méthodes d’IA qui sont implémentées dans ludii. Ce logiciel, programmé en Java (langage), permet de décrire n’importe quel jeu abstrait connu, à l’aide d’un langage de type Lisp. Par exemple pour y programmer alquerkonane avec des oiseaux et des poissons (les cases claires sont de l’eau ; les oiseaux ne peuvent y aller sinon ils se noient, et les poissons ne peuvent sortir de l’eau que pour sauter par dessus un oiseau ; de même les oiseaux peuvent piétiner les poissons pour aller d’une case noire à une autre orthogonalement), on peut écrire ce script :

(game "alquerkonane"
    ("TwoPlayersNorthSouth")
    (equipment
        {
         (board (rectangle <Rows:num> <Columns:num>)) 
            (piece "Fish"
                P1
                (or
                    (move Step (directions { BL BR }) (to if:(is Empty (to))))
                    (move
                        Hop
                        (between
                            if:(is Enemy (who at:(between)))
                            (apply (remove (between)))
                        )
                        (to if:(is Empty (to)))
                    )
                )
            )
            (piece "Goose"
                P2
                (or
                    (move Step (directions { BL BR }) (to if:(is Empty (to))))
                    (move
                        Hop
                        (between
                            if:(is Enemy (who at:(between)))
                            (apply (remove (between)))
                        )
                        (to if:(is Empty (to)))
                    )
                )
            )
        }
    )
    (rules
        (start
            {
                (place
                    "Goose2"
                    (intersection
                        (sites Phase 0)
                        (expand (sites Bottom) steps:1)
                    )
                )
                (place
                    "Fish1"
                    (intersection
                        (sites Phase 1)
                        (expand (sites Top) steps:1)
                    )
                )
            }
        )
        (play (forEach Piece))
        (end (if (no Moves Next) (result Mover Win)))
    )
)

(option "Rows" <Rows> args:{ <num> }
    {
    (item "2" <2> "The board has 2 rows.") 
    (item "3" <3> "The board has 3 rows.") 
    (item "4" <4> "The board has 4 rows.")* 
    (item "5" <5> "The board has 5 rows.") 
    (item "6" <6> "The board has 6 rows.")** 
    (item "7" <7> "The board has 7 rows.") 
    (item "8" <8> "The board has 8 rows.")* 
    (item "9" <9> "The board has 9 rows.") 
    (item "10" <10> "The board has 10 rows.") 
    }
)
(option "Columns" <Columns> args:{ <num> }
    {
    (item "2" <2> "The board has 2 columns.") 
    (item "3" <3> "The board has 3 columns.") 
    (item "4" <4> "The board has 4 columns.")** 
    (item "5" <5> "The board has 5 columns.") 
    (item "6" <6> "The board has 6 columns.")* 
    (item "7" <7> "The board has 7 columns.") 
    (item "8" <8> "The board has 8 columns.")* 
    (item "9" <9> "The board has 9 columns.") 
    (item "10" <10> "The board has 10 columns.") 
    }
)


(metadata
    (graphics (board  Style Chess))
)

Dans le logiciel ludii, ce script donne le rendu suivant :

Parmi les IA de ludii, l’élagage alpha-bêta est bien adapté à des jeux de stratégie comme alquerkonane. Mais ci-dessus on lui a préféré l’IA d’AlphaZero (son nom est UCT, c’est une amélioration de MCST qui signifie recherche arborescente Monte-Carlo) qui est plus recommandée pour la tactique (comme aux échecs). En effet alphaBeta ne joue pas de façon optimale : cette IA préfére gagner vite plutôt que gagner beaucoup. Tout le problème est de trouver une fonction d’évaluation.

On propose alors cet enrichissement du jeu : chaque joueur dispose, en plus de ses pions, de jetons, qui lui serviront durant la phase finale du jeu. Le perdant paye le droit de passer son tour en donnant un jeton au gagnant, et ce, jusqu’à ce que le gagnant ne puisse plus bouger non plus. Le nombre de fois que le perdant a passé son tour, est le score du gagnant. Alors on ne cherche plus seulement à bouger plus longtemps que l’adversaire, mais à maximiser le nombre de mouvements.

On utilise la notion de score d’un joueur :

(game "alquerkonane"
    ("TwoPlayersNorthSouth")
    (equipment
        {
         (board (rectangle <Rows:num> <Columns:num>))
            (piece "Ball" Each
                (or
                    (move Step (directions { FL FR }) (to if:(is Empty (to))))
                    (move
                        Hop
                        (between
                            if:(is Enemy (who at:(between)))
                            (apply (remove (between)))
                        )
                        (to if:(is Empty (to)))
                    )
                )
            )
       }
    )
    (rules
        (start
            {
                (set Score Each 0)
                (place
                    "Ball2"
                    (intersection
                        (sites Phase 0)
                        (expand (sites Top) steps:1)
                    )
                )
                (place
                    "Ball1"
                    (intersection
                        (sites Phase 1)
                        (expand (sites Bottom) steps:1)
                    )
                )
            }
        )
        (play
            (priority
                (forEach Piece)
                (move Pass (then
                    (if (= (score Mover) 0) (addScore Next 1))
                ))
            )
        )
        (end (if (all Passed) (byScore)))
    )
)

(option "Rows" <Rows> args:{ <num> }
    {
    (item "2" <2> "The board has 2 rows.")
    (item "3" <3> "The board has 3 rows.")
    (item "4" <4> "The board has 4 rows.")*
    (item "5" <5> "The board has 5 rows.")
    (item "6" <6> "The board has 6 rows.")**
    (item "7" <7> "The board has 7 rows.")
    (item "8" <8> "The board has 8 rows.")*
    (item "9" <9> "The board has 9 rows.")
    (item "10" <10> "The board has 10 rows.")
    }
)
(option "Columns" <Columns> args:{ <num> }
    {
    (item "2" <2> "The board has 2 columns.")
    (item "3" <3> "The board has 3 columns.")
    (item "4" <4> "The board has 4 columns.")**
    (item "5" <5> "The board has 5 columns.")
    (item "6" <6> "The board has 6 columns.")*
    (item "7" <7> "The board has 7 columns.")
    (item "8" <8> "The board has 8 columns.")*
    (item "9" <9> "The board has 9 columns.")
    (item "10" <10> "The board has 10 columns.")
    }
)

(metadata
    (graphics (board  Style Chess))
    (ai
        (alphaBeta
            (heuristics
                (score)
            )
        )
    )
)

Mais le score n’étant calculé qu’à la fin, l’ai alphaBeta ne l’utilise pas pour évaluer les positions. D’ailleurs elle joue assez mal à la fin :

  • les blancs font leur dernier mouvement :

Les petits points en rouge désignent les possibilités de jeu des noirs (il y a un onglet à cocher dans ludii pour activer cet affichage). Le pion noir qui est en B4 peut aller en A5 ou en C5, le pion noir qui est en C3 peut aller en D4 ou en C5, et dans ce dernier cas il prend le dernier pion blanc.

  • Les noirs prennent le pion blanc :

Ils peuvent alors marquer 4 points (le point du gagnant et les trois mouvements vers D6 puis vers A5 puis B6).

  • Ils commencent d’abord par la gauche, en amenant en A5 le pion qui était en B4 :

Les blancs ont passé leur tour et donné un point aux noirs (lisible en haut à droite)

  • Mais ils bougent le pion qui est en C5 vers la gauche (B6 au lieu de D6) :

Les blancs ne leur donnent donc qu’un second point. Les noirs se sont bloqués eux-mêmes parce que leur algorithme alphaBeta n’a pas daigné calculer le score.

Il faut donc une autre heuristique pour alphaBeta si on veut optimiser le score.

Malgré cela, l’élagage alpha-bêta joue bien et amène à conjecturer que la valeur du jeu sur un damier 4×4 est ±1 plutôt que ±2...


Documents joints

PDF - 110.3 kio

Portfolio

PNG - 40.6 kio

Commentaires