El joc del penjat en Python: gestionar estat i errors

Construïm el joc del penjat en Python per entendre com gestionem l’estat d’una partida: progrés, intents, lletres usades i validació d’entrada, amb codi clar i comentat.

En aquest post construirem una versió del joc del penjat en Python amb un objectiu clar: entendre com es gestiona l’estat del joc (progrés, intents, lletres usades) i com dissenyem una experiència d’usuari mínimament robusta quan treballem amb entrades reals.

Si el joc d’“Endevina el nombre” ens servia per practicar bucles i condicionals, aquí fem un pas endavant: ara el repte és mantenir coherència entre diverses variables que evolucionen al llarg de la partida.

Objectius d’aprenentatge

  • Modelar l’estat d’una partida (paraula, progrés, intents).
  • Evitar errors típics: entrades buides, repeticions, caràcters invàlids.
  • Fer servir un set per gestionar lletres ja utilitzades.
  • Millorar la “UX” amb feedback clar i missatges coherents.

1) Què vol dir “gestionar estat” en un joc

En un joc iteratiu, l’estat és el conjunt de valors que descriuen la partida en cada moment. En el penjat, com a mínim, necessitem:

  • paraula secreta: la solució.
  • progrés: quines lletres hem encertat (p. ex. _ _ a _ _).
  • intents restants: quants errors podem cometre.
  • lletres usades: per evitar repeticions i donar feedback.

El punt crític és que aquests elements han d’estar sincronitzats: cada jugada modifica (o no) l’estat i el programa ha de reflectir-ho correctament.

2) Decisió de disseny: com representem el progrés

Una estratègia habitual és representar el progrés amb una llista de caràcters (o guions baixos) perquè sigui fàcil actualitzar posicions concretes. Per exemple, si la paraula és "python", podem començar així:

paraula = "python"
progres = ["_"] * len(paraula)   # ['_', '_', '_', '_', '_', '_']

Aquesta representació ens permet substituir guions baixos per lletres quan encertem. Quan vulguem mostrar el progrés, simplement unim la llista amb espais:

print(" ".join(progres))

3) Evitar repeticions: set per a lletres usades

En termes de qualitat d’interacció, és important detectar si l’usuari repeteix una lletra. Un set és ideal perquè:

  • emmagatzema valors únics,
  • la consulta lletra in lletres_usades és molt eficient,
  • ens facilita donar feedback immediat.
lletres_usades = set()

4) Validació d’entrada: errors típics i criteris mínims

Quan treballem amb input(), hem d’assumir que l’entrada pot ser incorrecta. En un disseny mínimament robust, validarem que:

  • no sigui buida,
  • tingui longitud 1 (una sola lletra),
  • sigui una lletra (no números ni símbols),
  • no s’hagi provat abans.

Aquest tipus de validacions no són “decoració”: formen part del que diferencia un exercici de consola d’un programa que es pot utilitzar de manera consistent.

5) Implementació completa (versió consola)

A continuació presentem una versió completa i comentada. El focus està en la lògica i en la gestió de l’estat (no en la selecció de paraules ni en la interfície web, que abordarem quan portem el joc a Streamlit).

import random

PARAULES = [
    "python",
    "funcio",
    "llista",
    "diccionari",
    "streamlit",
    "algorisme",
]

def triar_paraula(paraules: list[str]) -> str:
    """Tria aleatòriament una paraula d'una llista."""
    return random.choice(paraules).lower()

def mostrar_estat(progres: list[str], intents: int, lletres_usades: set[str]) -> None:
    """Mostra l'estat actual de la partida de manera llegible."""
    print("\nProgrés:", " ".join(progres))
    print("Intents restants:", intents)
    if lletres_usades:
        print("Lletres usades:", ", ".join(sorted(lletres_usades)))

def demanar_lletra(lletres_usades: set[str]) -> str:
    """Demana una lletra i aplica validacions mínimes."""
    while True:
        entrada = input("Introdueix una lletra: ").strip().lower()

        if not entrada:
            print("Entrada buida. Escriu una lletra.")
            continue

        if len(entrada) != 1:
            print("Introdueix només una lletra.")
            continue

        if not entrada.isalpha():
            print("Introdueix una lletra (a-z).")
            continue

        if entrada in lletres_usades:
            print("Aquesta lletra ja l'hem provat. Tria'n una altra.")
            continue

        return entrada

def actualitzar_progres(paraula: str, progres: list[str], lletra: str) -> bool:
    """
    Actualitza el progrés si la lletra és dins la paraula.
    Retorna True si hi ha hagut encert, False si no.
    """
    encert = False
    for i, ch in enumerate(paraula):
        if ch == lletra:
            progres[i] = lletra
            encert = True
    return encert

def jugar_penjat() -> None:
    """Executa una partida del penjat (mode consola)."""
    paraula = triar_paraula(PARAULES)
    progres = ["_"] * len(paraula)
    lletres_usades: set[str] = set()
    intents = 6

    print("=== Joc del Penjat (Python) ===")
    print("Objectiu: endevinar la paraula abans de quedar-nos sense intents.")

    while intents > 0 and "_" in progres:
        mostrar_estat(progres, intents, lletres_usades)

        lletra = demanar_lletra(lletres_usades)
        lletres_usades.add(lletra)

        if actualitzar_progres(paraula, progres, lletra):
            print("✅ Encert!")
        else:
            intents -= 1
            print("❌ No hi és. Perdem un intent.")

    # Final de partida
    mostrar_estat(progres, intents, lletres_usades)
    if "_" not in progres:
        print(f"\n🎉 Hem guanyat! La paraula era: {paraula}")
    else:
        print(f"\n💀 Hem perdut. La paraula era: {paraula}")

if __name__ == "__main__":
    jugar_penjat()
Logo minimalista del joc del penjat en Python — DeGalaLab Arcade

6) Què hem practicat

  • Control de flux: bucle while amb condicions de finalització clares.
  • Estat del programa: coherència entre progres, intents i lletres_usades.
  • Estructures de dades: llista per al progrés i set per a evitar repeticions.
  • Validació d’entrada: gestió d’errors habituals en programes interactius.
  • UX mínima: feedback explícit per a guiar la partida.

Aquest joc forma part de DeGalaLab Arcade, una col·lecció de mini jocs fets amb Python i portats a web amb Streamlit.

⏭️ Següent post de la sèrie: Construir un quiz en Python (preguntes, puntuació i feedback).