El Día 011 del reto 100 Días de Python reúne las lecciones anteriores en un juego de Blackjack para la línea de comandos. El jugador compite contra un crupier controlado por la computadora usando cartas aleatorias y comparaciones de puntaje estándar.

Este es el proyecto más ambicioso de la serie hasta ahora: combina el reparto aleatorio de cartas, el cálculo de puntaje con una regla especial para los ases, una estructura completa de turnos entre jugador y crupier, y estadísticas acumuladas a lo largo de varias rondas, todo dividido en funciones enfocadas y bien nombradas.

Esta guía recorre el código fuente completo del Día 011, explica cómo funcionan el puntaje y la lógica del crupier, y sigue paso a paso un error real de puntuación con manos que tienen más de un as.

Descripción del Proyecto

El programa representa las cartas como números. Las cartas numéricas conservan su valor, las figuras se representan con 10 y un as empieza valiendo 11. La función deal_card() elige una carta al azar de una lista de 13 valores que representa una baraja estándar.

La función calculate_score() maneja dos reglas importantes: un total de 21 con dos cartas es Blackjack, representado internamente con 0, y un as cambia de 11 a 1 cuando la mano superaría 21.

Código Fuente

El proyecto del Día 011 está en day011/main.py, dentro del repositorio 100daysofpython:

from art import LOGO
import random

print(LOGO)
print("*******************************************")
print("Welcome to The Blackjack Game!")
print("*******************************************")

def deal_card():
    cards = [2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10, 11]
    card = random.choice(cards)
    return card

def calculate_score(cards):
    if sum(cards) == 21 and len(cards) == 2:
        return 0
    if 11 in cards and sum(cards) > 21:
        cards.remove(11)
        cards.append(1)
    return sum(cards)

def compare(u_score, c_score):
    if u_score == c_score:
        return "Draw 🙃", "draw"
    elif c_score == 0:
        return "Lose, opponent has Blackjack 😱", "lose"
    elif u_score == 0:
        return "Win with a Blackjack 😎", "win"
    elif u_score > 21:
        return "You went over. You lose 😭", "lose"
    elif c_score > 21:
        return "Opponent went over. You win 😁", "win"
    elif u_score > c_score:
        return "You win 😃", "win"
    else:
        return "You lose 😤", "lose"

wins = 0
losses = 0
draws = 0

def play_game():
    global wins, losses, draws

    print(LOGO)
    user_cards = []
    computer_cards = []
    computer_score = -1
    user_score = -1
    is_game_over = False

    for _ in range(2):
        user_cards.append(deal_card())
        computer_cards.append(deal_card())

    while not is_game_over:
        user_score = calculate_score(user_cards)
        computer_score = calculate_score(computer_cards)
        print(f"Your cards: {user_cards}, current score: {user_score}")
        print(f"Computer's first card: {computer_cards[0]}")

        if user_score == 0 or computer_score == 0 or user_score > 21:
            is_game_over = True
        else:
            user_should_deal = input("Type 'y' to get another card, type 'n' to pass: ")
            if user_should_deal == "y":
                user_cards.append(deal_card())
            else:
                is_game_over = True

    while computer_score != 0 and computer_score < 17:
        computer_cards.append(deal_card())
        computer_score = calculate_score(computer_cards)

    print(f"Your final hand: {user_cards}, final score: {user_score}")
    print(f"Computer's final hand: {computer_cards}, final score: {computer_score}")

    message, outcome = compare(user_score, computer_score)
    print(message)

    if outcome == "win":
        wins += 1
    elif outcome == "lose":
        losses += 1
    else:
        draws += 1

    print(f"\nScore — Wins: {wins} | Losses: {losses} | Draws: {draws}")

while input("Do you want to play a game of Blackjack? Type 'y' or 'n': ") == "y":
    print("\n" * 20)
    play_game()

Cómo Funciona el Programa

Representar las Cartas y Detectar el Blackjack

if sum(cards) == 21 and len(cards) == 2:
    return 0

En lugar de agregar una variable booleana aparte para saber “si es Blackjack”, la función devuelve 0 como valor centinela especial siempre que las dos primeras cartas sumen 21. Todas las funciones que revisan un puntaje (compare() y la comprobación de fin de partida) tratan el 0 como “esta mano es un Blackjack natural”, lo que mantiene la lógica de puntuación en un solo lugar.

La Estrategia Fija del Crupier

while computer_score != 0 and computer_score < 17:
    computer_cards.append(deal_card())
    computer_score = calculate_score(computer_cards)

El crupier sigue una regla simple y fija, común en el Blackjack real: seguir pidiendo cartas hasta llegar al menos a 17, a menos que ya tenga un Blackjack natural. A diferencia del jugador, el crupier nunca “decide” detenerse antes; su estrategia es completamente mecánica.

Dividir el Comportamiento en Funciones Pequeñas

El juego se divide en deal_card(), calculate_score(), compare() y play_game(), cada una con una responsabilidad clara. Esta separación permite razonar sobre la lógica de puntuación (y probarla) de forma independiente del flujo turno a turno del juego, un hábito que vale la pena llevar a cualquier programa más grande.

Un Error Real: Manos con Dos o Más Ases

Ahora observa con atención la lógica de ajuste de los ases:

if 11 in cards and sum(cards) > 21:
    cards.remove(11)
    cards.append(1)
return sum(cards)

Esto es un if, no un while: convierte como máximo un as de 11 a 1 por llamada, aunque la mano tenga varios ases que necesitarían convertirse para no pasarse de 21.

Sigue el recorrido con una mano de [11, 11, 10, 5]: dos ases, un 10 y un 5:

  1. sum(cards) es 37, 11 in cards es True, así que el ajuste se ejecuta una vez.
  2. Se quita un 11 y se agrega un 1: cards queda como [11, 10, 5, 1].
  3. La función devuelve sum(cards), que es 27, todavía por encima de 21.

Pero el valor correcto de esa mano en Blackjack cuenta ambos ases como 1: 1 + 1 + 10 + 5 = 17, una mano perfectamente válida que no se pasa. Como calculate_score() solo ajusta un as, al jugador (o al crupier) se le diría incorrectamente que se pasó con 27, cuando en realidad la mano vale 17.

Este tipo de error es fácil de pasar por alto en pruebas casuales, porque solo aparece cuando una mano tiene dos o más ases, algo relativamente raro. Por eso vale la pena probar deliberadamente casos límite como este en lugar de depender de que el juego normal los detecte.

Qué Enseña Este Proyecto

El Día 011 practica:

  • Listas y selección aleatoria para simular una baraja de cartas
  • Codificar un caso especial (Blackjack) como valor centinela de retorno
  • Dividir un programa más grande en funciones pequeñas y enfocadas
  • Estado a nivel de módulo (wins, losses, draws) compartido entre llamadas a funciones con global
  • Por qué los ajustes de una sola pasada pueden fallar con entradas que requieren corregirse más de una vez

Ejecutar el Proyecto

git clone https://github.com/victorhugo81/100daysofpython.git
cd 100daysofpython/day011
python main.py

El proyecto requiere el archivo auxiliar art.py en la misma carpeta.

Posibles Siguientes Pasos

Cuando entiendas el código existente, intenta ampliarlo:

  • Corrige el error de varios ases cambiando el if por un ciclo while, para que se conviertan todos los ases necesarios y no solo el primero
  • Omite por completo el ciclo de cartas del crupier cuando el jugador ya se pasó, ya que el resultado está decidido
  • Agrega soporte para dividir una mano cuando las dos primeras cartas son iguales
  • Guarda wins, losses y draws en un archivo para que las estadísticas acumuladas se conserven entre ejecuciones del programa

Conclusión

El Día 011 del reto 100 Días de Python une todo lo aprendido en los días anteriores (selección aleatoria, funciones, ciclos y estado a nivel de módulo) en un juego de Blackjack realmente jugable. También es un buen recordatorio de que “maneja el caso común” y “maneja todos los casos correctamente” no son lo mismo: el error de puntuación de los ases solo aparece con dos o más ases en una mano, justo el tipo de caso límite que conviene probar a propósito en lugar de esperar encontrarlo durante el juego normal.

Consulta el código fuente del Día 011 en GitHub y sigue explorando el índice de proyectos del repositorio mientras construyes más programas en Python, un día y un proyecto pequeño a la vez.