El joc Memory amb Python i Tkinter

En aquest post presento un joc de Memory, fet amb Pyhton i amb Tkinter.

El Memory és juga en un tauler de NxN cartes amb N parell. Hi han N²/2 cartes diferents, és dir, de cada carta n’hi han dues d’iguals. Les cartes estan barrejades i inicialment les N² cartes estan tapades. La mecànica del joc és anar destapant destapant parelles de cartes, si les cartes coincideixen, queden destapades, si no, es tornes a tapar i es continua amb una altre parella. Es repeteix fins que totes les parelles estan destapades.
L’objectiu del joc és haver de repetir el mínim nombre de cops. Per aconseguir-ho, doncs, cal memoritzar on són les cartes. Es tracta, doncs, d’un exercici de memorització.

El cas és que a Internet vaig trobar una imatge amb 50 súper-herois i súper-malvats dels còmics de Marvel i gairebé veient la imatge se’m va acudir fer el joc de cartes.

Vet aquí la imatge :

Abans de començar, però, vaig rumiar la mecànica del joc. El resultat va ser aquesta versió en mode text

Memory en mode text

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import random
import os
import time

instructions = '''
Memory
------

Memory és el joc clàssic de recordar les parelles.
Tenim un tauler de 4x4, 6x6 o 8x8 caselles. 
Cada casella es correspon amb un número.
Cada casella amaga el nom d'un objecte. Hi han 8, 16 o 32 objectes diferents.
Cada objecte apareix dues vegades.
En cada torn, el jugador ha de destapar dues caselles. 
Si a ambdues caselles hi ha el mateix objecte, les caselles queden destapades.
Si no, després d'un temps de 3 segons, les caselles tornen a tapar-se
El cicle es repeteix fins que el jugador ha destapat totes les caselles 
'''

class Card:
    nom = ""
    tapada = True

def initGame(n):
    cards = initCards()
    table = initTable(cards, n)
    return {"table":table, "cards":cards}
    
def initCards():
    cards = [Card() for i in range(0, 32)]
    cards[0].nom = 'Wolverine'
    cards[1].nom = 'Psylocke'    
    cards[2].nom = 'Storm'
    cards[3].nom = 'Jubilee'    
    cards[4].nom = 'X-23'
    cards[5].nom = 'Jean Grey'    
    cards[6].nom = 'Dark Fenix'
    cards[7].nom = 'Cyclops'    
    cards[8].nom = 'Gambit'
    cards[9].nom = 'Angel'    
    cards[10].nom = 'Beast'
    cards[11].nom = 'Nightcrawler'    
    cards[12].nom = 'Mystique'
    cards[13].nom = 'Professor X'    
    cards[14].nom = 'Magneto'
    cards[15].nom = 'Rogue'    
    cards[16].nom = 'Deadpool'
    cards[17].nom = 'Captain America'    
    cards[18].nom = 'Thor'
    cards[19].nom = 'Iron Man'    
    cards[20].nom = 'Spiderman'
    cards[21].nom = 'Black Widow'    
    cards[22].nom = 'Scarlet Witch'
    cards[23].nom = 'Ant Man'    
    cards[24].nom = 'Hulk'
    cards[25].nom = 'Colossus'    
    cards[26].nom = 'Venom'
    cards[27].nom = 'Doctor Strange'    
    cards[28].nom = 'Green Goblin'
    cards[29].nom = 'Ultron'    
    cards[30].nom = 'Vision'
    cards[31].nom = 'Black Panther'    
    return cards

def clrscr():
    os.system("clear")

def initTable(cards, n):
    table = [None for i in range(0, n * n)]
    for i in range(0, n * n / 2):
        while True:
            pos1 = random.randint(0, n * n - 1)
            pos2 = random.randint(0, n * n - 1)
            if (table[pos1] == None) and \
               (table[pos2] == None) and \
               (pos1 != pos2):
                table[pos1] = cards[i]
                table[pos2] = cards[i]
                break;
    return table

def showTable(table, n):
    for i in range(0, n * n):
        if not table[i].tapada:
            print "casella %d : %s" % (i + 1), table[i].nom

def getCells(table, n):
    while True:
        try:
            p1 = int(raw_input('\nguess cell 1 : '))
            p2 = int(raw_input('guess cell 2 : '))
            if not (p1 in range(1, n * n + 1)) or \
               not (p2 in range(1, n * n + 1)):
                print "Only numbers 1 to %d " % n * n
                continue
            if (not table[p1-1].tapada) or (not table[p2-1].tapada):
                print "Only closed cells"
                continue
            break
        except:
            print "Only integer values 1 to %d" % n * n
    return [p1-1, p2-1]

def analyzeCells(table, cells, n):
    endGame = True
    pos1 = cells[0]
    pos2 = cells[1]
    nom1 = table[pos1].nom 
    nom2 = table[pos2].nom 
    
    print "cell %d : %s" % (pos1 + 1, nom1)
    print "cell %d : %s" % (pos2 + 1, nom2)

    if (nom1 == nom2):
        table[pos1].tapada = False 
        table[pos2].tapada = False

    time.sleep(3)
    clrscr()
    
    for i in range(0, n * n):
        endGame = endGame and (not table[i].tapada)
          
    return not endGame

def showTable(table, n):
    for i in range(0, n * n):
        if not table[i].tapada:
            print "Posició %d : %s" % ((i + 1), table[i].nom)
        
def gameLoop(gameObjects, n):
    table = gameObjects.get("table")
    showTable(table, n)
    cells = getCells(table, n)
    continueGame = analyzeCells(table, cells, n)
    return continueGame

if __name__ == "__main__":
    clrscr()
    continueGame = True
    print(instructions)
    while True:
        n = raw_input("dimension NxN (4,6,8) ? ")
        if n in ['4','6','8']:
            break

    n = int(n)
    
    gameObjects = initGame(n)
    while continueGame:
        continueGame = gameLoop(gameObjects, n)

    print "Well done!"

El joc no té massa truc. Dins del if __name__==”__main__”: mostro les instruccions, espero a rebre un vlaor vàlid per N, i inicialitzo el joc.

L’array cells de 32 posicions manté la llista de cartes d’herois/malvats. Inicialitzo aquest array amb initCards. Noteu l’ús de la python comprehension per a inicialitzar l’array. També hagués pogut crear-lo inicialitzant amb [] i omplint-lo després amb append.

L’array table manté el tauler NxN. Aquest array es carrega amb initTable. Faig servir un algoritme molt simple de força bruta per omplir-lo de parelles : simplement trio un parell d’ubicacions a l’atzar dins del rang del tauler i comprovo si no estan repetides i si estan disponibles.

Un cop disposo de cells i table, ja només cal encetar la iteració del joc, és dir : demanar un parell de posicions, verificar que son vàlides, comprovar quina parella amaga i decidir si s’ha trobat una parella i s’ha d’acabar el joc, o si no és una parella vàlida i cal mostrar durant tres segons, abans d’esborrar la pantalla i continuar preguntant. Aquesta lògica es codifica a analyzeCells dins gameLoop.

És molt simple. L’aplicació fa servir entrada i sortida simpl per pantalla (es podria haver fet amb la llibreria ncurses). L’esborrat de pantalla es va invocant el programa clear amb os.system(“clear”), i l’espera de tres segons es fa amb time.sleep(3).

Amb aquesta versió en mode text, ja tinc les bases per a la versió amb GUI.

Memory amb GUI Tkinter

A la versió Tkinter he afegit algunes millores.

– Un tauler addicional de 10×10 i tinc, per tant, quatre possibles mides : 4×4, 6×6, 8×8 i 10×10
– Gestió per menú que permet controlar el final del joc, o començar-ne un de nou.
– una indicació del nombre de clicks utilitzats

Tinc, doncs, quatre frames :

– la finestra de l’aplicació, amb el menú que és on s’obren els frames de l’aplicació :

– el frame de nou joc, on es pot triar la mida del tauler

– el tauler NxN amb el joc pròpiament dit.

– la pantalla de final de joc on es diu quants clicks han calgut per destapar totes les cartes

Cal tenir present, a l’hora de treballar amb Tkinter que només pot haver un mainloop, l’aplicació es desenvolupa dins d’aquest mainloop. El mainloop és el fil principal de l’aplicació. Això és important perquè prefigura com ha de ser l’aplicació.

Les imatges

En un experiment inicial he fet servir Image i TkImage de PIL (Python Image Library) per a carregar directament la imatge jpg i processar-la per a retallar (crop) els de cada carta. Al final he optat per una altre alternativa : generar la imatge ppm de cada heroi/malvat per a fer-la servir amb un PhotoImage de Tkinter. Aquest objecte PhotoImage és estàndar del Tkinter, a diferència de les imatges amb PIL, llibreria que cal importar explícitament i que aporta funcionalitat de processament d’imatges. Tkinter.PhotoImage no permet gran cosa més que carregar la imatge i mostrar-la, a més només treballa amb imatges ppm, pgm i gif.

Per a crear les imatges ppm he fet servir aquest programa _

#!/usr/bin/python
# -*- coding: utf-8 -*-

import Tkinter as tk
from PIL import Image, ImageTk


# 450 293
xdim = 45
ydim = int(293.0 / 5.0)
cropDimensions= (xdim, ydim)

images = []

imageFace = Image.open("marvel-heros-villains-reduced.3.jpg")
imageHide = Image.open("diamond-shaped-texture-background.jpg")
       
for i in range(0, 50):
    y = i / 10
    x = i % 10
    x1 = x * xdim 
    x2 = x1 + xdim
    y1 = int(y * (293.0 / 5.0))
    y2 = y1 + ydim
    
    images.append(imageFace.crop((x1, y1, x2, y2)))

images.append(imageHide.crop((0, 0, xdim, ydim)))

print "working!"

for i in range(0, len(images) - 1):
    print "file %d " % i
    images[i].save("hero_%d.ppm" % i)

print "file 50"
images[50].save("hide_card.ppm")

print "done!"

El resultat de l’script anterior són les imatges amb les cares dels herois/malvats.

He utilitzat una aproximació basada en objectes. He assignat una classe a cada un dels objectes principals, i he posat cada classe en un fitxer, a més d’un fitxer memory_main.py principal des d’on es llença l’execució de l’aplicació.

Els objectes i els scripts corresponents són :

memory_main.py           --> incia el joc

memory_model.py          -->  class MemoryModel
memory_card.py           -->  class ImageButton
memory_controller.py     -->  class MemoryController

memory_app_window.py     -->  class MemoryAppWindow
memory_frame_new_game.py -->  class FrameNewGame
memory_frame_memory.py   -->  class FrameMemory
memory_frame_congrat.py  -->  class FrameCongrat

El joc s’inicia amb memory_main que executa el constructor de MemoryAppWindow

El constructor de MemoryAppWindow carrega la finestra principal i inicialitza tauler buit, llista de cartes, controlador del joc que es compartirà amb tots els frames i inicialitza els frames. A continuació mostra el frame newGame invocant-ne el constructor, al que li passa el controlador i la llista de frames

Des del frame de nou joc es pot triar la mida del tauler. EN fer click a una mida, passa o obrir-se el tauler amb la mida sol·licitada, per a fer-ho cal carregar el tauler amb cartes, que en aquest cas, són objectes de la classe ImageButton, que no és més que un embolcall del Button de Tkinter amb una PhotoImage. El cor de FrameMemory és un array de ImageButtons que es carrega al controlador. Es necessari que estigui allà perquè és al controlador on està la lògica d’anàlisi de la jugada i on es decideix si la parella queda descoberta, o es tapa, o si finalitza el joc.

Quan s’ha destapat totes les cartes, es mostra el frame de felicitacions i presentació el nombre de clicks. per iniciar un altre joc cal triar l’opció de nou joc al menú, o bé l’opció d’acabar si no es vol seguir.

Només comentar que, a diferència d’altres programes amb Tkinter que he fet, en aquest les classes Frame no deriven del Frame de Tkinter, si no que tenen el frame com un atribut. Seria una altre opció de codificació.

Com que he partit de la versió en modetext que ha seguit un paradigma de programació estructurada, el resultat és que aquest model d’objectes ja vingut donat per una redistribució del codi més que no pas per un anàlisi d’objectes pròpiament dit, a més que Tkinter imposa algunes característiques,com la necessitat d’un mainloop.

En tot cas, el joc queda força bonic, i la creació de la GUI amb Tkinter ha estat força directa.

Per si voleu fer un cop d’ull al codi, el podeu trobar al respositori GitHub : https://github.com/abaranguer/memory

Per acabar, un tauler de 10×10 amb una partida a punt d’acabar :

Caçar el Wumpus!

huntthewumpus

“Hunt the Wumpus” és un joc d’ordinador de 1972, inicialment escrit en mode text i desenvolupat en llenguatge BASIC. La seva simplicitat, a més de tenir les fonts disponibles, va motivar l’aparició de diverses i successives versions.

En aquest post escric la meva pròpia versió en català i en Python

Vet aquí les instruccions de “Caçar el Wumpus” :

Caçar el Wumpus
—————
El Wumpus és un monstre enorme i pesat amb la pell plena de ventoses
que s’alimenta de tot allò que cau a les seves urpes.
Tu ets un caçador que vol aconseguir el cap del Wumpus com a trofeu
i ets a un castell on se sap que n’hi ha un.
El castell té vint habitacions, cada habitació es connecta amb
altres tres habitacions.
Si entres a l’habitació on és el Wumpus, et menjarà.
Al castell hi ha un parell d’habitacions que tenen un fals terra.
Només entrar s’ensorra el terra i caus a un pou amb un potent àcid al fons.
Tothom que entra a una habitació amb pou, mor.
Menys el Wumpus. El Wumpus no hi cau perquè amb les seves ventoses
s’enganxa a les parets i no toca l’àcid.
Al castell també hi ha un parell de rats penats gegants.
Si entres a una habitació amb un rat penat, t’agafarà, se t’emportarà volant
i et deixarà caure a una habitació qualsevol del castell.
Els rats penats no poden endur-se al Wumpus perquè pesa massa per ells
i el Wumpus no es pot menjar als rats penats perquè són massa ràpids
per atrapar-los.
Saps que a alguna de les habitacions adjacents hi ha el Wumpus
perquè mai s’ha banyat i fa molta pudor, i el pots ensumar.
Saps que a les habitacions adjacents hi poden haver rats-penats
perquè sents el soroll del batec de les seves ales,
I, finalment, saps que a les habitacions adjacents poden haver pous
perquè sents el soroll de l’acid bullint.
Tens cinc fletxes. Has de moure’t pel castell
i quan dedueixis a quina habitació està el Wumpus, pots tirar-li
una fletxa des d’una de les habitacions adjacents.
Si l’habitació a la que has tirat la fletxa és la del Wumpus,
el mataràs i hauràs guanyat.
Però si el Wumpus no és a l’habitació, el soroll de la fletxa
potser el posarà en alerta i farà que es mogui a alguna habitació
contigua a la que ocupa.
Si esgotes totes les fletxes, aleshores ja no tens defensa
possible contra el Wumpus, que et trobarà i et menjarà.

Com he implementat les anteriors instruccions?

“El castell té vint habitacions, cada habitació es connecta amb altres tres habitacions.”

De fet, el castell del wumpus es pot modelar com un dodecaedre, on cada vèrtex representa una habitació i cada aresta ens porta a l’habitació contigua.

Vet aquí una bonica imatge d’un dodecaedre regular extreta de la Viquipèdia:

Si aplano aquesta figura obtinc una versió del mapa que em serà més útil.

Per a dibuixar el mapa del castell he fet servir el següent codi Python :

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import Tkinter as tk
import math

class Room:
    xc = 0
    yc = 0
    x0 = 0
    y0 = 0
    x1 = 0
    y1 = 0
    doors = []

if __name__ == "__main__":
    # initialize map
    rooms = [Room() for i in range(0, 20)]

    rooms[ 0].doors = [1, 4, 5]
    rooms[ 1].doors = [0, 2, 7]
    rooms[ 2].doors = [1, 3, 9]
    rooms[ 3].doors = [2, 4, 11]
    rooms[ 4].doors = [0, 3, 13]
    rooms[ 5].doors = [0, 6, 14]
    rooms[ 6].doors = [5, 7, 16]
    rooms[ 7].doors = [1, 6, 8]
    rooms[ 8].doors = [7, 9, 17]
    rooms[ 9].doors = [2, 8, 10]
    rooms[10].doors = [9, 11, 18]
    rooms[11].doors = [3, 10, 12]
    rooms[12].doors = [11, 13, 19]
    rooms[13].doors = [4, 12, 14]
    rooms[14].doors = [5, 13, 15]
    rooms[15].doors = [14, 16, 19]
    rooms[16].doors = [6, 15, 17]
    rooms[17].doors = [8, 16, 18]
    rooms[18].doors = [10, 17, 19]
    rooms[19].doors = [12, 15, 18]    
    
    root = tk.Tk()
    canvas = tk.Canvas(root, width=600, height=600, borderwidth=0, highlightthickness=0, bg="grey")
    j = 0
    R = 20
    for i in range(0, 5):
        x = 300 + int(80.0 * math.cos( (2.0 * math.pi / 5.0) * i)) 
        y = 300 + int(80.0 * math.sin( (2.0 * math.pi / 5.0) * i))

        rooms[j].xc = x
        rooms[j].yc = y
        rooms[j].x0 = x - R
        rooms[j].y0 = y - R
        rooms[j].x1 = x + R
        rooms[j].y1 = y + R
        j = j + 1

    for i in range(0, 10):
        x = 300 + int(180.0 * math.cos( (math.pi / 5.0) * i)) 
        y = 300 + int(180.0 * math.sin( (math.pi / 5.0) * i))

        rooms[j].xc = x
        rooms[j].yc = y
        rooms[j].x0 = x - R
        rooms[j].y0 = y - R
        rooms[j].x1 = x + R
        rooms[j].y1 = y + R
        j = j + 1

    for i in range(0, 5):
        x = 300 + int(280.0 * math.cos( (2.0 * math.pi / 5.0) * i - (math.pi / 5.0)))
        y = 300 + int(280.0 * math.sin( (2.0 * math.pi / 5.0) * i - (math.pi / 5.0)))

        rooms[j].xc = x
        rooms[j].yc = y
        rooms[j].x0 = x - R
        rooms[j].y0 = y - R
        rooms[j].x1 = x + R
        rooms[j].y1 = y + R
        j = j + 1

    for i in range(0, 20):
        canvas.create_line(rooms[i].xc, \
                           rooms[i].yc, \
                           rooms[rooms[i].doors[0]].xc, \
                           rooms[rooms[i].doors[0]].yc)

        canvas.create_line(rooms[i].xc, \
                           rooms[i].yc, \
                           rooms[rooms[i].doors[1]].xc, \
                           rooms[rooms[i].doors[1]].yc)
        
        canvas.create_line(rooms[i].xc, \
                           rooms[i].yc, \
                           rooms[rooms[i].doors[2]].xc, \
                           rooms[rooms[i].doors[2]].yc)

    for i in range(0, 20):
        canvas.create_oval(rooms[i].x0, rooms[i].y0, rooms[i].x1, rooms[i].y1, fill='white')
        canvas.create_text(rooms[i].xc, rooms[i].yc, text='%d' % i, fill='black')
        
    canvas.grid()
    root.wm_title("El Castell del Wumpus!")
    root.mainloop()

Destacar en el codi anterior l’us del canvas de Tkinter per a fer el dibuix, i l’us de la Python comprehension per a inicialitzar l’array rooms (just abans d’assignar les door de cada room).

Al codi anterior ja apareix l’estructura del mapa : el array rooms, de 20 posicions, on cada posició conté una instancia de la classe Room que, entre d’altres propietats, té l’array doors de tres posicions, que manté els números de les habitacions que es connecten a l’habitació.

“Al castell hi ha un parell d’habitacions que tenen un fals terra…
Al castell també hi ha un parell de rats penats gegants…
Tens cinc fletxes…·

Per tant, tenim els següents personatges o objectes del joc : un caçador, un wumpus, dos pous d’àcid, dos rats penats gegants i cinc fletxes. Les cinc fletxes les porta el caçador i no cal, doncs, ubicar-les específicament. El que cal fer en iniciar el joc és col·locar els objectes wumpus, caçador, pous i rats penats al castell, de forma que el caçador no coincideix amb el wumpus, els rats penats i els pous; els rats penats no coincideixen entre ells ; i els pous tampoc. És dir : el wumpus podria compartir habitació amb un rat-penat i un pou. Però això és possible perquè “El wumpus no hi cau [als pous] perquè amb les seves ventoses s’enganxa a les parets i en pot sortir sense perill.” i “Els rats penats no poden endur-se al wumpus perquè pesa massa per ells i el wumpus no es pot menjar als rats penats perquè són massa ràpids per ell.”.

Això es fa amb aquest codi :

Una classe molt simple GameObject, que manté la ubicació de l’objecte

class GameObject:
    roomNumber = -1

    def __init__(self, roomNumber):
        self.roomNumber = roomNumber
    # initialize characters and objects
    wumpusRoom = -1
    bat1Room = -1
    bat2Room = -1
    pit1Room = -1
    pit2Room = -1
    hunterRoom = -1
    arrows = 5

    while (hunterRoom == wumpusRoom) or \
          (hunterRoom == bat1Room) or   \
          (hunterRoom == bat2Room) or   \
          (hunterRoom == pit1Room) or   \
          (hunterRoom == pit2Room) or   \
          (pit1Room == pit2Room) or     \
          (bat1Room == bat2Room):
        
        hunterRoom = rnd.randint(0, 19)
        wumpusRoom = rnd.randint(0, 19)
        bat1Room = rnd.randint(0, 19)
        bat2Room = rnd.randint(0, 19)
        pit2Room = rnd.randint(0, 19)
        pit2Room = rnd.randint(0, 19)

    hunter = GameObject(hunterRoom)
    wumpus = GameObject(wumpusRoom)
    bat1 = GameObject(bat1Room)
    bat2 = GameObject(bat2Room)
    pit1 = GameObject(pit1Room)
    pit2 = GameObject(pit2Room)

    return {"rooms":rooms, "hunter":hunter, "wumpus":wumpus,
            "bat1":bat1, "bat2":bat2, "pit1":pit1, "pit2":pit2, "arrows":arrows}

L’estratègia utilitzada és repetir fins que es torna una combinació a l’atzar que ubica tots els objectes en habitacions diferents.

Destacar, també, l’us d’un diccionari per retornar els objectes.

“Si entres a l’habitació on és el Wumpus, et menjarà…
Tothom que hi cau [a una habitació amb pou], mor…
Si entres a una habitació amb un rat penat, t’agafarà, se t’emportarà volant
i et deixarà caure a una habitació qualsevol del castell…

A la funció enterRoom tenim la codificació de les regles anteriors

def enterRoom(gameObjects):
    continueGame = True
    rooms = gameObjects.get("rooms")
    hunterRoom = gameObjects.get("hunter").roomNumber
    wumpusRoom = gameObjects.get("wumpus").roomNumber
    bat1Room = gameObjects.get("bat1").roomNumber
    bat2Room = gameObjects.get("bat2").roomNumber
    pit1Room = gameObjects.get("pit1").roomNumber
    pit2Room = gameObjects.get("pit2").roomNumber
    
    print "\nEts a l'habitació número %d" % hunterRoom

    if wumpusRoom == hunterRoom:
        print "El wumpus és a l'habitació!"
        print "T'ha atrapat i comença a devorar-te!" 
        print "No és que sigui dolent... Et menja perquè és la seva natura de Wumpus..."    
        continueGame = False

    if continueGame and (hunterRoom in (pit1Room, pit2Room)):
        print "Hi ha un pou amb acid a l'habitació!"
        print "Has caigut dins i comences a dissoldre't de forma lenta i dolorosament agònica!"
        continueGame = False

    if continueGame and (hunterRoom in (bat1Room, bat2Room)):
        print "I un rat penat gegant a l'habitació! T'ha agafat i se t'emporta volant!"
        newHunterRoom = rnd.randint(0, 19)
        newBat1Room = rnd.randint(0, 19)
        newBat2Room = rnd.randint(0, 19)
        gameObjects.get("hunter").roomNumber = newHunterRoom
        gameObjects.get("bat1").roomNumber = newBat1Room
        gameObjects.get("bat2").roomNumber = newBat2Room
        print "El rat penat t'ha deixat caure a l'habitació número %d" % newHunterRoom
        continueGame = enterRoom(gameObjects)

    if not continueGame:
        print "Has mort de forma horrible!" 
        print "..." 
        print "Però altres vindran a intentar triomfar allà on tu has fracassat tan lamentablement "

    return continueGame


“Has de moure’t pel castell…”

Una de les possibles accions és, doncs, moure’s a una habitació adjacent.

El següent codi és la part de la funció actionHunter que recull l’acció “moure’s” del caçador :

def actionsHunter(gameObjects):
    continueGame = True
    wumpusMove = False
    hunterRoom = gameObjects.get("hunter").roomNumber
    wumpusRoom = gameObjects.get("wumpus").roomNumber
    arrows = gameObjects.get("arrows")
    rooms = gameObjects.get("rooms")
    [door0, door1, door2] = rooms[hunterRoom].doors

    while True:
        action = raw_input("Moure's 'm' o Tirar una fletxa 's' ? ")
        if action in ('m','s','M','S'):
            break

    while True:
        try:
            door = int(raw_input("A quina habitació %d, %d or %d ? " % (door0, door1, door2)))
            if door in (door0, door1, door2):
              break
        except:
            None

    if (action=='m' or action=='M'):
        gameObjects.get("hunter").roomNumber = door


“quan dedueixis a quina habitació està el Wumpus, pots tirar-li una fletxa des d’una de les habitacions adjacents.

Si l’habitació a la que has tirat la fletxa és la del wumpus, el mataràs i hauràs guanyat.

Però si el Wumpus no és a l’habitació, el soroll de la fletxa potser el posarà en alerta i farà que es mogui a alguna habitació contigua a la que ocupa…”

Això és el que es fa amb el següent fragment de actionsHunter

    if ((action=='s' or action=='S') and \
        ((wumpusRoom == door0) or \
         (wumpusRoom == door1) or \
         (wumpusRoom == door2))):
            print "La fletxa ha matat al Wumpus!"
            print "La protectora d'animals et denunciarà si et descobreixen!"
            print "Ets l'assassí del wumpus!"
            print "Un altre sàdic que mata pobres bestioletes indefenses!"
            print "Suposo que estaràs content, criminal!"
            print "En fi. Suposo que t'he de felicitar perquè has guanyat."
            continueGame = False

    if ((action=='s' or action=='S') and \
        ((wumpusRoom <> door0) and \
         (wumpusRoom <> door1) and \
         (wumpusRoom <> door2))):
            print "has fallat! No hi havia cap wumpus a l'habitació!"

            # move wumpus
            doorsWumpus = doors[wumpusRoom].doors
            randomDoor = rnd.random()
            if randomDoor < 0.75 :                 wumpusMove= True             if randomDoor >= 0 and randomDoor < 0.25:                 gameObjects.get("wumpus").roomNumber = doorsWumpus[0]             if randomDoor >= 0.25 and randomDoor < 0.50:                 gameObjects.get("wumpus").roomNumber = doorsWumpus[1]             if randomDoor >= 0.50 and randomDoor < 0.75:
                gameObjects.get("wumpus").roomNumber = doorsWumpus[2]


Si esgotes totes les fletxes, aleshores ja no tens defensa possible contra el wumpus, que et trobarà i et menjarà.

És la part final d’actionsHunter

            arrows = arrows - 1

            if arrow == 1:
                aux = "fletxa"
            else:
                aux = "fletxes"
            print "Ara tens %d %s" % (arrows, aux)

            if arrow == 0:
                print "No tens fletxes i, per tant, ja no pots matar el Wumpus."
                print "No trigarà a descobrir-te i, aleshores, et menjarà."
                print "Serà molt dolorós i horrible..."
                print "Ja se sent com ve!"
                print "Ara entra per la porta i es llença sobre teu!"
                print "La resistència és inútil.."
                print "Encara ets conscient quan se t'empassa!"
                print "Els àcids de la seva digestio fan la resta. Has mort."
                print "Però recorda que no és que el wumpus sigui dolent..." 
                print "Et menja perquè és la seva natura de Wumpus..."
            continueGame = False

            if continueGame and wumpusMove:
                print "La fletxa ha alertat el Wumpus i es mou! espero que no vingui cap aquí!"

“Saps que a alguna de les habitacions adjacents hi ha el Wumpus perquè mai s’ha banyat i fa molta pudor, i el pots ensumar.

Saps que a les habitacions adjacents hi poden haver rats-penats
perquè sents el soroll del batec de les seves ales.

I, finalment, saps que a les habitacions adjacents poden haver pous
perquè sents el soroll de l’acid bullint.”

Si el caçador pot entrar a una habitació, pot fer-se una idea de que es trobarà més endavant. És el que s’aconsegueix amb la funció examineRoom.

def examineRoom(gameObjects):
    rooms = gameObjects.get("rooms")
    hunterRoom = gameObjects.get("hunter").roomNumber
    arrows = gameObjects.get("arrows")
    wumpusRoom = gameObjects.get("wumpus").roomNumber
    bat1Room = gameObjects.get("bat1").roomNumber
    bat2Room = gameObjects.get("bat2").roomNumber
    pit1Room = gameObjects.get("pit1").roomNumber
    pit2Room = gameObjects.get("pit2").roomNumber
    [door0, door1, door2] = rooms[hunterRoom].doors
    
    print "L'habitació %d es connecta amb les habitacions %d, %d i %d" % (hunterRoom, door0, door1, door2)

    if arrows > 1:
        aux = "fletxes"
    else:
        aux = "fletxa"
    print "Encara tens %d %s" % (arrows, aux) 
    
    if wumpusRoom in rooms[hunterRoom].doors:
        print "Fa una insuportable fortor de wumpus!"
        print "Hi ha un wumpus pudent a una de les habitacions que es connecten des d'aquí!"

    if (bat1Room in rooms[hunterRoom].doors) or (bat2Room in rooms[hunterRoom].doors):
        print "Se sent soroll d'ales!"
        print "Hi ha almenys un rat penat gegant a les habitacions que es connecten des d'aquí"

    if (pit1Room in rooms[hunterRoom].doors) or (pit2Room in rooms[hunterRoom].doors):
        print "Se sent el soroll d'àcid bullint!"
        print "Hi ha almenys un pou ple d'àcid a les habitacions que es connecten des d'aquí"

En aquest moment, ja tinc tots els elements. Si ho junto tot el que obtinc és el següent codi :

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import random as rnd

class Room:
    doors = []

class GameObject:
    roomNumber = -1

    def __init__(self, roomNumber):
        self.roomNumber = roomNumber

def initializeObjects():
    # map
    rooms = [Room() for i in range(0, 20)]

    # initialize map
    rooms[ 0].doors = [1, 4, 5]
    rooms[ 1].doors = [0, 2, 7]
    rooms[ 2].doors = [1, 3, 9]
    rooms[ 3].doors = [2, 4, 11]
    rooms[ 4].doors = [0, 3, 13]
    rooms[ 5].doors = [0, 6, 14]
    rooms[ 6].doors = [5, 7, 16]
    rooms[ 7].doors = [1, 6, 8]
    rooms[ 8].doors = [7, 9, 17]
    rooms[ 9].doors = [2, 8, 10]
    rooms[10].doors = [9, 11, 18]
    rooms[11].doors = [3, 10, 12]
    rooms[12].doors = [11, 13, 19]
    rooms[13].doors = [4, 12, 14]
    rooms[14].doors = [5, 13, 15]
    rooms[15].doors = [14, 16, 19]
    rooms[16].doors = [6, 15, 17]
    rooms[17].doors = [8, 16, 18]
    rooms[18].doors = [10, 17, 19]
    rooms[19].doors = [12, 15, 18]

    # initialize characters and objects
    wumpusRoom = -1
    bat1Room = -1
    bat2Room = -1
    pit1Room = -1
    pit2Room = -1
    hunterRoom = -1
    arrows = 5

    while (hunterRoom == wumpusRoom) or \
          (hunterRoom == bat1Room) or   \
          (hunterRoom == bat2Room) or   \
          (hunterRoom == pit1Room) or   \
          (hunterRoom == pit2Room) or   \
          (pit1Room == pit2Room) or     \
          (bat1Room == bat2Room):
        
        hunterRoom = rnd.randint(0, 19)
        wumpusRoom = rnd.randint(0, 19)
        bat1Room = rnd.randint(0, 19)
        bat2Room = rnd.randint(0, 19)
        pit2Room = rnd.randint(0, 19)
        pit2Room = rnd.randint(0, 19)

    hunter = GameObject(hunterRoom)
    wumpus = GameObject(wumpusRoom)
    bat1 = GameObject(bat1Room)
    bat2 = GameObject(bat2Room)
    pit1 = GameObject(pit1Room)
    pit2 = GameObject(pit2Room)

    return {"rooms":rooms, "hunter":hunter, "wumpus":wumpus,
            "bat1":bat1, "bat2":bat2, "pit1":pit1, "pit2":pit2, "arrows":arrows}

def enterRoom(gameObjects):
    continueGame = True
    rooms = gameObjects.get("rooms")
    hunterRoom = gameObjects.get("hunter").roomNumber
    wumpusRoom = gameObjects.get("wumpus").roomNumber
    bat1Room = gameObjects.get("bat1").roomNumber
    bat2Room = gameObjects.get("bat2").roomNumber
    pit1Room = gameObjects.get("pit1").roomNumber
    pit2Room = gameObjects.get("pit2").roomNumber
    
    print "\nEts a l'habitació número %d" % hunterRoom

    if wumpusRoom == hunterRoom:
        print "El wumpus és a l'habitació!"
        print "T'ha atrapat i comença a devorar-te!" 
        print "No és que sigui dolent... Et menja perquè és la seva natura de Wumpus..."    
        continueGame = False

    if continueGame and (hunterRoom in (pit1Room, pit2Room)):
        print "Hi ha un pou amb acid a l'habitació!"
        print "Has caigut dins i comences a dissoldre't de forma lenta i dolorosament agònica!"
        continueGame = False

    if continueGame and (hunterRoom in (bat1Room, bat2Room)):
        print "I un rat penat gegant a l'habitació! T'ha agafat i se t'emporta volant!"
        newHunterRoom = rnd.randint(0, 19)
        newBat1Room = rnd.randint(0, 19)
        newBat2Room = rnd.randint(0, 19)
        gameObjects.get("hunter").roomNumber = newHunterRoom
        gameObjects.get("bat1").roomNumber = newBat1Room
        gameObjects.get("bat2").roomNumber = newBat2Room
        print "El rat penat t'ha deixat caure a l'habitació número %d" % newHunterRoom
        continueGame = enterRoom(gameObjects)

    if not continueGame:
        print "Has mort de forma horrible!" 
        print "..." 
        print "Però altres vindran a intentar triomfar allà on tu has fracassat tan lamentablement "

    return continueGame

def examineRoom(gameObjects):
    rooms = gameObjects.get("rooms")
    hunterRoom = gameObjects.get("hunter").roomNumber
    arrows = gameObjects.get("arrows")
    wumpusRoom = gameObjects.get("wumpus").roomNumber
    bat1Room = gameObjects.get("bat1").roomNumber
    bat2Room = gameObjects.get("bat2").roomNumber
    pit1Room = gameObjects.get("pit1").roomNumber
    pit2Room = gameObjects.get("pit2").roomNumber
    [door0, door1, door2] = rooms[hunterRoom].doors
    
    print "L'habitació %d es connecta amb les habitacions %d, %d i %d" % (hunterRoom, door0, door1, door2)

    if arrows > 1:
        aux = "fletxes"
    else:
        aux = "fletxa"
    print "Encara tens %d %s" % (arrows, aux) 
    
    if wumpusRoom in rooms[hunterRoom].doors:
        print "Fa una insuportable fortor de wumpus!"
        print "Hi ha un wumpus pudent a una de les habitacions que es connecten des d'aquí!"

    if (bat1Room in rooms[hunterRoom].doors) or (bat2Room in rooms[hunterRoom].doors):
        print "Se sent soroll d'ales!"
        print "Hi ha almenys un rat penat gegant a les habitacions que es connecten des d'aquí"

    if (pit1Room in rooms[hunterRoom].doors) or (pit2Room in rooms[hunterRoom].doors):
        print "Se sent el soroll d'àcid bullint!"
        print "Hi ha almenys un pou ple d'àcid a les habitacions que es connecten des d'aquí"
    
def actionsHunter(gameObjects):
    continueGame = True
    wumpusMove = False
    hunterRoom = gameObjects.get("hunter").roomNumber
    wumpusRoom = gameObjects.get("wumpus").roomNumber
    arrows = gameObjects.get("arrows")
    rooms = gameObjects.get("rooms")
    [door0, door1, door2] = rooms[hunterRoom].doors

    while True:
        action = raw_input("Moure's 'm' o Tirar una fletxa 's' ? ")
        if action in ('m','s','M','S'):
            break

    while True:
        try:
            door = int(raw_input("A quina habitació %d, %d or %d ? " % (door0, door1, door2)))
            if door in (door0, door1, door2):
              break
        except:
            None

    if (action=='m' or action=='M'):
        gameObjects.get("hunter").roomNumber = door

    if ((action=='s' or action=='S') and \
        ((wumpusRoom == door0) or \
         (wumpusRoom == door1) or \
         (wumpusRoom == door2))):
            print "La fletxa ha matat al Wumpus!"
            print "La protectora d'animals et denunciarà si et descobreixen!"
            print "Ets l'assassí del wumpus!"
            print "Un altre sàdic que mata pobres bestioletes indefenses!"
            print "Suposo que estaràs content, criminal!"
            print "En fi. Suposo que t'he de felicitar perquè has guanyat."
            continueGame = False

    if ((action=='s' or action=='S') and \
        ((wumpusRoom <> door0) and \
         (wumpusRoom <> door1) and \
         (wumpusRoom <> door2))):
            print "has fallat! No hi havia cap wumpus a l'habitació!"

            # move wumpus
            doorsWumpus = doors[wumpusRoom].doors
            randomDoor = rnd.random()
            if randomDoor < 0.75 :                 wumpusMove= True             if randomDoor >= 0 and randomDoor < 0.25:                 gameObjects.get("wumpus").roomNumber = doorsWumpus[0]             if randomDoor >= 0.25 and randomDoor < 0.50:                 gameObjects.get("wumpus").roomNumber = doorsWumpus[1]             if randomDoor >= 0.50 and randomDoor < 0.75:
                gameObjects.get("wumpus").roomNumber = doorsWumpus[2]

            arrows = arrows - 1

            if arrow == 1:
                aux = "fletxa"
            else:
                aux = "fletxes"
            print "Ara tens %d %s" % (arrows, aux)

            if arrow == 0:
                print "No tens fletxes i, per tant, ja no pots matar el Wumpus."
                print "No trigarà a descobrir-te i, aleshores, et menjarà."
                print "Serà molt dolorós i horrible..."
                print "Ja se sent com ve!"
                print "Ara entra per la porta i es llença sobre teu!"
                print "La resistència és inútil.."
                print "Encara ets conscient quan se t'empassa!"
                print "Els àcids de la seva digestio fan la resta. Has mort."
                print "Però recorda que no és que el wumpus sigui dolent..." 
                print "Et menja perquè és la seva natura de Wumpus..."
            continueGame = False

            if continueGame and wumpusMove:
                print "La fletxa ha alertat el Wumpus i es mou! espero que no vingui cap aquí!"

    return continueGame

def gameLoop(gameObjects):
    continueGame = True

    while continueGame:
        continueGame = enterRoom(gameObjects)
        if continueGame:
            examineRoom(gameObjects)
            continueGame = actionsHunter(gameObjects)

instructions = '''
Caçar el Wumpus
---------------
El Wumpus és un monstre enorme i pesat amb la pell plena de ventoses 
que s'alimenta de tot allò que cau a les seves urpes. 
Tu ets un caçador que vol aconseguir el cap del Wumpus com a trofeu 
i ets a un castell on se sap que n'hi ha un.  
El castell té vint habitacions, cada habitació es connecta amb 
altres tres habitacions.
Si entres a l'habitació on és el Wumpus, et menjarà.
Al castell hi ha un parell d'habitacions que tenen un fals terra. 
Només entrar s'ensorra el terra i caus a un pou amb un potent àcid al fons. 
Tothom que entra a una habitació amb pou, mor. 
Menys el Wumpus. El Wumpus no hi cau perquè amb les seves ventoses 
s'enganxa a les parets i no toca l'àcid.
Al castell també hi ha un parell de rats penats gegants. 
Si entres a una habitació amb un rat penat, t'agafarà, se t'emportarà volant 
i et deixarà caure a una habitació qualsevol del castell. 
Els rats penats no poden endur-se al Wumpus perquè pesa massa per ells 
i el Wumpus no es pot menjar als rats penats perquè són massa ràpids 
per atrapar-los.
Saps que a alguna de les habitacions adjacents hi ha el Wumpus
perquè mai s'ha banyat i fa molta pudor, i el pots ensumar.
Saps que a les habitacions adjacents hi poden haver rats-penats
perquè sents el soroll del batec de les seves ales,
I, finalment, saps que a les habitacions adjacents poden haver pous 
perquè sents el soroll de l'acid bullint.
Tens cinc fletxes. Has de moure't pel castell 
i quan dedueixis a quina habitació està el Wumpus, pots tirar-li 
una fletxa des d'una de les habitacions adjacents. 
Si l'habitació a la que has tirat la fletxa és la del Wumpus, 
el mataràs i hauràs guanyat. 
Però si el Wumpus no és a l'habitació, el soroll de la fletxa 
potser el posarà en alerta i farà que es mogui a alguna habitació 
contigua a la que ocupa.
Si esgotes totes les fletxes, aleshores ja no tens defensa 
possible contra el Wumpus, que et trobarà i et menjarà.
'''

if __name__ == "__main__":

    print instructions

    # gameObjects = [rooms, hunter, wumpus, bat1, bat2, pit1, pit2]
    gameObjects = initializeObjects()
    
    gameLoop(gameObjects)

Per si algú té ganes de millorar-lo -afegint una interfície gràfica, per exemple- el codi es pot descarregar del repositori GitHub.