Mostrando entradas con la etiqueta archivos de texto. Mostrar todas las entradas
Mostrando entradas con la etiqueta archivos de texto. Mostrar todas las entradas

martes, 4 de marzo de 2014

La Cueva del Tiempo- otro videojuego de texto

En el afán de explorar las posibilidades de la consola de texto para elaborar vídeo juegos, consideraremos los juegos de aventura basados en texto, de los cuales el legendario Zork es el más conocido exponente. En los juegos de aventura de texto, se le presenta al jugador la descripción de un lugar y mediante comandos sencillos con estructura verbo-objeto (por ejemplo, "go west","open door","go down", etc.), el jugador participa activamente en un mundo ficticio y controla el desenlace de una narrativa no lineal. Zork se hizo muy popular a principios de la década de los 80 con las primeras computadoras personales, aunque ciertamente, no fue el primer juego de su tipo- esta distinción le corresponde a Colossal Cave Adventure, un juego de aventura de texto desarrollado en FORTRAN para computadoras mainframe a mediados de los años 70. Si quiere tener una idea de cómo eran estos juegos, puede jugar Zork en su navegador Web accediendo a la siguiente dirección: http://thcnet.net/zork/.

Desde el punto de vista de la programación, hacer un juego como Zork es algo un tanto complejo para abordar la eseñanza de la programación en Python para principiantes. Quizás emprenda un juego de aventura en una entrada futura de este blog. Por los momentos, tengo en mente implementar una versión más sencilla de estos juegos, basada en los libros de ficción interactiva tipo "Elige tu Propia Aventura" ("Choose Your Own Adventure"). Estos libros también se hicieron muy famosos a principios de la década de los 80's y forman parte de los recuerdos nostálgicos de las personas de mi generación. La serie original "Choose Your Own Adventure" consiste en 185 titulos publicados desde 1979 hasta 1998. El primero de estos titulos fue "La Cueva del Tiempo" (The Cave of Time), ecrito por Edward Packard y magnificamente ilustrado (ver las ilustraciones en este post) por Paul Granger (seudónimo de Don Hedin). Este es el libro que vamos a versionar en Python en esta oportunidad.

Los libros de este genero de "ficción interactiva" utilizan el medio impreso (el libro, cuya invención se remonta a los albores de la Historia) de una forma bastante novedosa e inusitada. Uno no lee estos libros de forma lineal, desde la primera página hasta la última en orden consecutivo. Más bien, al iniciar su lectura, pronto se le presenta al lector una decisión, un punto de bifurcación en la narrativa. Entonces, según la decisión que toma el lector, debe saltar a la página correspondiente que se señala en el texto. De esta forma, el lector va saltando de página en página hasta que llega al final del hilo de la narrativa. "La Cueva del Tiempo", por ejemplo, tiene 40 finales o hilos de narrativa contenidos en las 115 páginas que conforman el libro. Décadas antes de los libros de Harry Potter, no se imaginan lo entretenido que resultaba la lectura de estos libros para los chamos de mi generación y es por ello que quise presentarles como tributo una versión de este libro desarrollada como video juego en Python.


En la gráfica anterior se muestra un texto característico de estos libros. En la página 6 se describe, mediante una narrativa en segunda persona, la situación en la cual estás y en la parte inferior de esa página te indican las opciones. Según aceptes o rechaces la invitación al castillo del Rey, debes saltar a la página 22 o a la 114, respectivamente. ¿Cómo se implementaria esto en código Python? Una primera aproximación a este problema sería implementar una serie de condicionales (if's) anidados. Por ejemplo:

print "Estás en un cuarto con tres puertas numeradas 1,2 y 3. ¿Qué quieres hacer?"
print "Escoge una de las siguientes opciones:"
print "'1' para abrir la puerta número 1"
print "'2' para abrir la puerta número 2"
print "'3' para abrir la puerta número 3"
print "'4' si deseas esperar y no abrir ninguna puerta"
seleccion=raw_input(": ")
while not(seleccion in ['1','2','3','4']):
    print "Selección no válida. Vuelva a escoger."
    seleccion=raw_input(": ")
if seleccion=='1':
    print "Abres la puerta 1 lentamente. Al pasar por el umbral de la puerta,"
    print "descubres que has entrado en el Infierno. FIN"
elif seleccion=='2':
    print "La puerta 2 conduce a un pasadizo al cabo del cual sales del"
    print "calabozo. !Eres libre! FIN"
elif seleccion=='3':
    print "Entras en una sala en el medio del cual hay un cofre de tesoro."
    print "Hay otras tres puertas aqui. Agarras el cofre del tesoro y"
    print "eliges ir por una de esas puertas..."
    :
    :
    :
elif seleccion=='4':
    print "Te mueres de hambre esperando. Para tí, es el ... FIN"

En el código anterior, los puntos suspensivos verticales (:) al cabo del último print en la selección '3' denotan código adicional en el cual se le da al jugador a escoger más opciones que a su vez van desencadenando en otros desenlaces. Es decir, dentro de esta parte del código se anidarían otros condicionales y dentro de estos condicionales se anidarían otros y así hasta agotar el hilo narrativo. Imagínense lo complicado que puede resultar esta estrategia de codificación para estos libros de ficción interactiva. Otro inconveniente de esta estrategia es que no puedes implementar bucles en la narración - imagínense por ejemplo al abrir la puerta 3, y luego de varias decisiones tomadas, vuelves al mismo punto de partida en el cual se te dan a escoger entre abrir 3 puertas o esperar y no abrir ninguna. Esto nos obliga a considerar utilizar alguna estructura de datos que nos permita navegar por esta especie de arbol en el cual los nodos son las descripciones narrativas y los arcos a otros nodos son las posibles opciones que tenemos en cada punto. Permítanme entonces hablarles de los diccionarios en Python ...

Los diccionarios son un tipo de estructura de datos que aún no he abordado en este blog. Un diccionario es parecido a una lista en el sentido en que consiste de una secuencia de elementos. Sin embargo, a diferencia de las listas, los elementos de un diccionario se pueden acceder haciendo referencia a un campo clave. Piensa por ejemplo en una libreta de direcciones:

libreta = {'Pedro' : 'pedroperez@hotmail.com', 'Diablodado' : 'diablodado@enanosiniestro.com', 'Nicolas' : 'maduro@burrosoy.gob.ve'}

de modo que si se quiere consultar el correo de Nicolas, escribiríamos libreta['Nicolas'] . El objeto 'libreta' definido arriba es en efecto un diccionario de Python. Nótese el uso de corchetes ( {} ) y las secuencias de claves : valores. La sintaxis general para definir un diccionario es:

mi_diccionario = { clave1 : valor1, clave2 : valor2, clave3 : valor3, ... }

Las claves pueden ser cadenas o números (generalmente son cadenas). Los valores pueden ser de cualquier tipo de datos en Python: cadenas, números, listas, tuplas o inclusive otros diccionarios. Para borrar un elemento del diccionario, utilizamos la instrucción "del". Así por ejemplo, para borrar a 'Nicolas' de nuestra libreta (ojalá fuese tan fácil), escribiríamos:

del libreta['Nicolas']

Si queremos agregar un elemento nuevo a 'libreta', basta con hacer una asignación como esta:

libreta['Maricori']='maricori@cia.gov'

Podemos verificar si tenemos a un elemento específico en nuestro diccionario:

'Diosdado' in libreta

Lo cual retornaría False, pues 'Diosdado' no está en nuestra libreta (quien está es 'Diablodado'). Ahora volviendo al asunto que nos ocupa, que es el de hacer un juego tipo "elige tu aventura". Debido a que estoy creando una versión en Python de un libro impreso y que dentro del libro, las opciones hacen referencia a otros números de páginas, creí conveniente representar esta estructura de hipertexto como un diccionario, en el cual las claves son los números de página (datos tipo cadena y no numérico). Estas opciones (los valores en el diccionario) las defino como una lista de tuplas, en la cual cada tupla consiste de dos valores: la cadena a imprimir para esa decisión y el número de página (como cadena) hacia el cual el lector se dirige en caso de tomar esa decisión. Cuando en una página no hay opciones sino que el texto indica ir a la siguiente página, escribo la palabra 'Siguiente' como el primer elemento de una tupla en la lista de decisiones para esa página. Cuando el hilo narrativo termina en una página, la lista de decisiones consta de una sola tupla: ('FIN','0'). De este modo, el programa esperará a que el usuario presione ENTER y volverá a comenzar en la página '0', que es la portada del libro. Para darles una idea de cómo funciona todo, reproduzco parte del código donde se define la estructura del libro (con puntos suspensivos verticales entre el renglón de '4' y '111' para indicar que hay más código):

libro={
'0' : [('Siguiente','1')], 
'1' : [('Siguiente','2')],
'2' : [('Siguiente','3')], 
'3' : [('Si decides volver a casa, pasa a la página 4.','4'), \
('Si decides esperar, pasa a la página 5.','5')], 
'4' : [('Si sigues hacía el rancho, pasa a la página 8.','8'), \
('Si vuelves a entrar en la cueva, pasa a la pagina 10.','10')], 
:
:
'111' : [('Siguiente','112')],
'112' : [('FIN','0')],
'113' : [('FIN','0')],
'114' : [('Siguiente','61')],
'115' : [('FIN','0')] \
}

Quizás se pregunte porque no hay texto en cada página. La respuesta es que el texto de cada página se encuentra en su respectivo archivo de texto: '0.txt' , '1.txt', '2.txt', etc. El hecho que el nombre de cada archivo sea el número de página seguido de la extensión '.txt' facilita las cosas, pues los números de clave son las páginas dentro del diccionario 'libro'. El bucle principal consiste en visualizar la página, imprimir cada una de las opciones en "libro" para esa página ('Siguiente' y 'FIN' son casos especiales), solicitar la opción al jugador mediante raw_input, y dirigirse a la nueva página. Para comenzar a jugar el juego "La Cueva del Tiempo" y comenzar a ver cómo se implementa en Python, puede descargar la carpeta comprimida con el código fuente en "libro.py" y los archivos de texto en este enlace. A continuación una captura de pantalla del juego:


Puede observar que el texto de algunas páginas contiene "gráficos" en ASCII. Opté por incorporar las ilustraciones originales de Paul Granger como arte ASCII junto con el texto, y no como gráficos, para mantener el juego completamente en modo texto y darle un toque "retro" al programa. Para visualizar correctamente el juego, debe configurar su terminal con una fuente de espaciado uniforme (tipo "Courier" o "monospace" por ejemplo), con un tamaño lo suficientemente pequeño como para que quepan 128 caracteres por línea. También debe colocar el color de fondo de la pantalla en blanco, con el texto en negro (ver captura de pantalla arriba).



Permítanme darles unos tips sobre cómo crear arte ASCII. Cada ilustración, en forma de archivo PNG o JPEG, se puede convertir a texto ASCII mediante la opción "Exportar" de GIMP (un software libre para el manejo de archivos gráficos muy bueno). Así por ejemplo: Para cada imagen, primero invertí los colores mediante "Invertir Colores" en el menú correspondiente, luego reduje cada imagen a 120 pixeles de ancho por la cantidad proporcional de pixeles de alto y finalmente reduje el tamaño vertical a un poco menos de la mitad sin reducir el tamaño horizontal, lo cual me daba imágenes aplastadas que se visualizaban correctamente cuando las exportaba a texto ASCII. Para crear las letras grandes, utilicé un programa de línea de comandos en Linux llamado "figlet". Así por ejemplo:

figlet Bibliografia

 ____  _ _     _ _                        __ _      
| __ )(_) |__ | (_) ___   __ _ _ __ __ _ / _(_) __ _
|  _ \| | '_ \| | |/ _ \ / _` | '__/ _` | |_| |/ _` |
| |_) | | |_) | | | (_) | (_| | | | (_| |  _| | (_| |
|____/|_|_.__/|_|_|\___/ \__, |_|  \__,_|_| |_|\__,_|
                         |___/             
         


  • Packard, E. (1983). La Cueva del Tiempo. Editorial TIMUN MAS, S.A., Barcelona.
  • Swinehart, C. (2013). One Book. Many Readings. Disponible en: http://samizdat.cc/cyoa
  • Choose Your Own Adventure. (2014, February 19). En Wikipedia, The Free Encyclopedia. Disponible en: http://en.wikipedia.org/w/index.php?title=Choose_Your_Own_Adventure.
  • Choose Your Own Pyventure. (2013, October 19). Wikibooks, The Free Textbook Project. Disponible en: http://en.wikibooks.org/w/index.php?title=Choose_Your_Own_Pyventure.
  • List of Choose Your Own Adventure books. (2013, December 13). En Wikipedia, The Free Encyclopedia. Disponible en: http://en.wikipedia.org/w/index.php?title=List_of_Choose_Your_Own_Adventure_books

domingo, 27 de mayo de 2012

Archivos de texto

En esta entrada veremos como trabajar con archivos simples en python.  El tipo más simple de archivos es sin duda el archivo de texto.  Un archivo de texto es una sucesión de caracteres organizada en secuencias de líneas que reside en "algún lugar" de un dispositivo de almacenamiento secundario (disco duro, pen drive, cd rom, etc.) y al cual podemos acceder mediante su ruta.  La ruta es el camino en la jerarquía de directorios que indica de manera univoca en que parte de cual dispositivo de almacenamiento secundario reside el archivo. 


Un poco sobre las rutas de archivos


En linux, la raiz de toda ruta es "/". A partir de allí se indican los directorios y subdirectorios sucesivos separando los mismos mediante la barra diagonal "/". Por ejemplo,

/home/jlaurentum/Documentos/nota.txt

sería la ruta completa de un archivo llamado "nota.txt" que reside en el disco duro de mi computadora.  Notese que el elemento final de la ruta es el nombre del archivo en sí y que los archivos tienen terminaciones o sufijos que se indican despues del punto "." en su nombre.  Así, algunas terminaciones comunes son ".tex", ".odt", ".bin" que van asociadas a los archivos de LaTeX, Libre Office Writer y programas ejecutables en Linbux respectivamente.  Un sufijo de nombre de archivo particularmente importante para nosotros es ".py", el cual denota archivos de código fuente en Python, los cuales por cierto son archivos de texto tambien.

En Güindou$, el caracter separador de directorio en las rutas es la barra diagonal invertida "\".  Para especificar la raiz, hay que identificar el dispositivo de almacenamiento o la partición del disco duro mediante una letra seguida de ":".  Así por ejemplo, en la computadora de mis esposa (que es Guindou$-dependiente), tengo un archivo cuya ruta es:

C:\Mis documentos\jlaurentum\nota.txt

Yo evito usar espacios y caracteres raros en los nombres de mis directorios o de mis archivos.  Para separar las palabras de un nombre, utilizo el caracter de piso "_" o el guión "-" en vez del espacio " ".  Bueno, con esto estamos listos para ver cómo se trabajan con archivos en python.

El protocolo de trabajo con archivos de texto: abrir, leer/escribir y cerrar


Desde el punto de vista de programación en Python, podemos ver los archivos como objetos.  Cuando creamos una instancia de tipo archivo (mediante la sentencia open), indicamos la ruta del archivo y el modo de acceso al archivo, siendo este:

  • de escritura, indicado por "w" en el argumento modo de open.  Sólo será posible escribir al archivo.  Si no existe el archivo en la ruta, al abrirlo se crea.
  • de lectura, indicado por "r" en el argumento modo de open.  Sólo será posible leer el arhivo.
  • para anexar, indicado por "a" en el argumento modo de open.  Se podrá escribir sólo al final del archivo, sin modificar el contenido existente.
  • de lectura/escritura, indicado por "r+" en el argumento modo de open.  Se podrá leer y escribir al archivo.
Existen otros argumentos para modo, pero con estos basta.  Si abrimos un archivo para lectura pero este no existe en la ruta proporcionada, se genera un error.  Por ejemplo, vamos a suponer que me equivoco al escribir el nombre de nota.txt y escribo nata.txt (el cual no existe). Entonces, al abrir el archivo en Python, tendría un error:


>>> mi_archivo = open("/home/jlaurentum/Documentos/nata.txt","r")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IOError: [Errno 2] No existe el archivo o el directorio: '/home/jlaurentum/Documentos/nata.txt'
>>>


En otro post veremos como acceder a algunas funciones del sistema operativo verificar si un archivo existe, obtener un listado de archivos en un directorio, etc.  Supóngase pues, que "nota.txt" es un archivo de texto que contiene lo siguiente (puedes copiar y pegar este archivo y guardarlo en tu computadora para hacer las pruebas):


Este es un archivo de prueba.
Esta es la segunda línea.
Y esta es la tercera línea.


Intentamos una vez más abrir el archivo (habiendolo guardado previamente en la ruta que indicamos):


>>> mi_archivo = open("/home/jlaurentum/Documentos/nota.txt","r")
>>>


¡Éxito!  Notese que el método open, cuando es exitoso, crea una instancia nueva de la clase file (archivo). Así, mi_archivo es un objeto tipo file:

>>> type(mi_archivo)
<type 'file'>
>>>



Una vez que abrimos un archivo para lectura, estamos listos para acceder a él.  Vamos a visualizar el contenido de "nota.txt" con el siguiente script:


>>> for linea in mi_archivo:
...     print linea
...
Este es un archivo de prueba.

Esta es la segunda línea.

Y esta es la tercera línea.

>>>


Observen la sentencia for- es bastante elocuente para entender que python "vé" los archivos de texto como una secuencia de líneas.  Sin darnos cuenta, ya hemos leido todas estas líneas e inclusive las hemos imprimido. Pero ... algo anda mal. ¿Porqué hay un espacio adicional entre las líneas?  Resulta que cada línea en el archivo original termina en "\n", que es el caracter invisible que indica una nueva línea.  Cuando se imprime el "\n" al final de cada línea, se imprime un salto de línea adicional al salto de línea que imprime la sentencia print.  Por eso tenemos el interlineado de arriba.  Para corregir esto, lo que tenemos que hacer es eliminar el caracter final de cada línea cuando este es un "\n".  Sin embargo, tenemos que volver a abrir el archivo nuevamente porque en el último for lo leimos todo hasta el final:


>>> mi_archivo=open("/home/jlaurentum/Documentos/nota.txt","r")
>>> for linea in mi_archivo:
...     if linea[-1]=="\n":
...             linea=linea[:-1]
...     print linea
...
Este es un archivo de prueba.
Esta es la segunda línea.
Y esta es la tercera línea.
>>>


¡Qué éxito! Pero se nos ha olvidado algo muy importante: cerrar el archivo tras haber trabajado con él:


>>> mi_archivo.close()
>>>


No se puede repetir lo suficiente lo importante que es cerrar los archivos al final.  Si no lo hacemos, los cambios que hacemos al archivo en modo escritura se pueden perder o peor aún, el archivo puede corromperse y quedar inservible.

Ahora vamos a probar la escritura de archivos.  Normalmente, cuando yo quiero procesar el contenido de un archivo, lo que hago es abrir una copia en modo lectura y crear otro archivo en modo escritura al cual voy a escribir el contenido del primer archivo procesado.  Por ejemplo, supóngase que quiero recorrer todo el archivo "nota.txt" para cambiar la palabra "la" por "su":


>>> mi_archivo.close()
>>> mi_archivo=open("/home/jlaurentum/Documentos/nota.txt","r")
>>> otro_archivo=open("/home/jlaurentum/Documentos/nota2.txt","w")
>>> for linea in mi_archivo:
...     linea=linea.replace("la","su")
...     otro_archivo.write(linea)
...
>>> otro_archivo.close()
>>> mi_archivo.close()
>>>



Entonces, buscamos el archivo "nota2.txt" y lo abrimos con el editor de texto, verificando su contenido:


Este es un archivo de prueba.
Esta es su segunda línea.
Y esta es su tercera línea.



Para este ejemplo, tuvimos suerte.  El método replace sustituye todas las ocurrencias de su primer argumento por la cadena suministrada en su segundo argumento.  Si hubiesemos tenido una palabra como "reemplazarla", se hubiese cambiado a "reemplazarsu", lo cual seguramente no hubiesemos querido.  En otra entrada de este blog hablaremos de expresiones regulares, que nos permitirán hacer un procesamiento más avanzado con data de texto.


Exiten otras formas de leer un archivo, por ejemplo, podemos leer el archivo caracter por caracter.  El siguiente script cuenta el número de caracteres de nuestro archivo de ejemplo:


>>> mi_archivo=open("/home/jlaurentum/Documentos/nota.txt","r")
>>> contador=0
>>> caracter=mi_archivo.read(1)
>>> while caracter!="":
...     contador+=1
...     caracter=mi_archivo.read(1)
...
>>> print contador
87
>>> mi_archivo.close()
>>>



Este archivo contiene 87 caracteres, es decir, su tamaño es de 87 bytes.  Observen que esto concuerda con lo que nos reporta la consulta de las propiedades de ese archivo.  Tambien podemos leer el archivo de un sólo trancazo en memoria, como una lista de cadenas:


>>> mi_archivo=open("/home/jlaurentum/Documentos/nota.txt","r")
>>> lineas=mi_archivo.readlines()
>>> print lineas
['Este es un archivo de prueba. \n', 'Esta es la segunda l\xc3\xadnea.\n', 'Y esta es la tercera l\xc3\xadnea.\n']
>>> mi_archivo.close()
>>>



Los elementos raros en esta lista de cadenas se corresponden a caracteres especiales, como la "í" con acento en "línea", cuya codificación  utf-8, en 2 bytes, es c3ad.

Finalmente, hasta las páginas web se pueden leer como archivos de texto.  Por ejemplo, el siguiente script abre la página de la primera entrada de este blog:


from urllib import *
pagina=urlopen("http://mont-epython.blogspot.com/2011/06/como-primera-entrada-de-este-blog.html")
for linea in pagina:
    print linea[:-1]
pagina.close()


No se indicó la salida del interprete python en rojo porque este script imprime muchas líneas.  Observen que el tratamiento de una página web es virtualmente idéntico al de un archivo de texto: lo abrimos, lo leemos y lo cerramos.  La ruta del archivo es simplemente la dirección URL de la página, pero al abrir esta página y leerla, no vemos lo que veriamos en el navegador, vemos otra cosa.  ¿Qué exactamente? ¡Corran el script y averigüenlo!






Referencias