Capítulo 10: Ta Te Ti

Temas Tratados En Este Capítulo:

Este capítulo presenta el juego Ta Te Ti contra una inteligencia artificial simple. Una inteligencia artificial (IA) es un programa de computadora que puede responder inteligentemente a los movimientos del jugador. Este juego no introduce ningún nuevo concepto que sea complicado. La inteligencia artificial del juego Ta Te Ti consiste en sólo unas pocas líneas de código.

Dos personas pueden jugar al Ta Te Ti con lápiz y papel. Un jugador es la X y el otro es la O. En un tablero consistente en nueve cuadrados, los jugadores toman turnos para colocar sus X u O. Si un jugador consigue ubicar tres de sus marcas en el tablero sobre la misma línea, columna o alguna de las dos diagonales, gana. Cuando el tablero se llena y ningún jugador ha ganado, el juego
termina en empate.

Este capítulo no introduce nuevos conceptos de programación. Hace uso de nuestro conocimiento adquirido hasta ahora para crear un jugador inteligente de Ta Te Ti. Empecemos mirando una prueba de ejecución del programa. El jugador hace su movimiento escribiendo el número del espacio en el que quiere jugar. Estos números están dispuestos de igual forma que las teclas numéricas en tu teclado (ver Figura 10-2).

Prueba de Ejecución de Ta Te Ti

Código fuente del Ta Te Ti

En una nueva ventana del editor de archivos, escribe el siguiente código y guárdalo como tateti.py. Luego ejecuta el juego pulsando F5.

Diseñando el Programa

La Figura 10-1 muestra cómo se vería un diagrama de flujo del Ta Te Ti. En el programa Ta Te Ti el jugador elige si quiere ser X u O. Quién toma el primer turno se elige al azar. Luego el jugador y la computadora toman turnos para jugar.

Los recuadros a la izquierda del diagrama de flujo son lo que ocurre durante el turno del jugador. El lado derecho muestra lo que ocurre durante el turno de la computadora. El jugador tiene un recuadro extra para dibujar el tablero ya que la computadora no precisa ver el tablero impreso en la pantalla. Después que el jugador o la computadora hacen su movimiento, revisamos si han ganado o a sido un empate, y entonces cambia el turno del juego. Trás terminar el juego, le preguntamos al jugador si desea jugar otra vez.

Figura 10-1: Diagrama de flujo para el Ta Te Ti
Figura 10-2: El tablero está ordenado igual que el teclado numérico de la computadora.

Representando el Tablero como Datos

Primero, necesitamos entender cómo vamos a representar el tablero como una variable. Sobre papel, el tablero de Ta Te Ti se dibuja como un par de líneas horizontales y un par de líneas verticales, con una X, una O o un espacio vacío en cada una de las nueve regiones formadas.

En el programa, el tablero del Ta Te Ti se representa como una lista de cadenas. Cada cadena representa uno de los nueve espacios en el tablero. Para que sea más fácil recordar qué índice de la lista corresponde a cada espacio, los ordenaremos igual que en el tablero numérico del teclado, como se muestra en la Figura 10-2.

Las cadenas serán ‘X’ para el jugador X, ‘O’ para el jugador O, o un espacio simple ‘ ‘ para un espacio vacío.

Entonces si una lista de diez cadenas se guardase en una variable llamada tablero, tablero[7] sería el espacio superior izquierdo en el tablero. De la misma forma tablero[5] sería el centro, tablero[4] sería el costado izquierdo, etcétera. El programa ignorará la cadena en el índice 0 de la lista. El jugador entrará un número de 1 a 9 para decirle al juego sobre qué espacio quiere jugar.

IA del Juego

La IA necesitará poder ver el tablero y decidir sobre qué tipo de espacios debe moverse. Para ser claros, definiremos tres tipos de espacios en el tablero de Ta Te Ti: esquinas, lados y el centro. La Figura 10-3 presenta un esquema de qué es cada espacio.

Figura 10-3: Ubicación de los lados, esquinas y centro en el tablero.

La astucia de la IA para jugar al Ta Te Ti seguirá un algoritmo simple. Un algoritmo es una serie finita de instrucciones para computar un resultado. Un único programa puede hacer uso de varios algoritmos diferentes. Un algoritmo puede representarse con un diagrama de flujo.

El algoritmo de la IA del Ta Te Ti calcula el mejor movimiento disponible, como se muestra en la Figura 10-4.

  1. Primero, ver si hay un movimiento con el que la computadora pueda ganar el juego. Si lo hay, hacer ese movimiento. En caso contrario, ir al paso 2.
  2. Ver si existe un movimiento disponible para el jugador que pueda hacer que la
    computadora pierda el juego. Si existe, la computadora debería jugar en ese lugar para bloquear la jugada ganadora. En caso contrario, ir al paso 3.
  3. Comprobar si alguna de las esquinas (espacios 1, 3, 7, ó 9) está disponible. Si lo está, mover allí. Si no hay ninguna esquina disponible, ir al paso 4.
  4. Comprobar si el centro está libre. Si lo está, jugar en el centro. Si no lo está, ir al paso 5.
  5. Jugar en cualquiera de los lados (espacios 2, 4, 6, u 8). No hay más pasos, ya que si hemos llegado al paso 5 los únicos espacios restantes son los lados.

Todo esto ocurre dentro del casillero “Obtener movimiento de la computadora.” en nuestro diagrama de flujo de la Figura 10-1. Podrías añadir esta información al diagrama de flujo con los recuadros de la Figura 10-4.

Figura 10-4: Los cinco pasos del algoritmo “Obtener movimiento de la computadora”. Las
flechas salientes van al recuadro “Comprobar si la computadora ha ganado”.

Este algoritmo es implementado en la función obtenerJugadaComputadora() y las otras funciones llamadas por obtenerJugadaComputadora().

El Comienzo del Programa

El primer par de líneas son un comentario y la importación del módulo random para poder llamar a la función randint().

Dibujando el Tablero en la Pantalla

La función dibujarTablero() imprimirá el tablero de juego representado por el parámetro tablero. Recuerda que nuestro tablero se representa como una lista de diez cadenas, donde la cadena correspondiente al índice 1 es la marca en el espacio 1 sobre el tablero del Ta Te Ti. La cadena en el índice 0 es ignorada. Muchas de nuestras funciones operarán pasando una lista de diez cadenas a modo de tablero.

Asegúrate de escribir correctamente los espacios en las cadenas, ya que de otra forma el tablero se verá raro al imprimirse en pantalla. Aquí hay algunas llamadas de ejemplo (con un argumento como tablero) a dibujarTablero() junto con las correspondientes salidas de la función:

Dejando al Jugador elegir X u O

La función ingresaLetraJugador() pregunta al jugador si desea ser X u O. Continuará preguntando al jugador hasta que este escriba X u O. La línea 27 cambia automáticamente la cadena devuelta por la llamada a input() a letras mayúsculas con el método de cadena upper().

La condición del bucle while contiene paréntesis, lo que significa que la expresión dentro del paréntesis es evaluada primero. Si se asignara ‘X’ a la variable letra, la expresión se evaluaría de esta forma:

Si letra tiene valor ‘X’ o ‘O’, entonces la condición del bucle es False y permite que la ejecución del programa continúe después del bloque while.

Esta función devuelve una lista con dos elementos. El primer elemento (la cadena del índice 0) será la letra del jugador, y el segundo elemento (la cadena del índice 1) será la letra de la computadora. Estas sentencias if-else elige la lista adecuada que nos va a devolver.

Decidiendo Quién Comienza

La función quienComienza() lanza una moneda virtual para determinar quien comienza entre la computadora y el jugador. El lanzamiento virtual de moneda se realiza llamando a random.randint(0, 1). Si esta llamada a función devuelve 0, la función quienComienza() devuelve la cadena ‘La computadora‘. De lo contrario, la función devuelve la cadena ‘El jugador‘. El código que llama a esta función usará el valor de retorno para saber quién hará el primer movimiento del juego.

Preguntando al Jugador si desea Jugar de Nuevo

La función jugarDeNuevo() pregunta al jugador si desea jugar de nuevo. Esta función devuelve True si el jugador escribe ‘‘ o ‘‘ o ‘s‘ o cualquier cosa que comience con la letra S. Con cualquier otra respuesta, la función devuelve False. Esta función es igual a la utilizada en el juego del Ahorcado.

Colocando una Marca en el Tablero

La función hacerJugada() es simple y consiste en sólo una línea. Los parámetros son una lista con diez cadenas llamada tablero, la letra de uno de los jugadores (‘X’ u ‘O’) llamada letra, y un espacio en el tablero donde ese jugador quiere jugar (el cual es un entero de 1 a 9) llamado jugada.

Pero espera un segundo. Este código parece cambiar uno de los elementos de la lista tablero por el valor en letra. Pero como este código pertenece a una función, el parámetro tablero será olvidado al salir de esta función y abandonar el entorno de la función. El cambio a tablero también será olvidado.

En realidad, esto no es lo que ocurre. Esto se debe a que las listas se comportan en forma especial cuando las pasas como argumentos a funciones. En realidad estás pasando una referencia a la lista y no la propia lista. Vamos a aprender ahora sobre la diferencia entre las listas y las referencias a listas.

Referencias

Prueba ingresar lo siguiente en la consola interactiva:

Esto tiene sentido a partir de lo que sabes hasta ahora. Asignas 42 a la variable spam, y luego copias el valor en spam y lo asignas a la variable cheese. Cuando cambias la variable spam a 100, esto no afecta al valor en cheese. Esto es porque spam y cheese son variables diferentes que almacenan valores diferentes.

Pero las listas no funcionan así. Cuando asignas una lista a una variable usando el signo =, en realidad asignas a la variable una referencia a esa lista. Una referencia es un valor que apunta a un dato. Aquí hay un ejemplo de código que hará que esto sea más fácil de entender. Escribe esto en la consola interactiva:

Esto se ve raro. El código sólo modificó la lista cheese, pero parece que tanto la lista cheese como la lista spam han cambiado. Esto se debe a que la variable spam no contiene a la propia lista sino una referencia a la misma, como se muestra en la Figura 10-5. La lista en sí misma no está contenida en ninguna variable, sino que existe por fuera de ellas.

Figura 10-5: Las variables no guardan listas, sino referencias a listas.

Observa que cheese = spam copia la referencia de la lista spam a cheese, en lugar de copiar el propio valor de lista. Tanto spam como cheese guardan una referencia que apunta al mismo valor de lista. Pero sólo hay una lista. No se ha copiado la lista, sino una referencia a la misma. La Figura 10-6 ilustra esta copia.

Figura 10-6: Dos variables guardan dos referencias a la misma lista.

Entonces la línea cheese[1] = ‘¡Hola!’ cambia la misma lista a la que se refiere spam. Es por esto que spam parece tener el mismo valor de lista que cheese. Ambas tienen referencias que apuntan a la misma lista, como se ve en la Figura 10-7.

Duplicando los Datos del Tablero

Si quieres que spam y cheese guarden dos listas diferentes, tienes que crear dos listas diferentes en lugar de copiar una referencia:

En el ejemplo anterior, spam y cheese almacenan dos listas diferentes (aunque el contenido de ambas sea idéntico). Pero si modificas una de las listas, esto no afectará a la otra porque las variables spam y cheese tienen referencias a dos listas diferentes:

La Figura 10-8 muestra como las dos referencias apuntan a dos listas diferentes.

Figura 10-8: Dos variables con referencias a dos listas diferentes.

Los diccionarios funcionan de la misma forma. Las variables no almacenan diccionarios, sino que almacenan referencias a diccionarios.

Usando Referencias a Listas en hacerJugada()

Volvamos a la función hacerJugada():

Cuando un valor de lista se pasa por el parámetro tablero, la variable local de la función es en realidad una copia de la referencia a la lista, no una copia de la lista. Pero una copia de la referencia sigue apuntando a la misma lista a la que apunta la referencia original. Entonces cualquier cambio a tablero en esta función ocurrirá también en la lista original. Así es cómo la función hacerJugada() modifica la lista original.

Los parámetros letra y jugada son copias de los valores cadena y entero que pasamos. Como son copias de valores, si modificamos letra o jugada en esta función, las variables originales que usamos al llamar a hacerJugada() no registrarán cambios.

Comprobando si el Jugador Ha Ganado

Las líneas 53 a 60 comprende la función esGanador() que en realidad es una larga sentencia return. Los nombres ta y le son abreviaturas de los parámetros tablero y letra para no tener que escribir tanto en esta función. Recuerda, a Python no le importa qué nombres uses para tus variables.

Hay ocho posibles formas de ganar al Ta Te Ti. Puedes formar una línea horizontal arriba, al medio o abajo. O puedes formar una línea vertical a la izquierda, al medio o a la derecha. O puedes formar cualquiera de las dos diagonales.

Fíjate que cada línea de la condición comprueba si los tres espacios son iguales a la letra pasada (combinados con el operador and) y usamos el operador or para combinar las ocho diferentes formas de ganar. Esto significa que sólo una de las ocho formas necesita ser verdadera para que podamos afirmar que el jugador a quien pertenece la letra en le es el ganador.

Supongamos que usted juega con ‘O‘, y el tablero se ve así:

Primero Python reemplazará las variables ta y le por los valores que contienen:

A continuación, Python evaluará todas las comparaciones == dentro de los paréntesis a un valor Booleano:

Luego el intérprete Python evaluará todas estas expresiones dentro de los paréntesis:

Como ahora hay sólo un valor dentro del paréntesis, podemos eliminarlos:

Ahora evaluamos la expresión conectada por todos los operadores or:

Una vez más, eliminamos los paréntesis y nos quedamos con un solo valor:

Entonces dados estos valores para ta y le, la expresión se evaluaría a True. Así es cómo el programa puede decir si uno de los jugadores ha ganado el juego.

Duplicando los Datos del Tablero

La función obtenerDuplicadoTablero() está aquí para que podamos fácilmente hacer una copia de una dada lista de 10 cadenas que representa un tablero de Ta Te Ti en nuestro juego. Algunas veces querremos que nuestro algoritmo IA haga modificaciones temporarias en una copia provisional del tablero sin cambiar el tablero original. En ese caso, llamaremos a esta función para hacer una copia de la lista del tablero. La nueva lista se crea en la línea 64, con los corchetes [] de lista vacía.

Pero la lista almacenada en dupTablero en la línea 64 es sólo una lista vacía. El bucle for recorre el parámetro tablero, agregando una copia de los valores de cadena desde el tablero original al tablero duplicado. Finalmente, después del bucle, se devuelve dupTablero. La función obtenerDuplicadoTablero() construye una copia del tablero original y devuelve una referencia a este nuevo tablero, y no al original.

Comprobando si un Espacio en el Tablero está Libre

Esta es una función simple que, dado un tablero de Ta Te Ti y una posible jugada, confirmará si esa jugada está disponible o no. Recuerda que los espacios libres en la lista tablero se indican como una cadena con un espacio simple. Si el elemento en el índice del espacio indicado no es igual a una cadena con un espacio simple, el espacio está ocupado y no es una jugada válida.

Permitiendo al Jugador Ingresar Su Jugada

La función obtenerJugadaJugador() pide al jugador que ingrese el número del espacio en el que desea jugar. El bucle se asegura de que la ejecución no prosiga hasta que el jugador haya ingresado un entero de 1 a 9. También comprueba que el espacio no esté ocupado, dado el tablero de Ta Te Ti pasado a la función en el parámetro tablero.

Las dos líneas de código dentro del bucle while simplemente piden al jugador que ingrese un número de 1 al 9. La condición de la línea 78 es True si cualquiera de las expresiones a la izquierda o a la derecha del operador or es True.

La expresión en el lado izquierdo comprueba si la jugada ingresada por el jugador es igual a ‘1‘, ‘2‘, ‘3‘, y así hasta ‘9‘ mediante la creación de una lista con estas cadenas (usando el método split()) y comprobando si la jugada está en esta lista.

‘1 2 3 4 5 6 7 8 9’.split() se evalúa a la lista [‘1’, ‘2’, ‘3’, ‘4’, ‘5’, ‘6’, ‘7’,
‘8’, ‘9’]
, pero es más fácil de escribir.

La expresión sobre el lado derecho comprueba si la jugada que el jugador ingresó es un espacio libre en el tablero. Lo comprueba llamando a la función hayEspacioLibre(). Recuerda que hayEspacioLibre() devolverá True si la jugada que le hemos pasado está disponible en el tablero. Observa que hayEspacioLibre() espera un entero en el parámetro jugada, así que empleamos la función int() para evaluar la forma entera de jugada.

Los operadores not se agregan a ambos lados de modo que la condición será True cuando cualquiera de estos requerimientos deje de cumplirse. Esto hará que el bucle pida al jugador una nueva jugada una y otra vez hasta que la jugada ingresada sea válida.

Finalmente, en la línea 81, se devuelve la forma entera de la jugada ingresada por el jugador. Recuerda que input() devuelve una cadena, así que la función int() es llamada para devolver la forma entera de la cadena.

Evaluación en Cortocircuito

Puede ser que hayas notado un posible problema en nuestra función obtenerJugadaJugador(). ¿Qué pasaría si el jugador ingresara ‘Z‘ o alguna otra cadena que no sea entera? La expresión jugada not in ‘1 2 3 4 5 6 7 8 9’.split() sobre el lado izquierdo devolvería False de acuerdo con lo esperado, y entonces evaluaríamos la expresión sobre el lado derecho del operador or.

Pero llamar a int(‘Z’) ocasionaría un error. Python muestra este error porque la función int() sólo puede tomar cadenas o caracteres numéricos, tales como ‘9‘ o ‘0‘, no cadenas como ‘Z‘.

Como un ejemplo de este tipo de error, prueba ingresar esto en la consola interactiva:

Pero cuando juegas al Ta Te Ti e intentas ingresar ‘Z‘ en tu jugada, este error no ocurre. La razón de esto es que la condición del bucle while está siendo cortocircuitada.

Evaluar en cortocircuito quiere decur que como el lado izquierdo de la palabra reservada or (jugada not in ‘1 2 3 4 5 6 7 8 9’.split()) se evalúa a True, el intérprete Python sabe que la expresión completa será evaluada a True. No importa si la expresión sobre el lado derecho de la palabra reservada or se evalúa a True o False, porque sólo uno de los valores junto al operador or precisa ser True.

Piensa en esto: La expresión True or False se evalúa a True y la expresión True or True también se evalúa a True. Si el valor sobre el lado izquierdo es True, no importa qué valor esté sobre el lado derecho:

False and <<<cualquier cosa>>> siempre se evalúa a False
True or <<<cualquier cosa>>> siempre se evalúa a True 

Entonces Python no comprueba el resto de la expresión y ni siquiera se molesta en evaluar la parte not hayEspacioLibre(tablero, int(jugada)). Esto significa que las funciones int() y hayEspacioLibre() nunca son llamadas mientras jugada not in ‘1 2 3 4 5 6 7 8 9’.split() sea True.

Esto funciona bien para el programa, pues si la expresión del lado izquierda es True entonces jugada no es una cadena en forma de número. Esto hace que int() devuelva un error. Las únicas veces que jugada not in ‘1 2 3 4 5 6 7 8 9’.split() se evalúa a False son cuando jugada no es una cadena compuesta por un único dígito. En ese caso, la llamada a int() no nos daría un error.

Un Ejemplo de Evaluación en Cortocircuito

Aquí hay un pequeño programa que sirve como un buen ejemplo de evaluación en cortocircuito. Prueba escribir lo siguiente en la consola interactiva:

La primera parte parece razonable: La expresión DevuelveFalse() or DevuelveTrue() llama a ambas funciones, por lo que puedes ver ambos mensajes impresos.

Pero la segunda expresión sólo muestra ‘La función «DevuelveTrue()» ha sido llamada.’ y no ‘La función «DevuelveFalse()»ha sido llamada.’. Esto se debe a que Python no ha llamado a DevuelveFalse(). Como el lado izquierdo del operador or es True, el resultado de DevuelveFalse() es irrelevante por lo que Python no se molesta en llamarla. La evaluación ha sido cortocircuitada.
Lo mismo ocurre con el operador and. Prueba escribir lo siguiente en la consola interactiva:

Si el lado izquierdo del operador and es False, entonces la expresión completa será False. No importa lo que sea el lado derecho del operador and, de modo que Python no se molesta en evaluarlo. Tanto False and True como False and False se evalúan a False, por lo que Python cortocircuita la evaluación.

Eligiendo una Jugada de una Lista de Jugadas

La función elegirAzarDeLista() es útil para el código IA más adelante en el programa. El parámetro tablero es una lista de cadenas que representa un tablero de Ta Te Ti. El segundo parámetro listaJugada es una lista de enteros con posibles espacios entre los cuales se puede elegir. Por ejemplo, si listaJugada es [1, 3, 7, 9], eso significa que elegirAzarDeLista() debería devolver el entero correspondiente a una de las esquinas.

Sin embargo, elegirAzarDeLista() comprobará primero que es válido realizar una jugada en ese espacio. La lista jugadasPosibles comienza siendo una lista vacía. El bucle for itera sobre listaJugada. Las jugadas para las cuales hayEspacioLibre() devuelve True se agregan a jugadasPosibles usando el método append().

En este punto, la lista jugadasPosibles contiene todas las jugadas que estaban en listaJugada y también son espacios libres en la lista tablero. Si la lista no está vacía, hay al menos una jugada posible.

La lista podría estar vacía. Por ejemplo, si listaJugada fuera [1, 3, 7, 9] pero todas las esquinas del tablero estuviesen tomadas, la lista jugadasPosibles sería []. En ese caso, len(jugadasPosibles) se evaluaría a 0 y la función devolvería el valor None. La próxima sección explicaremos el valor None.

El Valor None

El valor None representa la ausencia de un valor. None es el único valor del tipo de datos NoneType. Puede ser útil emplear el valor None cuando necesites un valor que exprese “no existe” o “ninguno de los anteriores”.

Pongamos por caso que tienes una variable llamada respuestaExámen para guardar la respuesta a una pregunta de selección múltiple. La variable podría contener True o False para indicar la respuesta del usuario. Podrías asignar None a respuestaExámen si el usuario saltease la pregunta sin responderla. Usar None es una forma clara de indicar que el usuario no ha respondido la
pregunta.

Las funciones que retornan llegando al final de la función (es decir, sin alcanzar una sentencia return) devolverán None. El valor None se escribe sin comillas, con una “N” mayúscula y las letras “one” en minúsculas. Como una nota al margen, None no se muestra en la consola interactiva como ocurriría con otros valores:

Las funciones que aparentan no devolver nada en realidad devuelven el valor None. Por ejemplo, print() devuelve None:

Creando la Inteligencia Artificial de la Computadora

La función obtenerJugadaComputadora() contiene al código de la IA. El primer argumento es un tablero de Ta Te Ti en el parámetro tablero. El segundo es la letra correspondiente a la computadora, sea ‘X‘ u ‘O‘ en el parámetro letraComputadora. Las primeras líneas simplemente asignan la otra letra a una variable llamada letraJugador. De esta forma el mismo código puede usarse independientemente de si la computadora es X u O.

La función devuelve un entero del 1 a 9 que representa el espacio en el que la computadora hará su jugada.

Recuerda cómo funciona el algoritmo del Ta Te Ti:

  • Primero, ver si hay una jugada con que la computadora pueda ganar el juego. Si la hay, hacer esa jugada. En caso contrario, continuar al segundo paso.
  • Segundo, ver si hay una jugada con la que el jugador pueda vencer a la computadora. Si la hay, la computadora debería jugar en ese lugar para bloquear al jugador. En caso contrario, continuar al tercer paso.
  • Tercero, comprobar si alguna de las esquinas (espacios 1, 3, 7, o 9) está disponible. Si ninguna esquina está disponible, continuar al cuarto paso.
  • Cuarto, comprobar si el centro está libre. Si lo está, jugar allí. En caso contrario,
  • continuar al quinto paso.
  • Quinto, jugar sobre cualquiera de los lados (espacios 2, 4, 6 u 8). No hay más pasos, pues si hemos llegado al quinto paso significa que sólo quedan los espacios sobre los lados.

La Computadora Comprueba si puede Ganar en Una Jugada

Antes que nada, si la computadora puede ganar en la siguiente jugada, debería hacer la jugada ganadora inmediatamente. El bucle for que empieza en la línea 105 itera sobre cada posible jugada de 1 a 9. El código dentro del bucle simula lo que ocurriría si la computadora hiciera esa jugada.

La primera línea en el bucle (línea 106) crea una copia de la lista tablero. Esto es para que la jugada simulada dentro del bucle no modifica el tablero real de Ta Te Ti guardado en la variable tablero. La función obtenerDuplicadoTablero() devuelve una copia idéntica pero independiente del tablero.

La línea 107 comprueba si el espacio está libre y, si es así, simula hacer la jugada en la copia del tablero. Si esta jugada resulta en una victoria para la computadora, la la función devuelve el entero correspondiente a esa jugada.

Si ninguna de las jugadas posibles resulta en una victoria, el bucle concluye y la ejecución del programa continúa en la línea 113.

La Computadora Comprueba si el Jugador puede Ganar en Una Jugada

A continuación, el código simula un movimiento del jugador en cada uno de los espacios. Este código es similar al bucle anterior, excepto que es la letra del jugador que se coloca sobre la copia del tablero. Si la función esGanador() muestra que el jugador ganaría con este movimiento, la computadora devuelve esta jugada para bloquear la victoria del jugador.

Si el jugador humano no puede ganar en la siguiente movida, el bucle for eventualmente concluye y la ejecución del programa continúa en la línea 121.

La llamada a elegirAzarDeLista() con la lista [1, 3, 7, 9] asegura que la función devuelva el entero de una de de las esquinas: 1, 3, 7, ó 9. Si todas las esquinas están tomadas, la función elegirAzarDeLista() devuelve None y la ejecución continúa en la línea 126.

Si ninguna de las esquinas está disponible, la línea 127 intentará jugar en el centro. Si el centro no está libre, la ejecución continúa sobre la línea 130.

Este código también llama a elegirAzarDeLista(), sólo que le pasamos una lista con los espacios sobre los lados ([2, 4, 6, 8]). Esta función no devolverá None pues los espacios sobre los lados son los únicos que pueden estar disponibles. Esto concluye la función obtenerJugadaComputadora() y el algoritmo IA.

Comprobando si el Tablero está Lleno

La última función es tableroCompleto(). Esta función devuelve True si la lista tablero pasada como argumento tiene una ‘X‘ o una ‘O‘ en cada índice (excepto por el índice 0, que es ignorado por el código). Si hay al menos un casillero en el tablero con espacio simple ‘ ‘ asignado, esta función devolverá False.

El bucle for nos permite comprobar los espacios 1 a 9 en el tablero de Ta Te Ti. En cuanto encuentra un espacio libre en el tablero (es decir, cuando hayEspacioLibre(tablero, i) devuelve True) la función tableroCompleto() devuelve False.

Si la ejecución concluye todas las operaciones del bucle, significa que ninguno de los espacios está libre. Entonces se ejecutará la línea 137 y devolverá True.

El Inicio del Juego

La línea 140 es la primera línea que no está dentro de una función, de modo que es la primera línea de código que se ejecuta al entrar a este programa. Consiste en el saludo al jugador.

Este bucle while tiene al valor True por condición, y continuará iterando hasta que la ejecución llegue a una sentencia break. La línea 144 configura el tablero principal de Ta Te Ti que usaremos, al cual llamaremos elTablero. Es una lista de 10 cadenas, donde cada una de ellas es un espacio simple ‘ ‘.

En lugar de escribir esta lista completa, la línea 144 usa replicación de listas. Es más corto y claro escribir [‘ ‘] * 10 que escribir [‘ ‘, ‘ ‘, ‘ ‘, ‘ ‘, ‘ ‘, ‘ ‘, ‘ ‘, ‘ ‘, ‘ ‘, ‘ ‘].

Decidiendo la Letra del Jugador y Quién Comienza

La función ingresaLetraJugador() permite al jugador elegir si quiere ser X u O. La función devuelve una lista de dos cadenas, la cual puede ser [‘X’, ‘O’] o [‘O’, ‘X’]. El truco de asignación múltiple asignará letraJugador al primer elemento en la lista devuelta y letraComputadora al segundo.

La función quienComienza() decide aleatoriamente quién comienza, y devuelve la cadena ‘El jugador’ o bien ‘La computadora’ y la línea 147 comunica al jugador quién comenzará.

El bucle de la línea 150 continuará alternando entre el código del turno del jugador y el del turno de la computadora, mientras juegoEnCurso tenga asignado el valor True.

El valor en la variable turno es originalmente asignado por llamada a la función quienComienza() en la línea 146. Su valor original es ‘El jugador’ o ‘La computadora’. Si turno es igual a ‘La computadora’, la condición es False y la ejecución salta a la línea 169.

Primero, la línea 153 llama a la función dibujarTablero() pasándole la variable elTablero para dibujar el tablero en la pantalla. Entonces la función obtenerJugadaJugador() permite al jugador ingresar su jugada (y también comprueba que sea un movimiento válida). La función hacerJugada() actualiza elTablero para reflejar esta jugada.

Después que el jugador ha jugado, la computadora debería comprobar si ha ganado el juego. Si la función esGanador() devuelve True, el código del bloque if muestra el tablero ganador y muestra un mensaje comunicando al jugador que ha ganado.

Se asigna el valor False a la variable juegoEnCurso para que la ejecución no continúe con el turno de la computadora.

Si el jugador no ganó con esta última jugada, tal vez esta movida ha llenado el tablero y ocasionado un empate. En este bloque else, la función tableroCompleto() devuelve True si no hay más movimientos disponibles. En ese caso, el bloque if que comienza en la línea 162 muestra el tablero empatado y comunica al jugador que ha habido un empate. La ejecución sale entonces del bucle while y salta a la línea 186.

Ejecutando el Turno de la Computadora

Las líneas 170 a 184 son casi idénticas al código del turno del jugador en las líneas 152 a 167. La única diferencia es que se comprueba si ha habido un empate después del turno de la computadora en lugar de hacerlo desde el turno del jugador.

Si no existe un ganador y no es un empate, la línea 184 cambia el turno al jugador. No hay más líneas de código dentro del bucle while, de modo que la ejecución vuelve a la sentencia while en la línea 150.

Las líneas 186 y 187 se encuentran inmediatamente a continuación del bloque while que comienza con la sentencia while de la línea 150. Se asigna False a juegoEnCurso cuando el juego ha terminado, por lo que en este punto se pregunta al jugador si desea jugar de nuevo.

Si jugarDeNuevo() devuelve False, la condición de la sentencia if es True (porque el operador not invierte el valor Booleano) y se ejecuta la sentencia break. Esto interrumpe la ejecución del bucle while que había comenzado en la línea 142. Como no hay más líneas de código a continuación de ese bloque while, se termina el programa.

Resumen

Crear un programa que pueda jugar un juego se reduce a considerar cuidadosamente todas las situaciones posibles en las que la IA pueda encontrarse y cómo responder en cada una de esas situaciones. La IA del Ta Te Ti es simple porque no hay muchos movimientos posibles en Ta Te Ti comparado con un juego como el ajedrez o las damas.

Nuestra IA simplemente comprueba si puede ganar en la próxima jugada. Si no es posible, bloquea la movida del jugador cuando está a punto de ganar. En cualquier otro caso la IA simplemente intenta jugar en cualquier esquina disponible, luego el centro y por último los lados. Este es un algoritmo simple y fácil de seguir.

La clave para implementar nuestro algoritmo IA es hacer una copia de los datos del tablero y simular jugadas sobre la copia. De este modo, el código de IA puede hacer esa jugada en el tablero real. Este tipo de simulación es efectivo a la hora de predecir si una jugada es buena o no.

Listado completo del Ta Te Ti

# Ta Te Ti

import random

def dibujarTablero(tablero):

    # Esta función dibuja el tablero recibido como argumento.
    # "tablero" es una lista de 10 cadenas representando la pizarra (ignora índice 0)
    print('   |   |')
    print(' ' + tablero[7] + ' | ' + tablero[8] + ' | ' + tablero[9])
    print('   |   |')
    print('-----------')
    print('   |   |')
    print(' ' + tablero[4] + ' | ' + tablero[5] + ' | ' + tablero[6])
    print('   |   |')
    print('-----------')
    print('   |   |')
    print(' ' + tablero[1] + ' | ' + tablero[2] + ' | ' + tablero[3])
    print('   |   |')

def ingresaLetraJugador():
    # Permite al jugador typear que letra desea ser.
    # Devuelve una lista con las letras de los jugadores como primer item, y la de la computadora como segundo.
    letra = ''
    while not (letra == 'X' or letra == 'O'):
        print('¿Deseas ser X o O?')
        letra = input().upper()

        # el primer elemento de la lista es la letra del jugador, el segundo es la letra de la computadora.
    if letra == 'X':
        return ['X', 'O']
    else:
        return ['O', 'X']

def quienComienza():
    # Elije al azar que jugador comienza.
    if random.randint(0, 1) == 0:
        return 'La computadora'
    else:
        return 'El jugador'

def jugarDeNuevo():
    # Esta funcion devuelve True (Verdadero) si el jugador desea volver a jugar, de lo contrario devuelve False (Falso).
    print('¿Deseas volver a jugar? (sí/no)?')
    return input().lower().startswith('s')

def hacerJugada(tablero, letra, jugada):
    tablero[jugada] = letra

def esGanador(ta, le):
    # Dado un tablero y la letra de un jugador, devuelve True (verdadero) si el mismo ha ganado.
    # Utilizamos reemplazamos tablero por ta y letra por le para no escribir tanto.
    return ((ta[7] == le and ta[8] == le and ta[9] == le) or # horizontal superior
    (ta[4] == le and ta[5] == le and ta[6] == le) or # horizontal medio
    (ta[1] == le and ta[2] == le and ta[3] == le) or # horizontal inferior
    (ta[7] == le and ta[4] == le and ta[1] == le) or # vertical izquierda
    (ta[8] == le and ta[5] == le and ta[2] == le) or # vertical medio
    (ta[9] == le and ta[6] == le and ta[3] == le) or # vertical derecha
    (ta[7] == le and ta[5] == le and ta[3] == le) or # diagonal
    (ta[9] == le and ta[5] == le and ta[1] == le)) # diagonal

def obtenerDuplicadoTablero(tablero):
    # Duplica la lista del tablero y devuelve el duplicado.
    dupTablero = []

    for i in tablero:
        dupTablero.append(i)

    return dupTablero

def hayEspacioLibre(tablero, jugada):
    # Devuelte true si hay espacio para efectuar la jugada en el tablero.
    return tablero[jugada] == ' '

def obtenerJugadaJugador(tablero):
    # Permite al jugador escribir su jugada.
    jugada = ' '
    while jugada not in '1 2 3 4 5 6 7 8 9'.split() or not hayEspacioLibre(tablero, int(jugada)):
        print('¿Cuál es tu próxima jugada? (1-9)')
        jugada = input()
    return int(jugada)

def elegirAzarDeLista(tablero, listaJugada):
    # Devuelve una jugada válida en el tablero de la lista recibida.
    # Devuelve None si no hay ninguna jugada válida.
    jugadasPosibles = []
    for i in listaJugada:
        if hayEspacioLibre(tablero, i):
            jugadasPosibles.append(i)

    if len(jugadasPosibles) != 0:
        return random.choice(jugadasPosibles)
    else:
        return None

def obtenerJugadaComputadora(tablero, letraComputadora):
    # Dado un tablero y la letra de la computadora, determina que jugada efectuar.
    if letraComputadora == 'X':
        letraJugador = 'O'
    else:
        letraJugador = 'X'

    # Aquí está nuestro algoritmo para nuestra IA (Inteligencia Artifical) del Ta Te Ti.
    # Primero, verifica si podemos ganar en la próxima jugada
    for i in range(1, 10):
        copia = obtenerDuplicadoTablero(tablero)
        if hayEspacioLibre(copia, i):
            hacerJugada(copia, letraComputadora, i)
            if esGanador(copia, letraComputadora):
                return i

    # Verifica si el jugador podría ganar en su próxima jugada, y lo bloquea.
    for i in range(1, 10):
        copia = obtenerDuplicadoTablero(tablero)
        if hayEspacioLibre(copia, i):
            hacerJugada(copia, letraJugador, i)
            if esGanador(copia, letraJugador):
                return i

    # Intenta ocupar una de las esquinas de estar libre.
    jugada = elegirAzarDeLista(tablero, [1, 3, 7, 9])
    if jugada != None:
        return jugada

    # De estar libre, intenta ocupar el centro.
    if hayEspacioLibre(tablero, 5):
        return 5

    # Ocupa alguno de los lados.
    return elegirAzarDeLista(tablero, [2, 4, 6, 8])

def tableroCompleto(tablero):
    # Devuelve True si cada espacio del tablero fue ocupado, caso contrario devuele False.
    for i in range(1, 10):
        if hayEspacioLibre(tablero, i):
            return False
    return True


print('¡Bienvenido al Ta Te Ti!')

while True:
    # Resetea el tablero
    elTablero = [' '] * 10
    letraJugador, letraComputadora = ingresaLetraJugador()
    turno = quienComienza()
    print(turno + ' irá primero.')
    juegoEnCurso = True

    while juegoEnCurso:
        if turno == 'El jugador':
            # Turno del jugador
            dibujarTablero(elTablero)
            jugada = obtenerJugadaJugador(elTablero)
            hacerJugada(elTablero, letraJugador, jugada)

            if esGanador(elTablero, letraJugador):
                dibujarTablero(elTablero)
                print('¡Felicidades, has ganado!')
                juegoEnCurso = False
            else:
                if tableroCompleto(elTablero):
                    dibujarTablero(elTablero)
                    print('¡Es un empate!')
                    break
                else:
                    turno = 'La computadora'

        else:
            # Turno de la computadora
            jugada = obtenerJugadaComputadora(elTablero, letraComputadora)
            hacerJugada(elTablero, letraComputadora, jugada)

            if esGanador(elTablero, letraComputadora):
                dibujarTablero(elTablero)
                print('¡La computadora te ha vencido! Has perdido.')
                juegoEnCurso = False
            else:
                if tableroCompleto(elTablero):
                    dibujarTablero(elTablero)
                    print('¡Es un empate!')
                    break
                else:
                    turno = 'El jugador'

    if not jugarDeNuevo():
        break