Le jeu des premiers

  • Niveau ciblé: classe de seconde.
  • Points du programme abordés:
    • Démonstration: pour une valeur numérique de a, la somme de deux multiples de a est multiple de a.
    • Algorithme: déterminer si un entier naturel a est multiple d’un entier naturel b.
    • Algorithme: déterminer si un entier naturel est premier.
    • Travailler sur les inégalités.
    • Lire, comprendre, modifier ou compléter un algorithme ou un programme.
  • Auteur: jean-manuel mény, groupe upo

Partie 1

Jouez contre l'ordinateur... Le but est d'essayer de comprendre quelles sont les règles de ce jeu et comment l'ordinateur choisit de jouer. Les parties suivantes reviennent en détail sur ces questions.

  • Le jeu se joue à deux (vous contre l'ordinateur).
  • Un tas d'allumettes est placé devant les deux joueurs.
  • Chacun à tour de rôle peut enlever des allumettes en respectant certaines contraintes sur le nombre d'allumettes à enlever (contraintes à deviner en lisant et exécutant le code).
  • Vous devez comprendre également qui gagne à ce jeu (quel est le critère permettant de savoir qui a gagné?).
In [ ]:
from random import randint

 

def verif(n):
    """
    n est un entier > 1
    """
    for k in range(2,n):
        if n % k == 0:
            return False
    return True

def autorise(tas, k):
    """
    tas est un entier naturel, c'est le nombre d'allumettes se trouvant devant les joueurs.
    k est un entier naturel, c'est le nombre d'allumettes que veut enlever le joueur dont c'est le tour.
    
    La fonction renvoie True si le coup est permis et renvoie False si le coup n'est pas permis.
    """
    return 0 < k <= tas and (verif(k) or k==1)

def jeu_ordi(tas):
    """
    tas est le nombre d'allumettes au moment de jouer.
    """
    if tas % 4 == 0:
        return 1
    else:
        return tas % 4
    
def tas_actuel(n):
    print("Le tas compte ", n, "allumettes.\n")
    
def choix_joueur1():
    choix = randint(0, 1)
    if choix == 0:
        print("Je commence.")
        return 0
    else:
        print("Vous commencez.")
        return 1
    
    
def une_partie():
    nb_allumettes = randint(20,40)
    tas_actuel(nb_allumettes)
    joueur = choix_joueur1()
    
    while nb_allumettes >= 0:
        if nb_allumettes == 0:
            if joueur == 0:
                print("J'ai perdu.\n")
            else:
                print("Vous avez perdu.\n")
            break # sortie de boucle quand la partie est finie
        else:
            if joueur == 0:
                nb_pris = jeu_ordi(nb_allumettes)
                nb_allumettes = nb_allumettes - nb_pris
                if nb_pris == 1:
                    print("Je prends ",  nb_pris, "allumette.\n")
                else:
                    print("Je prends ",  nb_pris, "allumettes.\n")
                tas_actuel(nb_allumettes)
                joueur = 1
            else:
                print("A vous de jouer.\n")
                nb_pris = 0
                while not(autorise(nb_allumettes, nb_pris)):
                    nb_pris = int(input("Combien d'allumettes prenez-vous ?\n"))
                    if not(autorise(nb_allumettes, nb_pris)):
                        print("Ce nombre n'est pas autorisé.\n")
                nb_allumettes = nb_allumettes - nb_pris
                if nb_pris == 1:
                    print("Vous avez pris ",  nb_pris, "allumette.\n")
                else:
                    print("Vous avez pris ",  nb_pris, "allumettes.\n")
                tas_actuel(nb_allumettes)
                joueur = 0
            
            
            
une_partie()        
    
    

Partie 2

Question 1

Un bon usage en programmation est de donner aux fonctions des noms explicites permettant de saisir l'essentiel de leur rôle.

Ce n'est pas ce qui a été fait avec le code ci-dessous.

  • Lisez le, cherchez à comprendre son rôle.
  • Testez le pour confirmer.
  • Renommez le de façon adéquate.
In [ ]:
def f(n):
    """
    n est un entier > 1
    """
    for k in range(2,n):
        if n % k == 0:
            return False
    return True
Réponse La fonction renvoie True si n est premier, False sinon. En effet elle renvoie False si elle trouve un diviseur de n (entre 2 et n-1) et renvoie True sinon. On remarquera que le cas de l'entier 2 est bien traité: le test de la division par 2 n'a pas lieu pour lui. On renommera la fonction f par exemple par le nom est_premier().

Question 2

Anissa affirme que la fonction suivante a le même rôle que f. Est-ce vrai?

In [5]:
from math import sqrt, floor
def g(n):
    """
    n est un entier > 1
    """
    for k in range(2,floor(sqrt(n))+1):
        if n % k == 0:
            return False
    return True
Réponse Lorsque n peut s'écrire n = a ⨉ b avec 1 < a ≤ b < n, on a a² ≤ ab (en multipliant par a) soit a² ≤ n, d'où a ≤ √n. En d'autres termes, si n possède un diviseur propre, il en possède un inférieur ou égal à sa racine. La fonction g détermine donc bien également si son paramètre d'entrée est premier ou non.

Partie 3

On joue à un jeu à deux joueurs.

  • Un tas d'allumettes est posé devant les joueurs et chacun à tour de rôle peut enlever un certain nombre d'allumettes.
  • Celui qui perd est celui qui ne peut plus enlever d'allumettes, c'est à dire celui qui se voit offrir par l'adversaire un tas à 0 allumette.

Question 1

La fonction python ci-dessous permet de contrôler si le coup proposé par un joueur est valide: il a devant lui un nombre d'allumettes appelé ici tas et il veut enlever k allumettes.

Explicitez les règles du jeu en lisant le code de la fonction.

In [11]:
def coup_autorisé(tas, k):
    """
    tas est un entier naturel, c'est le nombre d'allumettes se trouvant devant les joueurs.
    k est un entier naturel, c'est le nombre d'allumettes que veut enlever le joueur dont c'est le tour.
    
    La fonction renvoie True si le coup est permis et renvoie False si le coup n'est pas permis.
    """
    if not(0 < k <= tas):
        return False
    elif not(g(k) or k==1):
        return False
    else:
        return True
  
        
    

(la fonction g utilisée est celle de la partie précédente.)

Réponse Un joueur doit enlever au moins une allumette, ce nombre d'allumettes doit être égal à 1 ou être un nombre premier. Les règles sont donc:
  • Un tas d'allumettes est présent sur la table.
  • Chacun joue à son tour.
  • A son tour, un joueur peut enlever 1 allumette ou un nombre premier d'allumettes.
  • Le joueur qui ne peut pas enlever d'allumettes a perdu.

Question 2

Léna, qui a bien étudié le jeu, a programmé une stratégie. Sa fonction stratégie est donnée ci-dessous:

In [ ]:
def stratégie(tas):
    """
    tas est le nombre d'allumettes au moment de jouer.
    
    La fonction renvoie le nombre d'allumettes enlevées par Léna.
    """
    if tas % 4 == 0:
        return 1
    else:
        return tas % 4

Explicitez en français la stratégie de Léna en lisant le code de la fonction.

Réponse Si le nombre d'allumettes est multiple de 4, Léna enlève systématiquement une seule allumette. Si ce nombre n'est pas multiple de 4, Léna enlève le reste de la division du nombre d'allumettes par 4.

Question 3

Justifiez que lorsque le tas présent devant Léna n'est pas multiple de 4, alors Léna offre systématiquement à son adversaire un nombre d'allumettes multiple de 4.

Réponse Soit n le nombre d'allumettes devant Léna. n s'écrit 4q + k avec 0 < k ≤ 3 puisque n n'est pas multiple de 3. Léna enlève k allumettes d'après sa fonction stratégie et laisse donc 4q allumettes à son adversaire.

Question 4

Justifier que lorsque Léna a pu offrir un multiple de 4 à son adversaire, alors son adversaire ne pourra pas lui offrir un tas dont le nombre d'allumettes est multiple de 4.

Réponse Soit n = 4q le nombre d'allumettes devant l'adversaire de Léna. Le nombre d'allumettes que peut retirer cet adversaire est soit 1, soit un nombre premier. Ce n'est donc jamais un multiple de 4. Il ne peut donc retirer que des entiers R de la forme 4k+1, 4k+2 ou 4k+3. La différence entre n et R n'est donc jamais multiple de 4. En effet si l'on avait n-R multiple de 4 (avec j égal à 1, 2, ou 3), on aurait 4q -(4k+j) = 4t, d'où j = 4(q-k-t) multiple de 4, absurde avec j valant 1, 2 ou 3.

Question 5

Justifiez que si Léna arrive à offrir un tas multiple de 4 à son adversaire alors elle a la garantie de gagner.

Réponse
  • Léna offre un multiple de 4 à son adversaire.
  • Son adversaire lui offre alors nécessairement un non multiple de 4.
  • Léna, selon sa stratégie, lui offre à nouveau un multiple de 4.
  • Son adversaire, de nouveau, ne peut lui offrir qu'un non multiple de 4.
  • etc...
Qui offrira le tas à 0 allumette à l'autre ? Comme 0 est un multiple de 4, c'est nécessairement Léna qui offrira le tas à 0 à son adversaire. Sa victoire est donc assurée dès qu'elle peut offrir un multiple de 4 à l'adversaire.

Partie 4

Question 1

Léna et Victor jouent au jeu précédent. Ils décident de commencer avec un tas dont le nombre d'allumettes est égal à l'année en cours.

  • Aujourd'hui (2019), ils commencent avec un tas de 2019 allumettes.
  • Au prochain nouvel an, ils commenceront avec un tas de 2020 allumettes.

Victor, galant, laisse Léna décider qui commence la partie.

Quelle sera la décision de Léna (sachant qu'elle aime gagner!) ?

Réponse 2019 n'est pas un multiple de 4, donc celui qui commence peut offrir un multiple de 4 à son adversaire: Léna prend la main en 2019. Par contre 2020 est multiple de 4: mieux vaut laisser la main en 2020.

Question 2

Léna a une stratégie gagnante, mais il lui arrive de faire des erreurs de calcul pour calculer les restes des divisions par 4 avec des entiers de plus de deux chiffres.

Anissa lui propose de procéder suivant la méthode suivante:

  • pour un entier de plus de deux chiffres, ne garder que les deux derniers chiffres de droite.
  • Calculer le reste dans la division par 4 pour ces deux derniers chiffres.

Exemple: pour calculer le reste dans la division de 2019 par 4, on calcule à la place le reste de 19 par 4.

  • Donner un programme python prenant en entrée un entier naturel (d'au moins 3 chiffres) et renvoyant le reste dans la division par 4 des deux derniers chiffres de droite.
  • Le reste obtenu est-il bien le reste dans la division par 4 de l'entier donné en entrée ?
Réponse: un code

def reste_par_4(n):
    deux_chiffres = n%100
    return deux_chiffres % 4

Ca marche? L'entier r constitué des deux chiffres de droite de n est le reste de n dans la division par 100: n = 100 q + r. Divisons r par 4: r = 4k + j où j vaut 0, 1, 2 ou 3. D'où n = 4(25q+k) + j. Le reste dans la division par 4 est le même pour n et r.

Question 3

Le programme de stratégie de Léna peut être amélioré pour accélérer la victoire. Comment ? Proposer une modification du code Python de la fonction stratégie.

Une réponse Lorsque le tas dévant Léna est un entier tas = 4q+r non multiple de 4, Léna enlève r = (reste de tas dans la division par 4) allumettes. Elle peut aussi chercher le plus grand entier premier inférieur ou égal à tas de la forme 4k+r. Elle laisse ainsi à son adversaire le tas comportant (4q+r)-(4k+r) = 4(q-k) allumettes. La stratégie de proposer un multiple de 4 à l'adversaire est respectée mais le jeu avance plus vite! On peut modifier le programme ainsi:

    def stratégie_plus_rapide(tas):
        """
        tas est le nombre d'allumettes au moment de jouer.

        La fonction renvoie le nombre d'allumettes enlevées par Léna.
        """
        if tas % 4 == 0:
            return 1
        else:
            reste = tas % 4
            for k in range(tas, 0, -1):
                if g(k) and k%4 == reste: # si k est premier et a même reste modulo 4 que tas
                    return k # on renvoie le plus grand nombre premier de même reste que tas
            return reste # cas où un tel nombre premier n'est pas trouvé

Question 4

Dans la proposition de modification du code de la fonction stratégie de la question précédente, pouvez citer un cas où la recherche d'un entier premier de même reste que tas (dans la division par 4) est inutile?

Une réponse Lorsque le reste = tas % 4 vaut 2: il n'existe pas de nombre premier (distinct du reste 2) de la forme 4k+2 car 4k+2 = 2(2k+1) (et 2k+1 > 1 si l'on cherche un nombre premier autre que 2). On pourrait donc modifier le code comme suit:

def stratégie_plus_rapide(tas):
    """
    tas est le nombre d'allumettes au moment de jouer.

    La fonction renvoie le nombre d'allumettes enlevées par Léna.
    """
    if tas % 4 == 0:
        return 1
    else:
        reste = tas % 4
        if reste == 2:
            return 2
        else:
            for k in range(tas, 0, -1):
                if g(k) and k%4 == reste: # si k est premier et a même reste modulo 4 que tas
                    return k # on renvoie le plus grand nombre premier de même reste que tas
            return reste # cas où un tel nombre premier n'est pas trouvé