Jan 30th, 2024
Graduarme como ingeniero mecánico casi no me proporcionó conocimientos básicos necesarios de programación (excepto matemáticas y un poco de Matlab). Durante mis años de codificación profesional, adquirí enormes cantidades de conocimiento relacionado con la informática, pero ocasionalmente aún siento que hay mucho espacio para mejorar.
Es por eso que decidí que tomaré el curso CS50 de Harvard. Este es el primer curso CS50 de Harvard que me propuse completar.
Cada curso tiene un proyecto final, cuya implementación presentaré en estas publicaciones de blog relacionadas con CS50.
El juego sigue las reglas estándar del juego del ahorcado:
El juego tiene dos modos:
Según las reglas estándar del juego del ahorcado, se dibuja una parte del ahorcado por cada intento fallido. Si alcanzas diez intentos fallidos, se dibujará una imagen completa.
Ahorcado - Pantalla de inicio
Después de completar el juego (ganar o perder), puedes comenzar otro juego o salir del programa.
Ahorcado - Juego perdido
Ahorcado - Juego en curso
Ahorcado - Juego ganado
Para jugar al juego, clona el proyecto, instala Python, muévete al directorio raíz del repositorio, instala las dependencias con
pip install -r requirements.txt
y ejecuta
python project.py
en la terminal.
project.py
- como se indica en las pautas, el código consta de una función principal y tres funciones en el mismo nivel, necesarias para las pruebas. Sin embargo, por razones de mantenimiento de estado, he inicializado y mantenido todo mi juego dentro de una clase Game, que se encuentra en el mismo archivo. Quería evitar el uso de variables globales.
Las tres funciones requeridas en el mismo nivel de sangría que la función principal implementan llamadas a los métodos de la clase Game para que se cumplan los requisitos finales del proyecto. Pero debo señalar que también se probaron exhaustivamente todos los métodos de la clase.
test_project.py
- contiene todas las pruebas del proyecto
requirements.txt
- contiene todas las dependencias del paquete y sus versiones utilizadas en el proyecto
data/words.py
- contiene dos listas de palabras en inglés de cinco y seis letras utilizadas al elegir los modos de juego
components/separators.py
- contiene funciones utilizadas para dibujar el juego del ahorcado dentro de la interfaz de línea de comandos (CLI)
components/stages.py
- contiene las etapas de cada uno de los dibujos de intentos incorrectos para el juego del ahorcado. Las etapas no siguen intencionalmente el principio DRY, para que sean más fáciles de mantener y revisar. Si esta fuera una aplicación más grande, donde el rendimiento fuera crítico, estas etapas y los dibujos de la pantalla de inicio podrían optimizarse.
Como ya se mencionó anteriormente, la clase
Game
encapsula toda la lógica del juego.
Aquí se detalla el análisis de los métodos clave y sus propiedades:
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
@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
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
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
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
def clear_terminal(self):
if platform == "Windows":
os.system("cls")
else:
os.system("clear")
project.py - Game class
def restart(self):
self.clear_terminal()
self.game_screen = 0
self.used_letters = []
self.failures = 0
self.game_mode = "0"
project.py - Game class
Completar el curso fue un proyecto divertido. Conocí lo suficiente sobre los conceptos básicos de Python como para construir sobre ellos. Puedo entender por qué Python es tan popular hoy en día, pero al mismo tiempo, tengo algunas reservas hacia él. Encuentro su sintaxis un poco difícil de leer (funciones significativamente más largas) y preferiría ver que Python use más llaves y menos sangrías. La falta de seguridad de tipo en Python es también algo a lo que no puedo acostumbrarme.