Harvard CS50 - Introduction à la programmation avec Python
Jan 30th, 2024
Graduer en tant qu'ingénieur mécanique ne m'a presque pas fourni les connaissances de base nécessaires en programmation (à l'exception des mathématiques et un peu de Matlab). Au cours de mes années de codage professionnel, j'ai accumulé d'énormes connaissances en informatique, mais de temps en temps, je ressens encore qu'il y a beaucoup de place pour s'améliorer.
C'est pourquoi j'ai décidé de suivre le cours CS50 de Harvard. Il s'agit du premier cours CS50 de Harvard que je me suis fixé pour terminer.
Chaque cours a un projet final, et ma mise en œuvre de celui-ci sera présentée dans ces publications de blog liées à CS50.
Le Pendu - le jeu en ligne de commande (CLI)
Le jeu suit les règles standards du jeu du pendu:
- Vous avez dix chances pour essayer de deviner le mot lettre par lettre.
- Vous ne pouvez pas répéter les lettres déjà devinées.
- Si vous devinez correctement la lettre, cela ne compte pas dans votre total de 10 essais.
- Si vous atteignez dix essais infructueux, le jeu se termine et vous perdez.
- Si vous devinez le mot avant d'atteindre dix essais, vous gagnez.
Le jeu a deux modes :
- Mode normal | Mot anglais aléatoire de 5 lettres
- Mode difficile | Mot anglais aléatoire de 6 lettres
Selon les règles standard du jeu du pendu, une partie du pendu est dessinée pour chaque essai infructueux. Si vous atteignez dix tentatives infructueuses, un dessin complet sera réalisé.
Le Pendu - Écran de démarrage
Le Pendu - Écran de démarrage
Après avoir terminé le jeu (gagné ou perdu), vous pouvez commencer une autre partie ou quitter le programme.
Le Pendu - Partie perdue
Le Pendu - Partie perdue
Le Pendu - Partie en cours
Le Pendu - Partie en cours
Le Pendu - Partie gagnée
Le Pendu - Partie gagnée
Structure du code
Pour jouer au jeu, clonez le projet, installez Python, déplacez-vous dans le dossier racine du dépôt, installez les dépendances avec
pip install -r requirements.txt
et exécutez
python project.py
dans le terminal.
project.py
- comme indiqué dans les directives, le code se compose d'une fonction principale et de trois fonctions au même niveau, nécessaires pour les tests. Cependant, pour des raisons de conservation de l'état, j'ai initialisé et conservé l'ensemble de mon jeu à l'intérieur d'une classe Game, qui se trouve dans le même fichier. Je voulais éviter l'utilisation de variables globales.
Les trois fonctions requises au même niveau d'indentation que la fonction principale implémentent des appels aux méthodes de la classe Game afin que les exigences finales du projet soient remplies. Mais je tiens à souligner que toutes les méthodes de la classe ont également été testées de manière approfondie.
test_project.py
- contient tous les tests du projet
requirements.txt
- contient toutes les dépendances du package et leurs versions utilisées dans le projet
data/words.py
- contient deux listes de mots anglais de cinq et six lettres utilisées lors du choix des modes de jeu
components/separators.py
- contient des fonctions utilisées pour dessiner le jeu du pendu dans l'interface en ligne de commande (CLI)
components/stages.py
- contient les étapes de chacun des dessins des tentatives incorrectes pour le jeu du pendu. Les étapes ne suivent délibérément pas le principe DRY, afin qu'elles soient plus faciles à entretenir et à examiner. Si cela était une application plus importante, où la performance serait critique, ces étapes ainsi que les dessins de l'écran de démarrage pourraient être optimisés.
Classe Game - Description détaillée
Comme mentionné précédemment, la classe
Game
encapsule l'ensemble de la logique du jeu.
Voici la répartition des principales méthodes et de leurs propriétés:
- __init__: Ce méthode initialise les variables liées au jeu utilisées à la fois pour rendre les blocs de construction du jeu dans l'interface en ligne de commande (CLI) et pour la logique du jeu.
def __init__(self):
self.separator_length = 40
self.game_screen = 0
self.game_mode = "0"
self.guessed_letters = []
self.used_letters = []
self.word = ""
self.failures = 0
self.already_used_letter = ""
project.py - Game class
- Getters and Setters: J'ai utilisé ces propriétés à la fois pour "empêcher" la manipulation directe des variables d'état initialisées dans init et pour contrôler la manière dont ces variables devraient être mises à jour et récupérées.
@property
def game_screen(self):
return self._game_screen
@game_screen.setter
def game_screen(self, n):
self._game_screen = n
@property
def game_mode(self):
return self._game_mode
@game_mode.setter
def game_mode(self, n):
self._game_mode = n
@property
def guessed_letters(self):
return self._guessed_letters
@guessed_letters.setter
def guessed_letters(self, new_values):
if len(new_values) == 2 and 0 <= new_values[1] < len(self.guessed_letters):
self._guessed_letters[new_values[1]] = new_values[0]
elif len(new_values) == 1:
self._guessed_letters = new_values[0]
else:
self._guessed_letters = []
@property
def used_letters(self):
return self._used_letters
@used_letters.setter
def used_letters(self, new_values):
if len(new_values) == 2 and new_values[1]:
self._used_letters.append(new_values[0])
elif len(new_values) == 1:
self._used_letters = new_values[0]
else:
self._used_letters = []
@property
def word(self):
return self._word
@word.setter
def word(self, n):
if n == "1":
self._word = random.choice(words_five_letters).upper()
elif n == "2":
self._word = random.choice(words_six_letters).upper()
else:
self._word = n # for testing purposes
@property
def failures(self):
return self._failures
@failures.setter
def failures(self, n):
self._failures = n
@property
def already_used_letter(self):
return self._already_used_letter
@already_used_letter.setter
def already_used_letter(self, n):
self._already_used_letter = n
project.py - Game class
- run_game: C'est ainsi que démarre le jeu. Il utilise une boucle while True et des fonctions d'entrée pour s'arrêter et attendre à chaque itération de la boucle. Il contrôle l'ensemble du flux de la logique du jeu en exécutant d'autres méthodes telles que start_game et main_game. Il est également responsable de la plupart des changements de variables d'état.
def run_game(self):
self.clear_terminal()
while self.game_screen <= 1:
if self.game_screen == 0:
self.start_game()
while self.game_mode != "1" and self.game_mode != "2":
user_input = input("Mode: ")
self.game_mode = user_input
self.word = user_input
self.guessed_letters = [[" " for _ in range(len(self.word))]]
self.game_screen = 1
elif self.game_screen == 1:
self.clear_terminal()
self.main_game()
if self.is_game_finished():
user_input = input("Decision: ").upper()
else:
user_input = input("Guess a letter: ").upper()
self.already_used_letter = ""
if len(user_input) == 1 and user_input.isalpha():
###
## check if hit, miss, or repeat guess
# hit
if user_input in self.word and user_input not in self.used_letters:
indexes_of_hit = [index for index, char in enumerate(self.word) if char == user_input]
for index in indexes_of_hit:
self.guessed_letters = [user_input, index]
# repeat
elif user_input in self.used_letters:
self.already_used_letter = user_input
# miss
else:
self.failures = self.failures + 1
###
# add to used letters list
if user_input not in self.used_letters:
self.used_letters = [user_input, True]
elif user_input == "YES":
self.restart()
elif user_input == "NO":
self.quit()
break
project.py - Game class
- start_game: Affiche l'écran initial de l'interface en ligne de commande (CLI) que les utilisateurs voient lorsqu'ils commencent le jeu. Sur cet écran, l'utilisateur est invité à sélectionner la difficulté du jeu. En fonction de la sélection, la méthode run_game définit un mot aléatoire à deviner et guide l'utilisateur vers l'écran principal du jeu. Sinon, cette méthode n'a pas de logique spéciale, elle se contente de dessiner le jeu.
def start_game(self):
draw_separator(self.separator_length)
draw_separator(self.separator_length)
self.empty_space()
self.empty_space()
self.empty_space()
center_text(text="HANGMAN", length=self.separator_length, draw_border=True, double_border=True)
self.empty_space()
self.empty_space()
self.empty_space()
draw_separator(self.separator_length)
draw_separator(self.separator_length)
self.empty_space()
center_text(text="Enter mode to start the game", length=self.separator_length, draw_border=True)
center_text(text="[1] - Normal mode", length=self.separator_length, draw_border=True)
center_text(text="[2] - Hard mode", length=self.separator_length, draw_border=True)
self.empty_space()
draw_separator(self.separator_length)
print("2023 - Alan Jereb".rjust(self.separator_length))
center_text(text="", length=self.separator_length)
project.py - Game class
- main_game: Affiche l'écran principal du jeu dans l'interface en ligne de commande (CLI), qui, via run_game, suit les lettres déjà utilisées, les suppositions correctes de l'utilisateur et dessine les étapes de l'impiccato pour chacune des dix tentatives infructueuses. Après dix tentatives infructueuses ou la réussite du jeu, l'utilisateur est invité à redémarrer le jeu, ce qui ramène le joueur à l'écran initial, ou à terminer le jeu, ce qui ferme le programme. Sinon, cette méthode n'a pas de logique spéciale. Elle se contente de dessiner le jeu.
def main_game(self):
draw_separator(self.separator_length)
self.empty_space()
stage_functions = [stage.zero, stage.one, stage.two, stage.three, stage.four, stage.five, stage.six,
stage.seven, stage.eight, stage.nine, stage.ten]
if 0 <= self.failures <= 10:
stage_functions[self.failures](self.separator_length)
self.empty_space()
self.empty_space()
if self.user_has_won():
center_text(text="You win!", length=self.separator_length, draw_border=True, double_border=True)
elif self.user_has_failed():
center_text(text="You lose!", length=self.separator_length, draw_border=True, double_border=True)
self.empty_space()
if not self.user_has_failed():
center_text(text=(" ".join(self.guessed_letters)), length=self.separator_length, draw_border=True, double_border=True)
center_text(text=("__ " * len(self.word)), length=self.separator_length, draw_border=True, double_border=True)
else:
center_text(text=self.word, length=self.separator_length, draw_border=True, double_border=True)
self.empty_space()
draw_separator(self.separator_length)
if self.is_game_finished():
center_text(text="To play another game type: yes", length=self.separator_length, draw_border=True)
center_text(text="To quit the game type: no", length=self.separator_length, draw_border=True)
else:
center_text(text="Used letters:", length=self.separator_length, draw_border=True)
center_text(text=", ".join(self.used_letters[0:9]), length=self.separator_length, draw_border=True)
center_text(text=", ".join(self.used_letters[9:]), length=self.separator_length, draw_border=True)
draw_separator(self.separator_length)
if len(self.already_used_letter):
print("You have already used letter", self.already_used_letter ,"!")
else:
center_text(text="", length=self.separator_length)
center_text(text="", length=self.separator_length)
project.py - Game class
- clear_terminal: Cette méthode est utilisée pour effacer tout le contenu du terminal et ainsi donner l'illusion d'une "animation" lorsque différentes étapes de la page sont réaffichées dans la partie main_game de run_game.
def clear_terminal(self):
if platform == "Windows":
os.system("cls")
else:
os.system("clear")
project.py - Game class
- restart: Cette méthode initialise les variables aux valeurs par défaut afin que la boucle while de run_game renvoie le joueur à l'écran de démarrage (start_game).
def restart(self):
self.clear_terminal()
self.game_screen = 0
self.used_letters = []
self.failures = 0
self.game_mode = "0"
project.py - Game class
- quit: Cette méthode imprime un message indiquant qu'elle se termine et ferme le programme.
- is_game_finished: Une méthode auxiliaire pour déterminer si le jeu est terminé (si l'utilisateur a gagné ou perdu).
- user_has_failed: Une méthode auxiliaire pour déterminer si l'utilisateur a perdu.
- user_has_won: Une méthode auxiliaire pour déterminer si l'utilisateur a gagné.
- empty_space: Une méthode utilisée pour le rendu CLI afin d'éviter la répétition (DRY). Même si beaucoup plus de code pourrait encore être rendu moins répétitif.
Réflexions finales
Terminer le cours a été un petit projet amusant. J'ai appris suffisamment sur les bases de Python pour pouvoir construire par-dessus. Je comprends pourquoi Python est si populaire de nos jours, mais en même temps, j'ai quelques réserves à son égard. Je trouve sa syntaxe un peu difficile à lire (fonctions significativement plus longues) et j'aimerais voir Python utiliser plus d'accolades et moins d'indentation. Le manque de sécurité de type dans Python est également quelque chose à quoi je ne peux pas m'habituer.