5. Cifras y Letras: Prueba de Letras

portada

Cifras y Letras fue un progrma de la televisión española de los años 90 que se extendió durante más de 20 años. El juego se orquestaba en torno a dos participantes que concursaban en varias pruebas de dos tipos. Una de letras y otra de números. Nosotros vamos implementar una versión simplificada de ambas pruebas.

En este capítulo vamos a construir una versión de la prueba de letras del popular programa de televisión Cifras y Letras. Las reglas de este juego son las siguientes:

  • Se pide al usuario que introduza el número de letras con el que desea jugar. A mayor numero de letras, mayor dificultad.

  • El jugador recibe una serie de vocales y consonantes con las que debe formar la palabra más larga posible

  • Las letras con las que se juega se reciben de una en una y es el propio jugador quien decide si quiere recibir una vocal o una consonante

  • Solo se puede hacer uso de las letras que se han obtenido, sin repetirlas

  • La puntuación es proporcional a la longitud de la palabra construida, recibiendo una bonificación si la palabra generada utiliza todas las letras.

En nuestra versión de un sólo jugador, la palabra proporcionada sólo se considerará validada si aparece en el fichero words.txt. En caso de no ser así, si se puede comprobar por otros medios que la palabra construida ha sido aceptada recientemente por la RAE, el jugador puede incluir la palabra en el fichero words.txt para futuras partidas.

See also

Puedes ver el primer programa de Cifras y letras aquí o encontrar más información en su entrada de Wikipedia.

5.1. Vocales y consonantes

Vamos a empezar creando dos listas, una con las vocales y otra con las consonantes.

vowels = ['a', 'e', 'i', 'o', 'u']
consonants = ['b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'n', 'ñ', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z']

Tip

Recuerda que en Python las variables de tipo STRING comparten muchas propiedades con las listas. También son colecciones que se pueden indexar. Por lo que podríamos haber definido esas variables de la siguiente manera:

vowels = "aeiou"
consonants = "bcdfghjklmnñpqrstvwxyz"

5.2. Pedir un número

En primer lugar, el jugador debe decidir el nivel de dificultad del juego. Vamos a crear una función select_difficulty() para que el usuario introduzca el número de letras con las que quiere jugar. Cuantas más letras, más posibilidades de formar palabras, pero también más dificil conseguir la máxima puntuación utilizando todas las letras. El primer paso es asegurarnos de que el valor introducido es un número. Esta función no recibe ningún argumento de entrada y tiene que devolver el valor numérico seleccionado por el usuario.

Solución:

def select_difficulty():
    while True:
        try:
            number = int(input("Select the number of characters to play with: "))
            return number
        except ValueError:
            print("Error, you must use a number")
select_difficulty()
Select the number of characters to play with: three
Error, you must use a number
Select the number of characters to play with: 3
3

Note

El uso de excepciones ayuda mucho a conseguir códigos compactos y funcionales. La función select_difficulty() puede implementarse sin ellas, pero habría que leer el valor introducido por el jugador como una variable STRING y validar mediante las funciones isalpha(), isdecimal(), isdigit() o isnumeric() si el valor es correcto antes de convertirlo a un tipo numérico.

Por otro lado, el uso de bucles con condiciones que se validan siempre (bucle infinito) también es reemplazable por otro tipo de recursos, pero usados correctamente permite reducir considerablemente la complejidad de una función y favorecer su legibilidad.

5.3. Consonante o Vocal

En el programa de televisión los participantes iban diciendo “consonante” o “vocal” cada vez que la presentadora les ofrecía la posibilidad de recibir una nueva letra. Nosotros vamos a simular el juego permitiendo al jugador responder a la misma pregunta. Vamos a crear la función consonant_or_vowel() que tiene como único argumento la variable difficulty que contiene el número de letras con el que vamos a jugar. La función debe preguntar al jugador si quiere una consonante o una vocal, y dependiendo de su respuesta, generar una letra aleatoria de la categoría elegida. La función debe devolver las consonantes y vocales generadas en una variable tipo lista.

Es muy importante que el código vaya mostrando al usuario las letras que se han ido generando porque la siguiente respuesta del jugador dependerá de ello.

Tip

Para hacer el juego más ágil y evitar tener que escribir en cada ocasión las palabras “consonante” o “vocal”, se puede solicitar, por ejemplo, un 0 para consonante y un 1 para vocal.

Solución:

import random

def consonant_or_vowel(difficulty):
    vowels = ['a', 'e', 'i', 'o', 'u']
    consonants = ['b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'n', 'ñ', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z']
    letters = []
    for i in range(difficulty):
        ask = int(input("Consonant [0] or Vowel [1]? "))
        if ask == 0 :
            letters.append(random.sample(consonants,1)[0])
        else:
            letters.append(random.sample(vowels,1)[0])
        print("LETTERS: ",",".join(letters).upper())
    
    return letters
consonant_or_vowel(5)
Consonant [0] or Vowel [1]? 0
LETTERS:  Ñ
Consonant [0] or Vowel [1]? 1
LETTERS:  Ñ,E
Consonant [0] or Vowel [1]? 1
LETTERS:  Ñ,E,U
Consonant [0] or Vowel [1]? 4
LETTERS:  Ñ,E,U,I
Consonant [0] or Vowel [1]? 4
LETTERS:  Ñ,E,U,I,A
['ñ', 'e', 'u', 'i', 'a']

Note

En este caso, para simplificar el código, no estamos validando que el valor introducido es un número. Para que nuestro programa fuera resistente a fallos habría que aplicar un mecanismo similar el que utilizamos en la función select_difficulty().

Por otro lado, la solición aportada simplifica el código al máximo chequeando sólo si el valor introducido por el jugador es un cero. Esto quiere decir que la función select_difficulty también interpretaría que el jugador está solicitando una vocal si introduce cualquier otro número.

5.4. Validar la construcción

Cuando el jugador proporciona una palabra, antes de contemplar si existe o no dentro del diccionario de la Real Academia Española, hay que asegurar que ha sido construida con las letras disponibles en el juego.

Vamos a implementar la función validate_letters(word, letters) que recibe dos argumentos de entrada:

  • word variable de tipo STRING que contiene la palabra proporcionada por el jugador

  • letters variable de tipo LIST con las letras del juego

La función sólo tiene que devolver una variable booleana de valor True o False dependiendo de si la palabra cumple o no los requisitos.

Solución:

import copy

def validate_letters(word, letters):
    ltrs=copy.deepcopy(letters)
    try:
        for letter in word:
            ltrs.remove(letter)
        return True
    except:
        return False
validate_letters("home", ["h","o","m","e","s","u"])
True
validate_letters("villa", ["v","i","l","a","s","u"])
False

Warning

Como Python es un lenguaje que pasa sus argumentos por referencia, si modificamos una variable de un tipo mutable, como las listas, esa variable se verá modificada desde el programa principal. Por ejemplo, en el código:

mutable=["v","i","l","a","s","u"]
validate_letters("villa", mutable)
print(mutable)

La variable mutable, tras la ejecución de la función validate_letters, contendrá:

['a', 's', 'u']

Para evitarlo, podemos trabajar con una copia de la variable utilizando la función deepcopy().

5.5. Leer palabras diccionario

Para chequear si la palabra introducida por el jugador está contemplada por la Real Academia Española, tenemos que buscarla entre las palabras del fichero words.txt. Para ello vamos a implementar una función get_words(filename) que lea el fichero y guarde las palabras en una lista. Al mismo tiempo que leemos esas palabras vamos a asegurarnos que todas ellas están en minúscula y además, vamos a aprovechar para eliminar cualquier tipo de tilde.

La función get_words(filename) recibe como único argumento de entrada el nombre del fichero dentro de una variable de tipo STRING y devuelve una lista de elementos también de tipo STRING.

Tip

Recueda que el fichero Words.txt contiene las tildes del lenguaje español, puede que tengas que utilizar el encoding “utf8” para leer su contenido.

Una vez que hayas leído las palabras, puedes utilizar la librería unidecode para eliminar los acentos:

import unidecode
unaccented_string = unidecode.unidecode("áéíóú")

La única limitación de este código es que convierte las “ñ” en “n”. En la solución propuesta utilizamos la función translate y maketrans de las variables STRING para reemplazar sólo las vocales con tildes.

Solución:

import unidecode

def get_words(filename):
    with open(filename, "r", encoding="utf8") as file:
        words = file.read().splitlines()
    
    a,b = 'áéíóúü','aeiouu'
    trans = str.maketrans(a,b)
    output = [word.translate(trans)  for word in words]
    
    return output
rae = get_words("words.txt")
print(rae[:10])
['a', 'aaronita', 'aaronico', 'aba', 'ababa', 'ababillarse', 'ababol', 'abacal', 'abacalero', 'abacero']

Note

La conversión de un STRING a minúsculas es muy sencillo utilizando el método lower(). También existe un método complementario upper() para convertir cualquier STRING a mayúsculas.

See also

Puedes encontrar más información sobre la función open y el manejo de ficheros de texto aqui.

5.6. Validar la palabra

Una vez que hemos leído las palabras del diccionario, necesitamos una función que haga la validación. Vamos a construir la función validate word(word, rae) que recibe dos parámetros:

  • word es una variable de tipo STRING que contiene la palabra que queremos validar

  • rae es una variable de tipo LIST que contiene todas las palabras del fichero words.txt.

La función validate word(word, rae) sólo devuelve una salida de tipo BOOL que toma el valor True si la palabra existe o False en caso contrario.

Solución:

def validate_word(word, rae):    
    if word in rae:
        return True
    else:
        return False
validate_word("universidad", rae)
True
validate_word("uniforvidez", rae)
False

5.7. Añadir palabras al diccionario

Puede darse el caso que la palabra construida por el jugador se haya includio reciemtente en las nuevas revisiones del diccionario de la Real Academia Española dejando obsoleto nuestro fichero words.txt. También podría suceder que la palabra no esté aceptada por la RAE, pero los jugadores decidan validarla de común acuerdo. Para que esta nueva palabra forme parte de las palabras validadas en futuras partidas, vamos a implemetar una función que permite añadir una palabra al fichero words.txt. La función add_new_word(filename, new_word) recibe dos argumentos de entrada:

  • filename variable de tipo STRING con el nombre del fichero

  • new_word variable de tipo STRING con la palabra que queremos añadir al fichero

La función no devuelve ningún valor.

Solución:

def add_new_word(filename, new_word):
    with open(filename, "a") as file:
        file.write(new_word + "\n")

5.8. Calcular la puntuación

Aunque nuestro juego sólo está pensado para un sólo jugador, es necesaria una función que devuelva una puntuación que será función de la longitud de la palabra construida. Las reglas para calcular la puntuación son las siguientes:

  • La máxima puntuación es 100

  • La máxima puntuación sólo se consigue si el jugador proporciona una palabra utilizando todas las letras del juego

  • La puntuación para palabras más cortas se calcula de manera proporcionar a su longitud: $\(100\times\frac{len(word)}{len(letters)}\)$

  • Una palabra de la máxima longitud, además de obtener 100 puntos, obtiene un bonus extra de otros 100 puntos.

La función calculate_score(word, letters) recibe como argumentos:

  • word es una variable de tipo STRING con la palabra que ha introducido el usuario

  • letters es una variable de tipo LIST con las letras disponibles

Y devuelve un valor numérico, de tipo entero, con la puntuación. Por ejemplo, la siguiente llamada devuelve 200 puntos porque el usuario ha construido una palabra utilizando todas las letras disponibles.

calculate_score("home", ["h","o","m","e"])
200

Sin embargo, en el siguiente ejemplo, el usuario obtiene 50 puntos porque sólo ha usado la mitad de las letras disponibles:

calculate_score("home", ["h","o","m","e","a","r","r","s"])
50

Solución:

def calculate_score(word, letters):
    score = round(100*(len(word)/len(letters)))
    if len(word)==len(letters):
        score=score+100
    return score

5.9. Encontrar la mejor palabra posible

En la emisión televisiva, tras las intervenciones de los concursantes, la presentadora siempre aportaba alguna palabra de su propia cosecha, que en la mayoría de las ocasiones tenía una longitud superior a la de los concursantes. Vamos a emular este comportamiento con una función que nos devuelva la palabra con la mayor longitud posible para las letras con las que se está jugando.

La función find_optimal_word(words, letters, length) recibe los siguientes argumentos:

  • words contiene una variable de tipo LIST con las palabras del fichero words.txt

  • letters es una variable de tipo LIST con las letras disponibles

  • length es una variable de tipo INT con la longitud de la palabra construida por el jugador

Tip

Para encontrar la palabra óptima puedes utilizar la fuerza bruta probando todas las palabras del diccionario. Aunque un planteamiento más óptimo es ignorar las de longitud inferior a la palabra construida por el jugador o las que contengan letras que no se encuentran entre las disponibles.

Solución:

def find_optimal_word(words, letters,length):
    optimal_word = ""
    optimal_score = 0
    for w in words:
        if len(w)<length:
            pass
        elif validate_letters(w,letters)==False:
            pass
        else:
            score = calculate_score(w, letters)
            if score > optimal_score:
                optimal_score = score
                optimal_word = w
    return optimal_word
letters=["c","a","s","a","ñ","e","o","d","t"]
player="casa"
find_optimal_word(rae, letters, len(player))
'castañedo'

Note

Cuando en Python tenemos que incluir código pero no tenemos necesidad de que ese código haga ninguna acción en particular, podemos utilizar el comando pass.

5.10. Programa principal

Ahora por fin podemos construir el juego completo usando las funciones que hemos implementado. Los pasos a seguir son los siguientes:

  1. Solicitar al usuario el nivel de dificultad

  2. Generar la lista de letras (vocales y consonantes) con las que va a jugar

  3. Leer las palabras del fichero word.txt

  4. Preguntar al usuario por la palabra con la que va a jugar

  5. Validar si la palabra se ha construido legalmente

  6. Validar si la palabra existe en el fichero word.txt. En caso de no existir, proporcionar la posiblidad de incluirla para futuras partidas

  7. Dependiendo de las validaciones anteriores, mostrar mensajes explicativos por pantalla

  8. Terminar mostrando la puntuación y la mejor palabra que se podía conseguir

Solución:

El código presentado a continuación inicializa el juego generando las letras con las que vamos a jugar y leyendo la lista de palabras válidas

difficulty = select_difficulty()
letters = consonant_or_vowel(difficulty)
filename = "words.txt"
rae = get_words(filename)
Select the number of characters to play with: 5
Consonant [0] or Vowel [1]? 1
LETTERS:  I
Consonant [0] or Vowel [1]? 0
LETTERS:  I,P
Consonant [0] or Vowel [1]? 0
LETTERS:  I,P,G
Consonant [0] or Vowel [1]? 1
LETTERS:  I,P,G,E
Consonant [0] or Vowel [1]? 1
LETTERS:  I,P,G,E,A

Con la lista completa de letras, el siguiente comando le solicita al usuario su palabra. Notar que las letras se han presentado en mayúscula para mayor claridad, pero nuestro programa trabaja siempre en minúsuculas. Por ello convermitos la palabra del usuario a minúsculas.

word = input("Which word can you build with those letters? ")
word = word.lower()
Which word can you build with those letters? pie

La primera validación consiste en comprobar si hemos usado las letras que nos han ofrecido

validate_letters(word,letters)
True

Y la segunda, si mi palabra está en la lista de palabras admitidas

validate_word(word, rae)
True

Ahora tenemos que meter ambas validaciones en una estructura condicional que le de al usuario el feedback adecuado en cada caso. En este mismo bloque, si se da la situación de que la palabra construida es legal pero no se encuentra en la lista de palabras admitidas, se le debe dar al jugador la opción de validarla e incluirla en el fichero para futuras partidas. En todos los casos se proporcionará un valor a la variable score.

if validate_letters(word,letters) == False:
    print("Sorry, you lost. You dind't use the letters properly")
    score = 0
elif validate_word(word, rae) == False:
    print("Sorry, your word is not in RAE")
    append=input("Do you want to include it for future plays (Y/N)?")
    if append == "Y": 
        add_new_word(filename, word)
        print("Word {} appended to file".format(word))
        score = calculate_score(word, letters)
    else:
        score = 0
else:
    print("Congrats! Your word is valid")
    score = calculate_score(word, letters)
Congrats! Your word is valid

El juego termina mostrando el valor del score y la mejor palabra que se podía conseguir con las letras que se generaron.

print("Your score is: ",score)
print("You could have also think in: ",find_optimal_word(rae, letters,len(word)))
Your score is:  60
You could have also think in:  pega

5.11. Extensiones del juego

Esta es nuestra versión de la prueba de números del mítico programa Cifras y Letras. Tú puedes implementar la tuya. Aquí tienes algunas ideas.

  • Puedes dar la opción al jugador de elegir otros idiomas diferentes del español. Para ello necesitarás diferentes ficheros con listas de palabras en los otros idiomas

  • En el programa original, se realizaban varias pruebas de letras. Puedes mejorar el juego dándo la posibilidad de echar varias partidas y así tener más oportunidades de aumentar la puntuación

  • El juego original se realizaba contra reloj. Esto añadía una dificultad extra porque los concurstantes tenían un tiempo limitado. Puedes replicar esta característica contabilizando el tiempo que el usuario tarda en responder o incluso mostrando una cuenta regresiva.