Harvard CS50 - Introducción a la programación con Python
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.
Ahorcado - el juego de la interfaz de línea de comandos (CLI)
El juego sigue las reglas estándar del juego del ahorcado:
- Tienes diez oportunidades para intentar adivinar la palabra letra por letra.
- No puedes repetir letras que ya hayas adivinado.
- Si adivinas correctamente la letra, no cuenta como parte de tus 10 intentos.
- Si alcanzas diez intentos fallidos, el juego termina y pierdes.
- Si adivinas la palabra antes de llegar a diez intentos, ganas.
El juego tiene dos modos:
- Modo normal | Palabra en inglés aleatoria de 5 letras
- Modo difícil | Palabra en inglés aleatoria de 6 letras
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
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 perdido
Ahorcado - Juego en curso
Ahorcado - Juego en curso
Ahorcado - Juego ganado
Ahorcado - Juego ganado
Estructura del código
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.
Clase Game - Descripción detallada
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:
- __init__: Este método inicializa variables relacionadas con el juego utilizadas tanto para renderizar los bloques de construcción del juego en la interfaz de línea de comandos (CLI) como para la lógica del juego.
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: He utilizado estas propiedades tanto para "prevenir" la manipulación directa de las variables de estado inicializadas en init como para controlar la forma en que estas variables deben actualizarse y recuperarse.
@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: Así es como se inicia el juego. Utiliza un bucle while True y input para detenerse y esperar en cada iteración del bucle. Controla todo el flujo de la lógica del juego ejecutando otros métodos como start_game y main_game. También es responsable de la mayoría de los cambios en las variables de estado.
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: Muestra la pantalla inicial de la interfaz de línea de comandos (CLI) que los usuarios ven al iniciar el juego. En esta pantalla, se le pide al usuario que seleccione la dificultad del juego. Según la selección, el método run_game establece una palabra aleatoria para adivinar y guía al usuario a la pantalla principal del juego. De lo contrario, este método no tiene lógica especial, simplemente se encarga de dibujar el juego.
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: Muestra la pantalla principal del juego en la interfaz de línea de comandos (CLI), que, a través de run_game, realiza un seguimiento de las letras ya utilizadas, las suposiciones correctas del usuario y dibuja las etapas del ahorcado para cada uno de los diez intentos fallidos. Después de diez intentos fallidos o de completar exitosamente el juego, se le pide al usuario que reinicie el juego, lo que devuelve al jugador a la pantalla inicial, o que termine el juego, lo que sale del programa. De lo contrario, este método no tiene lógica especial. Simplemente se encarga de dibujar el juego.
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: Este método se utiliza para borrar todo el contenido de la terminal y así dar la ilusión de "animación" cuando se vuelven a renderizar las diferentes etapas de la página dentro de la parte main_game de run_game.
def clear_terminal(self):
if platform == "Windows":
os.system("cls")
else:
os.system("clear")
project.py - Game class
- restart: Este método establece las variables en los valores predeterminados para que el bucle while de run_game devuelva al jugador a la pantalla de inicio (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: Este método imprime un mensaje de que está saliendo y cierra el programa.
- is_game_finished: Un método auxiliar para determinar si el juego ha terminado (el usuario ha ganado o perdido).
- user_has_failed: Un método auxiliar para determinar si el usuario ha perdido.
- user_has_won: Un método auxiliar para determinar si el usuario ha ganado.
- empty_space: Un método utilizado para la representación en la interfaz de línea de comandos (CLI) para evitar la repetición (DRY). Aunque aún se podría reducir la repetición de código.
Pensamientos finales
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.