Explorez un plateau et déterrez le trésor caché tout en évitant les pièges dans ce jeu inspiré du démineur !
Règles
Le jeu se déroule sur une grille de dimensions 5 x 5 remplie de chiffres. Celle-ci cache des pics et un trésor à découvrir afin de gagner.
Chaque chiffre représente le nombre de cases adjacentes qui cachent un pique. Cela signifie que chaque chiffre peut s’étendre de 0 à 4 (les cases en diagonale ne sont pas comptées comme adjacentes, contrairement au démineur).
Vous pouvez choisir de retourner la case que vous désirez. Si la case retournée abrite le trésor, vous gagnez la partie. En revanche, si elle cache un pique (représenté par la couleur grise), vous perdez une vie. Lorsque vos trois vies sont épuisées, vous avez échoué.
Dans le cas où la case est vide, celle-ci affiche une couleur qui vous renseigne sur sa distance par rapport au trésor :
Une case verte est éloignée du trésor ;
Une case orange est à deux à quatre cases du trésor ;
Une case rouge est à une à deux cases du trésor.
Voyez le schéma ci-dessous :
Toutefois, ne vous laissez pas berner ! Parmi les cases colorées, trois sont fausses et indiquent la mauvaise couleur. La disposition des couleurs peut davantage ressembler à cela :
Ainsi, votre but est de découvrir suffisamment de cases colorées afin de pouvoir estimer la position du trésor. Il se peut tout à fait que vous retourniez la case gagnante du premier coup, donc n’hésitez pas à faire plusieurs parties !
Commandes
△▽◁ ▷
OK
DEL
Naviguer dans les menus Déplacer le curseur
Sélectionner une option Retourner une case
Marquer une case* Retour
*Marquer une case, contrairement au démineur, n’empêche pas de la retourner. Cette fonctionnalité sert simplement d’aide visuelle au joueur. Également, une fois qu’une case est marquée, on ne peut plus retirer la marque.
Captures d’écran
Pour plus d’informations
Jetez un coup d’œil à cet article si vous souhaitez en apprendre plus sur le développement du jeu !
MOTUS est à l’origine un jeu télévisé diffusé entre 1990 et 2019 sur France 2. Ce programme d’origine américaine s’inspire du jeu MasterMind, les lettres ayant remplacés les chiffres. Ce jeu a connu depuis de nombreuses variantes dont le célèbre WORDLE qui connait un très grand succès actuellement sur internet.
Introduction
Les règles du jeu sont plutôt simples : l’objectif est de trouver un mot d’un nombre fixé de lettres en au plus 6 essais. La première lettre du mot à découvrir est donnée et après chaque tentative :
les lettres communes et bien placées sont signalées par une case rouge
les lettres communes mais mal placées sont, quant à elles, signalées par une case jaune
Si le joueur parvient à découvrir le mot avant les 6 essais, il remporte la partie, sinon ce mot lui est dévoilé.
Certaines versions du jeu, dont WORDLE, se jouent à une personne, même si à l’origine le jeu télévisé se joue à deux. Dans la version à 2 joueurs, aux règles précédentes s’ajoutent les suivantes : un joueur perd la main si le mot proposé :
n’est pas de la bonne longueur
ne commence pas par la lettre proposée
est mal épelé
n’est pas donné dans les 8 secondes imparties
ne figure pas dans le dictionnaire
n’a pas été trouvé au terme des 6 essais
Dans chacun de ces cas, le joueur adverse récupère la main et bénéficie d’une lettre bonus : la première lettre non découverte lui est dévoilée. Il dispose alors du nombre d’essais restants (du joueur précédent) pour trouver le mot. Dans le cas ou il ne dispose que d’une tentative pour découvrir le mot, il conserve la main pour la grille suivante même en cas d’échec.
Notre projet est donc de programmer en langage python ce jeu. D’une part parce que nous aimons bien jouer à WORDLE (nous avons découvert le jeu original en visionnant une partie sur Youtube) et d’autre part car la programmation de ce jeu nous semblait accessible (pour notre niveau) sans être trop simple et offrait de nombreuses possibilités d’évolutions et améliorations (mode 2 joueurs, prise en compte de règles supplémentaires, interface graphique).
La première question qui s’est naturellement posée dès le départ concernait le choix de l’interface : en mode console ou plutôt avec une interface graphique ? Après une mure réflexion et avoir consulté plusieurs sites notamment de NSI, nous nous sommes rendus à l’évidence : une interface graphique nécessitait un passage obligé par le module tkinter particulièrement adapté à ce jeu. Et même si ce module semble accessible pour les novices que nous sommes, nous avons jugé plus prudent dans un premier temps, de travailler sur un jeu en mode console.
Après avoir réfléchi à la structure de notre programme, nous avons écrit ensemble les différentes fonctions que devra utiliser le jeu. Puis, comme il nous restait du temps, nous avons décidé de programmer à partir de ces fonctions, deux modes de jeu : un mode 1 joueur et un mode à 2. Nous nous sommes répartis le travail et chacun de nous a développé son propre mode et nous les avons assemblés dans le programme final.
Notre programme
Nous avons essayé de rester les plus fidèles au jeu original même si comme nous le verrons plus tard, nous n’avons pas implémenté toutes les règles présentées précedemment.
Concernant l’affichage, avant que le joueur ne fasse sa proposition, nous affichons en majuscules les lettres (bien placées) qui ont été découvertes jusque là (la première lettre étant donnée dès le départ), les lettres manquantes étant remplacées par un tiret. Puis après la saisie du mot, nous affichons en majuscule les lettres communes avec les mots à découvrir bien placées et en minuscule les lettres communes mal placées.
La partie : notre jeu offre la possibilité de jouer à un ou à deux . Après avoir sélectionner le nombre de joueur, la longueur du mot à deviner est demandé (entre 5 et 10 lettres). Puis dans le cas d’une partie à 2, le nombre de grilles de jeu sur lesquelles les adversaires souhaitent s’affronter est demandé. Dans le mode solo, chaque partie s’effectue sur une seule grille.
Les règles : dans la version 1 joueur pour qu’un essai soit validé, le mot proposé doit figurer dans le dictionnaire (et doit être bien orthographié), il doit commencer par la bonne lettre et doit être de la bonne longueur. Si le mot n’est pas valide, il est redemandé au joueur sans qu’il ne soit pénalisé par la perte d’un essai. Voici un exemple de partie :
Dans la version 2 joueurs, la main passe
dans chacun des cas précédents
au cas ou le mot n’est pas découvert au terme des 6 essais, auquel cas l’adversaire bénéficie d’une seule tentative pour découvrir le mot, mais conserve la main pour la grille suivante en cas d’échec.
Les fonctions
la fonction dico_n_lettres
def dico_n_lettres(dico):
#fonction qui crée une fois pour toute des dictionnaires
#de mots avec un nombre donné de lettres à partir d un
#fichier texte
for n in range(1,25):
path=os.getcwd()+'/' # définit le répertoire ou se trouve le ditionnaire
dico1 = path+dico+".txt" # fichier contenant le dictionnaire
dico2 = path+dico+str(n)+".txt" # fichier qui contiendra le dictionnaire des mots à n lettres
fichier1 = open(dico1, "r")
fichier2 = open(dico2, "w")
for mot in fichier1:
if len(mot) == n+1: # n+1 car on doit tenir compte du caractère de retour à la ligne
fichier2.write(mot)
fichier1.close()
fichier2.close()
Nous avons récupéré sur le site https://www.lama.univsavoie.fr/pagesmembres/hyvernat/Enseignement/1415/info113/tp6.html deux dictionnaires sous la forme de fichiers .txt . Le premier contrairement au second contient les formes conjuguées des verbes et les pluriels des noms. La fonction précédente a pour but de créer une seule fois (ils sont installés sur le répertoire courant lors de la première exécution du programme), à partir du dictionnaire choisi, des dictionnaires constitués uniquement des mots de même longueur.
La fonction choisir_un_mot
def choisir_un_mot(dico,n):
#fonction qui va choisir aléatoirement un mot
#de n lettres
path=os.getcwd()+'/'
dico = path+dico+str(n)+".txt"
fichier = open(dico, "r")
liste_mots = fichier.readlines() # met tous les mots du fichiers dans une liste
i = randint(1,len(liste_mots))
mot = liste_mots[i] # prend au hasard un mot dans la liste
mot = mot.replace('\n','') # supprime le caractère de retour à la ligne
fichier.close()
return(mot)
Cette fonction va utiliser le dictionnaire créé par la fonction précédente pour sélectionner aléatoirement un mot de n lettres qui servira de mot à deviner dans le jeu.
La fonction lettres_placées
def lettres_placees(liste1,liste2,liste3):
# fonction qui met dans liste1 les lettres
# communes bien placées des liste2 et liste3
liste = []
liste0 = []
for i in range(len(liste2)):
if liste2[i]==liste3[i]:
liste0.append(liste2[i])
else:
liste0.append('')
for i in range(len(liste1)):
if liste1[i]==liste0[i]:
liste.append(liste1[i])
else:
liste.append(liste1[i]+liste0[i])
return(liste)
Avec la fonction lettres_communes elles sont le cœur de notre programme. Cette fonction a pour but de mettre à jour la liste des lettres bien placées découvertes depuis le début de la partie. La variable liste1 contient la liste des lettres bien placées à mettre à jour et liste2 et liste3 les listes à comparer (le mot mystère et le dernier mot joué) et dont les lettres communes et bien placées vont être ajoutées à liste1.
La fonction lettres_communes
def lettres_communes(liste1,liste2):
# Retourne une liste avec toutes les lettres
# de la liste1 se trouvant dans la liste2
# les lettres bien placées sont en majuscule
# les autres sont en minuscule.
liste = []
liste3 = []
for i in range(len(liste1)):
liste.append('')
liste3.append(liste2[i])
for i in range(len(liste1)):
if liste1[i]==liste3[i]:
liste[i]=liste1[i].upper()
liste3[i]=''
for i in range(len(liste1)):
for j in range(len(liste1)):
if liste1[i]==liste3[j] and liste[i]=='':
liste[i]=liste1[i].lower()
liste3[j]=''
break
return(liste)
Cette fonction est chargée de trouver les lettres communes entre liste1 et liste2, (qui représentent le mot joué et le mot à deviner) retournant ces lettres en majuscule pour les lettres bien placées et en minuscules pour les autres. Une des principales difficulté dans la conception de cette fonction a été la gestion des lettres dont le nombre d’apparition était différente dans les deux mots.
La fonction teste_mot
def teste_mot(liste,lettre,dico,n):
# Teste si le mot proposé par le joueur
# est valide : il doit commencer par 'lettre'
# se trouver dans dico et être de n lettres.
test=True
mot=''.join(liste).lower() #transforme 'liste' en chaine de caractère en minuscule
path=os.getcwd()+'/'
dico = path+dico+str(n)+".txt"
fichier = open(dico, "r")
liste1 = fichier.readlines() # met tous les mots du dico dans une liste
liste_mots=[]
for mot1 in liste1:
liste_mots.append(mot1.replace('\n',''))
if len(liste)!=n: # teste si le mot est de la bonne longueur
test=False
print('Ce mot n est pas de la bonne longueur.')
elif (mot not in liste_mots): # teste si le mot est dans le dico
test=False
print('Ce mot n est pas dans le dictionnaire.')
elif liste[0]!=lettre: # teste si le mot commence par la bonne lettre
test=False
print('Ce mot ne commence pas par la lettre '+lettre)
fichier.close()
return(test)
Cette fonction a pour but la validation (ou pas) de chaque mot entré par le joueur. Le mot est validé s’il commence par la bonne lettre, est constitué du bon nombre de lettres et se trouve dans le dictionnaire.
Les difficultés rencontrées
1. Pour la création et la gestion des dictionnaires nous nous sommes aidés du site https://python.sdv.univ-paris-diderot.fr/07_fichiers/ qui explique clairement les manipulations de base des fichiers (ouverture en mode lecture/écriture, fermeture, mise sous forme de liste de chaîne de caractères du contenu,…) et nous avons dû nous familiariser avec le module os pour indiquer le répertoire dans lequel se trouve les dictionnaires. Une des difficulté qui est apparue lors de la création des dictionnaires vient du fait que la longueur des mots ne correspondait pas à celle demandée (par exemple les mots de 5 lettres apparaissaient lorsqu’on en demandait 6. C’est après plusieurs tests que nous avons compris que le décalage venait du caractère ‘saut de ligne’ présent mais qui n’apparaissait pas quand on affichait les mots.
2. Pour la fonction « lettres_communes » la gestion des lettres dont le nombre d’apparition était différente entre les mots à comparer nous a aussi posé quelques problèmes. Pour y remédier, la solution que nous avons trouvé a été d’ « effacer » du mot à deviner une lettre commune une fois qu’elle a été trouvée (pour éviter de la recompter si elle apparaît une nouvelle fois dans le mot joué), en remplaçant la lettre en question par le caractère ‘ ‘. Alors que cette opération d’effacement s’effectuait en local (uniquement à l’intérieur de la fonction), la liste modifiée l’était également dans le programme principal. Pour régler le problème, nous avons décidé d’utiliser une liste intermédiaire, pour éviter de toucher à la liste passée en argument. Mais le résultat fut le même. Après quelques recherches sur internet, nous avons compris que le problème était inhérent aux listes et après plusieurs tentatives , nous sommes parvenus à régler le problème en passant par une liste auxiliaire créée non plus par une simple affectation mais en la remplissant grâce à une boucle et la commande .append().
3. Nous avons tenté de prendre en compte la gestion du temps, c’est-a-dire limiter à 10 s la durée de chaque essai mais la simple utilisation du module time ne donnait pas de résultat satisfaisant. En cherchant sur internet, nous avons vu qu’il était possible de créer un minuteur grâce au module threading mais après quelques essais cela s’est avéré plus compliqué que prévu et nous avons décidé de renoncer à cette fonctionnalité pour cette version mais nous espérons et envisageons de l’ajouter dans le futur.
Ce projet libre nous a permis de réaliser notre premier jeu en python. Après avoir longuement hésité, nous avons finalement opté pour MOTUS qui s’est finalement révélé très intéressant à programmer avec quelques challenges. Bien sûr, il reste encore beaucoup à accomplir pour se rapprocher du jeu original, notamment l’utilisation d’une interface graphique grâce au module tkinter ou encore la gestion du temps. Nous avions également pensé à intégrer des niveaux de difficulté en créant par exemple des dictionnaires adaptés (en associant à chaque mot un coefficient indiquant sa « difficulté » sur le même principe que le Scrabble). Toutes ces améliorations feront peut-être l’objet d’un futur projet.
Les couchers de soleils vous fascinent ? Vous êtes ici au bon endroit pour apprendre à créer des levers et couchers de soleil de votre choix sans devoir attendre le crépuscule. Je vous explique en détail mon script qui permet de générer une image avec le module turtle en python.
Les prémices du projet
Dans un premier temps, j’ai longtemps réfléchi à ce que je pouvais faire pour le projet libre de cette fin d’année. Je me suis décidée à refaire une image générée par le module turtle en python. Effectivement, notre tout premier projet de l’année était le même, néanmoins j’avais tout de suite commencé très fort en faisant des fractales. Certains vous diront que cela est facile, mais mon niveau de début d’année ne me permettait pas de tout comprendre. Ce qui m’a frustré. Je me suis donc décidée à refaire une image avec mon niveau actuel (qui est encore débutant !) pour pouvoir gérer la conception de A à Z.
Par conséquent, le script et l’image peuvent vous paraître assez simple. Cependant, j’ai souhaité m’inspirer du projet https://nsi.xyz/art/urbanisme-la-skyline-de-new-york/ qui se rapproche du flat design (ils l’expliquent très bien dans leur article). Le rendu final est donc très épuré et très simple.
Le code
Pour concevoir cette image, j’ai procédé par étapes pour créer le code. En découpant mon image, en plusieurs parties.
Le fond
Tout d’abord, j’ai codé le fond c’est-à-dire le coucher de soleil. L’espace utilisé est découpé en morceau rectangulaire. Puis pour chaque rectangle est associé une couleur. Je définis donc en premier mes neuf couleurs. Ensuite, j’utilise une boucle for, qui prend à chaque itération de la boucle la couleur suivante contenu dans la liste couleur.
def coucherdesoleil():
couleur = ["#FFBA08",
"#FAA307",
"#F48C06",
"#E85D04",
"#DC2F02",
"#D00000",
"#9D0208",
"#6A040F",
"#370617"]
penup()
goto(640,-300)
setheading(180)
pendown()
for fond in couleur: # ChatGPT m'a aidé sur cette ligne et la ligne suivante
color(fond)
begin_fill()
forward(1280)
right(90)
forward(74)
right(90)
forward(1280)
end_fill()
left(180)
Le résultat :
Les étoiles
Puis, je crée une fonction qui permet de dessiner des étoiles. Elle prend en paramètre la longueur des branches, et l’emplacement de l’étoile, tout en définissant sa couleur.
def etoile(longueur, x, y):
goto(x,y)
pencolor("#DF7B0A")
pendown()
left(50)
for i in range(5):
forward(longueur)
right(144)
penup()
Je souhaitais que les étoiles se situent seulement en haut du ciel, tout en se positionnant de façon aléatoire, et de taille aléatoire. Ainsi par la boucle for , dix étoiles sont créées avec des paramètres précis (taille et position) mais de façon aléatoires.
for i in range(10):
etoile(randint(5,15), randint(-630,630), randint(293, 350))
Le résultat :
Le soleil
Ensuite, le script exécute la fonction créant le soleil. Il se positionne à des coordonnées précises, je positionne le curseur en direction du haut de l’image : setheading(90), puis j’utilise la fonction prédéfinie du cercle, en indiquant la taille du rayon et si je souhaite un demi-cercle ou un cercle.
Pour cette fonction, je me suis inspirée voir même j’ai repris la boucle créant l’ondulation des montagnes de mon ancien projet https://nsi.xyz/art/jardins-foret-enneigee/. Afin de créer une montagne, il faut lui donner comme paramètres d’entrée sa position initiale et finale (pour qu’ensuite elle puisse être remplie), la couleur, la direction, l’angle et la longueur des vagues.
De plus, pour varier la taille des montagnes, j’ai utilisé un test conditionnel afin de modifier l’inclinaison en fonction de sa position initiale.
Enfin, la boucle for permet de créer une vague, qui se répètent trois fois, formant une ondulation.
def rocher(x1, y1, x2, y2, couleur, direction, angle, vague1, vague2):
fillcolor(couleur)
pencolor(couleur)
goto(x1,y1)
if direction == 0:
setheading(0)
right(angle)
else:
setheading(direction)
left(angle)
begin_fill()
pendown()
for i in range (3): # boucle repris de mon ancien projet https://nsi.xyz/art/jardins-foret-enneigee/
for i in range (5):
forward(vague1)
right(2)
for i in range (5):
forward(vague2)
left(2)
goto(x2,y2)
penup()
end_fill()
Le résultat final
Les problèmes rencontrés et leurs solutions
Le premier problème était que les montagnes ne s’arrêtaient pas toutes à la même ligne, il y avait donc des débordements en dehors du cadre. Ma solution a été de créer une ligne blanche repassant sur ces débordements. Je pense qu’il y avait sûrement une solution ou un bout de code permettant de stopper les boucles quand elles arrivaient à un certain endroit, mais j’ai préféré utiliser cette méthode.
Le deuxième problème a été l’emplacement des étoiles. Effectivement, au début vingt étoiles étaient créées avec des emplacements aléatoires, par conséquent il y avait des superpositions. J’ai donc décidé de réduire le nombre, afin que les étoiles aient moins de chance d’obtenir la même position.
Le troisième problème n’en est pas vraiment un, mais cela était plutôt un souhait que je n’ai pas réussi à effectuer. En effet, je souhaitais faire un beau dégradé pour le coucher de soleil. J’en avais vu un sur cet article https://nsi.xyz/art/perspective-un-paysage-synthwave/ j’ai longtemps essayé de comprendre la fonction et j’ai même demandé à ChatGPT, mais sans succès. J’ai donc décidé de ne pas copier coller un code que je ne comprenais pas et j’ai laissé le coucher de soleil actuel.
Mes sources
J’ai principalement codé avec mes connaissances, les cours de NSI et les anciens projets disponibles sur le site https://nsi.xyz/. Toutefois, j’ai eu besoin de faire des recherches et j’ai utilisé ChatGPT à plusieurs reprises pour qu’il me montre mon erreur lorsque je ne comprenais pas le message d’erreur ou quand je ne comprenais pas tout à fait les explications que je trouvais sur internet.
En conclusion, je suis très fière de mon image. Même si son codage est effectué avec les base de la programmation et peut paraître facile. J’ai pris plaisir à programmer, comprendre, corriger toute seule mes erreurs, afin de concevoir une image propre à mes connaissances et à mes envies.
Télécharger le .py
Si vous souhaitez générer l’image et l’utiliser comme fond d’écran n’hésitez pas à télécharger le fichier ci-dessous.
Étudiante en spécialité NSI en classe de 1ère en 2022.
« I have never tried that before, so I think I should definitely be able to do that. » Astrid Lindgren
Dans cet article, nous allons nous interresser à la programmation et à l’architecture de la version 2 de Pong. Le jeu est écrit en Python pour numworks sous Oméga, car cette version offre plus de tas (100ko contre 32ko dans la version officielle).
Pourquoi une seconde version ?
La première version avait une architecture assez peu soignée car je ne l’avais pas pensé de manière globale. De plus, elle avais des manques de compression assez flagrants car elle fesait plus de 12ko. Pour le petite anecdote, j’avais du la compresser énormément pour pouvoir la faire tourner car elle souffrait d’un dépassement de mémoire. Paradoxalement, c’est un de mes jeux qui a le mieux marché, sans doute car il était très complet. Mais je ne voulais pas laisser à un programmeur qui tombe sur le jeu l’image que c’est un travail que je trouve a posteriori presque bâclé. J’ai donc entrepris de le réécrire.
Première étape : programmer les menus.
Moteur de menu
Pour une fois, j’ai voulu commencer par programmer le menu (j’ai appelé le moteur menulib). Je pourrais alors adapter l’architecture du moteur de jeu à l’architecture globale. Pour programmer ce moteur de menu, je suis parti d’une syntaxe de création de menu un peu alambiquée, qui rassemble touts les éléments iterractifs (le moteur ne s’occupe que des éléments interractifs, les titres ou autre doivent être dessinés à part): l’unique fonction prend en parametre un liste de tupple qui contiennent eux même le type, le titre et si besoin les valeurs de l’élément :
menu ( position x, position y, elements list , [color] , [background color] )
Les éléments on chacun un identifiant :
Type
ID
Syntaxe
Information
Button
« btn »
["btn","name"]
Simple button. Not intended to be used as a text
List
« lst »
["lst","name",("element1","element2"),current element index]
Element that allow the user making a choice beetwen the different elements purposed
Slider
« sld »
["sld","name",(min val,max val),current value]
Element that allow the user chosing a value bettwen min val and max val
(Extrait de la documentation, que j’avais écrite en anglais)
La fonction retourne alors le nom du bouton sélectionné et l’indexe de chaque liste ou slider.
De cette manière, j’ai pu créer facilement des menus et récupérer le résultat de l’interaction avec l’utilisateur. Exemple ci dessous avec le menu principal
def mainMenu(conf=base_conf):
transition(conf) #petite animation
drawSprite(LOGO,10,10,12,conf[Col2]) #dessiner le logo
drawRect(10,80,300,142,conf[Col3]) #dessiner le fond
act_el=[["btn","Play"],["lst","Mode",("Solo","2 Player","Vs Comp."),conf[Mode]],["btn","Game Options"],["btn","Graphics Options"]]
#défini les éléments
get_act=menu(20,100,act_el,conf[Col1],conf[Col3]) # appelle la fonction et affecte le résulat de l'action à get_act
conf[Mode]=get_act[1]["Mode"] #défini la configuration du mode de jeu à la sélection.
if get_act[0]=="Game Options":gameMenu(conf) #analyse quel bouton a été selectionné
elif get_act[0]=="Graphics Options":graphMenu(conf)
else:gameEngine(conf)
La configuration et la persistance des options
Vous avez peut-être remarqué que les fonctions mainMenu, graphMenu, gameMenu et transition prennent toutes en paramètre la variable conf. C’est une liste, en quelque sorte un fichier de configuration, qui contient toutes le informations et paramètres du programme. Cette liste est enregistrée dans un fichier sous forme de chaine grâce à la fonction str() elle est décompressé à partir de ce fichier de manière totalement transparente grâce à la fonction eval() :
def saveConf(conf):
try : #fonction try utilisée pour éviter des arrêts suite à une potentielle erreur.
with open("pong.conf","w") as f:
f.truncate(0) #on vide le fichier ...
f.write(str(conf)) #... et on réécrit la chaine
except: print("Saving configuration failed.") #message d'erreur en cas d'échec, mais cela ne stop pas le programme
def loadConf():
try: #fonction try utilisée pour éviter des arrêts suite à une potentielle erreur ou si la configuration n'a pas encore été enregistrée
with open("pong.conf","r") as f:return eval(f.readline()) #comme la chaine correspond à une liste, elle est évaluée comme une liste par eval()
except: #si le fichier n'existe pas
print("Loading configuration failed.")
return base_conf #on retourne la configuration par défaut
Pour plus de lisibilité du code, chaque élément de la configuration (qui rappelons-le, est une liste) n’est non pas atteint grace à un index mais grace à une constante qui a pour valeur l’index correspondant :
Pour gérer les theme, j’ai simplement utilisé la configuration. Les couleurs et le numéro du thème y sont contenus et mis à jour quand cela est nécessaire. Pour mettre à jour ces couleurs, une fonction récupère le numéro du thème dans la configuration et renvoie cette configuration avec les bonnes couleurs :
def setTheme(conf,nb):
conf[Theme]=nb #définit le numéro du thème dans la configuration
a,b,c,d=(255,255,255),(255,200,0),(100,100,100),(60,60,60) #défini les 4 couleurs du thème de base
if nb==1:a,b,c,d=d,(200,150,60),(200,200,200),a #thème light : réarrange et modifie les couleurs
elif nb==2:b=(220,50,50) #theme omega
elif nb==3:b=(200,100,200) #theme nsiOs
conf[Col1:BgCol+1]=a,b,c,d #on defini les couleurs dans la config
return conf #et on renvoie la config
Le moteur de jeu
Architecture et déplacement
J’ai longtemps hésité sur le choix de l’architecture. J’ai bien évidemment directement pensé à une classe en orienté objet qui pourrait s’occuper à la fois des pads et de la balle. Mais cette option est assez très gourmande en mémoire et j’ai aussi pensé à une fonction « constructrice » qui retournerais une liste avec toutes les information de l’objet. Finallement, j’ai tout de même opté pour une classe Entity() qui contient différentes fonctions très utiles :
class Entity():
def __init__(it,x,y,w,h,col,bg_col):
#Fonction d'initialisation des attributs
#Défini la position, la taille, la couleur, etc...
it.x,it.y,it.w,it.h,it.col,it.bg_col=x,y,w,h,col,bg_col
it.spd_x,it.spd_y=0,0
it.last_draw=(int(it.x-it.w//2),int(it.y-it.h//2),int(it.w),int(it.h),it.bg_col)
def hitBox(it,it2):
#Fonction qui renvoie vrai si it (objet 1) touche it2 (objet 2)
if it.x-it.w//2<it2.x+it2.w//2 and it.x+it.w//2>it2.x-it2.w//2 and it.y-it.h//2<it2.y+it2.h//2 and it.x+it.w>it2.x and it.y<it2.y+it2.h//2 and it.y+it.h//2>it2.y-it2.h//2:return True
else: return False
def applyVec(it):
#Fonction qui applique la vitesse de l'objet à sa position
it.x+=it.spd_x
it.y+=it.spd_y
def hideObj(it):
#Fonction qui dessine un rectangle de la couleur du fond pour cacher le dernier affichage de l'objet
drawRect(*it.last_draw)
def drawObj(it,detail=0):
#Fonction qui dessine un rectangle à la place de l'objet
it.last_draw=[int(it.x-it.w//2),int(it.y-it.h//2),int(it.w),int(it.h),it.bg_col]
if detail:
for x2,y2 in zip((2,0),(0,2)):drawRect(int(it.x-it.w//2)+x2,int(it.y-it.h//2)+y2,int(it.w)-x2*2,int(it.h)-y2*2,it.col)
else: drawRect(*it.last_draw[0:4]+[it.col])
Ensuite, j’ai amélioré le déplacement de la balle. Je lui ai ajouté un attribut ball.a qui représente son angle, et à chaque collision sa vitesse latérale et horizontale est redéfinie grace à une fonction :
def vec(s,a): #la fonction prend en parametre la vitesse s et l'angle a
a=radians(a)#les fonctions trigo prennent l'angle en radians
x=s*cos(a)
y=s*sin(a)
return x,y #retour du vecteur vitesse en 2D
Pour les collisions : c’est encore plus simple. J’utilise une fonction qui prend en paramètre l’angle de l’objet et de la surface, qui calcule l’angle d’incidence et qui retourne l’opposé :
def collide(a1,a2):
return a2-simp(a1-a2)
#la fonctio simp() simplifie un angle pour qu'il soit compris entre 0° et 360°.
Apres, je « n’avais qu’a » associer des touches aux pad, gérer leur collision avec le haut et la bas et détecter quand la balle est en dehors.
Afficher le score
Il ne restait plus qu’a afficher le score (bon, en fait il restait pas mal de petits truc à faire mais je vais pas les détailler ici). Pour cela, j’ai utilisé la technique de Schraf : Math-infos que j’ai adapté. J’ai d’abord créé une fonction drawSprite() un peu compliquée, qui grosso modo lit un chiffre pour le décompresser en binaire; et lit chaque bit. Si le bit est de 1, un carré est dessiné. Si le bit est de 0, on laisse la case vide. La fonction drawNumber permet d’automatiser l’affichage d’un sprite pour chaque chiffre du nombre désiré.
Dans le cadre du projet libre de fin d’année en NSI, je vous présente mon premier jeux vidéo en python fait à l’aide de pygame, une librairie spécialisé pour la création de jeux vidéo.
Librairie Pygame
Pygame est une bibliothèque de jeu open-source pour Python, qui permet de créer des jeux et des applications multimédias. Elle fournit des outils pour gérer les graphismes, le son, les entrées utilisateur, le réseau et plus encore.
Pygame utilise la bibliothèque SDL (Simple DirectMedia Layer) pour accéder aux fonctions du système d’exploitation, ce qui signifie que les jeux créés avec Pygame peuvent fonctionner sur de nombreuses plateformes, notamment Windows, macOS et Linux.
Pygame est une excellente option pour les débutants qui cherchent à créer des jeux simples (des base en programmation sont cependant nécessaire), mais il peut également être utilisé pour créer des jeux professionnels et des applications multimédias complexes.
Mon objectif sur ce projet a été de faire mes premiers pas avec la bibliothèque Pygame en Python. Cela a été un véritable défi qui m’aura fait découvrir de nombreuses mécaniques en Python, telles que le système de classe ou la programmation orientée objet, qui ont représenté pour moi un pas dans l’inconnu. Mon défi a donc été de créer un jeu simple pour débuter. Le but est de contrôler un personnage avec sa souris et d’éviter des briques qui apparaissent aléatoirement sur le côté de l’écran, tout cela en ayant le choix entre trois niveaux de difficulté différents.
Structure du Script
Pour la réalisation de mon jeu, j’ai décidé de me lancer un défi supplémentaire : l’utilisation de la Programmation Orientée Objet. Mais qu’est-ce que la Programmation Orientée Objet ?
La Programmation Orientée Objet est un paradigme de programmation qui permet de structurer un programme en utilisant des objets, qui sont des entités possédant des caractéristiques et des comportements spécifiques. Cette approche permet de modéliser des concepts du monde réel de manière plus naturelle et intuitive, en regroupant des fonctionnalités liées dans des classes, et en instanciant ces classes pour créer des objets.
En résumé, cela consiste à organiser son code de manière à ce qu’il soit ordonné à l’aide de classes contenant des fonctions et des constantes générales, ce qui conduit à un code plus lisible, plus maintenable et plus ordonné. Cependant, cela peut être relativement perturbant au début.
Voici un simple scripte sans utilisation de Programmation Orienté Objet qui permet de faire bougé une image nommé « ball.png » à l’aide des flèches directionnelles :
import pygame
pygame.init()
screen = pygame.display.set_mode((400, 400))
running = True
image = pygame.image.load("ball.png")
x = 0
y = 0
clock = pygame.time.Clock()
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
pressed = pygame.key.get_pressed()
if pressed[pygame.K_LEFT]:
x -= 1
if pressed[pygame.K_RIGHT]:
x += 1
if pressed[pygame.K_UP]:
y -= 1
if pressed[pygame.K_DOWN]:
y += 1
screen.fill((0, 0, 0))
screen.blit(image, (x, y))
pygame.display.flip()
clock.tick(60)
pygame.quit()
Le scripte est simple et efficace, pour autant si il venais à se complexifier cela deviendrait vite illisible. voici maintenant le même scripte avec l’utilisation de la Programmation Orienté Objet :
La particularité ici réside dans la clarté de chaque fonction et de leur utilité respective : « display » pour l’affichage, « handling_event » pour les interactions claviers-souris, « move » pour les mouvements du joueur… De plus, le programme est divisé en deux fichiers distincts : l’un pour le programme principal et l’autre pour le joueur. Ainsi, il est possible d’ajouter aisément de nouvelles fonctions pour des améliorations futures. Par exemple, la fonction « Update », actuellement inactive, pourrait être employée pour effectuer des vérifications, telles que les collisions.
Analyse du script
Passons maintenant à l’analyse du code.
Commençons par mon fichier « player.py », avec l’appel du module Pygame, la création de la classe et la fonction d’initialisation.
import pygame #On importe le module Pygame
class Player: #On crée la classe Player
# La fonction init est une fonction d'initialisation qui permet de déclarer toutes nos variables.
# Elle prend comme arguments self, qui est pris dans chaque fonction de la classe, x et y qui servent à définir la position initiale du joueur sur l'écran.
def init(self, x, y):
self.image = pygame.image.load("asset/Player.png")# On charge l'image du joueur
#Pour que vous puissiez être familiarisé avec le concept de rect, ce sont des rectangles que l'on ne voit pas, qui ont une taille que l'on leur définit et c'est grâce à ces rect, que l'on pourrait appeler hitbox, que l'on peut détecter des collisions.
self.rect = self.image.get_rect(center=(x, y))# On récupère le rectangle de l'image et on le centre en (x,y)
self.velocity = [0, 0]# On initialise la vitesse à 0 dans les deux directions
Une fois la mise en place de notre classe et de la fonction d’initialisation effectuée, nous créons la fonction de mouvement du joueur appelée « move », ainsi qu’une fonction « draw » qui permettra d’afficher notre personnage avec son rectangle.
def move(self, mouse_pos):# La fonction move permet de déplacer le joueur en fonction de la position de la souris, l'argument mouse_pos représente la position de la souris sur l'écran. Cette position est utilisée pour calculer la vitesse de déplacement du joueur, en soustrayant la position de la souris à la position actuelle du joueur.
# On calcule la différence entre la position de la souris et la position actuelle du joueur
self.velocity[0] = (mouse_pos[0] - self.rect.centerx)
self.velocity[1] = (mouse_pos[1] - self.rect.centery)
self.rect.move_ip(self.velocity)# On déplace le rectangle du joueur en fonction de la vitesse calculée
def draw(self, screen):# La fonction draw permet d'afficher le joueur sur l'écran
screen.blit(self.image, self.rect)# On blitte (st utilisée pour dessiner une surface sur une autre surface.) l'image du joueur sur le rectangle de celui-ci
Maintenait que je vous ai introduis mon fichier joueur passons au fichier jeu.
import pygame
import random
from player import Player
On importe les modules nécessaires au fonctionnement du jeu : Pygame pour gérer l’affichage graphique et l’interaction avec l’utilisateur, random pour générer des nombres aléatoires, et Player qui est une classe définie dans un fichier séparé et qui représente le joueur.
On définit la classe Game, qui représente le jeu en lui-même. Son constructeur prend en paramètre l’objet screen qui représente la surface sur laquelle le jeu sera affiché.
On initialise différentes variables, comme self.running qui permet de savoir si le jeu est en cours d’exécution ou non, self.player qui représente le joueur, self.background qui est l’image de fond, self.brique qui est l’image représentant les obstacles à éviter, et plusieurs variables liées à ces obstacles (self.brique_positions, self.brique_speed, self.brique_spawn_rate, self.brique_spawn_counter, self.brique_spawn_augmentation, self.brique_spawn_increase_counter).
On définit également self.total_time et self.total_time_max pour gérer le temps de jeu, et self.difficulté qui est utilisé pour régler la difficulté du jeu en fonction de la réponse de l’utilisateur à une question posée par input.
Enfin, on masque le curseur de la souris avec pygame.mouse.set_visible(False).
def gestion_events(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.running = False
self.player.move(pygame.mouse.get_pos())
La méthode gestion_events permet de gérer les événements liés à l’interaction de l’utilisateur avec la fenêtre Pygame.
Le code utilise une boucle for pour récupérer chaque événement dans la liste des événements de Pygame. Si l’un des événements est de type QUIT, cela signifie que l’utilisateur a cliqué sur le bouton de fermeture de la fenêtre. Dans ce cas, la variable self.running est définie sur False, ce qui interrompt la boucle de jeu.
La méthode appelle également la méthode move de l’objet Player pour mettre à jour sa position en fonction de la position de la souris de l’utilisateur.
def adaptative_brique_rects(self):
self.brique_rects = []
for pos in self.brique_positions:
rect = pygame.Rect(pos[0], pos[1], self.brique.get_width(), self.brique.get_height())
self.brique_rects.append(rect)
La méthode adaptative_brique_rects est appelée pour mettre à jour les rects de collision des briques en fonction de leurs nouvelles positions.
def spawn_brique(self):
y = random.randint(0, self.screen.get_height() - self.brique.get_height())
self.brique_positions.append((-self.brique.get_width(), y))
La méthode spawn_brique est appelée pour générer une nouvelle brique à une position aléatoire sur l’axe y et avec une position initiale en dehors de la fenêtre sur l’axe x.
def move_briques(self):
for i in range(len(self.brique_positions)):
pos = list(self.brique_positions[i])
pos[0] += self.brique_speed
self.brique_positions[i] = tuple(pos)
self.adaptative_brique_rects()
La méthode move_briques met à jour la position des briques en ajoutant la valeur de self.brique_speed à la coordonnée x de chaque position dans self.brique_positions.
def update(self):
# Incrémenter le compteur de temps de spawn des briques par la difficulté du jeu
self.brique_spawn_counter += self.difficulté
# Augmenter la vitesse des briques si le compteur de spawn des briques est un multiple de 120
if self.brique_spawn_counter % 120 == 0:
self.brique_speed += 0.2
# Vérifier si le compteur d'augmentation de spawn des briques a atteint sa limite
self.brique_spawn_increase_counter += 1
if self.brique_spawn_increase_counter >= self.brique_spawn_augmentation * 60:
self.brique_spawn_increase_counter = 0
self.brique_spawn_rate -= 10
# Ajouter une nouvelle brique à la liste des positions de briques si le compteur de spawn des briques a atteint le taux de spawn des briques
if self.brique_spawn_counter >= self.brique_spawn_rate:
self.brique_spawn_counter = 0
self.spawn_brique()
# Déplacer toutes les briques dans la direction de la vitesse actuelle des briques
self.move_briques()
# Incrémenter le temps total de jeu de 1
self.total_time += 1
# Vérifier si le joueur a perdu en entrant en collision avec une brique
for rect in self.brique_rects:
if rect.colliderect(self.player.rect):
print("_____________________________________________________________________________\n")
print(" Vous avez perdu, Votre temps est de", self.total_time/100, "secondes, bien joué à vous ;)")
print("_____________________________________________________________________________\n")
self.running = False
break
else:
pass
La méthode update est appelée à chaque boucle de jeu pour mettre à jour les positions des briques, vérifier si le joueur a perdu en entrant en collision avec une brique et pour augmenter la difficulté du jeu.
Elle commence par incrémenter le compteur de temps de spawn des briques par la difficulté du jeu, pour que les briques apparaissent plus rapidement avec des niveaux de difficulté plus élevés.
Ensuite, si le compteur de spawn des briques est un multiple de 120, la vitesse de déplacement des briques est augmentée de 0,2.
La méthode vérifie également si le compteur d’augmentation de spawn des briques a atteint sa limite, qui est définie par self.brique_spawn_augmentation * 60. Si c’est le cas, le compteur est réinitialisé et le taux de spawn des briques est diminué de 10.
Si le compteur de spawn des briques atteint le taux de spawn des briques défini par self.brique_spawn_rate, la méthode spawn_brique est appelée pour ajouter une nouvelle brique à la liste des positions de briques.
La méthode move_briques est ensuite appelée pour déplacer toutes les briques dans la direction de la vitesse actuelle des briques.
Le temps total de jeu est ensuite incrémenté de 1.
Enfin, pour chaque rect de brique dans la liste self.brique_rects, la méthode vérifie s’il y a une collision avec le rectangle de la zone de collision du joueur. Si c’est le cas, la méthode affiche le message de fin de jeu et met la variable self.running à False pour arrêter la boucle de jeu.
def display(self):
# Afficher le fond d'écran
self.screen.blit(self.background, (-200, -350))
# Parcourir toutes les positions des briques et les afficher
for pos in self.brique_positions:
self.screen.blit(self.brique, pos)
# Afficher le joueur
self.player.draw(self.screen)
# Mettre à jour l'affichage
pygame.display.flip()
La méthode display(self) affiche les éléments du jeu sur l’écran. Elle utilise la méthode blit pour afficher le fond d’écran, chaque brique de la liste brique_positions et le joueur (player) sur l’écran. Ensuite, elle appelle la méthode flip de la classe pygame.display pour mettre à jour l’affichage.
def run(self):
# Boucle principale du jeu
while self.running:
# Gérer les événements (ex: appuyer sur une touche, quitter le jeu)
self.gestion_events()
# Mettre à jour le jeu (ex: déplacer les briques, vérifier les collisions)
self.update()
# Afficher le jeu
self.display()
# Limiter le nombre de frames par seconde à 60
self.clock.tick(60)
# Réafficher la souris et quitter Pygame
pygame.mouse.set_visible(True)
pygame.quit()
La méthode run(self) contient la boucle principale du jeu. Elle s’exécute tant que l’attribut running est vrai. À chaque itération de la boucle, elle gère les événements (appel à gestion_events()), met à jour les éléments du jeu (appel à update()), affiche le contenu du jeu sur l’écran (appel à display()) et attend un certain temps défini par la méthode tick de pygame.time.Clock. Enfin, elle quitte le jeu (pygame.quit()) lorsque la boucle est terminée.
# Initialiser Pygame
pygame.init()
# Créer une fenêtre de jeu
screen = pygame.display.set_mode((1080, 720))
# Créer une instance de la classe Game en lui passant la fenêtre de jeu en argument
game = Game(screen)
# Lancer le jeu
game.run()
# Quitter Pygame
pygame.quit()
La partie en dehors de la classe initialise la bibliothèque Pygame (pygame.init()) et crée une fenêtre de jeu (pygame.display.set_mode((1080, 720))). Ensuite, elle crée une instance de la classe Game avec l’écran en tant qu’argument et appelle la méthode run de l’instance pour démarrer le jeu. Enfin, elle quitte la bibliothèque Pygame (pygame.quit()) après la fin du jeu.
En conclusion, ce projet est un petit jeu simple mais amusant, il est sur l’univers de Mario. Si vous voulez essayer ce jeu par vous-même, vous pouvez le télécharger en cliquant sur le lien ci-dessous. Pour lancer le jeu, assurez-vous d’écrire le niveau de difficulté souhaité (1, 2 ou 3) dans la console Python. Si vous ne le faites pas, la fenêtre restera noire et le jeu ne se lancera pas. On commande le personnage avec sa souris.
L’AxiDraw est une machine permettant de dessiner sur une feuilles avec un stylo selon un programme. En spé Nsi nous avons la possibilité de l’utiliser pour ce projet de fin première.
Introduction
J’ai décidé de reprendre un logo tiré du manga « 20th century boys », car j’aime beaucoup le design et il me semblait simple à recréer avec le code python pour l’AxiDraw.
Le début du travail
Nous avons appris comment utiliser l’AxiDraw avec nos professeurs pendant le cours, et donc la première chose à comprendre a été de coder ce dessin. C’est plutôt simple à comprendre, en effet l’AxiDraw marche seulement avec les coordonnés, on a des fonctions « segment » et « segments » fourni.
def segment(x_depart:int, y_depart:int, x_arrivee:int, y_arrivee:int):
if not coord_in_cadre(x_depart,y_depart) or not coord_in_cadre(x_arrivee,y_arrivee):
# raise ValueError("Dépassement du cadre imposé")
global dessin_valide
dessin_valide = False
print("Segment ne pouvant être tracé")
return
if axi_connect and dessin_valide:
ad.goto(x_depart * 8.267 / 793.7, y_depart * 8.267 / 793.7)
ad.pendown() # Le stylo est baissé
ad.lineto(x_arrivee * 8.267 / 793.7, y_arrivee * 8.267 / 793.7)
ad.penup() # Le stylo est relevé en fin de tracé
else:
draw.line((x_depart, y_depart, x_arrivee, y_arrivee), fill = black) # Tracé du segment
def segments(points:list):
for i in range(len(points) - 1):
if not coord_in_cadre(points[i][0],points[i][1]) or not coord_in_cadre(points[i+1][0],points[i+1][1]):
global dessin_valide
dessin_valide = False
print("Segments ne pouvant être tracés")
return
if axi_connect and dessin_valide:
for i in range(len(points)):
points[i][0], points[i][1] = points[i][0] * 8.267 / 793.7, points[i][1] * 8.267 / 793.7
ad.goto(points[0][0], points[0][1])
ad.draw_path(points)
ad.penup()
else:
for i in range(len(points)-1):
draw.line((points[i][0], points[i][1], points[i+1][0], points[i+1][1]), fill = black)
Ces deux fonctions ont besoin de coordonnés précises pour fonctionner. Pour avoir ces coordonnés j’ai eu une idée plutôt simple à utiliser, j’ai utilisé Paint car l’application donne les coordonné de la souris en temps réel. La dernière étape était de simplifier mon dessin d’origine en usant de segments et de points, puis j’ai pu reprendre tout les coordonnées et les mettre dans mon code python.
Différentes parties du dessin :
Pour l’intérieur de l’oeil j’ai utilisé les fonctions cercles codé par mes professeurs, cette fonction a besoin des coordonnés du centre du cercle et du rayon pour pouvoir dessiner les cercles. C’est assez facile à dessiner, voici le résultat :
Pour tout le contour de l’oeil et la main à l’intérieur, j’ai utilisé la fonction segments qui permet de dessiner plusieurs segments les uns à la suite des autres à partir des coordonné de chaque point noté sur le screenshot de paint juste au dessus.
Le dessin que j’ai choisi n’étant pas vraiment répétitif je n’ai pas pu utilisé de fonction crée par moi même alors j’ai simplement tout fait avec les fonctions segment, segments, cercle et polygone.
Voici tout le code que j’ai écris et l’image final :
Nous avons réalisé ce projet dans le cadre d’un travail demandé par nos professeurs de NSI, nous avions le choix entre différents projets mais avons choisi le morpion car nous voulions réaliser un jeu pour finir l’année en beauté.
Introduction
Cet article vous présentera le dernier projet de la spécialité NSI que nous avons réalisé avec Bilal L., Corentin K. et Joseph A.
Pour ce dernier projet nous avons décidé de faire le jeu du Morpion sur la calculatrice Numworks en python. Pour cela nous avons dû utiliser différents modules et faire des recherches variées pour savoir comment coder ce jeu. C’est ce que nous allons vous expliquer dans ce compte rendu.
Bonne lecture !
Quelques fonctions basiques mais importantes
Premièrement, nous avons regardé quelques vidéos et sites web pour savoir par où commencer, nous sommes finalement tombés sur la vidéo de « Schraf : Maths-info” (une chaîne YouTube spécialisée en informatique) : “Python – Jeu du type Morpion avec la tortue (Part 1)”.
Dans cette vidéo, il commence par faire la grille donc nous l’avons suivi et avons récupéré le script de celle-ci après l’avoir compris :
H, L =320 , 222
def grille(nb):
case = min(H,L) // nb
H_m, L_m = -nb * case / 2, -nb * case /2
for i in range(1,nb):
trait(L_m, H_m + case * i, 0, nb * case)
trait(L_m + case * i, H_m, 90, nb * case)
return case, H_m, L_m
grille(3)
La 1ère ligne permet, en fonction de la taille de l’écran de faire les différentes cases demandées par le joueur.
La seconde ligne, elle, permet de tracer les différentes cases avec le bon écart.
Ensuite, grâce à la fonction trait : la grille est tracée.
Une fois la fonction achevée, on l’appelle avec le nombre de case que l’on veut, ici, il est de 3.
Puis, il nous faut le cercle et la croix pour les placer sur la grille, on a donc fait une fonction pour chaque :
def croix():
pensize(2)
right(45)
pendown()
for i in range(4):
right(90)
forward(40)
penup()
backward(40)
pendown()
setheading(90)
penup()
Au début, nous avons eu un problème avec le cercle car il ne se crée pas directement au centre mais sur les côtés, on a donc dû rajouter à la fonction un forward pour que le centre du cercle se place au centre de la colonne.
Pour que les joueurs puissent comprendre les règles du jeu pour ce qu’il ne connaissent pas ou ne savent pas appuyer sur quelles touches de la calculatrice pour jouer, nous avons décidé de faire deux pages d’accueil pour tout expliquer.
draw_string("Appuyez sur EXE pour continuer",20,4
0,(255,128,0))draw_string("Règles du jeu : \n\n ·Chaque joueur possèdent un \n symbole différent.\n ·Le premier avec 3 symboles\n sur une ligne gagne.",0,75,(0, 0,0))
while not (keydown(KEY_EXE)):True
Nous avons fait la première page d’accueil qui indique les règles du jeu en utilisant la fonction draw_string qui permet d’écrire du texte de la façon demandée. Ensuite avec le module Ion, il nous suffit d’appuyer sur la touche “EXE” de la calculatrice pour passer à la seconde page d’explication.
Le cœur du morpion
Le déplacement entre les cases
Cela fait, il nous fallait pouvoir déplacer naviguer entre les différentes cases.
On a donc utilisé le module Ion qui est sur la calculatrice Numworks, il permet d’utiliser les touches de la calculatrice pour y relier des actions, ici : se déplacer ou dessiner les croix et cercles.
Nous avons eu beaucoup de mal à utiliser ce module au début mais c’est en regardant un autre code de “Schraf : Maths-info” sur le jeu de la vie que nous avons compris comment il fonctionnait.
Dans un premier temps, il faut utiliser keydown puis mettre la touche que l’on veut utiliser en paramètre.
Par exemple:
if keydown(KEY_ONE):
goto(-75,-74)
croix()
Ici, quand la touche 1 de la calculatrice est enfoncée, la tortue se déplace aux coordonnées demandées et se met à dessiner la croix.
Dans l’ordre des choses, il suffit donc que chaque case soit assignée à une touche.
Et à partir de là, on peut dessiner toutes les croix ou des cercles à n’importe quelle case.
Cela nous a permis de voir le début d’un jeu fonctionnel et nous a largement motivés quant à la suite de ce projet !
La vérification de victoire
La vérification permet de vérifier si un des joueurs a gagné, et si oui, lequel. C’est sûrement ce qu’il nous a le plus posé de problème au cours du projet.
D’abord, nous avons dû apprendre à savoir ce qu’est une matrice (une liste de liste) :
C’est une liste de listes où chaque liste contient 3 éléments, ici vides.
Comme dit plus haut, on doit percevoir cette matrice comme la grille du morpion, pour en quelque sorte les “relier” :
if j == 1:
if keydown(KEY_ONE) and matrice[0][0] == " ":
indice = 0
goto(-75,-74)
croix()
matrice[0][0] = j
j = 2
verification_croix()
Dans la ligne « matrice[0][0] = j », on associe le 1er emplacement de la 1ère liste de la matrice à j, donc à 1 (ce même nombre renvoie à l’utilisation de la croix).
On associe alors indirectement la case en haut à gauche à la croix, puis on lance la fonction verification_croix.
def verification_croix():
for i in range (3):
if matrice[i][0] == matrice[i][1] == matrice[i][2] == 1:
victoire_croix()
for i in range(3):
if matrice[0][i] == matrice[1][i] == matrice[2][i] == 1:
victoire_croix()
if matrice[0][0] == matrice[1][1] == matrice[2][2] == 1:
victoire_croix()
if matrice[0][2] == matrice[1][1] == matrice[2][0] == 1:
victoire_croix()
Dans cette fonction qui vérifie si le joueur croix a gagné, on vérifie pour les 8 combinaisons gagnantes du morpion si les emplacements de listes de la matrice correspondants (donc de la grille) sont égaux à 1 (donc à la croix).
Si c’est effectivement le cas, on lance la fonction victoire_croix qui affiche un écran de victoire bien mérité pour le joueur croix.
Difficultés rencontrées
Lors de la conception du jeu, nous sommes tombés sur plusieurs pépins.
Bien sûr, nous avons dû les surmonter, cependant, il y a eu de l’apprentissage mais également du bricolage.
On peut prendre l’exemple de la 1ère tentative de vérification qui était quelque peu fatiguée :
def verification_cercle():
if get_pixel(-75,-74) and get_pixel(0,-74) and get_pixel(75,-74) = (254, 254, 254):
victoire_cercle()
elif get_pixel(-75,-0) and get_pixel(0,0) and get_pixel(75,0) = (254, 254, 254):
victoire_cercle()
elif get_pixel(-75,-74) and get_pixel(0,74) and get_pixel(75,74) = (254, 254, 254):
victoire_cercle()
On utilise la couleur du milieu de la croix et du cercle pour vérifier si la combinaison est gagnante.
La croix possède un pixel noir en son milieu et le cercle fait quelques aller-retours en son milieu en une couleur quasi-blanche (en RBG) afin de la différencier du fond blanc.
À l’aide de la méthode get_pixel(), on récupère la couleur d’un pixel à une coordonnée précise et on la compare à une valeur fixe, ici (254,254,254), du quasi-blanc.
Si 3 pixels de la même et de cases à combinaison gagnante sont alignés, l’écran de victoire du joueur concerné s’affiche. Cependant, rien ne se passait quand une combinaison gagnante apparaissait, on a donc décidé de changer d’approche malgré la tristesse de ne pas pouvoir exploiter cette méthode de pure ingéniosité…
Ensuite l’un des autres problèmes majeurs était de lier la matrice à la grille.
En effet, lors de la conception du jeu, nous voulions au début faire déplacement libre sur la matrice avant d’appuyer sur un 2ème bouton pour valider la case dans laquelle on veut jouer.
Nous avons donc décidé d’enlever le déplacement libre, c’est-à-dire que dès que l’on clique sur un chiffre, une croix ou un rond se met directement à l’emplacement voulu, cela nous a permis de pouvoir relier la matrice à la grille plus facilement:
if keydown(KEY_ONE) and matrice[0][0] == " ":
goto(-75,-74)
croix()
matrice[0][0] = j
j = 2
verification_croix()
if keydown(KEY_TWO) and matrice[0][1] == " ":
goto(0,-74)
croix()
matrice[0][1] = j
j = 2
verification_croix()
Comme on peut le voir sur le script ci-dessus, la matrice et la grille sont constamment liées.
Conclusion
Ainsi, nous avons l’immense fierté d’avoir conçu ce jeu vidéo sur la calculatrice, nous avons pu en apprendre plus sur le python et les différents modules qu’il possède qui nous étaient alors inconnus jusqu’à présent.
Au début de la conception de ce jeu vidéo, le travail nous paraissait immense et nous n’avions aucune idée d’où commencer ni de quoi faire, des problèmes ont été rencontrés mais nous avons pu trouver des solutions en codant, testant, en faisant des erreurs, nous corrigeant et en s’améliorant.
Il est parfois compliqué de se motiver pour travailler surtout en période de vacance scolaire. C’est pourquoi j’ai choisis lors de ce projet de NSI de mêler travail et passion en vous proposant un projet en lien avec l’aéronautique.
Naissance du projet :
Lorsque nos professeurs ont présenté les différentes possibilités parmi lesquelles nous pourrions réaliser notre projet, j’ai tout de suite pensé à utiliser l’ Axidraw et ce pour plusieurs raisons.
Tout d’abord, pour découvrir cette machine mais aussi pour les nombreuses possibilités d’utilisation que cette mécanique avait à m’offrir.
Ensuite, j’ai utilisé la maxime » Il faut joindre l’utile à l’agréable » en choisissant d’utiliser ma passion, qui est celle de l’aéronautique, afin de pouvoir travailler sans regret durant les vacances.
C’ est ainsi que je me suis mis à coder un Rafale ( un célèbre avion de chasse Français ), en python !
Développement du projet
Pour commencer, il me fallait une image sur papier, une sorte de plan, de ce à quoi je voulais que mon Rafale ressemble, et ce tout en traits puisque l’Axidraw ne peut tracer de courbes.
Ma « feuille de route », une véritable œuvre d’art…
Mise en place
Une fois après avoir réalisé mon esquisse, il ne restait plus qu’à mettre à l’échelle mes différentes valeurs que je pouvais dès maintenant mesurer à la règle.
Ainsi, on obtenait :
1cm sur la feuille en longueur -> 30 de longueur pour l’AXIDRAW
1cm sur la feuille en largeur -> 36.5 de largeur pour l’AXIDRAW.
Ensuite, j’ai crée de nombreuses listes que j’ai ensuite appelé en utilisant les fonctions mises à notre disposition par nos professeurs de NSI :
Ainsi, dans l’exemple ci-dessus nous pouvons voir la liste permettant de faire la perche qui sert au ravitaillement en vol du rafale avec son appellation par la fonctions segments qui relie les points aux coordonnées x et y définis dans ma liste. L’appellation de la fonction polygone va elle rajouter un segment qui ira de mon premier point au dernier présent dans la liste haut_aileron afin de former un polygone.
Création des verrières du rafale.
Comme vous avez pu le voir dans mon esquisse de l’œuvre que je voulais produire, les verrières du rafale devaient être constellées de traits afin de donner un esthétique au rendu final mais aussi afin d’introduire quelques fonctions dans ma création.
Mais, je ne pouvais pas créer une seule et même fonction que j’appellerai pour les trois verrières puisque ces mêmes verrières étaient disposées à des endroits différents et séparés par l’armature de l’avion.
image des différentes verrières que je voulais remplir
Ainsi, j’ai du créer trois verrières :
Première verrière :
Pour cette première verrière, vu sa configuration, je n’avais pas besoin de créer un code trop important. En effet, une simple boucle for permettrait de répéter x fois en fonction du nombre de répétitions des segments, grâce à la fonctions segments fournie, en décalant x_depart, y_depart et y_arrivee à chaque répétition. Ainsi, nous obtenons ce code :
def trait(x_depart: float, y_depart: float, x_arrivee: float, y_arrivee: float, repetitions: int):
for i in range(repetitions):
segment(x_depart, y_depart, x_arrivee, y_arrivee)
x_depart += 4.5
y_depart -= 2.25
y_arrivee -= 2.25
Seconde verrière :
Pour cette verrière, il n’était cette fois plus question de prendre une boucle for puisque si vous regardez bien elle se compose de deux parties. J’ai donc utilisé une succession de deux boucles while consécutives qui détermineraient les variations des points x et y en fonction des coordonnées d’arrivée des points de mes segments.
Pour cette troisième verrière, j’ai réutilisé le code de ma seconde verrière en changeant les points x_depart, x_arrivee, y_depart, y_arrivee ainsi que les variations de ces points en fonction des boucles while ce qui nous donne :
Ainsi, après avoir appelé ces fonctions j’obtenais les verrières en image ci-dessous :
Néanmoins, comme vous vous en doutez surement, cette réalisation n’a pas été aussi facile que ce qu’il vous a été présenté puisque il a fallu calculer les variations des points x et tester différentes variations des points y afin d’obtenir le meilleur rendu visuel.
Création du réacteur :
Pour le réacteur, j’ai souhaité ajouter des segments de la même manière que pour mes verrières. J’ai donc repris le code utilisant les boucles while que j’ai développé puisqu’il ma fallu créer une troisième boucle while pour obtenir le rendu souhaité.
L’exécution de ce code ma ensuite donné ce rendu :
Dernières finitions :
Maintenant que les parties complexes de mon code ont été réalisées, il ne me restait plus que deux choses à faire :
Positionner la fameuse cocarde tricolore ( qui sera unicolore sur mon rendu graphique car l’Axidraw ne prend pas en charge les couleurs ) sur mon rafale.
« Légender » mon œuvre en écrivant le nom de l’avion représenté en dessous.
Pour la cocarde, je n’eu qu’à utiliser la fonction cercle déjà donnée et en tracer deux consécutivement, avec le même centre mais avec le rayon du second cercle moitié moins grand que le rayon du premier cercle.
Pour légender le dessin obtenu, il ne me restait seulement qu’ à exécuter une succession de segments formant le mot RAFALE.
Conclusion :
Ainsi, ce projet m’a permis de normaliser mon utilisation des fonctions et des listes. Néanmoins, je pense que de nombreuses améliorations auraient pu être possible avec un peu plus de temps notamment en ajoutant des segments pour rendre mon image plus précise mais aussi en modifiant la bordure dessinée par l’Axidraw afin qu’elle mette plus en valeur le rafale représenté.
Mais je dois quand même ajouter que je suis fier de ce que j’ai déjà réalisé car jamais je ne me serais attendu à un rendu comme celui là en n’ utilisant que des segments !
Pour ce dernier projet de cette année, j’ai décidé de faire, le tout sur la calculatrice Numworks, un ensemble de Julia en utilisant des palettes de couleurs, cela donnera une image bien plus belle que le script Mandelbrot de la calculatrice par exemple.
D’où vient l’idée ?
J’ai déjà réalisé des projets sur les fractales, mais surtout sur les ensemble de Mandelbrot. Cette fois, j’ai voulu faire un ensemble de Julia, la « mère » des ensemble de Mandelbrot. Mais je voulais pousser la technique un peu plus loin.
Pour rendre mes fractales encore plus belles, j’ai voulu créer une palette de couleur ce qui rendra les dégradés de couleurs bien plus beaux.
Qu’est-ce qu’un ensemble de Julia ?
Avant d’expliquer comment faire cette palette, qu’est-ce qu’un ensemble de Julia ?
Un ensemble de Julia, c’est une fractale qui peut avoir plusieurs formes, ici, j’utilise l’ensemble ayant pour valeur du nombre complexe c : 0,36 + 0,36i ce qui nous donne cela quand on met 100 itérations maximum :
Comment ajouter et rendre les couleurs de ma fractale plus belle ainsi que les problèmes rencontrés ?
Avant de commencer à regarder les modifications, il faut comprendre la différence entre les couleurs de mon premier script et les couleurs de la palette. Dans le premier script, ma couleur prenait le nombre d’itérations pour se définir (voir si elle est rouge, bleu, vert…). La palette, elle, regarde à quel point le pixel est loin ou près de la fractale pour choisir la teinte et la couleur du pixel.
Pour créer une fractale de l’ensemble de Julia en utilisant des palettes de couleurs, j’ai procédé comme cela :
Tout d’abord à partir du script Mandelbrot de la Numworks et grâce à des recherches, je crée un script Julia qui fabrique un ensemble de Julia avec c = 0,36 + 0,36J. Cela donne une image comme ci-dessus. les lignes :
z = complex(0,0)
c = complex(xmin+*x/319-2.5, -2.5*y/221+1.25)
Deviennent :
z = complex(xmin+(xmax-xmin)*x/320+(ymax-(ymax-ymin)*y/222)*1J)
c = complex(0.36,0.36)
Ensuite, je crée dans mon script deux boucles for j in range qui vont me fabriquer une liste « palette » de 255 listes ayant 3 valeurs chacune qui représente les valeurs RGB :
palette = []
r = 255
g = 255
b = 255
for j in range(0,128):
b = 255 -2 * j
palette.append([r,g,b])
for j in range(128,256):
r = 255 -2 * (j-128)
g = 255 -2 * (j-128)
palette.append([r,g,b])
Explication : Pour modifier la couleur, je crée au début de ma fonction JULIA 3 variables, R pour le rouge, G pour le vert et B pour le bleu qui ont chacune comme valeur initiale 255. Pour réaliser la fractale, je rentre dans le range() les valeurs 0 et 128 pour faire une coupure entre quand je transforme le blanc en bleu et quand je transforme le bleu en noir. Je modifie les variables R et G en faisant 255 – 2 * j ce qui va diminuer les deux valeurs, puis je les rajoute dans une liste de ma palette qui contient mes valeurs R, G et B à chaque nouvelles valeurs de j dans une autre boucle for qui va de 128 à 255 je modifie la valeur du bleu avec l’équation 255 – 2 * (j – 128) et je rajoute à nouveau une liste avec les valeurs r g et b.
Je modifie la définition de ma variable « col » pour qu’elle récupère les trois valeurs contenues dans l’une des listes de ma palette et je rajoute une variable couleur pour rendre plus simple le code :
couleur = palette[int(255*i/N_iteration)]
col = color(couleur[0],couleur[1],couleur[2])
Et voilà, quand je lance mon programme avec 100 itérations, cela me donne cette fractale :
Code complet :
from kandinsky import*
def Julia(N_iteration):
palette = []
xmax = 2
xmin = -2
ymax = 1.3875
ymin = -1.387
r = 255
g = 255
b = 255
for j in range(0,128):
b = 255 -2 * j
palette.append([r,g,b])
for j in range(128,256):
r = 255 -2 * (j-128)
g = 255 -2 * (j-128)
palette.append([r,g,b])
for x in range(320):
for y in range(222):
i = 0
z = complex(xmin+(xmax-xmin)*x/320+(ymax-(ymax-ymin)*y/222)*1J)
c = complex(0.36,0.36)
while i < N_iteration and abs(z) < 2:
i = i + 1
z = z*z+c
couleur = palette[int(255*i/N_iteration)]
col = color(couleur[0],couleur[1],couleur[2])
set_pixel(x,y,col)
Tout d’abord la couleur, pour l’instant, je n’ai seulement réussi à faire les 6 couleurs des synthèses additive et soustractive (rouge, vert, bleu, magenta, cyan et jaune). Je m’y suis pris trop tard pour faire les autres couleurs, car il faut modifier les équations des lignes qui modifie les variables R, G et B. Pour faire cela il faut modifier le code comme ceci :
for j in range(0,128):
b = 255 -2 * j
palette.append([r,g,b])
for j in range(128,256):
r = 255 -2 * (j-128)
g = 255 -2 * (j-128)
palette.append([r,g,b])
On peut modifier le point de vue en modifiant les valeurs de ces lignes :
xmax = 2
xmin = -2
ymax = 1.3875
ymin = -1.387
Il faut faire attention aux proportions sinon l’image sera distordue, par exemple si on met ces valeurs, on obtient :
Et on peut aussi faire comme sur l’image ci-dessous en passant par plusieurs couleurs en faisant plusieurs boucles for j in range :
Ici, on passe du gris, au jaune, au rouge, puis au noir.
En conclusion :
Même si ce travail ne m’a pas permis d’apprendre de nouvelles choses en python, il m’a fait travailler mon raisonnement pour programmer de futurs codes. Je me suis bien amusé à le faire, car je l’ai fait avec mon père qui avait déjà fait la même chose, mais dans d’autres langages de programmation comme RUST, et vu qu’il n’est pas très fort en python, même si il a la réflexion pour le code, nous avons pu croiser nos compétences pour finir le programme. Le seul bémol, la résolution de la calculatrice est tellement faible que cela nous donne une image très pixelisée.
Vous souhaitez dessiner avec précision mais vous n’avez pas le coup de main ? Vous désirez représenter plusieurs fois des images ou textes à l’identique ? L’AxiDraw est fait pour vous !
Qu’est ce que l’AxiDraw ?
Dans le cadre de notre projet libre de fin d’année en NSI, nos professeurs, Monsieur Robert et Monsieur Clemente, nous ont présenté l’AxiDraw qui permet de dessiner un projet que l’on a imaginé.
L’AxiDraw est une machine composée de deux moteurs et d’un bras automatisé au bout duquel il est possible d’ajouter différents types d’outils de graphisme.
Son fonctionnement est assez simple : il suffit de programmer les traits que l’on souhaite tracer, à l’aide du logiciel « Inskape » ou avec du langage python, et l’AxiDraw réalise alors notre modèle au format SVG (Scalable Vector Graphics).
Par où commencer ?
Au préalable, il est primordial de définir parfaitement le rendu final désiré.
Il existe différentes façons de le faire. Pour ma part, j’ai choisi de dessiner mon modèle à la main mais on peut aussi préparer son projet à l’aide de logiciels comme Paint.
J’ai donc dessiné une maison sur une feuille à petits carreaux afin de respecter plus facilement une échelle donnée. En effet, notre réalisation ne devait pas être supérieure à un cadre défini par une abscisse comprise entre 20 et 1040 pixels et une ordonnée comprise entre 20 et 730 pixels.
Programme et fonctions de codage
Une fois mon modèle prêt, il ne restait plus qu’à passer au codage !
Nos professeurs nous ont fourni un programme où de nombreux paramètres étaient déjà prédéfinis, comme la bordure de la feuille, la vitesse de l’AxiDraw ou encore différentes fonctions (pouvant tracer une succession de segments par exemple). J’ai donc utilisé ce programme et je l’ai inséré dans l’IDE Thonny. J’ai ensuite installé la bibliothèque Pyaxidraw et Pillow afin que ce dernier puisse fonctionner.
J’ai alors pu commencer réellement la programmation de mon dessin.
Tout d’abord, il était nécessaire de convertir toutes les longueurs, sur ma feuille en centimètres, en pixels. A partir des valeurs obtenues, j’ai alors défini les points délimitant les segments que je souhaitais tracer.
J’ai notamment utilisé la fonction « segments », qui permet de tracer une succession de segments en fonction d’une liste d’abscisses et d’ordonnées de points successifs, comme présentée ci dessous:
def segments(points:list):
for i in range(len(points) - 1):
if not coord_in_cadre(points[i][0],points[i][1]) or not coord_in_cadre(points[i+1][0],points[i+1][1]):
global dessin_valide
dessin_valide = False
print("Segments ne pouvant être tracés")
return
if axi_connect and dessin_valide:
for i in range(len(points)):
points[i][0], points[i][1] = points[i][0] * 8.267 / 793.7, points[i][1] * 8.267 / 793.7
ad.goto(points[0][0], points[0][1])
ad.draw_path(points)
ad.penup()
else:
for i in range(len(points)-1):
draw.line((points[i][0], points[i][1], points[i+1][0], points[i+1][1]), fill = black)
ma_liste = [(926.25,661.5625),(958.75,661.5625),(975,593.125),(910,593.125),(926.25,661.5625)]
segments(ma_liste)
Cet extrait de code permet, une fois la fonction « segments » définie, de produire très simplement et d’un seul trait l’image suivante:
D’autres fonctions comme celles traçant des cercles, des rectangles ou des paraboles m’ont aussi été très utiles.
Utilisation de boucles for
Lors de la réalisation de mon dessin, j’ai veillé à utiliser des motifs qui se répétaient afin de réduire le nombre de lignes de code et ainsi, me faciliter le travail.
J’ai donc par exemple utilisé la boucle for i in range() afin de créer un panneau de bois sur la façade de la maison en soustrayant une même longueur à la hauteur du rectangle à chaque incrémentation :
def rectangle(x_depart,y_depart,delta_x,delta_y):
polygone([[x_depart,y_depart],[x_depart + delta_x,y_depart],[x_depart + delta_x,y_depart + delta_y],[x_depart,y_depart + delta_y]])
for i in range(9):
rectangle(568.75,319.375,113.75, 91.25 - 11.40625*i)
Image associée :
J’ai aussi voulu dessiner des platebandes d’herbe avec des boucles for i in range() mais ne sachant pas comment coder un demi-cercle, j’ai utilisé la fonction parabole, ce qui a d’ailleurs donné un rendu plus réaliste. Afin de donner un effet de profondeur à la succession de paraboles, l’ordonnée du point initiant la parabole devait être différente de l’ordonnée du point terminant la parabole. Pour cela, il fallait veiller à ajouter la bonne valeur à chaque incrémentation. En effet, il ne fallait donc plus ajouter l’écart exact entre les deux abscisses (ici 6) mais cette valeur moins 1 (ce qui revenait à ajouter 5).
def parabole1(x_depart, y_depart):
return[(a+x_depart,a**2+y_depart) for a in range(-3,3)]
for i in range(37):
segments(parabole1(341.25+5*i,638.75))
for i in range(26):
segments(parabole1(480+5*i,604))
Image associée :
Rendu final :
Après ces petites explications, je vous partage le rendu final de mon travail qui peut être réalisé par l’AxiDraw que nous avons la chance d’avoir au Lycée Louis Pasteur d’Avignon !
Merci pour votre lecture.
Télécharger le .py :
Et pour finir, vous pouvez télécharger mon projet afin que vous puissiez le découvrir plus en détail !