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 :

Anuncis

Deixa un comentari

Fill in your details below or click an icon to log in:

WordPress.com Logo

Esteu comentant fent servir el compte WordPress.com. Log Out /  Canvia )

Google photo

Esteu comentant fent servir el compte Google. Log Out /  Canvia )

Twitter picture

Esteu comentant fent servir el compte Twitter. Log Out /  Canvia )

Facebook photo

Esteu comentant fent servir el compte Facebook. Log Out /  Canvia )

S'està connectant a %s