Activité pratique

Durée2h30

Consignes globales

Lors de cette séance, vous écrirez et exécuterez vos premiers tests unitaires.

Important

Le but de cette séance est de vous aider à maîtriser des notions importantes en informatique. Un assistant de programmation intelligent tel que GitHub Copilot, que vous avez peut-être déjà installé, pourra vous fournir une solution à ces exercices uniquement à partir d’un nom de fichier judicieusement choisi.

Pour des raisons de formation, nous vous conseillons de désactiver d’abord ces outils.

À la fin de l’activité pratique, nous vous suggérons de retravailler l’exercice avec ces outils activés. Suivre ces deux étapes améliorera vos compétences à la fois fondamentalement et pratiquement.

De plus, nous vous fournissons les solutions des exercices. Assurez-vous de ne les consulter qu’après avoir une solution aux exercices, à des fins de comparaison ! Même si vous êtes sûr que votre solution est correcte, veuillez les consulter, car elles fournissent parfois des éléments supplémentaires que vous auriez pu manquer.

Contenu de l’activité

Préparation de l’environnement

Avant de commencer, créez un projet uv dédié à cette séance. Dans un terminal, placez-vous dans le dossier de la séance (par exemple imt/s5/info/prog/session3), puis lancez la commande suivante :

​
uv init --no-package
uv init --no-package
uv init --no-package
uv init --no-package

Vous écrirez tous les fichiers de cette séance dans ce projet, en ajoutant les bibliothèques dont vous avez besoin avec uv add et en exécutant vos programmes dans l’environnement virtuel associé. Au besoin, revoyez la fiche Gestion moderne de projet Python avec uv.

Argmax

On veut écrire une fonction argmax(lst) qui prend en paramètre une liste de valeurs numériques et retourne l’indice de la (première occurrence de la) valeur maximale dans cette liste. Si la liste est vide (len(lst) == 0) ou non initialisée (None), la fonction retourne -1.

Avant de coder la fonction, nous avons choisi d’écrire des tests unitaires pour décrire le comportement attendu de la fonction. Voici un exemple de test pour la fonction (copiez-collez-le dans un fichier tests_argmax.py), en supposant que argmax se trouve dans un fichier argmax.py :

​
# Needed imports
import unittest
from argmax import argmax



# Define a class for unit tests
class TestArgmax (unittest.TestCase):

    # Here, we define a method with a simple test
    # You will have to write new tests, possibly in new methods
    def test_argmax_standardcase (self):
        self.assertEqual(1, argmax([1, 5, 3, 4, 2, 0]))



# Run the tests
if __name__ == '__main__':
    _ = unittest.main(verbosity=2)

Vous allez coder la fonction argmax(lst) et écrire d’autres tests pour vous assurer qu’elle fonctionne correctement. Avant de coder, vous devez lister quelques exemples uniques que vous pourrez également utiliser pour vos tests. Vous devez aussi documenter la fonction et, si nécessaire, commenter le code.

Important

L’instruction unittest.main() exécute les tests définis dans le fichier. La bonne pratique est de séparer les fonctions et les tests. N’oubliez donc pas d’importer le fichier (et les fonctions) dans le module contenant les tests.

À faire

Répondez à la question ci-dessus.

Correction
Cliquez ici pour afficher la correction.

Dans un fichier nommé, par exemple, argmax.py :

​
def argmax (lst: list[int]) -> int:
    
    """
        This function returns the index of the maximum value occurring in a list of values.
        In case of an empty list, it returns -1.
        If the maximum value occurs multiple times, the index of the first occurrence is returned.
        In:
            * lst: The list of values.
        Out:
            * The index of the first occurrence of the maximum value, or -1.
    """

    # This whole function is equivalent to:
    # return lst.index(max(lst))
    # We detail the algorithm for pedagocical purpose

    # If not found
    if lst is None or len(lst) == 0:
        return -1
    
    # Otherwise, iterate on contents to find max
    max_val = lst[0]
    max_index = 0
    for i, v in enumerate(lst):
        if v > max_val:
            max_val = v
            max_index = i
    return max_index

Puis, dans tests_argmax.py :

​
# Needed imports
import unittest
from argmax import argmax



# Define a class for unit tests
class TestArgmax (unittest.TestCase):

    # Here, we define a method with a simple test
    # You will have to write new tests, possibly in new methods
    def test_argmax_standardcase (self):
        self.assertEqual(1, argmax([1, 5, 3, 4, 2, 0]))
        self.assertEqual(0, argmax([8, 5]))
        self.assertEqual(1, argmax([-2, 9]))

    def test_argmax_none (self):        
        self.assertEqual(-1, argmax(None))
    
    def test_argmax_empty (self):
        self.assertEqual(-1, argmax([]))
    
    def test_argmax_multiple_max (self):
        self.assertEqual(2, argmax([1, 3, 5, 5, 2, 5]))



# Run the tests
if __name__ == '__main__':
    _ = unittest.main(argv=[""], verbosity=2, exit=False)

Année bissextile

Dans cet exercice, vous devrez écrire une fonction is_leap(year) qui prend une année en paramètre sous forme d’entier positif, et retourne une valeur booléenne qui est vraie si l’année est bissextile et fausse sinon.

Toute année divisible exactement par quatre est une année bissextile, sauf les années divisibles exactement par 100, mais ces années séculaires sont bissextiles si elles sont divisibles exactement par 400 (Wikipedia).

Nous ajoutons une règle supplémentaire qui précise que si l’entier passé en paramètre est strictement négatif, alors la valeur retournée est False.

Écrire les tests

Commençons par écrire un placeholder pour la fonction, qui sert juste à ce que la fonction soit définie :

​
def is_leap (year: int) -> bool:

    """
        The function returns True if the input is a leap year and False otherwise.
        This function only works for positive integers.
        In:
            * year: The year to be checked.
        Out:
            * True if the year is a leap year, False otherwise.
    """

    return False

Avant de coder le corps de la fonction, vous écrirez les tests. Suivant le principe d’un test par concept, vous créerez les cinq tests suivants :

  1. La valeur retournée est False pour une valeur négative (donnée en exemple).
  2. Si la valeur d’entrée est non divisible par 4, alors le résultat attendu est False.
  3. Si la valeur d’entrée est divisible par 100 (et donc par 4) mais pas par 400, alors le résultat attendu est False.
  4. Si la valeur d’entrée est divisible par 4 mais pas par 100, alors le résultat attendu est True.
  5. Si la valeur d’entrée est divisible par 400, alors le résultat attendu est True.

Voici un code de base pour commencer (en supposant que is_leap est dans un fichier isleap.py) :

​
# Needed imports
import unittest
from isleap import is_leap



class TestIsLeap (unittest.TestCase):

    def test_negative_value (self):
        self.assertFalse(is_leap(-1000))



# Run the tests
if __name__ == '__main__':
    _ = unittest.main(verbosity=2)  
À faire

Complétez le code ci-dessus.

Correction
Cliquez ici pour afficher la correction.
​
# Needed imports
import unittest
from isleap import is_leap



class TestIsLeap (unittest.TestCase):

    def test_negative_value (self):
        self.assertFalse(is_leap(-1000))

    def test_even_not_divisivle_by_4 (self):
        self.assertFalse(is_leap(2018))
        self.assertFalse(is_leap(2026))

    def test_divisible_by_4_and_by_100_and_not_by_400 (self):
        self.assertFalse(is_leap(1900))
        self.assertFalse(is_leap(2100))

    def test_divisible_by_4_and_not_by_100 (self):
        self.assertTrue(is_leap(2008))
        self.assertTrue(is_leap(2024))

    def test_divisible_by_400 (self):
        self.assertTrue(is_leap(1600))
        self.assertTrue(is_leap(2000))



# Run the tests
if __name__ == '__main__':
    _ = unittest.main(argv=[""], verbosity=2, exit=False)  

Écrire la fonction

Vous pouvez maintenant coder la logique de la fonction conformément aux spécifications. Vous devez commenter les lignes de manière significative pour rendre le code plus clair, en utilisant #.

Important

Assurez-vous que tous les tests passent !

À faire

Répondez à la question ci-dessus.

Correction
Cliquez ici pour afficher la correction.
​
def is_leap (year: int) -> bool:
    
    """
        This function takes a year as input and returns True if the year is a leap year, False otherwise.
        An additional constraint is that the year must be greater than 0, otherwise the function will return False.
        In:
            * year: The year to be checked.
        Out:
            * True if year is a leap year, False otherwise.
    """
    
    if year >= 0:  
        if year % 4 == 0:
            if year % 100 == 0:
                return year % 400 == 0
                # since the year is divisible by 100, 
                # it must also be divisible by 400 to be a leap year     
            else:
                # divisible by 4 and not by 100
                return True
        else:
            # not divisible by 4
            return False
    else:
        # negative years are not leap years
        return False
def is_leap (year: int) -> bool:
    
    """
        This function takes a year as input and returns True if the year is a leap year, False otherwise.
        An additional constraint is that the year must be greater than 0, otherwise the function will return False.
        In:
            * year: The year to be checked.
        Out:
            * True if year is a leap year, False otherwise.
    """
    return year >= 0 and (year % 4 == 0 and (year % 100 != 0 or year % 400 == 0))

Mot de passe valide

Pour être considéré comme suffisamment sécurisé, un mot de passe doit respecter les règles suivantes :

  1. Doit contenir au moins 8 caractères.
  2. Doit contenir au moins une lettre majuscule.
  3. Doit contenir au moins une lettre minuscule.
  4. Doit contenir au moins un chiffre.
  5. Doit contenir au moins un caractère spécial (parmi !@#$%^&*()).

Écrivez la fonction is_strong_password(password) qui prend en entrée une chaîne de caractères (a.k.a., séquence de caractères) et retourne un booléen indiquant si le mot de passe entré est fort. Pour aider à concevoir la logique de la fonction, vous pouvez la décomposer en sous-fonctions, chacune étant testée indépendamment. Un squelette de la fonction est fourni ci-dessous, divisé en sous-fonctions. Vous devez aussi documenter et commenter votre code.

Enfin, vous devez vérifier que les règles exprimées dans le cahier des charges sont bien prises en compte en écrivant des tests unitaires. La classe de test n’est pas fournie, vous devrez donc l’écrire vous-même. Chaque sous-fonction doit être testée indépendamment avec une variété d’exemples. Une fois chaque sous-fonction testée, vous pouvez tester la fonction principale.

Cette dernière étape peut être réalisée en binôme : chaque personne écrit les tests pour l’autre. Ensuite, vous pouvez évaluer la justesse de votre fonction en lançant les tests. Possiblement, soit la fonction, soit les tests devront être modifiés pour garantir que la fonction fonctionne correctement.

Voici un fichier pour vous lancer :

​
À faire

Complétez le code ci-dessus.

Correction
Cliquez ici pour afficher la correction.
​
"""
A class to check if a password is strong.
There are 5 rules to check if a password is strong:
1. Must contain at least 8 characters.
2. Must contain at least one uppercase letter.
3. Must contain at least one lowercase letter.
4. Must contain at least one number.
5. Must contain at least one special character (from `!@#$%^&*()`).
"""

special_chars = "!@#$%^&*()"


def check_len(password: str) -> bool:
    """Check if the password has at least 8 characters.

    Parameters:
        password (str): The password

    Returns:
        bool: True, if password contains at least 8 characters, False otherwise
    """
    return len(password) >= 8


def check_uppercase(password: str) -> bool:
    """
    Check if the password has at least one uppercase letter.
    :param password: str, the password
    :return: bool, True if the password has at least one uppercase letter, False otherwise
    """
    return any(char.isupper() for char in password)


def check_lowercase(password: str) -> bool:
    """
    Check if the password has at least one lowercase letter.
    :param password: str, the password
    :return: bool, True if the password has at least one lowercase letter, False otherwise
    """
    return any(char.islower() for char in password)


def check_number(password: str) -> bool:
    """
    Check if the password has at least one number.
    :param password: str, the password
    :return: bool, True if the password has at least one number, False otherwise
    """
    return any(char.isdigit() for char in password)


def check_special_char(password: str) -> bool:
    """
    Check if the password has at least one special character.
    :param password: str, the password
    :return: bool, True if the password has at least one special character, False otherwise
    """
    return any(char in special_chars for char in password)


def is_strong(password: str) -> bool:
    """
    Check if the password is strong.
    To be strong, the password must satisfy the 5 rules.
    :param password: str, the password
    :return: bool, True if the password is strong, False otherwise
    """
    return all([
        check_len(password),
        check_uppercase(password),
        check_lowercase(password),
        check_number(password),
        check_special_char(password)
    ])


if __name__ == '__main__':
    password = "StrongPassword123!"
    print(is_strong(password))
​
import unittest

import strongpassword as sp

class StrongPasswordTestCase(unittest.TestCase):

    def test_check_len(self):
        self.assertFalse(sp.check_len("1234567"))
        self.assertTrue(sp.check_len("12345678"))

    def test_check_uppercase(self):
        self.assertFalse(sp.check_uppercase("abcdefgh"))
        self.assertTrue(sp.check_uppercase("abcDefgh"))

    def test_check_lowercase(self):
        self.assertFalse(sp.check_lowercase("ABCDEFGH"))
        self.assertTrue(sp.check_lowercase("abcDEFGH"))

    def test_check_number(self):
        self.assertFalse(sp.check_number("abcdefgh"))
        self.assertTrue(sp.check_number("abc12345"))

    def test_check_special_char(self):
        self.assertFalse(sp.check_special_char("abcdefgh"))
        self.assertTrue(sp.check_special_char("abc!@#$%"))

    def test_is_strong(self):
        self.assertFalse(sp.is_strong("1234567"))
        self.assertFalse(sp.is_strong("abcdefgh"))
        self.assertFalse(sp.is_strong("abc12345"))
        self.assertFalse(sp.is_strong("abc!@#$%"))
        self.assertTrue(sp.is_strong("abcD123!"))

if __name__ == '__main__':
    unittest.main()

Vérificateur Takuzu

Takuzu est un jeu de logique où vous devez compléter une grille pré-remplie $n\times n$ en utilisant uniquement des 0 et des 1. Une grille est correctement complétée si elle respecte les trois règles :

  1. Chaque ligne et chaque colonne doit contenir un nombre égal de 0 et de 1.
  2. Pas plus de deux nombres similaires consécutifs sont autorisés.
  3. Chaque paire de lignes est différente, et chaque paire de colonnes est différente.

Un exemple de grille $4\times 4$ correctement complétée :

0 1 1 0 
1 0 0 1 
0 0 1 1 
1 1 0 0 

Vous êtes invité(e) à coder un vérificateur de grille Takuzu. Un squelette de module servira de base à votre travail. Vous devez respecter la partition des fonctions et implémenter chacune d’elles. Bien sûr, vous devrez aussi créer et compléter une classe de test.

Voici un fichier pour vous lancer :

​
À faire

Complétez le code ci-dessus.

Correction
Cliquez ici pour afficher la correction.
​
"""
Takuzu is a binary puzzle game.
The objective is to fill a nxn grid with 0s and 1s according to the following rules:
1. Each row and column must contain an equal number of 0s and 1s.
2. No more than two consecutive similar numbers are allowed.
3. Each pair of rows is different, and each pair of columns is different.
The game is won when the grid is filled according to the rules.
This class provides a method to check if a grid is valid.
"""

def check_is_square(grid: list[list[int]]) -> bool:
    """
    Checks if the grid is square, that is, it has the same number of rows and columns.
    :return: bool, True if the grid is square, False otherwise
    """
    return all(len(row) == len(grid) for row in grid)


def check_only_0s_and_1s(grid: list[list[int]]) -> bool:
    """
    Checks if the grid contains only 0s and 1s.
    :return: bool, True if the grid contains only 0s and 1s, False otherwise
    """
    return all(all(cell in [0, 1] for cell in row) for row in grid)


def check_number_of_0s_and_1s(seq: list[int]) -> bool:
    """
    Checks if the sequence satisfies the first rule of Takuzu,
    that is, each row and column must contain an equal number of 0s and 1s.
    :return: bool, True if the sequence satisfies the rule, False otherwise
    """
    return seq.count(0) == seq.count(1)


def check_no_more_than_two_similar_numbers_consecutive(seq: list[int]) -> bool:
    """
    Checks if the sequence satisfies the second rule of Takuzu,
    that is, no more than two consecutive similar numbers are allowed.
    :return: bool, True if the sequence satisfies the rule, False otherwise
    """
    return all(seq[i] != seq[i + 1] or seq[i] != seq[i + 2] for i in range(len(seq) - 2))


def check_uniqueness(seq1: list[int], seq2: list[int]) -> bool:
    """
    Checks if pair of rows (resp. columns) is different. 
    This helps verifying the 3rd rule of Takuzu.
    :param seq1: list[int], the first sequence
    :param seq2: list[int], the second sequence
    :return: bool, True if the sequences are unique, False otherwise
    """
    return seq1 != seq2


def grid_checker(grid: list[list[int]]) -> bool:
    """
    Checks if the grid is valid according to the rules of 
    The three rules are:
    1. Each row and column must contain an equal number of 0s and 1s.
    2. No more than two similar numbers adjacent to each other are allowed.
    3. Each pair of rows is different, and each pair of columns is different.
    :param grid: list[list[int]], the grid to be checked
    :return: bool, True if the grid is valid, False otherwise
    """
    if not check_is_square(grid):
        return False
    if not check_only_0s_and_1s(grid):
        return False
    for row in grid:
        if not check_number_of_0s_and_1s(row):
            return False
        if not check_no_more_than_two_similar_numbers_consecutive(row):
            return False
    for col in range(len(grid)):
        if not check_number_of_0s_and_1s([grid[row][col] for row in range(len(grid))]):
            return False
        if not check_no_more_than_two_similar_numbers_consecutive(
                [grid[row][col] for row in range(len(grid))]):
            return False
    for i in range(len(grid)):
        for j in range(i + 1, len(grid)):
            if not check_uniqueness(grid[i], grid[j]):
                return False
            if not check_uniqueness([grid[row][i] for row in range(len(grid))],
                                            [grid[row][j] for row in range(len(grid))]):
                return False
    return True


if __name__ == '__main__':
    grid = [[0, 1, 1, 0], [1, 0, 0, 1], [0, 0, 1, 1], [1, 1, 0, 0]]
    print(grid_checker(grid))
​
import unittest

from takuzu import *

class TakuzuTestCase(unittest.TestCase):

    def test_check_is_square(self):
        self.assertTrue(check_is_square([[0, 1], [1, 0]]))
        self.assertFalse(check_is_square([[0, 1], [1, 0], [1, 1]]))
        self.assertFalse(check_is_square([[0, 1], [1, 0, 1]]))

    def test_check_only_0s_and_1s(self):
        self.assertTrue(check_only_0s_and_1s([[0, 1], [1, 0]]))
        self.assertFalse(check_only_0s_and_1s([[0, 1], [1, 2]]))

    def test_check_number_of_0s_and_1s(self):
        self.assertTrue(check_number_of_0s_and_1s([0, 1, 0, 1]))
        self.assertFalse(check_number_of_0s_and_1s([0, 1, 0, 0]))
        self.assertFalse(check_number_of_0s_and_1s([0, 1, 0, 1, 1]))

    def test_check_no_more_than_two_similar_numbers_consecutive(self):
        self.assertTrue(check_no_more_than_two_similar_numbers_consecutive([0, 1, 0, 1]))
        self.assertFalse(check_no_more_than_two_similar_numbers_consecutive([0, 1, 0, 0, 0]))
        self.assertFalse(check_no_more_than_two_similar_numbers_consecutive([0, 1, 0, 1, 1, 1]))

    def test_check_uniqueness(self):
        self.assertTrue(check_uniqueness([0, 1, 0, 1], [1, 0, 1, 0]))
        self.assertFalse(check_uniqueness([0, 1, 0, 1], [0, 1, 0, 1]))

    def test_grid_checker(self):
        self.assertTrue(grid_checker([[0, 1], [1, 0]]))
        self.assertFalse(grid_checker([[0, 1], [1, 0], [1, 1]]))
        self.assertFalse(grid_checker([[0, 1], [1, 2]]))
        self.assertFalse(grid_checker([[0, 1], [1, 0, 1]]))
        self.assertFalse(grid_checker([[0, 1, 0, 0], [1, 0, 1, 1], [0, 1, 0, 1], [1, 0, 1, 0]]))

if __name__ == '__main__':
    unittest.main()

Mastermind

Préparez-vous à coder le jeu Mastermind. Dans ce jeu, un codeur crée un code utilisant 4 chiffres de 1 à 6. Un décodeur doit faire jusqu’à dix essais pour casser le code. Pour chaque essai, le codeur donne des indices au décodeur. Les indices sont composés de 2 chiffres :

  • Le premier chiffre indique le nombre de bons chiffres à la bonne position.
  • Le second chiffre indique le nombre de bons chiffres à la mauvaise position.

Les occurrences multiples d’une valeur sont autorisées. S’il y a des valeurs en double, elles ne peuvent pas toutes recevoir un indice à moins qu’elles correspondent au même nombre de couleurs dupliquées dans le code caché. Par exemple, si le code est [6, 5, 1, 4] et la proposition est [1, 1, 2, 2], les indices sont (0, 1). En fait, la valeur 1 apparaît deux fois dans la proposition, mais une seule fois dans le code. Puisqu’elle n’est pas à la bonne place, elle compte pour une dans la mauvaise place. De plus, la valeur 2 apparaît aussi deux fois dans la proposition, mais pas dans le code. Elle n’apparaît pas dans les indices.

Pour avancer progressivement, nous allons décomposer la logique du jeu en fonctions et les coder au fur et à mesure. Nous utiliserons la description fournie pour documenter le code et écrire des tests.

Nous fournissons un projet composé de plusieurs classes (Mastermind, Player) partiellement implémentées. Des commentaires sont fournis pour toutes les méthodes présentes, qu’elles soient implémentées ou non. Des classes de tests sont également fournies pour vous aider à valider votre code.

Vous devrez coder les méthodes check_code(code), has_remaining_attempts(), is_right_answer(hints) et guess_pattern(code, guess). Nous recommandons de traiter une fonction après l’autre, en écrivant les tests avant ou juste après avoir codé la fonction. Comme la dernière fonction est la plus compliquée, vous pouvez, si vous le souhaitez, la décomposer en sous-fonctions, chacune testée indépendamment.

À faire

Pour commencer, vous devez télécharger le squelette de code fourni dans le dossier compresser mastermind.zip et le décompresser dans un dossier de votre choix. Puis, depuis VS Code Fichier > Ouvrir Dossier... sélectionnez le dossier décompressé.

Correction
Cliquez ici pour afficher la correction.

Seuls les quatre fichiers ci-dessous diffèrent du squelette fourni : main.py et mastermind/__init__.py sont à prendre tels quels.

​
import random


class Mastermind:
    """
    Classe représentant le jeu Mastermind.
    Mastermind est un jeu où le joueur doit deviner un code secret composé de 4 chiffres entre 1 et 6.
    Le joueur dispose de 10 tentatives pour deviner le code et gagne s'il réussit à le trouver en moins de 10 essais.
    À chaque tentative, le jeu retourne un tuple de 2 nombres entre 0 et 4.
    Les indices sont les suivants :
    - Le premier chiffre indique le nombre de chiffres corrects à la bonne position
    - Le second chiffre indique le nombre de chiffres corrects mais à la mauvaise position
    Remarque : S'il y a des couleurs en double dans la proposition, elles ne peuvent pas toutes recevoir un indice sauf
    si elles correspondent au même nombre de couleurs en double dans le code secret.
    """

    def __init__(self, chances: int = 10, nb_digits: int = 4, nb_colors: int = 6):
        """
        Initialise le jeu Mastermind avec un code secret.
        :param chances: int, le nombre de chances dont dispose le joueur pour deviner le code. Par défaut 10.
        :param nb_digits: int, le nombre de chiffres dans le code. Par défaut 4.
        :param nb_colors: int, le nombre de couleurs (ou chiffres) pouvant être utilisés dans le code. Par défaut 6.
        """
        self.__nb_digits__ = nb_digits
        self.__nb_colors__ = nb_colors
        self.__code__ = [random.randint(1, nb_colors) for _ in range(nb_digits)]
        self.__chances__ = chances

    def set_code(self, code: list[int]):
        """
        Définit le code secret. Cette méthode est utile à des fins de test.
        Elle appelle la méthode check_code pour valider le code.
        :param code: list[int], le code secret
        :raise ValueError: si le code est invalide
        """
        if self.check_code(code):
            self.__code__ = code
        else:
            raise ValueError(
                f"Invalid code. Please enter {self.__nb_digits__} numbers between 1 and {self.__nb_colors__}.")

    def check_code(self, code: list[int]) -> bool:
        """
        Vérifie si le code est valide.
        :param code: list[int], le code à vérifier
        :return: bool, True si le code est valide, False sinon
        """
        # BEGIN SOLUTION
        return len(code) == self.__nb_digits__ and all(1 <= x <= self.__nb_colors__ for x in code)
        # END SOLUTION

    @staticmethod
    def provide_hints(code: list[int], guess: list[int]) -> tuple[int, int]:
        """
        Après chaque proposition du joueur, le jeu renverra un couple de nombres entre 0 et 4.
        Les indices sont les suivants :
        - Le premier chiffre indique le nombre de chiffres corrects à la bonne position
        - Le second chiffre indique le nombre de chiffres corrects mais à la mauvaise position
        Remarque : S'il y a des couleurs en double dans la proposition, elles ne peuvent pas toutes recevoir un indice sauf
        si elles correspondent au même nombre de couleurs en double dans le code secret.
        :param code: list[int], le code secret
        :param guess: list[int], la proposition du joueur
        :return: tuple[int, int], les indices
        """
        # BEGIN SOLUTION
        nb_correct = 0
        nb_misplaced = 0
        i = 0
        # travailler sur des copies pour éviter de modifier les listes originales
        code = code.copy()
        guess = guess.copy()
        # D'abord, compte le nombre de chiffres corrects à la bonne position
        while i < len(code):
            if code[i] == guess[i]:
                nb_correct += 1
                code.pop(i)
                guess.pop(i)
            else:
                i += 1
        # Ensuite, compte le nombre de chiffres corrects mais à la mauvaise position
        i = 0
        while i < len(code):
            if guess[i] in code:
                nb_misplaced += 1
                code.remove(guess[i])
                guess.pop(i)
            else:
                i += 1

        return nb_correct, nb_misplaced
        # END SOLUTION

    def evaluate_guess(self, guess: list[int]) -> tuple[bool, int, int]:
        """
        Evalue la proposition du joueur et renvoie les indices.
        :param guess: list[int], la proposition
        :return: tuple[bool, int, int], le résultat de la proposition et les indices
        """
        # BEGIN SOLUTION
        self.__chances__ -= 1
        nb_correct, nb_misplaced = self.provide_hints(self.__code__, guess)
        success = self.is_right_answer((nb_correct, nb_misplaced))
        return success, nb_correct, nb_misplaced
        # END SOLUTION

    def is_right_answer(self, hints: tuple[int, int]) -> bool:
        """
        Check if the guess is the right answer.
        Instead of checking the guess, the hints are provided as input.
        :return: bool, True if the hints are (4,0), False otherwise
        """
        # BEGIN SOLUTION
        return hints == (self.__nb_digits__, 0)
        # END SOLUTION

    def has_remaining_attempts(self) -> bool:
        """
        Check if the player has remaining attempts.
        :return: bool, True if the player has remaining attempts, False otherwise
        """
        # BEGIN SOLUTION
        return self.__chances__ > 0
        # END SOLUTION
from abc import ABC, abstractmethod

import itertools
import mastermind.mastermind

class Player(ABC):
    """
    Classe abstraite pour un joueur dans le jeu Mastermind.
    Cette classe définit les méthodes que tout joueur doit implémenter pour interagir avec le jeu Mastermind.
    """

    @abstractmethod
    def __init__(self, nb_digits: int = 4, nb_colors: int = 6):
        """
        Initialise le joueur.
        Cette méthode peut être étendue par les sous-classes pour initialiser des attributs spécifiques au joueur.
        """
        self.nb_digits = nb_digits
        self.nb_colors = nb_colors

    @abstractmethod
    def guess(self):
        """
        Méthode pour faire une proposition de code secret.
        Cette méthode doit être implémentée par les sous-classes pour permettre au joueur de faire une devinette.
        :return: list[int], la proposition du joueur sous forme de liste d'entiers.
        """
        pass

    @abstractmethod
    def receive_feedback(self, success:bool, nb_correct:int, nb_misplaced:int):
        """
        Méthode pour recevoir des retours sur la dernière devinette. Doit être implémentée par les sous-classes.

        :param success: Indique si la devinette était correcte.
        :param nb_correct: Nombre de positions correctes.
        :param nb_misplaced: Nombre de couleurs mal placées.
        """
        pass


class HumanPlayer(Player):
    """
    Classe pour un joueur humain dans le jeu Mastermind.
    Cette classe implémente les méthodes de la classe abstraite Player pour permettre à un joueur humain de jouer.
    """

    def __init__(self, nb_digits: int = 4, nb_colors: int = 6):
        """
        Initialise le joueur humain.
        :param nb_digits: int, le nombre de chiffres dans le code. Par défaut 4.
        :param nb_colors: int, le nombre de couleurs (ou chiffres) pouvant être utilisés dans le code. Par défaut 6.
        """
        super().__init__(nb_digits, nb_colors)


    def guess(self):
        """
        Demande au joueur de faire une devinette et retourne la proposition sous forme de liste d'entiers.
        :return: list[int], la proposition du joueur.
        """
        user_guess = input("Entrez votre proposition (sans caractères entre chaque chiffre): ")
        # Convert the guess as a n-digit number to a list of integers
        # Assuming the input is not space-separated digits
        guess = [int(digit) for digit in user_guess if digit.isdigit()]
        return guess
    
    def receive_feedback(self, nb_correct:int, nb_misplaced:int):
        """
        Reçoit les retours du jeu après avoir fait une devinette.

        :param success: Indique si la devinette était correcte.
        :param nb_correct: Nombre de positions correctes.
        :param nb_misplaced: Nombre de couleurs mal placées.
        """
        # Aucune action nécessaire pour le joueur humain
        pass
  

class KnuthPlayer(Player):
    """
    Classe pour un joueur utilisant l'algorithme de Knuth dans le jeu Mastermind.
    Cette classe implémente les méthodes de la classe abstraite Player pour permettre à un joueur utilisant l'algorithme de Knuth de jouer.
    """

    def __init__(self, nb_digits: int = 4, nb_colors: int = 6):
        """
        Initialise le joueur Knuth.
        """
        super().__init__(nb_digits, nb_colors)
        # BEGIN SOLUTION
        self.attempts = 0
    
        # Initialisation des codes possibles selon les règles du jeu
        self.possible_codes = [list(code) for code in itertools.product(range(1, nb_colors + 1), repeat=nb_digits)]
        # La proposition courante du joueur
        self.current_guess = None
        # END SOLUTION

    def guess(self):
        """
        Implémentation de la méthode guess pour l'algorithme de Knuth.
        :return: list[int], la proposition du joueur.
        """
        # BEGIN SOLUTION
        if self.attempts == 0:
            # Premier coup de Knuth, si il y a 4 couleurs, on commence par 1,1,2,2
            self.current_guess = [1, 1, 2, 2]
        else:
            # Algo minmax pour choisir la prochaine proposition
            # On choisit la première proposition possible qui n'a pas été éliminée par les retours précédents
            self.current_guess = self._minimax_guess()

        # Retourne la proposition actuelle
        self.attempts += 1
        return self.current_guess  

    def _minimax_guess(self):
        """
        Implémente l'algorithme minimax pour choisir la prochaine proposition.
        :return: list[int], la prochaine proposition du joueur.
        """
        # Initialise les scores pour chaque code possible
        scores = {}
        # Pour chaque code possible restant
        for code in self.possible_codes:
            # Considère ce code comme une solution potentielle
            feedback_counts = {}
            # Pour chaque proposition possible
            for guess in self.possible_codes:
                # Compare la proposition (guess) à la solution (code)
                feedback = mastermind.mastermind.Mastermind.provide_hints(code, guess)
                # Compte le nombre de propositions donnant le même retour
                feedback_counts[feedback] = feedback_counts.get(feedback, 0) + 1
            # Le score du code est le nombre maximal de propositions pour un même retour
            scores[tuple(code)] = max(feedback_counts.values())
        # Sélectionne le code avec le score minimal
        best_guess = min(scores, key=scores.get)
        return list(best_guess)
        # END SOLUTION

    def receive_feedback(self, nb_correct:int, nb_misplaced:int):
        """
        Reçoit les retours du jeu après avoir fait une devinette.

        :param nb_correct: Nombre de positions correctes.
        :param nb_misplaced: Nombre de couleurs mal placées.
        """
        # BEGIN SOLUTION
        # Implémentation spécifique pour traiter les retours
        # Retire de S tout code qui ne donnerait pas cette réponse de pions colorés et blancs
        # si la proposition actuelle était la solution
        self.possible_codes = [
            code for code in self.possible_codes
            if mastermind.mastermind.Mastermind.provide_hints(self.current_guess, code) == (nb_correct, nb_misplaced)]
        # END SOLUTION
import unittest

from mastermind.mastermind import Mastermind


class MastermindTestCase(unittest.TestCase):

    def setUp(self):
        self.game = Mastermind(10, 4, 6)

    def test_check_code_len(self):
        self.assertFalse(self.game.check_code([1, 1, 1]))
        # BEGIN SOLUTION
        self.assertTrue(self.game.check_code([1, 1, 1, 1]))
        self.assertFalse(self.game.check_code([1, 1, 1, 1, 1]))
        # END SOLUTION

    def test_check_code_range(self):
        self.assertFalse(self.game.check_code([1, 1, 1, -3]))
        # BEGIN SOLUTION
        self.assertFalse(self.game.check_code([1, 1, -2, 1]))
        self.assertFalse(self.game.check_code([1, -1, 1, 1]))
        self.assertFalse(self.game.check_code([0, 1, 1, 1]))
        self.assertTrue(self.game.check_code([1, 2, 3, 4]))
        self.assertTrue(self.game.check_code([6, 5, 4, 3]))
        self.assertFalse(self.game.check_code([7, 6, 6, 6]))
        self.assertFalse(self.game.check_code([6, 8, 6, 6]))
        self.assertFalse(self.game.check_code([6, 6, 9, 6]))
        self.assertFalse(self.game.check_code([6, 6, 6, 10]))
        # END SOLUTION

    def test_has_remaining_attempts(self):
        # BEGIN SOLUTION
        self.game.__chances__ = 0
        self.assertFalse(self.game.has_remaining_attempts())
        self.game.__chances__ = 1
        self.assertTrue(self.game.has_remaining_attempts())
        # END SOLUTION

    def test_is_right_answer(self):
        # BEGIN SOLUTION
        self.assertTrue(self.game.is_right_answer((4, 0)))
        self.assertFalse(self.game.is_right_answer((0, 4)))
        self.assertFalse(self.game.is_right_answer((0, 0)))
        self.assertFalse(self.game.is_right_answer((4, 1)))
        # END SOLUTION

    def test_guess_pattern(self):
        # BEGIN SOLUTION
        code = [6, 5, 1, 4]
        self.assertEqual((0, 1), self.game.provide_hints(code, [1, 1, 2, 2]))
        self.assertEqual((1, 1), self.game.provide_hints(code, [6, 1, 2, 3]))
        self.assertEqual((1, 0), self.game.provide_hints(code, [1, 1, 1, 1]))
        self.assertEqual((1, 0), self.game.provide_hints(code, [6, 6, 6, 6]))
        self.assertEqual((4, 0), self.game.provide_hints(code, [6, 5, 1, 4]))
        # END SOLUTION


if __name__ == '__main__':
    unittest.main()
import unittest
from unittest.mock import patch

from mastermind.player import *

class HumanPlayerTestCase(unittest.TestCase):

    def setUp(self):
        self.player = HumanPlayer()

    def test_guess(self):
        with patch('builtins.input', return_value='1 2 3 4'):
            guess = self.player.guess()
            self.assertEqual(guess, [1, 2, 3, 4])


class KnuthPlayerTestCase(unittest.TestCase):

    def setUp(self):
        self.player = KnuthPlayer()

    # BEGIN SOLUTION
    def test_guess_first_attempt(self):
        guess = self.player.guess()
        self.assertEqual(guess, [1, 1, 2, 2])

    def test_receive_feedback(self):
        self.player.current_guess = [1, 1, 2, 2]
    
        nb_correct, nb_misplaced = 2, 1
        self.player.receive_feedback(nb_correct, nb_misplaced)
        self.assertNotIn([1, 1, 2, 2], self.player.possible_codes)
        self.assertIn([2, 1, 2, 3], self.player.possible_codes)

        self.player.attempts += 1
        guess = self.player.guess()
        self.assertIn(guess, self.player.possible_codes)            

    def test_guess_after_feedback(self):
        game = mastermind.mastermind.Mastermind(10, 4, 6)
        game.set_code([2, 4, 3, 1])

        guess = self.player.guess()
        won, correct, misplaced = game.evaluate_guess(guess)
        self.assertFalse(won)
        self.assertEqual(correct, 0)
        self.assertEqual(misplaced, 2)
        self.player.receive_feedback(correct, misplaced)
        self.assertIn([2, 4, 3, 1], self.player.possible_codes)
        self.assertNotIn(guess, self.player.possible_codes)
        self.assertEqual(self.player.attempts, 1)
        guess = self.player.guess()
        won, correct, misplaced = game.evaluate_guess(guess)
        self.assertFalse(won)
        self.assertEqual(correct, 1)
        self.assertEqual(misplaced, 3)
        self.player.receive_feedback(correct, misplaced)
        self.assertIn([2, 4, 3, 1], self.player.possible_codes)
        guess = self.player.guess()
        won, correct, misplaced = game.evaluate_guess(guess)
        self.assertTrue(won)
        self.assertEqual(correct, 4)
        self.assertEqual(misplaced, 0)
        self.assertIn([2, 4, 3, 1], self.player.possible_codes)
    # END SOLUTION

if __name__ == '__main__':
    unittest.main()

Pour aller plus loin

Algorithme de Knuth pour le Mastermind

L’algorithme de Knuth est une méthode efficace pour résoudre le jeu Mastermind en un nombre minimal de coups. Il utilise une stratégie d’élimination pour réduire le nombre de possibilités à chaque coup. L’algorithme commence par générer une liste de tous les codes possibles. Ensuite, il fait une supposition initiale, généralement [1, 1, 2, 2]. Après chaque coup, il utilise les indices fournis pour éliminer les codes qui ne correspondent pas aux indices. Il continue à faire des suppositions jusqu’à ce qu’il trouve le code secret ou qu’il ait épuisé le nombre de coups autorisés. L’algorithme de Knuth est connu pour sa capacité à résoudre le jeu en un maximum de 5 coups, ce qui en fait une méthode très efficace pour jouer au Mastermind.

Vous pouvez implémenter cet algorithme en Python, en déclarant une classe KnuthPlayer qui devra s’intégrer dans le jeu Mastermind.

Dans premier temps, si vous le souhaitez, vous pouvez ne pas implémenter l’algorithme minimax, mais simplement éliminer les combinaisons ne correspondant pas aux indices.

Bien entendu, il est indispensable de tester chaque fonction que vous implémentez pour vous assurer de leur bon fonctionnement.

À faire

Liens utiles :

Correction
Cliquez ici pour afficher la correction.

Bien entendu, il est indispensable de tester chaque fonction que vous implémentez pour vous assurer de leur bon fonctionnement.

Liens utiles :