AlanJereb.com
Programmazione

Harvard CS50 - Introduzione alla programmazione con Python

Jan 30th, 2024

La laurea in ingegneria meccanica non mi ha fornito quasi nessuna conoscenza di base necessaria per la programmazione (tranne un po' di matematica e Matlab). Durante i miei anni di codifica professionale, ho acquisito enormi quantità di conoscenza legata all'informatica, ma occasionalmente sento ancora che c'è molto spazio per migliorare.

Ecco perché ho deciso di frequentare il corso CS50 di Harvard. Questo è il primo corso CS50 di Harvard che mi sono proposto di completare.

Ogni corso ha un progetto finale, la cui implementazione presenterò in questi post del blog legati a CS50.

L'impiccato - il gioco della linea di comando (CLI)

Il gioco segue le regole standard del gioco dell'impiccato:

  • Hai dieci possibilità di cercare di indovinare la parola lettera per lettera.
  • Non puoi ripetere le lettere già indovinate.
  • Se indovini correttamente la lettera, non conta nel tuo totale di 10 tentativi.
  • Se raggiungi dieci tentativi falliti, il gioco termina e perdi.
  • Se indovini la parola prima di raggiungere dieci tentativi, vinci.

Il gioco ha due modalità:

  • Modalità normale | Parola inglese casuale di 5 lettere
  • Modalità difficile | Parola inglese casuale di 6 lettere

Secondo le regole standard del gioco dell'impiccato, una parte dell'impiccato viene disegnata per ogni tentativo fallito. Se raggiungi dieci tentativi falliti, verrà disegnata un'immagine completa.

L'impiccato - Schermata iniziale

L'impiccato - Schermata iniziale

Dopo aver completato il gioco (vincendo o perdendo), puoi iniziare un altro gioco o uscire dal programma

L'impiccato - Partita persa

L'impiccato - Partita in corso

L'impiccato - Partita vinta

Struttura del codice

È possibile visualizzare l'intero codice del progetto qui:

.

Per giocare al gioco, clona il progetto, installa Python, spostati nella cartella principale del repository, installa le dipendenze con

pip install -r requirements.txt

ed esegui

python project.py

nel terminale.

project.py

- come indicato nelle linee guida, il codice è composto da una funzione principale e tre funzioni allo stesso livello, necessarie per i test. Tuttavia, per motivi di gestione dello stato, ho inizializzato e mantenuto l'intero gioco all'interno di una classe Game, che si trova nello stesso file. Volevo evitare l'uso di variabili globali.

Le tre funzioni richieste allo stesso livello di indentazione della funzione principale implementano chiamate ai metodi della classe Game in modo che siano soddisfatti i requisiti finali del progetto. Ma devo sottolineare che tutti i metodi della classe sono stati testati accuratamente.

test_project.py

- contiene tutti i test del progetto

requirements.txt

- contiene tutte le dipendenze del pacchetto e le loro versioni utilizzate nel progetto

data/words.py

- contiene due liste di parole in inglese di cinque e sei lettere utilizzate nella selezione delle modalità di gioco

components/separators.py

- contiene funzioni utilizzate per disegnare il gioco dell'impiccato all'interno dell'interfaccia a riga di comando (CLI)

components/stages.py

- contiene le fasi di ciascuno dei disegni degli errori per il gioco dell'impiccato. Le fasi non seguono intenzionalmente il principio DRY, in modo che siano più facili da mantenere e revisionare. Se questa fosse un'applicazione più grande, dove le prestazioni sarebbero cruciali, queste fasi insieme ai disegni dello schermo iniziale potrebbero essere ottimizzate.

Classe Game - Descrizione dettagliata

Come già menzionato in precedenza, la classe

Game

racchiude l'intera logica del gioco.

Ecco la scomposizione dei principali metodi e delle loro proprietà:

  • __init__: Questo metodo inizializza le variabili legate al gioco utilizzate sia per la rappresentazione grafica dei blocchi di costruzione del gioco nella linea di comando (CLI) che per la logica del gioco.
    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: Ho utilizzato queste proprietà sia per "prevenire" la manipolazione diretta delle variabili di stato inizializzate in init sia per controllare il modo in cui queste variabili dovrebbero essere aggiornate e recuperate.
    @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: Ecco come si avvia il gioco. Utilizza un ciclo while True e funzioni di input per fermarsi ed attendere ad ogni iterazione del ciclo. Controlla l'intero flusso della logica del gioco eseguendo altri metodi come start_game e main_game. È anche responsabile della maggior parte dei cambiamenti delle variabili di stato.
    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: Mostra la schermata iniziale dell'interfaccia a riga di comando (CLI) che gli utenti vedono all'avvio del gioco. In questa schermata, all'utente viene chiesto di selezionare la difficoltà del gioco. In base alla selezione, il metodo run_game imposta una parola casuale da indovinare e guida l'utente alla schermata principale del gioco. In caso contrario, questo metodo non ha logica speciale, si occupa solo del disegno del gioco.
    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: Mostra la schermata principale del gioco nell'interfaccia a riga di comando (CLI), che, tramite run_game, tiene traccia delle lettere già utilizzate, delle congetture corrette dell'utente e disegna le fasi dell'impiccato per ciascuno dei dieci tentativi falliti. Dopo dieci tentativi falliti o il completamento riuscito del gioco, all'utente viene chiesto di riavviare il gioco, il che riporta il giocatore alla schermata iniziale, o di terminare il gioco, il che esce dal programma. In caso contrario, questo metodo non ha logica speciale. Si occupa solo del disegno del gioco.
    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: Questo metodo è utilizzato per cancellare tutto il contenuto del terminale e quindi dare l'illusione di "animazione" quando diverse fasi della pagina vengono ridisegnate all'interno della parte main_game di run_game.
    def clear_terminal(self):
        if platform == "Windows":
            os.system("cls")
        else:
            os.system("clear")

project.py - Game class

  • restart: Questo metodo imposta le variabili ai valori predefiniti in modo che il ciclo while di run_game riporti il giocatore alla schermata di inizio (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: Questo metodo stampa un messaggio che sta uscendo e chiude il programma.
  • is_game_finished: Un metodo ausiliario per determinare se il gioco è finito (se l'utente ha vinto o perso).
  • user_has_failed: Un metodo ausiliario per determinare se l'utente ha perso.
  • user_has_won: Un metodo ausiliario per determinare se l'utente ha vinto.
  • empty_space: Un metodo utilizzato per il rendering CLI per evitare la ripetizione (DRY). Anche se molto altro codice potrebbe essere reso meno ripetitivo.

Considerazioni finali

Completare il corso è stato un piccolo progetto divertente. Ho imparato abbastanza sulla sintassi di base di Python da poter costruire sopra di essa. Posso capire perché Python sia così popolare al giorno d'oggi, ma al tempo stesso ho alcune riserve. Trovo la sua sintassi un po' difficile da leggere (funzioni significativamente più lunghe) e preferirei vedere Python utilizzare più parentesi graffe e meno rientranze. La mancanza di sicurezza dei tipi in Python è anche qualcosa a cui non riesco ad abituarmi.

Return to top button