Mokona Guu Center

Test Driven Development : les pions valides

Publié le

La fois dernière, j'avais extrait une interface de validation des mouvements. Il est maintenant temps d'implémenter les restrictions de mouvements des différentes pièces du Doubutsu Shogi. Et ceci, toujours selon la méthode TDD, en Python.

Pour le moment, j'ai extrait une interface grâce à un Mock Object.

Pièce de Shôgi en bois

    class MovementValidator:
        def is_movement_valid(self, piece, source, destination):
            pass

Comme je suis sur le point d'y adjoindre des tests indépendants, je l'exporte dans son propre fichier (movementvalidator.py), je créé le fichier de tests correspondant et j'ajoute l'import maquant dans doubutsugame.py.

Je commence par tenter de valider un déplacement simple : le pion/poussin qui avance d'une case.

    class MovementValidatorTestCase(unittest.TestCase):
        def test_can_validate_a_pawn(self):
            validator = MovementValidator()
            self.assertTrue(validator.is_movement_valid(Piece(PAWN, P1), (1, 1), (1, 2)))

Qui pour passer ne nécessite qu'un simple : return True dans is_movement_valid(). Pour le moment, le test ne vérifie que le fait de pouvoir créer le système de validation.

J'ajoute tous les mouvements de 1 case invalides :

    def test_can_validate_a_pawn(self):
        validator = MovementValidator()
        self.assertTrue(validator.is_movement_valid(Piece(PAWN, P1), (1, 1), (1, 2)))

        # Stays still
        self.assertFalse(validator.is_movement_valid(Piece(PAWN, P1), (1, 1), (1, 1)))

        # Goes Backward
        self.assertFalse(validator.is_movement_valid(Piece(PAWN, P1), (1, 1), (0, 0)))
        self.assertFalse(validator.is_movement_valid(Piece(PAWN, P1), (1, 1), (1, 0)))
        self.assertFalse(validator.is_movement_valid(Piece(PAWN, P1), (1, 1), (2, 0)))

        # Goes forward diagonal
        self.assertFalse(validator.is_movement_valid(Piece(PAWN, P1), (1, 1), (0, 2)))
        self.assertFalse(validator.is_movement_valid(Piece(PAWN, P1), (1, 1), (2, 2)))

        # Moves side
        self.assertFalse(validator.is_movement_valid(Piece(PAWN, P1), (1, 1), (0, 1)))
        self.assertFalse(validator.is_movement_valid(Piece(PAWN, P1), (1, 1), (2, 1)))

        # Moves forward two spaces
        self.assertFalse(validator.is_movement_valid(Piece(PAWN, P1), (1, 1), (1, 3)))

Dont la résolution est simple. Le mouvement est valide si le x des coordonnées ne change pas et si le y n'est différent que d'un, dans la bonne direction.

    def is_movement_valid(self, piece, source, destination):
        return source[1] + 1 == destination[1] and source[0] == destination[0]

Et puisqu'on parle de direction, parlons du pion/poussin du joueur 2 :

    def test_can_validate_a_pawn_for_player_2(self):
        validator = MovementValidator()

        source = (1, 2)

        self.assertTrue(validator.is_movement_valid(Piece(PAWN, P2), (1, 2), (1, 1)))

        # Stays still
        self.assertFalse(validator.is_movement_valid(Piece(PAWN, P2), source, source))

        # Goes Backward
        self.assertFalse(validator.is_movement_valid(Piece(PAWN, P2), source, (0, 3)))
        self.assertFalse(validator.is_movement_valid(Piece(PAWN, P2), source, (1, 3)))
        self.assertFalse(validator.is_movement_valid(Piece(PAWN, P2), source, (2, 3)))

        # Goes forward diagonal
        self.assertFalse(validator.is_movement_valid(Piece(PAWN, P2), source, (0, 1)))
        self.assertFalse(validator.is_movement_valid(Piece(PAWN, P2), source, (2, 1)))

        # Moves side
        self.assertFalse(validator.is_movement_valid(Piece(PAWN, P2), source, (0, 2)))
        self.assertFalse(validator.is_movement_valid(Piece(PAWN, P2), source, (2, 2)))

        # Moves forward two spaces
        self.assertFalse(validator.is_movement_valid(Piece(PAWN, P2), source, (1, 0)))

Que je peux faire passer simplement comme ceci :

    def is_movement_valid(self, piece, source, destination):
        if piece.get_controller() == P1:
            return source[1] + 1 == destination[1] and source[0] == destination[0]
        return source[1] - 1 == destination[1] and source[0] == destination[0]

Et que je transforme vite en :

    from pieces import P1, P2

    forward_factor = {P1: 1, P2: -1}

    class MovementValidator:
        def is_movement_valid(self, piece, source, destination):
            controller = piece.get_controller()
            assert(controller in [P1, P2])

            return source[1] + (forward_factor[controller]) == destination[1] and source[0] == destination[0]

Cette version de la validation est en fait une progression en plusieurs étapes. Au début, le forward_factor était assigné dans in if/else en fonction de P1. Cependant, cela signifiait qu'une entrée éventuelle avec un P3 (qui n'existe pas actuellement) aurait rendu la fonction invalide. P3 aurait eu le même effet que P2.

J'ai donc décidé de fournir à la fonction de validation de mouvement un contrat de type 'précondition'. Ce contrat se traduit par l'utilisation d'un assert() qui enverra une exception si la condition en argument n'est pas respectée. Autrement dit, si le controller de la pièce n'est ni P1 ni P2, la fonction sortira brutalement par l'emission d'une exception.

De manière générale, le système construit jusqu'à maintenant ignore les fausses manipulations. Cependant, ici, il ne s'agit pas d'une erreur d'entrée, mais d'une fonction qui n'a pas de résultat possible : elle ne peut ni valider ni invalider la résultat pour certaines valeurs qui sortent de son domaine de définition.

Note : pour autant que je sache, des décorateurs de fonctions de type precondition / postcondition n'existent pas dans la bibliothèque actuellement (jusqu'à Python 3.3). Ce qui ne vous empêche pas d'en implémenter ou d'utiliser des exemples trouvés sur le net.

Tout ça est quand même affreusement dupliqué. Tout d'abord, le validator devient un self.validator créé dans le setUp().

Je créé alors un objet pour m'aider à dé-dupliquer. Je ne le teste pas, il est testé par l'utilisation que j'en fais dans MovementValidatorTestCase.

    class MovementAssert:
        def __init__(self, testcase, piece, position):
            self.testcase = testcase
            self.piece = piece
            self.position = position

        def _get_validation(self, displacement):
            destination = (self.position[0] + displacement[0], self.position[1] + displacement[1])
            return self.testcase.validator.is_movement_valid(self.piece, self.position, destination)

        def can_move(self, displacement):
            self.testcase.assertTrue(self._get_validation(displacement))

        def cannot_move(self, displacement):
            self.testcase.assertFalse(self._get_validation(displacement))

Cela me permet d'alléger dans un premier temps l'écriture des tests de mouvements. Voici le début d'un test réécrit :

    def test_can_validate_a_pawn_for_player_1(self):
        asserter = MovementAssert(self, Piece(PAWN, P1), (1, 1))

        asserter.can_move((0, 1)) # Move forward
        asserter.cannot_move((0, 0)) # Stays still

Je réécris tous mes tests sur ce principe. Ça reste encore pas mal dupliqué au niveau des appels de l'asserter. Forcément, cela donne envie de donner une liste de mouvements valides et de mouvements invalides puis de les parcourir avec une boucle.

Puisque cette même boucle est identique dans les deux tests, au final, voilà ce que cela donne :

    import unittest
    from movementvalidator import MovementValidator
    from pieces import Piece, PAWN, P1, P2

    class MovementAssert:
        def __init__(self, testcase, piece, position):
            self.testcase = testcase
            self.piece = piece
            self.position = position

        def _get_validation(self, displacement):
            destination = (self.position[0] + displacement[0], self.position[1] + displacement[1])
            return self.testcase.validator.is_movement_valid(self.piece, self.position, destination)

        def can_move(self, displacement):
            self.testcase.assertTrue(self._get_validation(displacement))

        def cannot_move(self, displacement):
            self.testcase.assertFalse(self._get_validation(displacement))

        def move_validation(self, list_of_allowed_moves, list_of_forbidden_moves):
            for move in list_of_allowed_moves:
                self.can_move(move)

            for move in list_of_forbidden_moves:
                self.cannot_move(move)

    class MovementValidatorTestCase(unittest.TestCase):
        def setUp(self):
            self.validator = MovementValidator()

        def test_can_validate_a_pawn_for_player_1(self):
            asserter = MovementAssert(self, Piece(PAWN, P1), (1, 1))

            list_of_allowed_moves = [(0, 1)]
            list_of_forbidden_moves = [
                (0, 0),  # Stays still
                # Goes Backward
                (-1, -1),
                (0, -1),
                (1, -1),
                # Goes forward diagonal
                (-1, 1),
                (1, 1),
                # Moves side
                (-1, 0),
                (1, 0),
                # Moves forward two spaces
                (0, 2),
            ]

            asserter.move_validation(list_of_allowed_moves, list_of_forbidden_moves)

        def test_can_validate_a_pawn_for_player_2(self):
            asserter = MovementAssert(self, Piece(PAWN, P2), (1, 2))

            list_of_allowed_moves = [(0, -1)]
            list_of_forbidden_moves = [
                (0, 0),  # Stays still
                # Goes Backward
                (-1, 1),
                (0, 1),
                (1, 1),
                # Goes forward diagonal
                (-1, -1),
                (1, -1),
                # Moves side
                (-1, 0),
                (1, 0),
                # Moves forward two spaces
                (0, -2),
            ]

            asserter.move_validation(list_of_allowed_moves, list_of_forbidden_moves)

Évidemment, pour le moment, avec un seul type de pièce, la validation est limitée. Après le pion, je valide le Lion, qui est assez simple : il peut bouger d'une case dans chaque direction.

    def test_can_validate_a_lion_for_player_1(self):
        asserter = MovementAssert(self, Piece(LION, P1), (1, 0))

        list_of_allowed_moves = [
            (0, -1),
            (0, 1),
            (-1, -1),
            (-1, 0),
            (-1, 1),
            (1, -1),
            (1, 0),
            (1, 1)
        ]
        list_of_forbidden_moves = [
            (0, 0),  # Stays still
            # Moves more than one space
            (0, 2),
            (0, 3)
        ]

        asserter.move_validation(list_of_allowed_moves, list_of_forbidden_moves)

Comme j'écris beaucoup de mouvements différent d'un coup, je m'aide d'un message d'erreur de test un peu plus construit, afin de me guider sur les mouvements qui ne passent pas :

    class MovementAssert:
        def can_move(self, displacement):
            self.testcase.assertTrue(self._get_validation(displacement), msg="Should allow movement %s" % str(displacement))

        def cannot_move(self, displacement):
            self.testcase.assertFalse(self._get_validation(displacement), msg="Should forbid movement %s" % str(displacement))

Cela me permet d'avoir ce genre de message d'erreur :

    ======================================================================
    FAIL: test_can_validate_a_lion_for_player_1 (movementvalidator_tests.MovementValidatorTestCase)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
        File "movementvalidator_tests.py", line 96, in test_can_validate_a_lion_for_player_1
            asserter.move_validation(list_of_allowed_moves, list_of_forbidden_moves)
        File "movementvalidator_tests.py", line 26, in move_validation
            self.cannot_move(move)
        File "movementvalidator_tests.py", line 19, in cannot_move
            self.testcase.assertFalse(self._get_validation(displacement), msg="Should forbid movement %s" % str(displacement))
    AssertionError: Should forbid movement (0, 2)
    ----------------------------------------------------------------------

Bien plus pratique qu'un simple True is not False.

Une solution pour la validation peut être :

    class MovementValidator:
        def _is_lion_movement_valid(self, piece, source, destination):
            move_x = abs(destination[0] - source[0])
            move_y = abs(destination[1] - source[1])
            return move_x == 1 or move_y == 1

        def is_movement_valid(self, piece, source, destination):
            controller = piece.get_controller()
            assert(controller in [P1, P2])

            if piece == Piece(LION, P1):
                return self._is_lion_movement_valid(piece, source, destination)

            return source[1] + (forward_factor[controller]) == destination[1] and source[0] == destination[0]

Plusieurs observations :

  1. la validation du lion fonctionnerait pour le joueur 1 comme le joueur 2, cependant, je n'ai pas de moyen (autre que d'aller fouiller dans l'instance piece) de vérifier que la pièce est d'un type précis.
  2. en continuant comme ça, je vais avoir une série de branchements à base de if ou bien d'un tableau d'associations. Ça ne va pas être joli joli. Peu flexible.
  3. mon système de validation va être très couplé à mes pièces. Tout changement dans les pièces (si je veux implémenter une autre version de Shogi par exemple) va donc être coûteux en temps.
  4. j'ai déjà un fichier qui contient la définition des pieces.

Est-ce que ça vaudrait le coup d'ajouter à pieces.py la définition des mouvements légaux ?

La responsabilité du fichier est de décrire les pièces du jeu. Cependant, c'est aussi ajouter une responsabilité différente à celle déjà présente. Pour le moment, je ne fais que décrire les différents types de pièces. Y ajouter les mouvements possibles est une responsabilité en plus.

D'un autre côté, si j'ajoute un pieces_mouvements.py, je risque de créer un couplage fort entre pieces.py et ce nouveau fichier.

Je préfère quand même cette seconde solution, qui a le mérite de bien séparer les choses.

Cela réduit le validateur à ceci :

    from pieces import Piece, P1, P2, LION
    from pieces_movements import is_lion_movement_valid, is_pawn_movement_valid

    class MovementValidator:
        def is_movement_valid(self, piece, source, destination):
            controller = piece.get_controller()
            assert(controller in [P1, P2])

            if piece == Piece(LION, P1):
                return is_lion_movement_valid(piece, source, destination)

            return is_pawn_movement_valid(piece, source, destination)

Avec le contenu du nouveau fichier :

    from pieces import P1, P2

    forward_factor = {P1: 1, P2: -1}

    def is_lion_movement_valid(piece, source, destination):
        move_x = abs(destination[0] - source[0])
        move_y = abs(destination[1] - source[1])
        return move_x == 1 or move_y == 1

    def is_pawn_movement_valid(piece, source, destination):
        controller = piece.get_controller()
        return source[1] + (forward_factor[controller]) == destination[1] and source[0] == destination[0]

Il s'agit là d'un simple déplacement de code fait en phase de refactoring.

Je vois deux choses dans ce nouveau fichiers :

  1. je vais me retrouver avec une série de fonctions de même signature qui doivent répondre de la même manière
  2. chacune de ces fonctions est associée à un type de pièce particulier

Étant donné que chaque pièce est amenée à bouger, est-ce que ça ne vaudrait pas le coup d'associer la fonction de validation au type de pièce ? Cela simplifierait encore plus le code du validateur de mouvement en évitant par la même occasion toute une série de tests.

J'ai joué un moment avec l'idée d'ajouter la fonction de validation aux Piece. Le problème que je vois est que cela va alourdir la classe qui décrit une pièce. C'est exactement la même réflexion que juste précédemment.

D'un autre côté, ne pas le faire m'oblige à aller tester une information de type dans l'instance de piece, ce qui conservera la série de tests ou le passage par un tableau d'associations.

Ce que je choisi de faire est donc que le constructeur de Piece aille demander à un service une fonction de validation en fonction de son type de piece. Cette fonction sera ensuite disponible à l'accès.

J'ai besoin d'un patch

Le problème de tester une fonction appelée en dure dans un constructeur est que je ne passe pas de fonction à Piece qui pourrait être un MockObject. Pas d'injection de dépendance.

Heureusement, avec le framework mock de Python, il y a une possibilité de faire un patch local d'une fonction. Les appels à cette fonction dans le contexte du test seront interceptés par un MockObject que je pourrai alors interroger.

Dans des langages moins dynamiques, il serait probablement obligatoire de passer par un service intermédiaire qui fournirait la fonction dont j'ai besoin et dont les dépendances pourraient être changées, afin d'y injecter le MockObject nécessaire.

J'ajoute donc à pieces_tests un nouveau test :

    @mock.patch('pieces_movements.get_validator_from_piece_type')
    def test_piece_construction_calls_a_validation_service(self, mock_get):
        Piece(LION, P1)
        mock_get.assert_called_with(LION)

Note : c'est le décorateur patch qui s'occupe des importations de fonction. Il n'est donc pas besoin d'importer get_validator_from_piece_type, que l'on doit du coup spécifier dans le bon espace de nom. Notez aussi que la fonction est spécifiée en tant que chaîne de texte.

Note 2 : le MockObject créé par le décorateur est passé en paramètre supplémentaire à la fonction de tests.

Le test ne passe pas, bien entendu, puisque la fonction n'existe pas. Puis ne passe pas, une fois la fonction créée, puisqu'elle n'est pas appelée.

Mais si je tente de l'appeler, j'ai un problème. pieces_movements.py importe des symboles de pieces.py et inversement. Ce n'est pas possible. Chaque fichier essaye d'initialiser l'autre avant de continuer.

Cependant, l'import dans pieces_movements.py est from pieces import P1, P2. C'est à dire un import d'information de joueurs. Je me demande donc ce qu'elles font là (c'est historique) et je déplace dans les symboles P1 et P2 dans player.py.

Cela nécessite du remaniement. Je ne peux pas le faire avec un test qui n'est toujours pas passé. Je mets donc ce test en commentaires (en toute rigueur, il faudrait l'effacer, mais dans un cas comme cela où j'ai besoin de faire une petite modification, je me le permets. Attention cependant à vérifier à ce que des tests commentés ne soient pas présents lors du prochain commit sur le gestionnaire de sources).

Pendant ce remaniement, je fais bien attention de lancer régulièrement l'intégralité des tests.

Si vous avez un framework qui lance automatiquement vos tests à chaque modification, c'est ici qu'il prend tout son sens. Sinon, en ligne de commande :

    $ python -m unittest board_tests movementvalidator_tests player_tests tray_tests doubutsugame_tests pieces_tests session_tests
    ...........................................................
    ----------------------------------------------------------------------
    Ran 59 tests in 0.021s

    OK

En déplaçant les symboles vers player.py, le premier échec sera peut-être celui de pieces.py. Attention : from player import P1, P2 dans ce fichier résoudra tous les problèmes, sans toutefois être bien correct. En effet, tous les fichiers qui importent pieces.py ont accès à P1 et P2 à travers cet import.

Note : je ne connais pas de moyen en Python que cet import soit privé. Le concept de privé n'existant que par convention en Python.

Je fais donc la chasse aux imports de P1 et P2 depuis pieces et je les change en import depuis player.

Afin de m'aider dans cette chasse, j'ai choisi ce moyen :

    grep "from pieces" *py | grep -w P1

Retour au patch

Je dois vous l'avouer, j'ai un peu tourné en rond avec cette histoire de patch. Mon actualité personnelle est un peu chargée (comme cela peut se voir avec le ralentissement de ces articles).

Le test tel que je l'avais écrit initialement ne peut pas fonctionner, car patch ne peut pas patcher une fonction globale. Par contre, patch peut modifier les appels à une méthode statique. Ça m'ennuie un peu car cela contraint du code que je n'avais pas spécialement envie d'écrire. Mais bon... c'est comme ça :

    @mock.patch('pieces_movements.ValidatorProvider.from_piece_type')
    def test_piece_construction_calls_a_validation_service(self, mock_get):
        Piece(LION, P1)
        mock_get.assert_called_with(LION)

On en revient donc au moment où le test ne passe pas. Mais cette fois, je peux écrire l'appel dans le constructeur de Piece :

    from pieces_movements import ValidatorProvider

    class Piece:
        def __init__(self, piece_type, player):
            self.piece = (piece_type, player)
            self._movement_validator = ValidatorProvider.from_piece_type(piece_type)

Méthode que j'implémente comme ceci dans pieces_movements.py :

    class ValidatorProvider:
        @staticmethod
        def from_piece_type(piece):
            return None

Ça valide.

Reste à déplacer le code des validateurs vers le ValidatorProvider. Je commence par préparer le terrain et encore une fois, je dois appeler une définition de pieces. Toujours le même problème. Cette fois, je décide d'emmener les symboles de pieces dans un fichier à part : pieces_symbols.py.

J'implémente donc réellement ValidatorProvider.

    class ValidatorProvider:
        @staticmethod
        def from_piece_type(piece):
            if piece == LION:
                return is_lion_movement_valid
            return is_pawn_movement_valid

Ce qui simplifie bien MovementValidator et le rend générique.

    from player import P1, P2

    class MovementValidator:
        def is_movement_valid(self, piece, source, destination):
            controller = piece.get_controller()
            assert controller in [P1, P2]

            validation_function = piece.get_validation_function()

            return validation_function(piece, source, destination)

Cela le simplifie tellement que son intérêt réside dans un test de la validité du contrôleur et d'une protection contre une pièce qui n'aurait pas de fonction de validation.

Je pourrais très bien tout simplifier et appeler directement la fonction de validation sur les pièces. Seulement, je ne le ferai pas, pas tout de suite en tout cas. En effet, passer par le MovementValidator me permet actuellement de ne pas m'occuper de l'implémentation de la validation. Si je trouve un autre moyen à un moment de simplifier la validation sans utiliser une fonction dans Piece, le code a réécrire sera très localisé.

Un autre argument pourrait être : il faudrait réécrire tous les tests de movementvalidator_tests.py...

Les autres pièces

J'avance rapidement, voici les tests pour deux autres pièces :

    def test_can_validate_a_elephant_for_player_1(self):
        asserter = MovementAssert(self, Piece(ELEPHANT, P1), (1, 1))

        list_of_allowed_moves = [
            (1, 1), (1, -1),
            (-1, 1), (-1, -1)
        ]
        list_of_forbidden_moves = [
            (0, 0),  # Stays still
            (0, 1), (0, -1),  # Sides
            (1, 0), (-1, 0),  # Forward and Backward
            (0, 2), (0, 3)  # Moves more than one space
        ]

        asserter.move_validation(list_of_allowed_moves, list_of_forbidden_moves)

        asserter = MovementAssert(self, Piece(ELEPHANT, P1), (0, 0))

        list_of_forbidden_moves = [
            (2, 2)  # Moves more than one space in diagonal
        ]

        asserter.move_validation(list_of_allowed_moves, list_of_forbidden_moves)


    def test_can_validate_a_giraffe_for_player_1(self):
        asserter = MovementAssert(self, Piece(GIRAFFE, P1), (1, 0))

        list_of_allowed_moves = [
            (0, 1), (0, -1),
            (-1, 0), (1, 0)
        ]
        list_of_forbidden_moves = [
            (0, 0),  # Stays still
            (1, 1), (1, -1),
            (-1, 1), (-1, -1),
            (0, 2), (0, 3)  # Moves more than one space
        ]

        asserter.move_validation(list_of_allowed_moves, list_of_forbidden_moves)

Ce sont des pièces à mouvement symétriques pour les deux joueurs, je n'écris le test que pour le joueur 1.

Et les validations correspondantes :

    def is_elephant_movement_valid(piece, source, destination):
        move_x = abs(destination[0] - source[0])
        move_y = abs(destination[1] - source[1])
        return move_x == 1 and move_y == 1

    def is_giraffe_movement_valid(piece, source, destination):
        move_x = abs(destination[0] - source[0])
        move_y = abs(destination[1] - source[1])
        return xor(move_x == 1, move_y == 1)

Reste la poule. Dont voici le test :

    def test_can_validate_a_hen_for_player_1(self):
        asserter = MovementAssert(self, Piece(HEN, P1), (1, 0))

        list_of_allowed_moves = [
            (-1, 1), (0, 1), (1, 1),  # Forward
            (-1, 0), (1, 0),  # Sides
            (0, -1)  # Backward
        ]
        list_of_forbidden_moves = [
            (0, 0),  # Stays still
            (-1, -1), (1, -1),  # Backward diagonal
            (0, 2), (2, 2)  # Moves more than one space
        ]

        asserter.move_validation(list_of_allowed_moves, list_of_forbidden_moves)

Ici, le mouvement n'est pas symétrique. La poule est comme un lion, mais sans les diagonales vers l'arrière.

Cela peut se traduire directement par :

    def is_hen_movement_valid(piece, source, destination):
        if not is_lion_movement_valid(piece, source, destination):
            return False

        list_of_forbidden_moves = [
            (-1, -1), (1, -1)  # Backward diagonal
        ]

        move_x = destination[0] - source[0]
        move_y = destination[1] - source[1]

        return (move_x, move_y) not in list_of_forbidden_moves

Cela signifie tout de même que chaque validation positive va calculer deux fois la même chose pour move_x et move_y, à une valeur absolue prêt.

Mais au moins, le test passe. Du coup, je peux essayer diverses choses pour simplifier le mouvement tout en gardant les tests au vert.

Par exemple :

    def is_hen_movement_valid(piece, source, destination):
        list_of_forbidden_moves = [
            (-1, -1), (1, -1)  # Backward diagonal
        ]

        move_x = destination[0] - source[0]
        move_y = destination[1] - source[1]

        return (abs(move_x) == 1 or abs(move_y) == 1) and (move_x, move_y) not in list_of_forbidden_moves

Il faut maintenant écrire le test pour le joueur 2 :

    def test_can_validate_a_hen_for_player_2(self):
        asserter = MovementAssert(self, Piece(HEN, P2), (2, 0))

        list_of_allowed_moves = [
            (-1, -1), (0, -1), (1, -1),  # Forward
            (-1, 0), (1, 0),  # Sides
            (0, 1)  # Backward
        ]
        list_of_forbidden_moves = [
            (0, 0),  # Stays still
            (-1, 1), (1, 1),  # Backward diagonal
            (0, -2), (2, 2)  # Moves more than one space
        ]

        asserter.move_validation(list_of_allowed_moves, list_of_forbidden_moves)

Qui peut être résolu par exemple avec :

    def is_hen_movement_valid(piece, source, destination):
        list_of_forbidden_moves = [
            (-1, -1), (1, -1)  # Backward diagonal
        ]

        controller = piece.get_controller()
        factor = forward_factor[controller]
        move_x = (destination[0] - source[0]) * factor
        move_y = (destination[1] - source[1]) * factor

        return ((abs(move_x) == 1 or abs(move_y) == 1) and
                (move_x, move_y) not in list_of_forbidden_moves)

Et voilà

Toutes les pièces ont leur validation.

Cet article a été très long a écrire à cause d'une actualité personnelle assez fournie. Pour le prochaine épisode, je vais devoir faire le point pour trouver les prochaines étapes.