Archive for 5 septiembre 2010

Tetris (parte 1)

05/09/2010

Edit 2/10: El código completo está en github

Hace una semana empecé a escribir un Tetris simple en Python (para
ir aprendiendo el lenguaje). Al poco tiempo me dejé ir con la abstracción y generalización, hasta que vi que terminarlo iba a ser imposible.

Por eso decidí empezar de nuevo con una versión más simple y no pensar demasiado a futuro, para mantener el proyecto manejable. Lo que conseguí, como primera etapa, es la lógica de juego básica de Tetris, por el momento sin interfase gráfica.

Estructuras y funciones

Lo principal es la matriz contenida en grilla, la cual almacena el contenido de cada celda. Un cero representa una celda vacía, y otros números representan los distintos colores, texturas o como sea que la interfase diferencie a los bloques.

filas = 14
columnas = 10
# Guarda el color de cada celda de la grilla (0 = vacío)
grilla = [[0 for i in range(columnas)] for j in range(filas)]
colores_max = 5

Por otra parte está la lista pieza. Cada elemento es una dupla con las coordenadas de una celda que pertenece a la pieza. De este modo, cuando quiero que la pieza en juego caiga o se mueva de costado, sé qué celdas tengo que actualizar. El color de la pieza en juego se guarda en colorPieza. Ambas estructuras se actualizan al llamar piezaNueva(). Para la generación de piezas nuevas, está la lista formas, que contiene matrices que describen las piezas posibles (se carga de un archivo de texto).

formas = cargarPiezas("piezas.txt")
# Duplas con las coordenadas de cada celda que pertenece a la pieza actual
pieza = []
colorPieza = 0

mover(dx,dy)

Esta función se usa tanto para que el jugador mueva la pieza de costado como para hacerla caer. Los dos parámetros son el desplazamiento en x e y. Lo primero es verificar que haya lugar en la nueva posición:

def mover(dx,dy):
    global pieza
    # Me fijo que haya lugar para mover la pieza y que no se salga del tablero
    piezaNueva = []
    for (x,y) in pieza:
        if y+dy == filas or x+dx == columnas or y+dy < 0 or x+dx < 0 or \
            (x+dx,y+dy) not in pieza and grilla[y+dy][x+dx] != 0:
            return True
        piezaNueva.append((x+dx,y+dy))

Si hay lugar creo una nueva lista piezaNueva, con las nuevas coordenadas de la pieza. Me va a convenir poder comparar las coordenadas viejas con las nuevas para saber qué celdas necesito actualizar:

    # La muevo
    for (x,y) in pieza:
        if (x,y) not in piezaNueva:
            grilla[y][x] = 0
    for (x,y) in piezaNueva:
        grilla[y][x] = colorPieza
    pieza = piezaNueva
    return False

cargarPiezas(archivo)

Las piezas que puedo generar están guardadas en un archivo con el siguiente formato:

 1
111

1
1
11

11
11

Para traducir esto a una estructura manejable, leo el archivo línea por línea y voy generando la lista formas:

def cargarPiezas(archivo):
    piezas= [[]]
    # Las piezas están en un archivo de texto, separadas por lineas vacías
    f = open(archivo,'r')
    nueva = None
    for linea in f:
        if linea.isspace():
            piezas.append([])
            continue
        piezas[-1].append([(0,1)[c != " "] for c in linea.rstrip()])

    # Ahora me aseguro que las filas de cada pieza tengan largo uniforme
    for pieza in piezas:
        ancho = max([len(fila) for fila in pieza])
        for fila in pieza:
            fila.extend([0]*(ancho-len(fila)))
    return piezas

piezaNueva()

Cuando una pieza termina de caer, genero otra nueva. Esta función elija una forma al azar, genera la lista pieza, elije un color y verifica que haya lugar para colocarla. Luego la copia al tablero.

def piezaNueva():
    from random import random
    global pieza, grilla, colorPieza
    # Selecciono una forma al azar
    nueva = formas[int(random()*len(formas))]
    ancho = max([len(fila) for fila in nueva ])
    pieza = []
    # Posición inicial
    dx = int(random()*(columnas-ancho))
    # Traduzco de la grilla que describe la pieza al array de coordenadas
    for (y,fila) in enumerate(nueva):
        for (x,celda) in enumerate(fila):
            if celda != 0:
                pieza.append((x+dx,y))
    colorPieza = int(random()*colores_max)+1
    for (x,y) in pieza:
        if grilla[y][x] != 0:
            # No hay lugar para la pieza, perdiste.
            return True
    for (x,y) in pieza:
        grilla[y][x] = colorPieza

Interfase textual

Por el momento el tablero se muestra como una grilla de números en la consola, mediante una función muy simple:

def mostrarGrilla():
    for fila in grilla:
        for celda in fila:
            sys.stdout.write('{0:d}'.format(celda))
        print

La lógica del juego está contenida aquí. Se encarga de aceptar entrada del usuario y las distintas etapas del juego. No permite iniciar un juego nuevo (sin reiniciar el programa), y si bien avisa cuando uno pierde, no hace terminar el juego.

if __name__ == "__main__":
    piezaNueva()
    while 1:
        # Hago caer la pieza, si da true no pudo caer
        if mover(0,1):
            print "Se trabó"
            # La pieza no puede caer más. 
            # Me fijo si completó una fila y agrego una pieza nueva
            pieza = []
            # Me fijo qué filas falta completar
            incompletas = []
            for fila in grilla:
                if min(fila) == 0:
                    incompletas.append(fila)
            ncompletas = filas-len(incompletas)
            if ncompletas > 0:
                print ncompletas,"filas completas"
            # Esas filas son las únicas que van a quedar, todas al fondo
            grilla[ncompletas:] = incompletas
            # Lo demás queda vacío
            grilla[:ncompletas] = [[0 for i in range(columnas)]
                for j in range(ncompletas)] 
            if piezaNueva():
                print "Perdiste"
                break 
        mostrarGrilla()
        print
        entrada = raw_input(">")
        if entrada == "q":
            break
        elif entrada == "a":
            mover(-1,0)
        elif entrada == "d":
            mover(1,0)

En el siguiente artículo voy a contar cómo implemento una interfase gráfica con Qt y OpenGL.
Edit 12/9: también voy a permitirle rotar las piezas, sino no sería exactamente un tetris…

Script para renovar la IP con Tomato y módem Motorola SBV5120

05/09/2010

Este es un script que escribí para cuando estoy bajando cosas con JDownloader y necesito reiniciar la IP. JDownloader viene con varios scripts que cumplen esta función, entre ellos algunos para el firmware (Tomato) de mi router. Sin embargo no me funcionaba muy bien y quería aprender algo de Python así que escribí mi propio script. Las únicas librerías externas que usa son curl para los pedidos HTTP y re para procesar la respuesta.

Manualmente

El proceso manual para conseguir una nueva IP consiste en:

  • Entrar a la página del router y asignar una MAC nueva:
  • Entrar a la página del módem (la IP de fábrica es http://192.168.100.1 ) e ir a Configuration.
  • Apretar “Restart Cable Modem”.

En menos de un minuto el modem debería estar conectado nuevamente y con una IP distinta.

Script

Ahora hace falta automatizar el proceso para poder dejar el JDownloader corriendo durante la noche. El script principal es muy conciso:

ipnueva.py

#!/usr/bin/env python
import router
import modem

modem.reset_modem()

mac = router.random_mac()
httpid = router.router_id()
print 'Probando...'
router.set_mac(mac,httpid)
print 'Listo.'

Las funciones que llama están definidas en otros 2 scripts, que se encargan del módem y el router.

modem.py

Esta es la configuración de fábrica del módem, debería funcionar a menos que la hayan cambiado.

#!/usr/bin/env python
import pycurl
host = 'http://192.168.100.1/'
user = 'admin'
pw = 'motorola'

Primero pido la página de login, de lo contrario no me va a permitir realizar el paso siguiente

def reset_modem():
    c = pycurl.Curl()
    c.setopt(c.URL, host+'loginData.htm?loginUsername='+user+
        '&amp;loginPassword='+pw+'&amp;LOGIN_BUTTON=Login')
    import StringIO
    b = StringIO.StringIO()
    c.setopt(pycurl.WRITEFUNCTION, b.write)
    c.perform()

Ahora le digo que reinicie

    c.setopt(c.URL, host+'reset.htm')
    c.perform()
    return b.getvalue()

modem.py

Estos valores corresponden a la dirección, usuario y contraseña que hayan elegido

#!/usr/bin/env python
import pycurl
import re

host = 'http://ip-de-tu-router/'
user = 'usuario'
pw = 'contraseña'

Tomato tiene una medida de seguridad contra ataques cross-site-scripting o de inyección o no-se-qué, así que luego de loguearnos necesitamos buscar en el código fuente una clave que nos da:

def router_id():
    c = pycurl.Curl()
    c.setopt(c.USERPWD, user+':'+pw)
    c.setopt(c.URL, host)
    import StringIO
    b = StringIO.StringIO()
    c.setopt(pycurl.WRITEFUNCTION, b.write)

    c.perform()
    tid = re.search('_http_id=(TID[a-zA-Z0-9]{2,})',b.getvalue()).group(0)
    return tid

Esto simplemente genera una MAC al azar

def random_mac():
    import random
    mac = '00'
    for i in range(5):
        mac += ":" + "%2X" % random.randint(0,255)
    return mac

Y aquí hacemos el pedido de cambiar MAC, con las credenciales necesarias y la clave que tomamos antes

def set_mac(mac,httpid):
    c = pycurl.Curl()
    c.setopt(c.USERPWD, user+':'+pw)
    import urllib
    c.setopt(c.POSTFIELDS, '_service=*&amp;mac_wan='+urllib.quote(mac)+
        '&amp;mac_wl=&amp;'+httpid)
    c.setopt(c.URL, host+'tomato.cgi')
    import StringIO
    b = StringIO.StringIO()
    c.setopt(pycurl.WRITEFUNCTION, b.write)
    c.perform()

Configuración de JDownloader

Una vez que tenemos los scripts guardados en algún lugar, hay que configurar JDownloader para que ejecute ipnueva.py cuando quiera reconectarse. En Ajustes, vamos a Reconexión y Router -> Externo. En el campo Comando, ingresar la ruta a ipnueva.py o buscarlo con el botón Seleccionar. Luego hay que marcar el botón de Reconexión automática en la barra de herramientas.