Exercices autour des dames

Comment écrire une fonction qui retourne tous les pions qu’une dame peut prendre au jeu de dames.

Après avoir créé un damier, on créé deux fonctions :

  • une fonction qui retourne les dames et les pions

  • une fonction qui retourne les pions qu’une dame peut prendre

Il suffira d’appeler la fonction pour toutes les dames du jeu pour connaître tous les pions qu’un joueur peut prendre avec ses dames.

Dans un premier temps, il convient de représenter le damier. On choisira une matrice numpy qu’il faut remplir de valeur numérique :

  • 0: case vide

  • 1: pion blanc

  • 2: pion noir

  • 3: dame blanche

  • 4: dame noir

Les valeurs numériques sont toujours plus efficace que des chaînes de caractères. Elles prennent moins de place en mémoire et les opérations sont plus efficaces.

Partie I : sans les classes

[1]:
import numpy as np


def damier_exemple(n: int = 10):
    d = np.zeros(
        (n, n), dtype=int  # il est préférable d'avoir des entiers plutôt que des réels
    )
    d[0, 0] = 3
    d[4, 4] = 2
    d[4, 6] = 2
    return d


damier = damier_exemple()
print(damier)
[[3 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 2 0 2 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]]

On cherche maintenant toutes les dames. On retourne une liste de position. Il n’est pas utile de retourner ce que contient chaque case. On peut retrouver cette information avec le damier.

[2]:
def cherche_dames(damier) -> list[tuple[int, int]]:
    res = []
    for i in range(damier.shape[0]):
        for j in range(damier.shape[1]):
            if damier[i, j] > 0:
                res.append((i, j))
    return res


res = cherche_dames(damier)
res
[2]:
[(0, 0), (4, 4), (4, 6)]

Et maintenant, le plat principal, une fonction retourne les possibles prises pour une dame. L’idée est de regarder dans une direction, de chercher un pion ou une dame de couleur différente et une autre case derrière, et plus précisément, la dernière case où la dame peut se poser. La fonction retourne deux positions : [(i1, j1), (i2, j2)]. La première position est le pion ou la dame à prendre. La dame peut alors se poser dans l’intervalle entre ces deux positions, la première exlue.

Comme cet algorithme est le même quelle que soit la direction, nous allons créer deux fonctions, une pour traiter une direction, l’autre pour les quatre.

[3]:
def position_prise_direction(
    damier, position_dame: tuple[int, int], direction: tuple[int, int]
) -> list[tuple[int, int]]:
    assert damier[position_dame] >= 3, f"ce n'est pas une dame {damier[position_dame]}"
    couleur = damier[position_dame] % 2
    prise = None
    pose = None
    i, j = position_dame
    di, dj = direction
    i += di
    j += dj
    while i >= 0 and i < damier.shape[0] and j >= 0 and j < damier.shape[1]:
        case = damier[i, j]
        if prise is None:
            if case == 0:
                i += di
                j += dj
                continue
            if case % 2 == couleur:  # même couleur
                return None, None
            # sinon on prend
            prise = i, j
            i += di
            j += dj
            continue
        # si la prise a déjà eu lieu
        if case == 0:
            # on peut poser la dame
            pose = i, j
            i += di
            j += dj
            continue

        # sinon
        if prise is None:
            # pas de case libre derrière donc on ne peut pas prendre
            return None, None

        return prise, pose

    # La boucle est terminée sans passer par une instruction return?
    return prise, pose


prise, pose = position_prise_direction(damier, (0, 0), (1, 1))
print(f"prise={prise}, pose={pose}")
prise=(4, 4), pose=(9, 9)

Et la fonction suivante pour traiter les quatre directions :

[4]:
def position_prise(damier, position_dame: tuple[int, int]) -> list[tuple[int, int]]:
    res = []
    for di in [-1, 1]:
        for dj in [-1, 1]:
            prise, pose = position_prise_direction(damier, position_dame, (di, dj))
            if prise is None:
                continue
            res.append((prise, pose))
    return res


res = position_prise(damier, (0, 0))
print(res)
[((4, 4), (9, 9))]

Partie 2 : Avec les classes

Dans cette partie, on construit deux classes Coup et Damier avec pour objectif de déterminer si le fait de commencer est un avantage dans une partie où les deux joueurs jouent de façon aléatoire. Pour y répondre, il faut simuler un grand nombre de parties.

La classe Coup contient au moins deux positions, celle de départ et celle d’arrivée. Elle peut en contenir plus si le pion ou la dame peut en prendre plusieurs.

[5]:
class Coup:

    def __init__(self, positions: list[tuple[int, int]]):

        self.positions = positions

    def __len__(self) -> int:
        "Retourne le nombre de positions."
        return len(self.positions)

    def __str__(self) -> str:
        "Appelée implicitement par Python si print(c) où c est un Coup"
        return str(self.positions)

    def __getitem__(self, i):
        "Donne un sens à c[0] où c est de type Coup."
        return self.positions[i]


# Vérification rapide.
c = Coup([(0, 0), (3, 3)])
print(c)
[(0, 0), (3, 3)]

Maintenant le Damier:

[6]:
class Damier:

    def __init__(self, N: int = 10):

        self.damier = np.zeros((N, N), dtype=int)

    def __str__(self) -> str:
        "Appelée implicitement par Python si print(c) où c est un Coup"
        return str(self.damier)

    def __len__(self) -> int:
        "Retourne la dimension du damier."
        return len(self.damier)

    def init(self):
        "Initialise le damier pour un début de partie."
        N = len(self)
        for i in range(N):
            if i in ((N - 1) // 2, N // 2):
                continue
            c = 3 if i < N // 2 else 4
            for j in range(N):
                if (i + j) % 2 == 0:
                    self.damier[i, j] = c

    def joue(self, coup: Coup):
        "Joue un coup. On suppose que celui-ci est valide."
        for i in range(1, len(coup)):
            self.annule(coup[i - 1], coup[i])
        self.damier[coup[1]] = self.damier[coup[0]]
        self.damier[coup[0]] = 0

    def annule(self, p1: tuple[int, int], p2: tuple[int, int]):
        "Annule toutes les cases du damier entre deux positions."
        di = (p2[0] - p1[0]) // abs(p2[0] - p1[0])
        dj = (p2[1] - p1[1]) // abs(p2[1] - p1[1])
        for k in range(1, abs(p2[0] - p1[0])):
            self.damier[p1[0] + di * k, p1[1] + dj * k] = 0


d = Damier()
d.init()
print(d)  # équivalent à print(d.__str__())
[[3 0 3 0 3 0 3 0 3 0]
 [0 3 0 3 0 3 0 3 0 3]
 [3 0 3 0 3 0 3 0 3 0]
 [0 3 0 3 0 3 0 3 0 3]
 [0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]
 [4 0 4 0 4 0 4 0 4 0]
 [0 4 0 4 0 4 0 4 0 4]
 [4 0 4 0 4 0 4 0 4 0]
 [0 4 0 4 0 4 0 4 0 4]]

On écrit un test unitaire pour vérifier que la méthode init est valide.

[7]:
def test_init():
    d = Damier(4)
    d.init()
    assert d.damier.tolist() == [
        [3, 0, 3, 0],
        [0, 0, 0, 0],
        [0, 0, 0, 0],
        [0, 4, 0, 4],
    ], f"{d.damier.tolist()}"


test_init()

On fait de même pour la méthode joue.

[8]:
def test_coup():
    d = Damier(4)
    d.init()
    d.joue(Coup([(0, 0), (1, 1)]))
    assert d.damier.tolist() == [
        [0, 0, 3, 0],
        [0, 3, 0, 0],
        [0, 0, 0, 0],
        [0, 4, 0, 4],
    ], f"{d.damier.tolist()}"


test_coup()

Notebook on github