Tutorial on programming a memory card game
Quando estou a criar um jogo, gosto de o ir desenvolvendo por etapas funcionais: partir o processo em várias fases que representem pontos nos quais eu tenho algo que posso testar. Deste modo, não só o processo se torna muito mais interessante, como posso ir controlando o aspeto do que estou a produzir. Deixo de seguida uma lista das etapas que eu pensei para este projeto; cada ponto da lista descreve a funcionalidade que o jogo já suporta:
- Criar um ecrã onde mostro todas as cartas dispostas, face para baixo;
- Clicar em cima de uma carta faz com que ela se vire para cima;
- Clicar na segunda carta verifica se encontrei um par ou não e trata as cartas de acordo com isso: se forem um par, retira-as; se forem diferentes volta-as de novo para baixo;
- Um temporizador impede que gastemos tempo infinito num só jogo; acertar num par dá mais tempo e falhar num par retira tempo;
- Um ecrã final que diz se ganhámos/perdemos e quantos pontos fizémos;
- Um pequeno ficheiro de configuração para que se possa alterar o número de cartas em cima da mesa.
Etapa 0: parte da frente das cartas
Antes de começar a programar o jogo decidi que as cartas teriam vários polígonos regulares para serem emparelhados. A razão pela qual fiz isso é porque, por um lado gerar essas imagens todas seria fácil, e por outro lado tornaria o jogo difícil: à medida que o número de lados aumenta, torna-se cada vez mais difícil distinguir os diferentes polígonos. Para este efeito, escrevi um pequeno script que pede repetidamente valores inteiros para desenhar polígonos regulares (um por cada cor) com o número de lados especificados. Com esse script criei uma série de imagens para serem usadas nas minhas cartas. O script segue.from PIL import Image, ImageDraw
import sys
import os
from math import pi, sin, cos
def generate_polygon(n, colours):
theta = (2*pi)/n
vertices = [(20*cos(theta*j), 20*sin(theta*j))
for j in range(0, n)]
for j in range(1, n//2+1):
vertices[j] = (vertices[n-j][0], -vertices[n-j][1])
vertices = [(v[0]+25, v[1]+25) for v in vertices]
im = Image.new("RGB", (50,50), (255,255,255))
for name in colours.keys():
c = colours[name]
draw = ImageDraw.Draw(im)
draw.polygon(vertices, fill=c, outline=c)
im.save(os.path.join("polygonbin",
"sides"+str(n)+name+".png"))
# colours taken from https://www.w3schools.com/tags/ref_colornames.asp
COLOURS = {
"Black": (0,0,0),
"Red": (255,0,0),
"Green": (0,255,0),
"Blue": (0,0,255),
"Aqua": (0,255,255),
"Brown": (165, 42, 42),
"Chocolate": (210, 105, 30),
"Crimson": (220, 20, 60),
"DarkGoldenRod": (184, 134, 11),
"DarkGreen": (0, 100, 0),
"DarkOrange": (255, 140, 0),
"Fuchsia": (255, 0, 255),
"Gold": (255, 190, 0),
"SeaGreen": (46, 139, 87),
"Yellow": (255, 255, 0)
}
if __name__ == "__main__":
n = -1
while True:
try:
n = int(input("# of sides >> "))
if n <= 0:
sys.exit()
except Exception:
continue
generate_polygon(n, COLOURS)
Etapa 1: mostrar as cartas todas voltadas para baixo
Para podermos mostrar uma "mesa" com as cartas todas voltadas para baixo precisamos de decidir quantas cartas estão na mesa e com que configuração (quantas linhas/colunas). Precisamos também de uma imagem para representar a parte de trás de uma carta.from pygame.locals import *
import pygame
import sys
def load_image(name, transparent=False):
"""Function that handles image loading
Returns the image and its Rect"""
try:
img = pygame.image.load(name)
except pygame.error:
raise SystemExit("Could not load image " + name)
if not transparent:
img = img.convert()
img = img.convert_alpha()
img_rect = img.get_rect()
return img, img_rect
width = 5
height = 4
CARD_PIXEL_WIDTH = 50
PIXEL_BORDER = 5
WIDTH = width*(CARD_PIXEL_WIDTH) + (width+1)*PIXEL_BORDER
HEIGHT = height*(CARD_PIXEL_WIDTH) + (height+1)*PIXEL_BORDER
BACKGROUND_COLOR = (20, 200, 20)
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
cardback, cardbackrect = load_image("cardback.png")
# initialize the screen
screen.fill(BACKGROUND_COLOR)
for x in range(width):
for y in range(height):
xc = (x+1)*PIXEL_BORDER + x*CARD_PIXEL_WIDTH
yc = (y+1)*PIXEL_BORDER + y*CARD_PIXEL_WIDTH
screen.blit(cardback, (xc, yc))
pygame.display.update()
while True:
for ev in pygame.event.get():
if ev.type == QUIT:
pygame.quit()
sys.exit()
sys
para podermos terminar a aplicação quando o utilizador carregar na cruzinha vermelha da janela. A função load_image
é uma função que (quase de certeza absoluta) copiei da internet; o que ela faz é importar a imagem que estiver no argumento que se passa à função. Podemos também indicar se a imagem tem um fundo transparente ou não. De seguida inicializamos algumas variáveis globais que nos vão ser úteis. width
e height
são para o número de colunas e linhas, respetivamente, em termos de cartas. WIDTH
e HEIGHT
são para o comprimento e altura da janela do jogo em pixéis. Definimos ainda a cor de fundo, a margem (em pixéis) que vamos ter entre cada carta e o tamanho (em pixéis) de cada carta, que assumimos ser um quadrado.O ciclo infinito incluído é bastante standard e serve para a aplicação correr sem "congelar"; a única ação que produz algum resultado é carregar na cruz vermelha, que fecha a janela.
Etapa 2: clicar numa carta faz com que ela se volte para cima
Para completar este passo é preciso acrescentar várias coisas que tratam já de alguma lógica do jogo: é preciso distribuir os pares de cartas pelas suas posições e é preciso reconhecer quando um clique do rato foi feito em cima de uma das cartas.Começamos por acrescentar a escolha das cartas e a distribuição das mesmas pela mesa de jogo. Através das dimensões dadas pelas variáveis
width, height
conseguimos saber de quantas cartas precisamos. Basta-nos abrir o diretório onde estão guardadas as cartas todas e escolher aleatoriamente o número certo de cartas. Depois de escolhidas, guardamo-las num dicionário. O passo seguinte é criar uma matriz, com as dimensões da mesa de jogo, onde cada posição da matriz indica que carta está nessa posição da mesa: para isso duplicamos o vetor com os nomes das cartas escolhidas, baralhamos esse vetor, e partimos o vetor nas diversas colunas da mesa. # does the board have legal dimensions?
if width*height % 2:
print("Either 'width' or 'height' must be an even number")
sys.exit()
# choose the cards to be used
cards = random.sample(os.listdir(IMAGE_BIN), (width*height)//2)
images = dict()
for card in cards:
path = os.path.join(IMAGE_BIN, card)
images[card] = load_image(path)
cards = cards*2
random.shuffle(cards)
# card_list is a 2D array with the same structure as the game table
card_list = [cards[height*i:height*(i+1)] for i in range(width)]
random
como a biblioteca os
. Agora que o jogo já sabe que cartas ficam em que posições, precisamos de fazer com que as cartas escolhidas sejam voltadas para cima. Depois de carregarmos em cima de uma delas temos ainda de a desenhar para mostrar a barte de baixo. Porque nos vai dar bastante jeito, vamos ainda criar uma função auxiliar, board_to_pixels
que recebe um par de inteiros (x,y), que dizem respeito à posição de uma carta na lista card_list
, e devolve um outro par de inteiros: a posição, em pixéis, do canto superior esquerdo da mesma carta. Isto vai ser útil para conseguirmos criar os retângulos necessários para redesenhar a carta e para atualizar a porção certa do ecrã. def board_to_pixels(coords):
# receives a pair (x, y) pertaining a card position on the table
# transforms it into a pair (xc, yc) of pixel coordinates of the
# top left corner of the card
xc = (coords[0]+1)*PIXEL_BORDER + coords[0]*CARD_PIXEL_WIDTH
yc = (coords[1]+1)*PIXEL_BORDER + coords[1]*CARD_PIXEL_WIDTH
return xc, yc
if
dentro do ciclo que processa os eventos do pygame
. Podemos usar um pouco de matemática e a função divmod
para descobrir em que carta tentámos carregar e perceber se acertámos no espaço entre duas cartas ou não. while True:
for ev in pygame.event.get():
if ev.type == QUIT:
pygame.quit()
sys.exit()
elif ev.type == MOUSEBUTTONDOWN:
# find the card in which we clicked; ignore clicks in the gaps between cards
x, pad = divmod(ev.pos[0], PIXEL_BORDER+CARD_PIXEL_WIDTH)
if x >= width or pad < PIXEL_BORDER:
continue
y, pad = divmod(ev.pos[1], PIXEL_BORDER+CARD_PIXEL_WIDTH)
if y >= height or pad < PIXEL_BORDER:
continue
# find the top left corner of the clicked card
xc, yc = board_to_pixels((x, y))
rect = pygame.Rect(xc, yc, CARD_PIXEL_WIDTH, CARD_PIXEL_WIDTH)
screen.blit(images[card_list[x][y]][0], (xc, yc))
pygame.display.update(rect)
from pygame.locals import *
import pygame
import random
import os
import sys
def load_image(name, transparent=False):
"""Function that handles image loading
Returns the image and its Rect"""
try:
img = pygame.image.load(name)
except pygame.error:
raise SystemExit("Could not load image " + name)
if not transparent:
img = img.convert()
img = img.convert_alpha()
img_rect = img.get_rect()
return img, img_rect
width = 5
height = 4
CARD_PIXEL_WIDTH = 50
PIXEL_BORDER = 5
WIDTH = width*(CARD_PIXEL_WIDTH) + (width+1)*PIXEL_BORDER
HEIGHT = height*(CARD_PIXEL_WIDTH) + (height+1)*PIXEL_BORDER
BACKGROUND_COLOR = (20, 200, 20)
IMAGE_BIN = "polygonbin"
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
def board_to_pixels(coords):
# receives a pair (x, y) pertaining a card position on the table
# transforms it into a pair (xc, yc) of pixel coordinates of the
# top left corner of the card
xc = (coords[0]+1)*PIXEL_BORDER + coords[0]*CARD_PIXEL_WIDTH
yc = (coords[1]+1)*PIXEL_BORDER + coords[1]*CARD_PIXEL_WIDTH
return xc, yc
# does the board have legal dimensions?
if width*height % 2:
print("Either 'width' or 'height' must be an even number")
sys.exit()
# choose the cards to be used
cards = random.sample(os.listdir(IMAGE_BIN), (width*height)//2)
images = dict()
for card in cards:
path = os.path.join(IMAGE_BIN, card)
images[card] = load_image(path)
cards = cards*2
random.shuffle(cards)
# card_list is a 2D array with the same structure as the game table
card_list = [cards[height*i:height*(i+1)] for i in range(width)]
cardback, cardbackrect = load_image("cardback.png")
# initialize the screen
screen.fill(BACKGROUND_COLOR)
for x in range(width):
for y in range(height):
xc = (x+1)*PIXEL_BORDER + x*CARD_PIXEL_WIDTH
yc = (y+1)*PIXEL_BORDER + y*CARD_PIXEL_WIDTH
screen.blit(cardback, (xc, yc))
pygame.display.update()
while True:
for ev in pygame.event.get():
if ev.type == QUIT:
pygame.quit()
sys.exit()
elif ev.type == MOUSEBUTTONDOWN:
# find the card in which we clicked; ignore clicks in the gaps between cards
x, pad = divmod(ev.pos[0], PIXEL_BORDER+CARD_PIXEL_WIDTH)
if x >= width or pad < PIXEL_BORDER:
continue
y, pad = divmod(ev.pos[1], PIXEL_BORDER+CARD_PIXEL_WIDTH)
if y >= height or pad < PIXEL_BORDER:
continue
# find the top left corner of the clicked card
xc, yc = board_to_pixels((x, y))
rect = pygame.Rect(xc, yc, CARD_PIXEL_WIDTH, CARD_PIXEL_WIDTH)
screen.blit(images[card_list[x][y]][0], (xc, yc))
pygame.display.update(rect)
Etapa 3: pares encontrados são retirados da mesa
Se queremos que os pares encontrados sejam retirados, é óbvio que temos de saber se há alguma carta virada para cima e que carta é essa (quando há). Tendo isso em conta, quando se carrega em cima de uma carta temos de a virar para cima e ver se é o par de outra carta que esteja virada para cima. Adicionamos então uma série de variáveis antes do ciclo principal e alteramos o processamento dos cliques do rato.# auxiliary variables to control the state of the game
is_flipped = False
flipped_card = None
flipped_coords = None
to_find = len(cards)/2
while True:
for ev in pygame.event.get():
if ev.type == QUIT:
pygame.quit()
sys.exit()
elif ev.type == MOUSEBUTTONDOWN:
# find the card in which we clicked; ignore clicks in the gaps between cards
x, pad = divmod(ev.pos[0], PIXEL_BORDER+CARD_PIXEL_WIDTH)
if x >= width or pad < PIXEL_BORDER:
continue
y, pad = divmod(ev.pos[1], PIXEL_BORDER+CARD_PIXEL_WIDTH)
if y >= height or pad < PIXEL_BORDER:
continue
# find the top left corner of the clicked card
xc, yc = board_to_pixels((x, y))
rect = pygame.Rect(xc, yc, CARD_PIXEL_WIDTH, CARD_PIXEL_WIDTH)
if not is_flipped:
screen.blit(images[card_list[x][y]][0], (xc, yc))
pygame.display.update(rect)
is_flipped = True
flipped_card = card_list[x][y]
flipped_coords = (x, y)
# there is a card face up
else:
# I just clicked it
if flipped_coords == (x, y):
continue
else:
# turn this new card face up; wait
screen.blit(images[card_list[x][y]][0], (xc, yc))
pygame.display.update(rect)
pygame.time.delay(800)
# if we got it right
if flipped_card == card_list[x][y]:
to_find -= 1
pygame.draw.rect(screen, BACKGROUND_COLOR, rect)
pygame.display.update(rect)
# erase the previously clicked card from the game table
xc, yc = board_to_pixels(flipped_coords)
rect = pygame.Rect(xc, yc, CARD_PIXEL_WIDTH, CARD_PIXEL_WIDTH)
pygame.draw.rect(screen, BACKGROUND_COLOR, rect)
pygame.display.update(rect)
# if we got it wrong
else:
# cover both cards again
screen.blit(cardback, (xc, yc))
pygame.display.update(rect)
xc, yc = board_to_pixels(flipped_coords)
rect = pygame.Rect(xc, yc, CARD_PIXEL_WIDTH, CARD_PIXEL_WIDTH)
screen.blit(cardback, (xc, yc))
pygame.display.update(rect)
is_flipped = False
found_cards
onde guardo as coordenadas das cartas encontradas; de seguida alteramos o if
que decide o que fazer quando se carrega em cima de uma carta. Precisamos ainda de guardas as cartas que encontramos, quando as encontramos. O código final desta etapa fica então: from pygame.locals import *
import pygame
import random
import os
import sys
def load_image(name, transparent=False):
"""Function that handles image loading
Returns the image and its Rect"""
try:
img = pygame.image.load(name)
except pygame.error:
raise SystemExit("Could not load image " + name)
if not transparent:
img = img.convert()
img = img.convert_alpha()
img_rect = img.get_rect()
return img, img_rect
width = 5
height = 4
CARD_PIXEL_WIDTH = 50
PIXEL_BORDER = 5
WIDTH = width*(CARD_PIXEL_WIDTH) + (width+1)*PIXEL_BORDER
HEIGHT = height*(CARD_PIXEL_WIDTH) + (height+1)*PIXEL_BORDER
BACKGROUND_COLOR = (20, 200, 20)
IMAGE_BIN = "polygonbin"
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
def board_to_pixels(coords):
# receives a pair (x, y) pertaining a card position on the table
# transforms it into a pair (xc, yc) of pixel coordinates of the
# top left corner of the card
xc = (coords[0]+1)*PIXEL_BORDER + coords[0]*CARD_PIXEL_WIDTH
yc = (coords[1]+1)*PIXEL_BORDER + coords[1]*CARD_PIXEL_WIDTH
return xc, yc
# does the board have legal dimensions?
if width*height % 2:
print("Either 'width' or 'height' must be an even number")
sys.exit()
# choose the cards to be used
cards = random.sample(os.listdir(IMAGE_BIN), (width*height)//2)
images = dict()
for card in cards:
path = os.path.join(IMAGE_BIN, card)
images[card] = load_image(path)
cards = cards*2
random.shuffle(cards)
# card_list is a 2D array with the same structure as the game table
card_list = [cards[height*i:height*(i+1)] for i in range(width)]
cardback, cardbackrect = load_image("cardback.png")
# initialize the screen
screen.fill(BACKGROUND_COLOR)
for x in range(width):
for y in range(height):
xc = (x+1)*PIXEL_BORDER + x*CARD_PIXEL_WIDTH
yc = (y+1)*PIXEL_BORDER + y*CARD_PIXEL_WIDTH
screen.blit(cardback, (xc, yc))
pygame.display.update()
# auxiliary variables to control the state of the game
is_flipped = False
flipped_card = None
flipped_coords = None
to_find = len(cards)/2
found_cards = []
while to_find > 0:
for ev in pygame.event.get():
if ev.type == QUIT:
pygame.quit()
sys.exit()
elif ev.type == MOUSEBUTTONDOWN:
# find the card in which we clicked; ignore clicks in the gaps between cards
x, pad = divmod(ev.pos[0], PIXEL_BORDER+CARD_PIXEL_WIDTH)
if x >= width or pad < PIXEL_BORDER:
continue
y, pad = divmod(ev.pos[1], PIXEL_BORDER+CARD_PIXEL_WIDTH)
if y >= height or pad < PIXEL_BORDER:
continue
# find the top left corner of the clicked card
xc, yc = board_to_pixels((x, y))
rect = pygame.Rect(xc, yc, CARD_PIXEL_WIDTH, CARD_PIXEL_WIDTH)
if (x,y) in found_cards:
continue
elif not is_flipped:
screen.blit(images[card_list[x][y]][0], (xc, yc))
pygame.display.update(rect)
is_flipped = True
flipped_card = card_list[x][y]
flipped_coords = (x, y)
# there is a card face up
else:
# I just clicked it
if flipped_coords == (x, y):
continue
else:
# turn this new card face up; wait
screen.blit(images[card_list[x][y]][0], (xc, yc))
pygame.display.update(rect)
pygame.time.delay(800)
# if we got it right
if flipped_card == card_list[x][y]:
to_find -= 1
pygame.draw.rect(screen, BACKGROUND_COLOR, rect)
pygame.display.update(rect)
# erase the previously clicked card from the game table
xc, yc = board_to_pixels(flipped_coords)
rect = pygame.Rect(xc, yc, CARD_PIXEL_WIDTH, CARD_PIXEL_WIDTH)
pygame.draw.rect(screen, BACKGROUND_COLOR, rect)
pygame.display.update(rect)
# flag these two cards as found
found_cards.append(flipped_coords)
found_cards.append((x,y))
# if we got it wrong
else:
# cover both cards again
screen.blit(cardback, (xc, yc))
pygame.display.update(rect)
xc, yc = board_to_pixels(flipped_coords)
rect = pygame.Rect(xc, yc, CARD_PIXEL_WIDTH, CARD_PIXEL_WIDTH)
screen.blit(cardback, (xc, yc))
pygame.display.update(rect)
is_flipped = False
Etapa 4: acrescentar um temporizador
Nesta etapa vamos acrescentar uma funcionalidade que impõe um tempo limite ao jogador. De cada vez que o jogador encontrar um par, vai ser bonificado com tempo extra para jogar; de cada vez que falhar, vai perder algum do tempo disponível. Para o jogador saber quanto tempo ainda tem, vamos apresentar uma barra à direita que vai ficando mais pequena à medida que o tempo passa. Para isso vamos precisar de uma função que desenhe a barra do tempo e vamos precisar de uma maneira de controlar quando é que o jogador perde. Vamos ainda dar tempo extra de cada vez que um par é encontrado e retirar tempo de cada vez que se falha um par. Começamos por criar duas variáveisBONUSTIME
e PENALTYTIME
que têm, em milissegundos, respetivamente a bonificação e a penalização que acabámos de descrever. Os valores são arbitrários; eu decidi BONUSTIME = 3000
e PENALTYTIME = 600
Se definirmos que desenhamos a barra do tempo à direita da mesa de jogo, podemos tomar como único argumento a percentagem de tempo que ainda nos resta. Essa informação é suficiente para desenhar a barra, desde que acrescentemos uma variável para controlar a largura da barra. É ainda necessário atualizar a largura da janela em pixéis,
WIDTH
, para ter em consideração o espaço extra necessário para a barra: WIDTH = width*(CARD_PIXEL_WIDTH) + (width+2)*PIXEL_BORDER + TIMEBARWIDTH
TIMEBARWIDTH = 25
# ...
def draw_timebar(percentage):
# draws a black timebar to let the user know how much time is left
# find the total height of the bar
height_used = height*(CARD_PIXEL_WIDTH) + (height-1)*PIXEL_BORDER
# cover the existing timebar with the background color
pygame.draw.rect(screen, BACKGROUND_COLOR, pygame.Rect(width*(CARD_PIXEL_WIDTH) + (width+1)*PIXEL_BORDER,
PIXEL_BORDER,
TIMEBARWIDTH, height_used))
# draw the timebar frame
pygame.draw.rect(screen, (0,0,0), pygame.Rect(width*(CARD_PIXEL_WIDTH) + (width+1)*PIXEL_BORDER,
PIXEL_BORDER,
TIMEBARWIDTH, height_used), 3)
# draw the time that is still left
pygame.draw.rect(screen, (0,0,0), pygame.Rect(width*(CARD_PIXEL_WIDTH) + (width+1)*PIXEL_BORDER,
PIXEL_BORDER+(1-percentage)*height_used,
TIMEBARWIDTH, percentage*height_used))
# update the timebar area
pygame.display.update(pygame.Rect(width*(CARD_PIXEL_WIDTH) + (width+1)*PIXEL_BORDER,
PIXEL_BORDER,
TIMEBARWIDTH, height_used))
end
e score
onde guardamos, respetivamente, o momento do fim do jogo e as bonificações/penalizações acumuladas. Podemos definir o momento do fim do jogo com facilidade e alterar a guarda do ciclo principal para que o jogo acabe quando ficarmos sem tempo: end = pygame.time.get_ticks() + BONUSTIME*width*height
score = 0
while to_find > 0 and pygame.time.get_ticks() < end + score:
score
quando encontramos um par ou quando nos enganámos, fazendo uso das variáveis BONUSTIME
e PENALTYTIME
: ...
if flipped_card == card_list[x][y]:
to_find -= 1
score += BONUSTIME
...
else:
score -= PENALTYTIME
while to_find > 0 and pygame.time.get_ticks() < end + score:
# find the percentage of time left and update the timebar
perc = min(1, (end+score-pygame.time.get_ticks())/(BONUSTIME*width*height))
draw_timebar(perc)
pygame.time.delay(800)
, que faz com que todo o pygame fique parado durante 800ms. Uma maneira de darmos a volta a isto é se criarmos uma variável auxiliar, wait
, que nos diz se estamos numa pausa porque duas cartas estão viradas para cima, ou não. Caso não estejamos, então o jogo decorre normalmente. Se estivermos, temos de esperar que a pausa acabe e depois ou mandamos fora as duas cartas ou voltamo-las de novo para baixo.As alterações que têm de ser feitas são:
- Quando viramos uma segunda carta para cima, guardamos a sua posição e definimos um tempo de espera;
- Quando entramos no ciclo principal do jogo, temos de perceber se estamos em pausa ou não;
- Quando estamos em pausa, temos de desligar a capacidade do jogador de continuar a carregar em cartas;
- Quando a pausa acaba, voltar a virar as duas cartas para baixo ou removê-las do jogo
### -->
e ### <--
# auxiliary variables to control the state of the game
is_flipped = False
### -->
flipped_card = []
flipped_coords = []
wait = False
wait_until = None
### <--
to_find = len(cards)/2
found_cards = []
end = pygame.time.get_ticks() + BONUSTIME*width*height
score = 0
while to_find > 0 and pygame.time.get_ticks() < end + score:
clock.tick(60)
# find the percentage of time left and update the timebar
perc = min(1, (end+score-pygame.time.get_ticks())/(BONUSTIME*width*height))
draw_timebar(perc)
### --> this is VERY similar to what used to be in the end of the loop
if wait and pygame.time.get_ticks() > wait_until:
# we have waited already, now we take care of the cards
wait = False
pygame.event.set_allowed(MOUSEBUTTONDOWN)
# if we got it right
x1, y1 = flipped_coords[0]
x2, y2 = flipped_coords[1]
if card_list[x1][y1] == card_list[x2][y2]:
to_find -= 1
# this is the old rect pointing to the card
# that was most recently turned up
pygame.draw.rect(screen, BACKGROUND_COLOR, rect)
pygame.display.update(rect)
# erase the oldest facing up card from the game table
xc, yc = board_to_pixels((x1, y1))
rect = pygame.Rect(xc, yc, CARD_PIXEL_WIDTH, CARD_PIXEL_WIDTH)
pygame.draw.rect(screen, BACKGROUND_COLOR, rect)
pygame.display.update(rect)
# flag these two cards as found
found_cards.append((x1, y1))
found_cards.append((x2, y2))
score += BONUSTIME
# if we got it wrong
else:
# cover both cards again
screen.blit(cardback, (xc, yc))
pygame.display.update(rect)
xc, yc = board_to_pixels((x1, y1))
rect = pygame.Rect(xc, yc, CARD_PIXEL_WIDTH, CARD_PIXEL_WIDTH)
screen.blit(cardback, (xc, yc))
pygame.display.update(rect)
score -= PENALTYTIME
is_flipped = False
### <--
for ev in pygame.event.get():
if ev.type == QUIT:
pygame.quit()
sys.exit()
elif ev.type == MOUSEBUTTONDOWN:
if wait:
continue
# find the card in which we clicked; ignore clicks in the gaps between cards
x, pad = divmod(ev.pos[0], PIXEL_BORDER+CARD_PIXEL_WIDTH)
if x >= width or pad < PIXEL_BORDER:
continue
y, pad = divmod(ev.pos[1], PIXEL_BORDER+CARD_PIXEL_WIDTH)
if y >= height or pad < PIXEL_BORDER:
continue
# find the top left corner of the clicked card
xc, yc = board_to_pixels((x, y))
rect = pygame.Rect(xc, yc, CARD_PIXEL_WIDTH, CARD_PIXEL_WIDTH)
if (x,y) in found_cards:
continue
elif not is_flipped:
screen.blit(images[card_list[x][y]][0], (xc, yc))
pygame.display.update(rect)
is_flipped = True
### -->
flipped_card = [card_list[x][y]]
flipped_coords = [(x, y)]
### <--
# there is a card face up
else:
# I just clicked it
if flipped_coords[0] == (x, y): ### just a minor change, use index notation
continue
else:
### -->
# set a waiting interval where no events are allowed
wait = True
# turn this new card face up; wait
screen.blit(images[card_list[x][y]][0], (xc, yc))
pygame.display.update(rect)
# disable new clicks
pygame.event.set_blocked(MOUSEBUTTONDOWN)
if flipped_card[0] != card_list[x][y]:
flipped_card.append(card_list[x][y])
wait_until = pygame.time.get_ticks() + 800
else:
wait_until = pygame.time.get_ticks() + 300
flipped_coords.append((x,y))
### <--
Agora que temos todas estas alterações implementadas, o nosso programa já cumpre tudo o que era suposto nesta etapa. O código todo, até esta etapa, segue:
from pygame.locals import *
import pygame
import random
import os
import sys
def load_image(name, transparent=False):
"""Function that handles image loading
Returns the image and its Rect"""
try:
img = pygame.image.load(name)
except pygame.error:
raise SystemExit("Could not load image " + name)
if not transparent:
img = img.convert()
img = img.convert_alpha()
img_rect = img.get_rect()
return img, img_rect
width = 2
height = 3
CARD_PIXEL_WIDTH = 50
PIXEL_BORDER = 5
TIMEBARWIDTH = 25
WIDTH = width*(CARD_PIXEL_WIDTH) + (width+2)*PIXEL_BORDER + TIMEBARWIDTH
HEIGHT = height*(CARD_PIXEL_WIDTH) + (height+1)*PIXEL_BORDER
BACKGROUND_COLOR = (20, 200, 20)
IMAGE_BIN = "polygonbin"
BONUSTIME = 3000
PENALTYTIME = 600
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
def board_to_pixels(coords):
# receives a pair (x, y) pertaining a card position on the table
# transforms it into a pair (xc, yc) of pixel coordinates of the
# top left corner of the card
xc = (coords[0]+1)*PIXEL_BORDER + coords[0]*CARD_PIXEL_WIDTH
yc = (coords[1]+1)*PIXEL_BORDER + coords[1]*CARD_PIXEL_WIDTH
return xc, yc
def draw_timebar(percentage):
# draws a black timebar to let the user know how much time is left
# find the total height of the bar
height_used = height*(CARD_PIXEL_WIDTH) + (height-1)*PIXEL_BORDER
# cover the existing timebar with the background color
pygame.draw.rect(screen, BACKGROUND_COLOR, pygame.Rect(width*(CARD_PIXEL_WIDTH) + (width+1)*PIXEL_BORDER,
PIXEL_BORDER,
TIMEBARWIDTH, height_used))
# draw the timebar frame
pygame.draw.rect(screen, (0,0,0), pygame.Rect(width*(CARD_PIXEL_WIDTH) + (width+1)*PIXEL_BORDER,
PIXEL_BORDER,
TIMEBARWIDTH, height_used), 3)
# draw the time that is still left
pygame.draw.rect(screen, (0,0,0), pygame.Rect(width*(CARD_PIXEL_WIDTH) + (width+1)*PIXEL_BORDER,
PIXEL_BORDER+(1-percentage)*height_used,
TIMEBARWIDTH, percentage*height_used))
# update the timebar area
pygame.display.update(pygame.Rect(width*(CARD_PIXEL_WIDTH) + (width+1)*PIXEL_BORDER,
PIXEL_BORDER,
TIMEBARWIDTH, height_used))
# does the board have legal dimensions?
if width*height % 2:
print("Either 'width' or 'height' must be an even number")
sys.exit()
# choose the cards to be used
cards = random.sample(os.listdir(IMAGE_BIN), (width*height)//2)
images = dict()
for card in cards:
path = os.path.join(IMAGE_BIN, card)
images[card] = load_image(path)
cards = cards*2
random.shuffle(cards)
# card_list is a 2D array with the same structure as the game table
card_list = [cards[height*i:height*(i+1)] for i in range(width)]
cardback, cardbackrect = load_image("cardback.png")
# initialize the screen
screen.fill(BACKGROUND_COLOR)
for x in range(width):
for y in range(height):
xc = (x+1)*PIXEL_BORDER + x*CARD_PIXEL_WIDTH
yc = (y+1)*PIXEL_BORDER + y*CARD_PIXEL_WIDTH
screen.blit(cardback, (xc, yc))
pygame.display.update()
# auxiliary variables to control the state of the game
is_flipped = False
flipped_card = []
flipped_coords = []
to_find = len(cards)/2
found_cards = []
wait = False
wait_until = None
end = pygame.time.get_ticks() + BONUSTIME*width*height
score = 0
while to_find > 0 and pygame.time.get_ticks() < end + score:
# find the percentage of time left and update the timebar
perc = min(1, (end+score-pygame.time.get_ticks())/(BONUSTIME*width*height))
draw_timebar(perc)
if wait and pygame.time.get_ticks() > wait_until:
# we have waited already, now we take care of the cards
wait = False
pygame.event.set_allowed(MOUSEBUTTONDOWN)
# if we got it right
x1, y1 = flipped_coords[0]
x2, y2 = flipped_coords[1]
if card_list[x1][y1] == card_list[x2][y2]:
to_find -= 1
# this is the old rect pointing to the card
# that was most recently turned up
pygame.draw.rect(screen, BACKGROUND_COLOR, rect)
pygame.display.update(rect)
# erase the oldest facing up card from the game table
xc, yc = board_to_pixels((x1, y1))
rect = pygame.Rect(xc, yc, CARD_PIXEL_WIDTH, CARD_PIXEL_WIDTH)
pygame.draw.rect(screen, BACKGROUND_COLOR, rect)
pygame.display.update(rect)
# flag these two cards as found
found_cards.append((x1, y1))
found_cards.append((x2, y2))
score += BONUSTIME
# if we got it wrong
else:
# cover both cards again
screen.blit(cardback, (xc, yc))
pygame.display.update(rect)
xc, yc = board_to_pixels((x1, y1))
rect = pygame.Rect(xc, yc, CARD_PIXEL_WIDTH, CARD_PIXEL_WIDTH)
screen.blit(cardback, (xc, yc))
pygame.display.update(rect)
score -= PENALTYTIME
is_flipped = False
for ev in pygame.event.get():
if ev.type == QUIT:
pygame.quit()
sys.exit()
elif ev.type == MOUSEBUTTONDOWN:
if wait:
continue
# find the card in which we clicked; ignore clicks in the gaps between cards
x, pad = divmod(ev.pos[0], PIXEL_BORDER+CARD_PIXEL_WIDTH)
if x >= width or pad < PIXEL_BORDER:
continue
y, pad = divmod(ev.pos[1], PIXEL_BORDER+CARD_PIXEL_WIDTH)
if y >= height or pad < PIXEL_BORDER:
continue
# find the top left corner of the clicked card
xc, yc = board_to_pixels((x, y))
rect = pygame.Rect(xc, yc, CARD_PIXEL_WIDTH, CARD_PIXEL_WIDTH)
if (x,y) in found_cards:
continue
elif not is_flipped:
screen.blit(images[card_list[x][y]][0], (xc, yc))
pygame.display.update(rect)
is_flipped = True
flipped_card = [card_list[x][y]]
flipped_coords = [(x, y)]
# there is a card face up
else:
# I just clicked it
if flipped_coords[0] == (x, y):
continue
else:
# set a waiting interval where no events are allowed
wait = True
# turn this new card face up; wait
screen.blit(images[card_list[x][y]][0], (xc, yc))
pygame.display.update(rect)
# disable new clicks
pygame.event.set_blocked(MOUSEBUTTONDOWN)
if flipped_card[0] != card_list[x][y]:
flipped_card.append(card_list[x][y])
wait_until = pygame.time.get_ticks() + 800
else:
wait_until = pygame.time.get_ticks() + 300
flipped_coords.append((x,y))
Etapa 5: ecrã com pontuação
Acrescentar uma mensagem de vitória com os pontos ou uma mensagem de derrota é relativamente fácil; podemos fazê-lo depois do ciclo principal do jogo, acrescentando o seguinte código:
# add the time left to the score in case we won
score += (end+score)-pygame.time.get_ticks()
# initialize a font to print the results
pygame.font.init()
font = pygame.font.Font(None, 40)
if to_find:
img = font.render("You lost!", True, (0,0,0))
pygame.display.set_caption("You lost!")
else:
img = font.render("You scored {}!".format(score), True, (0,0,0))
pygame.display.set_caption("You won!")
screen = pygame.display.set_mode((img.get_width()+60,
img.get_height()+60))
screen.fill(BACKGROUND_COLOR)
screen.blit(img, (30, 30))
pygame.display.update()
while True:
for ev in pygame.event.get():
if ev.type == QUIT:
pygame.quit()
sys.exit()
Etapa 6: ficheiro de configuração
Vamos criar um ficheiro de configuração onde possamos, com facilidade, alterar o número de cartas a usar durante o jogo. Vamos ainda acrescentar a possiblidade de se alterar o valor das variáveis BONUSTIME
e PENALTYTIME
. Após uma breve pesquisa, vemos que o módulo configparser
pode ser de grande ajuda; importamo-lo, e de seguida definimos uma função com o propósito de ler as configurações do ficheiro no início do jogo. Incluímos ainda a possibilidade de o ficheiro não existir; nesse caso a função cria-o com valores por defeito para essas variáveis. A função parse_configurations
trata das tarefas descritas. Incluímos a sua implementação, bem como a secção onde as variáveis globais são definidas; note-se que apagámos as linhas que inicializam as variáveis width
, height
, BONUSTIME
e PENALTYTIME
.
import configparser
# ...
def parse_configurations():
configfile = "cardconfig.ini"
c = configparser.ConfigParser()
r = c.read(configfile)
if not r:
# create the configfile
global width, height, BONUSTIME, PENALTYTIME
c["DEFAULT"] = {"width": 5,
"height": 4,
"BONUSTIME": 3000,
"PENALTYTIME": 600
}
with open(configfile, "w") as f:
c.write(f)
# create the globals
global width, height, BONUSTIME, PENALTYTIME
width = int(c["DEFAULT"]["width"])
height = int(c["DEFAULT"]["height"])
BONUSTIME = int(c["DEFAULT"]["bonustime"])
PENALTYTIME = int(c["DEFAULT"]["penaltytime"])
parse_configurations()
CARD_PIXEL_WIDTH = 50
PIXEL_BORDER = 5
TIMEBARWIDTH = 25
WIDTH = width*(CARD_PIXEL_WIDTH) + (width+2)*PIXEL_BORDER + TIMEBARWIDTH
HEIGHT = height*(CARD_PIXEL_WIDTH) + (height+1)*PIXEL_BORDER
BACKGROUND_COLOR = (20, 200, 20)
IMAGE_BIN = "polygonbin"
O código completo final, em não mais do que 230 linhas, segue de seguida:
from pygame.locals import *
import pygame
import random
import os
import sys
import configparser
def load_image(name, transparent=False):
"""Function that handles image loading
Returns the image and its Rect"""
try:
img = pygame.image.load(name)
except pygame.error:
raise SystemExit("Could not load image " + name)
if not transparent:
img = img.convert()
img = img.convert_alpha()
img_rect = img.get_rect()
return img, img_rect
def parse_configurations():
configfile = "cardconfig.ini"
c = configparser.ConfigParser()
r = c.read(configfile)
if not r:
# create the configfile
global width, height, BONUSTIME, PENALTYTIME
c["DEFAULT"] = {"width": 5,
"height": 4,
"BONUSTIME": 3000,
"PENALTYTIME": 600
}
with open(configfile, "w") as f:
c.write(f)
# create the globals
global width, height, BONUSTIME, PENALTYTIME
width = int(c["DEFAULT"]["width"])
height = int(c["DEFAULT"]["height"])
BONUSTIME = int(c["DEFAULT"]["bonustime"])
PENALTYTIME = int(c["DEFAULT"]["penaltytime"])
parse_configurations()
CARD_PIXEL_WIDTH = 50
PIXEL_BORDER = 5
TIMEBARWIDTH = 25
WIDTH = width*(CARD_PIXEL_WIDTH) + (width+2)*PIXEL_BORDER + TIMEBARWIDTH
HEIGHT = height*(CARD_PIXEL_WIDTH) + (height+1)*PIXEL_BORDER
BACKGROUND_COLOR = (20, 200, 20)
IMAGE_BIN = "polygonbin"
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
def board_to_pixels(coords):
# receives a pair (x, y) pertaining a card position on the table
# transforms it into a pair (xc, yc) of pixel coordinates of the
# top left corner of the card
xc = (coords[0]+1)*PIXEL_BORDER + coords[0]*CARD_PIXEL_WIDTH
yc = (coords[1]+1)*PIXEL_BORDER + coords[1]*CARD_PIXEL_WIDTH
return xc, yc
def draw_timebar(percentage):
# draws a black timebar to let the user know how much time is left
# find the total height of the bar
height_used = height*(CARD_PIXEL_WIDTH) + (height-1)*PIXEL_BORDER
# cover the existing timebar with the background color
pygame.draw.rect(screen, BACKGROUND_COLOR, pygame.Rect(width*(CARD_PIXEL_WIDTH) + (width+1)*PIXEL_BORDER,
PIXEL_BORDER,
TIMEBARWIDTH, height_used))
# draw the timebar frame
pygame.draw.rect(screen, (0,0,0), pygame.Rect(width*(CARD_PIXEL_WIDTH) + (width+1)*PIXEL_BORDER,
PIXEL_BORDER,
TIMEBARWIDTH, height_used), 3)
# draw the time that is still left
pygame.draw.rect(screen, (0,0,0), pygame.Rect(width*(CARD_PIXEL_WIDTH) + (width+1)*PIXEL_BORDER,
PIXEL_BORDER+(1-percentage)*height_used,
TIMEBARWIDTH, percentage*height_used))
# update the timebar area
pygame.display.update(pygame.Rect(width*(CARD_PIXEL_WIDTH) + (width+1)*PIXEL_BORDER,
PIXEL_BORDER,
TIMEBARWIDTH, height_used))
# does the board have legal dimensions?
if width*height % 2:
print("Either 'width' or 'height' must be an even number")
sys.exit()
# choose the cards to be used
cards = random.sample(os.listdir(IMAGE_BIN), (width*height)//2)
images = dict()
for card in cards:
path = os.path.join(IMAGE_BIN, card)
images[card] = load_image(path)
cards = cards*2
random.shuffle(cards)
# card_list is a 2D array with the same structure as the game table
card_list = [cards[height*i:height*(i+1)] for i in range(width)]
cardback, cardbackrect = load_image("cardback.png")
# initialize the screen
screen.fill(BACKGROUND_COLOR)
for x in range(width):
for y in range(height):
xc = (x+1)*PIXEL_BORDER + x*CARD_PIXEL_WIDTH
yc = (y+1)*PIXEL_BORDER + y*CARD_PIXEL_WIDTH
screen.blit(cardback, (xc, yc))
pygame.display.update()
# auxiliary variables to control the state of the game
is_flipped = False
flipped_card = []
flipped_coords = []
to_find = len(cards)/2
found_cards = []
wait = False
wait_until = None
end = pygame.time.get_ticks() + BONUSTIME*width*height
score = 0
while to_find > 0 and pygame.time.get_ticks() < end + score:
# find the percentage of time left and update the timebar
perc = min(1, (end+score-pygame.time.get_ticks())/(BONUSTIME*width*height))
draw_timebar(perc)
if wait and pygame.time.get_ticks() > wait_until:
# we have waited already, now we take care of the cards
wait = False
pygame.event.set_allowed(MOUSEBUTTONDOWN)
# if we got it right
x1, y1 = flipped_coords[0]
x2, y2 = flipped_coords[1]
if card_list[x1][y1] == card_list[x2][y2]:
to_find -= 1
# this is the old rect pointing to the card
# that was most recently turned up
pygame.draw.rect(screen, BACKGROUND_COLOR, rect)
pygame.display.update(rect)
# erase the oldest facing up card from the game table
xc, yc = board_to_pixels((x1, y1))
rect = pygame.Rect(xc, yc, CARD_PIXEL_WIDTH, CARD_PIXEL_WIDTH)
pygame.draw.rect(screen, BACKGROUND_COLOR, rect)
pygame.display.update(rect)
# flag these two cards as found
found_cards.append((x1, y1))
found_cards.append((x2, y2))
score += BONUSTIME
# if we got it wrong
else:
# cover both cards again
screen.blit(cardback, (xc, yc))
pygame.display.update(rect)
xc, yc = board_to_pixels((x1, y1))
rect = pygame.Rect(xc, yc, CARD_PIXEL_WIDTH, CARD_PIXEL_WIDTH)
screen.blit(cardback, (xc, yc))
pygame.display.update(rect)
score -= PENALTYTIME
is_flipped = False
for ev in pygame.event.get():
if ev.type == QUIT:
pygame.quit()
sys.exit()
elif ev.type == MOUSEBUTTONDOWN:
if wait:
continue
# find the card in which we clicked; ignore clicks in the gaps between cards
x, pad = divmod(ev.pos[0], PIXEL_BORDER+CARD_PIXEL_WIDTH)
if x >= width or pad < PIXEL_BORDER:
continue
y, pad = divmod(ev.pos[1], PIXEL_BORDER+CARD_PIXEL_WIDTH)
if y >= height or pad < PIXEL_BORDER:
continue
# find the top left corner of the clicked card
xc, yc = board_to_pixels((x, y))
rect = pygame.Rect(xc, yc, CARD_PIXEL_WIDTH, CARD_PIXEL_WIDTH)
if (x,y) in found_cards:
continue
elif not is_flipped:
screen.blit(images[card_list[x][y]][0], (xc, yc))
pygame.display.update(rect)
is_flipped = True
flipped_card = [card_list[x][y]]
flipped_coords = [(x, y)]
# there is a card face up
else:
# I just clicked it
if flipped_coords[0] == (x, y):
continue
else:
# set a waiting interval where no events are allowed
wait = True
# turn this new card face up; wait
screen.blit(images[card_list[x][y]][0], (xc, yc))
pygame.display.update(rect)
# disable new clicks
pygame.event.set_blocked(MOUSEBUTTONDOWN)
if flipped_card[0] != card_list[x][y]:
flipped_card.append(card_list[x][y])
wait_until = pygame.time.get_ticks() + 800
else:
wait_until = pygame.time.get_ticks() + 300
flipped_coords.append((x,y))
# add the time left to the score in case we won
score += (end+score)-pygame.time.get_ticks()
# initialize a font to print the results
pygame.font.init()
font = pygame.font.Font(None, 40)
if to_find:
img = font.render("You lost!", True, (0,0,0))
pygame.display.set_caption("You lost!")
else:
img = font.render("You scored {}!".format(score), True, (0,0,0))
pygame.display.set_caption("You won!")
screen = pygame.display.set_mode((img.get_width()+60,
img.get_height()+60))
screen.fill(BACKGROUND_COLOR)
screen.blit(img, (30, 30))
pygame.display.update()
while True:
for ev in pygame.event.get():
if ev.type == QUIT:
pygame.quit()
sys.exit()
When I am coding a game, I like to set functional milestones: stages at which I can test whatever I have up to that point. When I follow this method, the whole process becomes much more interesting and engaging. It also makes it easier for me to control how the end product will turn out to be. In what follows I laid out a list of said milestones to be achieved. Each item is a new functionality to be added:
- Creates the screen and shows it with all the cards face down;
- Clicking a card flips it face up;
- Clicking a second card flips it up and then: if the two cards match they are removed from the table; if the cards don't match they are turned face down again;
- The player now has a time restriction; matching two cards awards the player with extra time and failing to match two cards penalizes the player;
- A final screen tells the player whether he lost or won. If the player won, display the score;
- A configuration file is used to set how many cards are initially on the table.
Step 0: the face of the cards
Before starting to code the memory game I decided the front face of the cards would have different regular polygons. This meant it was very easy to create a series of images for the cards and it also made the game mildly difficult. Polygons with more sides start to become hard to distinguish when you have little time to look at them. To create all the polygons I wrote a small script that repeatedly asks for an integer and then draws different coloured polygons with the specified number of sides. With that script I created several images to use as my cards.from PIL import Image, ImageDraw
import sys
import os
from math import pi, sin, cos
def generate_polygon(n, colours):
theta = (2*pi)/n
vertices = [(20*cos(theta*j), 20*sin(theta*j))
for j in range(0, n)]
for j in range(1, n//2+1):
vertices[j] = (vertices[n-j][0], -vertices[n-j][1])
vertices = [(v[0]+25, v[1]+25) for v in vertices]
im = Image.new("RGB", (50,50), (255,255,255))
for name in colours.keys():
c = colours[name]
draw = ImageDraw.Draw(im)
draw.polygon(vertices, fill=c, outline=c)
im.save(os.path.join("polygonbin",
"sides"+str(n)+name+".png"))
# colours taken from https://www.w3schools.com/tags/ref_colornames.asp
COLOURS = {
"Black": (0,0,0),
"Red": (255,0,0),
"Green": (0,255,0),
"Blue": (0,0,255),
"Aqua": (0,255,255),
"Brown": (165, 42, 42),
"Chocolate": (210, 105, 30),
"Crimson": (220, 20, 60),
"DarkGoldenRod": (184, 134, 11),
"DarkGreen": (0, 100, 0),
"DarkOrange": (255, 140, 0),
"Fuchsia": (255, 0, 255),
"Gold": (255, 190, 0),
"SeaGreen": (46, 139, 87),
"Yellow": (255, 255, 0)
}
if __name__ == "__main__":
n = -1
while True:
try:
n = int(input("# of sides >> "))
if n <= 0:
sys.exit()
except Exception:
continue
generate_polygon(n, COLOURS)
Step 1: show all the cards face down
In order to have a "table" with all the cards face down, we need to decide how many cards we are going to have and in what configuration (how many rows/columns). We also need an image to represent the back of the cards.from pygame.locals import *
import pygame
import sys
def load_image(name, transparent=False):
"""Function that handles image loading
Returns the image and its Rect"""
try:
img = pygame.image.load(name)
except pygame.error:
raise SystemExit("Could not load image " + name)
if not transparent:
img = img.convert()
img = img.convert_alpha()
img_rect = img.get_rect()
return img, img_rect
width = 5
height = 4
CARD_PIXEL_WIDTH = 50
PIXEL_BORDER = 5
WIDTH = width*(CARD_PIXEL_WIDTH) + (width+1)*PIXEL_BORDER
HEIGHT = height*(CARD_PIXEL_WIDTH) + (height+1)*PIXEL_BORDER
BACKGROUND_COLOR = (20, 200, 20)
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
cardback, cardbackrect = load_image("cardback.png")
# initialize the screen
screen.fill(BACKGROUND_COLOR)
for x in range(width):
for y in range(height):
xc = (x+1)*PIXEL_BORDER + x*CARD_PIXEL_WIDTH
yc = (y+1)*PIXEL_BORDER + y*CARD_PIXEL_WIDTH
screen.blit(cardback, (xc, yc))
pygame.display.update()
while True:
for ev in pygame.event.get():
if ev.type == QUIT:
pygame.quit()
sys.exit()
sys
module so we can terminate the program when the user clicks the red x. The function load_image
is a function that I (almost surely) copied from the internet; what it does is import the given image and then return its surface and rect. We can also specify if the image has a transparent background or not. After that we initialize some global variables that will be needed along the way. width
and height
are the number of columns and rows of cards we are going to have. WIDTH
and HEIGHT
are the dimensions of the window, in pixels. We also set the background colour, the padding (in pixels) between each card and the size (in pixels) of each card, which we assume to be a square.The infinite cycle we also included is fairly standard and it keeps the game from freezing; as of now, the only action allowed is to close the game window.
Step 2: clicking a card turns it face up
To complete this step we will need to add a couple of things that handle some game logic: we need to shuffle the pairs of cards into their positions and we need to recognize when a card is clicked on.We start by choosing the cards to be used and we distribute them on the table. Using the variables
width, height
we can know how many cards will be needed. All we have to do is open the directory where the images are and load randomly as many cards as needed. After we choose them, we store them in a dictionary and then create a matrix, with the same dimensions as the table, where we store which card is in which position. For that we start by shuffling all the cards in a vector and then we split the vector to create a matrix. # does the board have legal dimensions?
if width*height % 2:
print("Either 'width' or 'height' must be an even number")
sys.exit()
# choose the cards to be used
cards = random.sample(os.listdir(IMAGE_BIN), (width*height)//2)
images = dict()
for card in cards:
path = os.path.join(IMAGE_BIN, card)
images[card] = load_image(path)
cards = cards*2
random.shuffle(cards)
# card_list is a 2D array with the same structure as the game table
card_list = [cards[height*i:height*(i+1)] for i in range(width)]
random
and os
.The game already knows what cards to put where, now we need to turn them face up. After we click the screen we need to recognize if we clicked a card and then turn it face up. As it will turn out to be very helpful, let us create a function
board_to_pixels
that receives a pair of integers (x,y), a card position on the card matrix card_list
, and returns another pair of integers: the position, in pixels, of the top-left corner of the given card. This will be useful to create some rectangles that are going to be needed to update the screen and to redraw the cards. def board_to_pixels(coords):
# receives a pair (x, y) pertaining a card position on the table
# transforms it into a pair (xc, yc) of pixel coordinates of the
# top left corner of the card
xc = (coords[0]+1)*PIXEL_BORDER + coords[0]*CARD_PIXEL_WIDTH
yc = (coords[1]+1)*PIXEL_BORDER + coords[1]*CARD_PIXEL_WIDTH
return xc, yc
if
statement inside our main cycle. We can use a bit of maths and the divmod
functoin to know which card was clicked. We will also ignore clicks between cards. while True:
for ev in pygame.event.get():
if ev.type == QUIT:
pygame.quit()
sys.exit()
elif ev.type == MOUSEBUTTONDOWN:
# find the card in which we clicked; ignore clicks in the gaps between cards
x, pad = divmod(ev.pos[0], PIXEL_BORDER+CARD_PIXEL_WIDTH)
if x >= width or pad < PIXEL_BORDER:
continue
y, pad = divmod(ev.pos[1], PIXEL_BORDER+CARD_PIXEL_WIDTH)
if y >= height or pad < PIXEL_BORDER:
continue
# find the top left corner of the clicked card
xc, yc = board_to_pixels((x, y))
rect = pygame.Rect(xc, yc, CARD_PIXEL_WIDTH, CARD_PIXEL_WIDTH)
screen.blit(images[card_list[x][y]][0], (xc, yc))
pygame.display.update(rect)
from pygame.locals import *
import pygame
import random
import os
import sys
def load_image(name, transparent=False):
"""Function that handles image loading
Returns the image and its Rect"""
try:
img = pygame.image.load(name)
except pygame.error:
raise SystemExit("Could not load image " + name)
if not transparent:
img = img.convert()
img = img.convert_alpha()
img_rect = img.get_rect()
return img, img_rect
width = 5
height = 4
CARD_PIXEL_WIDTH = 50
PIXEL_BORDER = 5
WIDTH = width*(CARD_PIXEL_WIDTH) + (width+1)*PIXEL_BORDER
HEIGHT = height*(CARD_PIXEL_WIDTH) + (height+1)*PIXEL_BORDER
BACKGROUND_COLOR = (20, 200, 20)
IMAGE_BIN = "polygonbin"
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
def board_to_pixels(coords):
# receives a pair (x, y) pertaining a card position on the table
# transforms it into a pair (xc, yc) of pixel coordinates of the
# top left corner of the card
xc = (coords[0]+1)*PIXEL_BORDER + coords[0]*CARD_PIXEL_WIDTH
yc = (coords[1]+1)*PIXEL_BORDER + coords[1]*CARD_PIXEL_WIDTH
return xc, yc
# does the board have legal dimensions?
if width*height % 2:
print("Either 'width' or 'height' must be an even number")
sys.exit()
# choose the cards to be used
cards = random.sample(os.listdir(IMAGE_BIN), (width*height)//2)
images = dict()
for card in cards:
path = os.path.join(IMAGE_BIN, card)
images[card] = load_image(path)
cards = cards*2
random.shuffle(cards)
# card_list is a 2D array with the same structure as the game table
card_list = [cards[height*i:height*(i+1)] for i in range(width)]
cardback, cardbackrect = load_image("cardback.png")
# initialize the screen
screen.fill(BACKGROUND_COLOR)
for x in range(width):
for y in range(height):
xc = (x+1)*PIXEL_BORDER + x*CARD_PIXEL_WIDTH
yc = (y+1)*PIXEL_BORDER + y*CARD_PIXEL_WIDTH
screen.blit(cardback, (xc, yc))
pygame.display.update()
while True:
for ev in pygame.event.get():
if ev.type == QUIT:
pygame.quit()
sys.exit()
elif ev.type == MOUSEBUTTONDOWN:
# find the card in which we clicked; ignore clicks in the gaps between cards
x, pad = divmod(ev.pos[0], PIXEL_BORDER+CARD_PIXEL_WIDTH)
if x >= width or pad < PIXEL_BORDER:
continue
y, pad = divmod(ev.pos[1], PIXEL_BORDER+CARD_PIXEL_WIDTH)
if y >= height or pad < PIXEL_BORDER:
continue
# find the top left corner of the clicked card
xc, yc = board_to_pixels((x, y))
rect = pygame.Rect(xc, yc, CARD_PIXEL_WIDTH, CARD_PIXEL_WIDTH)
screen.blit(images[card_list[x][y]][0], (xc, yc))
pygame.display.update(rect)
Step 3: matches are removed from the table
If we want the matches to be removed from the table we will need to know if there is a card face up and which one it is. Having that in mind, we will add some auxiliary variables that will help us check if the second card to be turned face up is a match or not. We also need to modify the processing of the mouse clicks to take this into consideration.# auxiliary variables to control the state of the game
is_flipped = False
flipped_card = None
flipped_coords = None
to_find = len(cards)/2
while True:
for ev in pygame.event.get():
if ev.type == QUIT:
pygame.quit()
sys.exit()
elif ev.type == MOUSEBUTTONDOWN:
# find the card in which we clicked; ignore clicks in the gaps between cards
x, pad = divmod(ev.pos[0], PIXEL_BORDER+CARD_PIXEL_WIDTH)
if x >= width or pad < PIXEL_BORDER:
continue
y, pad = divmod(ev.pos[1], PIXEL_BORDER+CARD_PIXEL_WIDTH)
if y >= height or pad < PIXEL_BORDER:
continue
# find the top left corner of the clicked card
xc, yc = board_to_pixels((x, y))
rect = pygame.Rect(xc, yc, CARD_PIXEL_WIDTH, CARD_PIXEL_WIDTH)
if not is_flipped:
screen.blit(images[card_list[x][y]][0], (xc, yc))
pygame.display.update(rect)
is_flipped = True
flipped_card = card_list[x][y]
flipped_coords = (x, y)
# there is a card face up
else:
# I just clicked it
if flipped_coords == (x, y):
continue
else:
# turn this new card face up; wait
screen.blit(images[card_list[x][y]][0], (xc, yc))
pygame.display.update(rect)
pygame.time.delay(800)
# if we got it right
if flipped_card == card_list[x][y]:
to_find -= 1
pygame.draw.rect(screen, BACKGROUND_COLOR, rect)
pygame.display.update(rect)
# erase the previously clicked card from the game table
xc, yc = board_to_pixels(flipped_coords)
rect = pygame.Rect(xc, yc, CARD_PIXEL_WIDTH, CARD_PIXEL_WIDTH)
pygame.draw.rect(screen, BACKGROUND_COLOR, rect)
pygame.display.update(rect)
# if we got it wrong
else:
# cover both cards again
screen.blit(cardback, (xc, yc))
pygame.display.update(rect)
xc, yc = board_to_pixels(flipped_coords)
rect = pygame.Rect(xc, yc, CARD_PIXEL_WIDTH, CARD_PIXEL_WIDTH)
screen.blit(cardback, (xc, yc))
pygame.display.update(rect)
is_flipped = False
found_cards
where we store all positions that have been found. We add a small check so we ignore these clicks and then change the code to store the cards found when we find them. The final code for this step is: from pygame.locals import *
import pygame
import random
import os
import sys
def load_image(name, transparent=False):
"""Function that handles image loading
Returns the image and its Rect"""
try:
img = pygame.image.load(name)
except pygame.error:
raise SystemExit("Could not load image " + name)
if not transparent:
img = img.convert()
img = img.convert_alpha()
img_rect = img.get_rect()
return img, img_rect
width = 5
height = 4
CARD_PIXEL_WIDTH = 50
PIXEL_BORDER = 5
WIDTH = width*(CARD_PIXEL_WIDTH) + (width+1)*PIXEL_BORDER
HEIGHT = height*(CARD_PIXEL_WIDTH) + (height+1)*PIXEL_BORDER
BACKGROUND_COLOR = (20, 200, 20)
IMAGE_BIN = "polygonbin"
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
def board_to_pixels(coords):
# receives a pair (x, y) pertaining a card position on the table
# transforms it into a pair (xc, yc) of pixel coordinates of the
# top left corner of the card
xc = (coords[0]+1)*PIXEL_BORDER + coords[0]*CARD_PIXEL_WIDTH
yc = (coords[1]+1)*PIXEL_BORDER + coords[1]*CARD_PIXEL_WIDTH
return xc, yc
# does the board have legal dimensions?
if width*height % 2:
print("Either 'width' or 'height' must be an even number")
sys.exit()
# choose the cards to be used
cards = random.sample(os.listdir(IMAGE_BIN), (width*height)//2)
images = dict()
for card in cards:
path = os.path.join(IMAGE_BIN, card)
images[card] = load_image(path)
cards = cards*2
random.shuffle(cards)
# card_list is a 2D array with the same structure as the game table
card_list = [cards[height*i:height*(i+1)] for i in range(width)]
cardback, cardbackrect = load_image("cardback.png")
# initialize the screen
screen.fill(BACKGROUND_COLOR)
for x in range(width):
for y in range(height):
xc = (x+1)*PIXEL_BORDER + x*CARD_PIXEL_WIDTH
yc = (y+1)*PIXEL_BORDER + y*CARD_PIXEL_WIDTH
screen.blit(cardback, (xc, yc))
pygame.display.update()
# auxiliary variables to control the state of the game
is_flipped = False
flipped_card = None
flipped_coords = None
to_find = len(cards)/2
found_cards = []
while to_find > 0:
for ev in pygame.event.get():
if ev.type == QUIT:
pygame.quit()
sys.exit()
elif ev.type == MOUSEBUTTONDOWN:
# find the card in which we clicked; ignore clicks in the gaps between cards
x, pad = divmod(ev.pos[0], PIXEL_BORDER+CARD_PIXEL_WIDTH)
if x >= width or pad < PIXEL_BORDER:
continue
y, pad = divmod(ev.pos[1], PIXEL_BORDER+CARD_PIXEL_WIDTH)
if y >= height or pad < PIXEL_BORDER:
continue
# find the top left corner of the clicked card
xc, yc = board_to_pixels((x, y))
rect = pygame.Rect(xc, yc, CARD_PIXEL_WIDTH, CARD_PIXEL_WIDTH)
if (x,y) in found_cards:
continue
elif not is_flipped:
screen.blit(images[card_list[x][y]][0], (xc, yc))
pygame.display.update(rect)
is_flipped = True
flipped_card = card_list[x][y]
flipped_coords = (x, y)
# there is a card face up
else:
# I just clicked it
if flipped_coords == (x, y):
continue
else:
# turn this new card face up; wait
screen.blit(images[card_list[x][y]][0], (xc, yc))
pygame.display.update(rect)
pygame.time.delay(800)
# if we got it right
if flipped_card == card_list[x][y]:
to_find -= 1
pygame.draw.rect(screen, BACKGROUND_COLOR, rect)
pygame.display.update(rect)
# erase the previously clicked card from the game table
xc, yc = board_to_pixels(flipped_coords)
rect = pygame.Rect(xc, yc, CARD_PIXEL_WIDTH, CARD_PIXEL_WIDTH)
pygame.draw.rect(screen, BACKGROUND_COLOR, rect)
pygame.display.update(rect)
# flag these two cards as found
found_cards.append(flipped_coords)
found_cards.append((x,y))
# if we got it wrong
else:
# cover both cards again
screen.blit(cardback, (xc, yc))
pygame.display.update(rect)
xc, yc = board_to_pixels(flipped_coords)
rect = pygame.Rect(xc, yc, CARD_PIXEL_WIDTH, CARD_PIXEL_WIDTH)
screen.blit(cardback, (xc, yc))
pygame.display.update(rect)
is_flipped = False
Step 4: adding a timer
At this point we will add a timer for the game. Whenever the player finds a match a time bonus is awarded and whenever the player fails to find a match, a time penalty is applied. For the player to keep track of the time left we will also include a time bar. We will draw it to the right of the table and it will empty itself as the time goes by. We will create a function to draw the bar and we will create two global variablesBONUSTIME
and PENALTYTIME
to store, in milliseconds, the time bonus and the time penalty. We will also change the main loop to stop whenever the time is up. As a default, we set BONUSTIME = 3000
and PENALTYTIME = 600
If we set the time bar to be drawn to the right, the function that draws the bar only needs the percentage of time left. That information is all we need, as long as we also have a variable controlling the width of the time bar. We will also need to update the variable
WIDTH
to reflect the extra pixels needed to draw the bar: WIDTH = width*(CARD_PIXEL_WIDTH) + (width+2)*PIXEL_BORDER + TIMEBARWIDTH
TIMEBARWIDTH = 25
# ...
def draw_timebar(percentage):
# draws a black timebar to let the user know how much time is left
# find the total height of the bar
height_used = height*(CARD_PIXEL_WIDTH) + (height-1)*PIXEL_BORDER
# cover the existing timebar with the background color
pygame.draw.rect(screen, BACKGROUND_COLOR, pygame.Rect(width*(CARD_PIXEL_WIDTH) + (width+1)*PIXEL_BORDER,
PIXEL_BORDER,
TIMEBARWIDTH, height_used))
# draw the timebar frame
pygame.draw.rect(screen, (0,0,0), pygame.Rect(width*(CARD_PIXEL_WIDTH) + (width+1)*PIXEL_BORDER,
PIXEL_BORDER,
TIMEBARWIDTH, height_used), 3)
# draw the time that is still left
pygame.draw.rect(screen, (0,0,0), pygame.Rect(width*(CARD_PIXEL_WIDTH) + (width+1)*PIXEL_BORDER,
PIXEL_BORDER+(1-percentage)*height_used,
TIMEBARWIDTH, percentage*height_used))
# update the timebar area
pygame.display.update(pygame.Rect(width*(CARD_PIXEL_WIDTH) + (width+1)*PIXEL_BORDER,
PIXEL_BORDER,
TIMEBARWIDTH, height_used))
end
and score
in which we store the time at which the game ends and all the bonuses and penalties the player got. We then change the main loop to stop whenever we run out of time: end = pygame.time.get_ticks() + BONUSTIME*width*height
score = 0
while to_find > 0 and pygame.time.get_ticks() < end + score:
score
whenever we match two cards or fail at doing so, making use of the variables BONUSTIME
and PENALTYTIME
... ...
if flipped_card == card_list[x][y]:
to_find -= 1
score += BONUSTIME
...
else:
score -= PENALTYTIME
while to_find > 0 and pygame.time.get_ticks() < end + score:
# find the percentage of time left and update the timebar
perc = min(1, (end+score-pygame.time.get_ticks())/(BONUSTIME*width*height))
draw_timebar(perc)
pygame.time.delay(800)
, which stops pygame for 800ms. A way to circumvent this problem is by creating an auxiliary variable wait
that tells the program to keep running BUT to ignore all user input. After that waiting time, we process the two cards that are facing up and then let the game flow as usual.These are the changes that are due:
- When we flip a second card face up, we store its position and define a waiting time;
- When we are inside the main loop check if it is time for us to end the pause;
- When we are in the waiting time, do not let the user click any more cards;
- When the waiting time is over, turn the two cards face down or remove them from the table.
### -->
and ### <--
) # auxiliary variables to control the state of the game
is_flipped = False
### -->
flipped_card = []
flipped_coords = []
wait = False
wait_until = None
### <--
to_find = len(cards)/2
found_cards = []
end = pygame.time.get_ticks() + BONUSTIME*width*height
score = 0
while to_find > 0 and pygame.time.get_ticks() < end + score:
clock.tick(60)
# find the percentage of time left and update the timebar
perc = min(1, (end+score-pygame.time.get_ticks())/(BONUSTIME*width*height))
draw_timebar(perc)
### --> this is VERY similar to what used to be in the end of the loop
if wait and pygame.time.get_ticks() > wait_until:
# we have waited already, now we take care of the cards
wait = False
pygame.event.set_allowed(MOUSEBUTTONDOWN)
# if we got it right
x1, y1 = flipped_coords[0]
x2, y2 = flipped_coords[1]
if card_list[x1][y1] == card_list[x2][y2]:
to_find -= 1
# this is the old rect pointing to the card
# that was most recently turned up
pygame.draw.rect(screen, BACKGROUND_COLOR, rect)
pygame.display.update(rect)
# erase the oldest facing up card from the game table
xc, yc = board_to_pixels((x1, y1))
rect = pygame.Rect(xc, yc, CARD_PIXEL_WIDTH, CARD_PIXEL_WIDTH)
pygame.draw.rect(screen, BACKGROUND_COLOR, rect)
pygame.display.update(rect)
# flag these two cards as found
found_cards.append((x1, y1))
found_cards.append((x2, y2))
score += BONUSTIME
# if we got it wrong
else:
# cover both cards again
screen.blit(cardback, (xc, yc))
pygame.display.update(rect)
xc, yc = board_to_pixels((x1, y1))
rect = pygame.Rect(xc, yc, CARD_PIXEL_WIDTH, CARD_PIXEL_WIDTH)
screen.blit(cardback, (xc, yc))
pygame.display.update(rect)
score -= PENALTYTIME
is_flipped = False
### <--
for ev in pygame.event.get():
if ev.type == QUIT:
pygame.quit()
sys.exit()
elif ev.type == MOUSEBUTTONDOWN:
if wait:
continue
# find the card in which we clicked; ignore clicks in the gaps between cards
x, pad = divmod(ev.pos[0], PIXEL_BORDER+CARD_PIXEL_WIDTH)
if x >= width or pad < PIXEL_BORDER:
continue
y, pad = divmod(ev.pos[1], PIXEL_BORDER+CARD_PIXEL_WIDTH)
if y >= height or pad < PIXEL_BORDER:
continue
# find the top left corner of the clicked card
xc, yc = board_to_pixels((x, y))
rect = pygame.Rect(xc, yc, CARD_PIXEL_WIDTH, CARD_PIXEL_WIDTH)
if (x,y) in found_cards:
continue
elif not is_flipped:
screen.blit(images[card_list[x][y]][0], (xc, yc))
pygame.display.update(rect)
is_flipped = True
### -->
flipped_card = [card_list[x][y]]
flipped_coords = [(x, y)]
### <--
# there is a card face up
else:
# I just clicked it
if flipped_coords[0] == (x, y): ### just a minor change, use index notation
continue
else:
### -->
# set a waiting interval where no events are allowed
wait = True
# turn this new card face up; wait
screen.blit(images[card_list[x][y]][0], (xc, yc))
pygame.display.update(rect)
# disable new clicks
pygame.event.set_blocked(MOUSEBUTTONDOWN)
if flipped_card[0] != card_list[x][y]:
flipped_card.append(card_list[x][y])
wait_until = pygame.time.get_ticks() + 800
else:
wait_until = pygame.time.get_ticks() + 300
flipped_coords.append((x,y))
### <--
from pygame.locals import *
import pygame
import random
import os
import sys
def load_image(name, transparent=False):
"""Function that handles image loading
Returns the image and its Rect"""
try:
img = pygame.image.load(name)
except pygame.error:
raise SystemExit("Could not load image " + name)
if not transparent:
img = img.convert()
img = img.convert_alpha()
img_rect = img.get_rect()
return img, img_rect
width = 2
height = 3
CARD_PIXEL_WIDTH = 50
PIXEL_BORDER = 5
TIMEBARWIDTH = 25
WIDTH = width*(CARD_PIXEL_WIDTH) + (width+2)*PIXEL_BORDER + TIMEBARWIDTH
HEIGHT = height*(CARD_PIXEL_WIDTH) + (height+1)*PIXEL_BORDER
BACKGROUND_COLOR = (20, 200, 20)
IMAGE_BIN = "polygonbin"
BONUSTIME = 3000
PENALTYTIME = 600
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
def board_to_pixels(coords):
# receives a pair (x, y) pertaining a card position on the table
# transforms it into a pair (xc, yc) of pixel coordinates of the
# top left corner of the card
xc = (coords[0]+1)*PIXEL_BORDER + coords[0]*CARD_PIXEL_WIDTH
yc = (coords[1]+1)*PIXEL_BORDER + coords[1]*CARD_PIXEL_WIDTH
return xc, yc
def draw_timebar(percentage):
# draws a black timebar to let the user know how much time is left
# find the total height of the bar
height_used = height*(CARD_PIXEL_WIDTH) + (height-1)*PIXEL_BORDER
# cover the existing timebar with the background color
pygame.draw.rect(screen, BACKGROUND_COLOR, pygame.Rect(width*(CARD_PIXEL_WIDTH) + (width+1)*PIXEL_BORDER,
PIXEL_BORDER,
TIMEBARWIDTH, height_used))
# draw the timebar frame
pygame.draw.rect(screen, (0,0,0), pygame.Rect(width*(CARD_PIXEL_WIDTH) + (width+1)*PIXEL_BORDER,
PIXEL_BORDER,
TIMEBARWIDTH, height_used), 3)
# draw the time that is still left
pygame.draw.rect(screen, (0,0,0), pygame.Rect(width*(CARD_PIXEL_WIDTH) + (width+1)*PIXEL_BORDER,
PIXEL_BORDER+(1-percentage)*height_used,
TIMEBARWIDTH, percentage*height_used))
# update the timebar area
pygame.display.update(pygame.Rect(width*(CARD_PIXEL_WIDTH) + (width+1)*PIXEL_BORDER,
PIXEL_BORDER,
TIMEBARWIDTH, height_used))
# does the board have legal dimensions?
if width*height % 2:
print("Either 'width' or 'height' must be an even number")
sys.exit()
# choose the cards to be used
cards = random.sample(os.listdir(IMAGE_BIN), (width*height)//2)
images = dict()
for card in cards:
path = os.path.join(IMAGE_BIN, card)
images[card] = load_image(path)
cards = cards*2
random.shuffle(cards)
# card_list is a 2D array with the same structure as the game table
card_list = [cards[height*i:height*(i+1)] for i in range(width)]
cardback, cardbackrect = load_image("cardback.png")
# initialize the screen
screen.fill(BACKGROUND_COLOR)
for x in range(width):
for y in range(height):
xc = (x+1)*PIXEL_BORDER + x*CARD_PIXEL_WIDTH
yc = (y+1)*PIXEL_BORDER + y*CARD_PIXEL_WIDTH
screen.blit(cardback, (xc, yc))
pygame.display.update()
# auxiliary variables to control the state of the game
is_flipped = False
flipped_card = []
flipped_coords = []
to_find = len(cards)/2
found_cards = []
wait = False
wait_until = None
end = pygame.time.get_ticks() + BONUSTIME*width*height
score = 0
while to_find > 0 and pygame.time.get_ticks() < end + score:
# find the percentage of time left and update the timebar
perc = min(1, (end+score-pygame.time.get_ticks())/(BONUSTIME*width*height))
draw_timebar(perc)
if wait and pygame.time.get_ticks() > wait_until:
# we have waited already, now we take care of the cards
wait = False
pygame.event.set_allowed(MOUSEBUTTONDOWN)
# if we got it right
x1, y1 = flipped_coords[0]
x2, y2 = flipped_coords[1]
if card_list[x1][y1] == card_list[x2][y2]:
to_find -= 1
# this is the old rect pointing to the card
# that was most recently turned up
pygame.draw.rect(screen, BACKGROUND_COLOR, rect)
pygame.display.update(rect)
# erase the oldest facing up card from the game table
xc, yc = board_to_pixels((x1, y1))
rect = pygame.Rect(xc, yc, CARD_PIXEL_WIDTH, CARD_PIXEL_WIDTH)
pygame.draw.rect(screen, BACKGROUND_COLOR, rect)
pygame.display.update(rect)
# flag these two cards as found
found_cards.append((x1, y1))
found_cards.append((x2, y2))
score += BONUSTIME
# if we got it wrong
else:
# cover both cards again
screen.blit(cardback, (xc, yc))
pygame.display.update(rect)
xc, yc = board_to_pixels((x1, y1))
rect = pygame.Rect(xc, yc, CARD_PIXEL_WIDTH, CARD_PIXEL_WIDTH)
screen.blit(cardback, (xc, yc))
pygame.display.update(rect)
score -= PENALTYTIME
is_flipped = False
for ev in pygame.event.get():
if ev.type == QUIT:
pygame.quit()
sys.exit()
elif ev.type == MOUSEBUTTONDOWN:
if wait:
continue
# find the card in which we clicked; ignore clicks in the gaps between cards
x, pad = divmod(ev.pos[0], PIXEL_BORDER+CARD_PIXEL_WIDTH)
if x >= width or pad < PIXEL_BORDER:
continue
y, pad = divmod(ev.pos[1], PIXEL_BORDER+CARD_PIXEL_WIDTH)
if y >= height or pad < PIXEL_BORDER:
continue
# find the top left corner of the clicked card
xc, yc = board_to_pixels((x, y))
rect = pygame.Rect(xc, yc, CARD_PIXEL_WIDTH, CARD_PIXEL_WIDTH)
if (x,y) in found_cards:
continue
elif not is_flipped:
screen.blit(images[card_list[x][y]][0], (xc, yc))
pygame.display.update(rect)
is_flipped = True
flipped_card = [card_list[x][y]]
flipped_coords = [(x, y)]
# there is a card face up
else:
# I just clicked it
if flipped_coords[0] == (x, y):
continue
else:
# set a waiting interval where no events are allowed
wait = True
# turn this new card face up; wait
screen.blit(images[card_list[x][y]][0], (xc, yc))
pygame.display.update(rect)
# disable new clicks
pygame.event.set_blocked(MOUSEBUTTONDOWN)
if flipped_card[0] != card_list[x][y]:
flipped_card.append(card_list[x][y])
wait_until = pygame.time.get_ticks() + 800
else:
wait_until = pygame.time.get_ticks() + 300
flipped_coords.append((x,y))
Step 5: scoreboard
To add a victory/defeat screen is relatively easy. We can go about it by adding a bit of code after the main game loop:# add the time left to the score in case we won
score += (end+score)-pygame.time.get_ticks()
# initialize a font to print the results
pygame.font.init()
font = pygame.font.Font(None, 40)
if to_find:
img = font.render("You lost!", True, (0,0,0))
pygame.display.set_caption("You lost!")
else:
img = font.render("You scored {}!".format(score), True, (0,0,0))
pygame.display.set_caption("You won!")
screen = pygame.display.set_mode((img.get_width()+60,
img.get_height()+60))
screen.fill(BACKGROUND_COLOR)
screen.blit(img, (30, 30))
pygame.display.update()
while True:
for ev in pygame.event.get():
if ev.type == QUIT:
pygame.quit()
sys.exit()
Step 6: configuration file
In this step we will create a configuration file so we can easily change the number of cards on the table (through thewidth
and height
variables), as well as change the bonus and penalty times, through BONUSTIME
and PENALTYTIME
. After a bit of googling I found the module configparser
would be of great help; we import it and then define a function with the purpose of reading the configuration file and parsing it; if the file doesn't exist, we create it with default values. We called this function parse_configurations
. We include its implementation, as well as the section where the global variables for the whole game are initialized; notice we removed the lines regarding width
, height
, BONUSTIME
and PENALTYTIME
. import configparser
# ...
def parse_configurations():
configfile = "cardconfig.ini"
c = configparser.ConfigParser()
r = c.read(configfile)
if not r:
# create the configfile
global width, height, BONUSTIME, PENALTYTIME
c["DEFAULT"] = {"width": 5,
"height": 4,
"BONUSTIME": 3000,
"PENALTYTIME": 600
}
with open(configfile, "w") as f:
c.write(f)
# create the globals
global width, height, BONUSTIME, PENALTYTIME
width = int(c["DEFAULT"]["width"])
height = int(c["DEFAULT"]["height"])
BONUSTIME = int(c["DEFAULT"]["bonustime"])
PENALTYTIME = int(c["DEFAULT"]["penaltytime"])
parse_configurations()
CARD_PIXEL_WIDTH = 50
PIXEL_BORDER = 5
TIMEBARWIDTH = 25
WIDTH = width*(CARD_PIXEL_WIDTH) + (width+2)*PIXEL_BORDER + TIMEBARWIDTH
HEIGHT = height*(CARD_PIXEL_WIDTH) + (height+1)*PIXEL_BORDER
BACKGROUND_COLOR = (20, 200, 20)
IMAGE_BIN = "polygonbin"
from pygame.locals import *
import pygame
import random
import os
import sys
import configparser
def load_image(name, transparent=False):
"""Function that handles image loading
Returns the image and its Rect"""
try:
img = pygame.image.load(name)
except pygame.error:
raise SystemExit("Could not load image " + name)
if not transparent:
img = img.convert()
img = img.convert_alpha()
img_rect = img.get_rect()
return img, img_rect
def parse_configurations():
configfile = "cardconfig.ini"
c = configparser.ConfigParser()
r = c.read(configfile)
if not r:
# create the configfile
global width, height, BONUSTIME, PENALTYTIME
c["DEFAULT"] = {"width": 5,
"height": 4,
"BONUSTIME": 3000,
"PENALTYTIME": 600
}
with open(configfile, "w") as f:
c.write(f)
# create the globals
global width, height, BONUSTIME, PENALTYTIME
width = int(c["DEFAULT"]["width"])
height = int(c["DEFAULT"]["height"])
BONUSTIME = int(c["DEFAULT"]["bonustime"])
PENALTYTIME = int(c["DEFAULT"]["penaltytime"])
parse_configurations()
CARD_PIXEL_WIDTH = 50
PIXEL_BORDER = 5
TIMEBARWIDTH = 25
WIDTH = width*(CARD_PIXEL_WIDTH) + (width+2)*PIXEL_BORDER + TIMEBARWIDTH
HEIGHT = height*(CARD_PIXEL_WIDTH) + (height+1)*PIXEL_BORDER
BACKGROUND_COLOR = (20, 200, 20)
IMAGE_BIN = "polygonbin"
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
def board_to_pixels(coords):
# receives a pair (x, y) pertaining a card position on the table
# transforms it into a pair (xc, yc) of pixel coordinates of the
# top left corner of the card
xc = (coords[0]+1)*PIXEL_BORDER + coords[0]*CARD_PIXEL_WIDTH
yc = (coords[1]+1)*PIXEL_BORDER + coords[1]*CARD_PIXEL_WIDTH
return xc, yc
def draw_timebar(percentage):
# draws a black timebar to let the user know how much time is left
# find the total height of the bar
height_used = height*(CARD_PIXEL_WIDTH) + (height-1)*PIXEL_BORDER
# cover the existing timebar with the background color
pygame.draw.rect(screen, BACKGROUND_COLOR, pygame.Rect(width*(CARD_PIXEL_WIDTH) + (width+1)*PIXEL_BORDER,
PIXEL_BORDER,
TIMEBARWIDTH, height_used))
# draw the timebar frame
pygame.draw.rect(screen, (0,0,0), pygame.Rect(width*(CARD_PIXEL_WIDTH) + (width+1)*PIXEL_BORDER,
PIXEL_BORDER,
TIMEBARWIDTH, height_used), 3)
# draw the time that is still left
pygame.draw.rect(screen, (0,0,0), pygame.Rect(width*(CARD_PIXEL_WIDTH) + (width+1)*PIXEL_BORDER,
PIXEL_BORDER+(1-percentage)*height_used,
TIMEBARWIDTH, percentage*height_used))
# update the timebar area
pygame.display.update(pygame.Rect(width*(CARD_PIXEL_WIDTH) + (width+1)*PIXEL_BORDER,
PIXEL_BORDER,
TIMEBARWIDTH, height_used))
# does the board have legal dimensions?
if width*height % 2:
print("Either 'width' or 'height' must be an even number")
sys.exit()
# choose the cards to be used
cards = random.sample(os.listdir(IMAGE_BIN), (width*height)//2)
images = dict()
for card in cards:
path = os.path.join(IMAGE_BIN, card)
images[card] = load_image(path)
cards = cards*2
random.shuffle(cards)
# card_list is a 2D array with the same structure as the game table
card_list = [cards[height*i:height*(i+1)] for i in range(width)]
cardback, cardbackrect = load_image("cardback.png")
# initialize the screen
screen.fill(BACKGROUND_COLOR)
for x in range(width):
for y in range(height):
xc = (x+1)*PIXEL_BORDER + x*CARD_PIXEL_WIDTH
yc = (y+1)*PIXEL_BORDER + y*CARD_PIXEL_WIDTH
screen.blit(cardback, (xc, yc))
pygame.display.update()
# auxiliary variables to control the state of the game
is_flipped = False
flipped_card = []
flipped_coords = []
to_find = len(cards)/2
found_cards = []
wait = False
wait_until = None
end = pygame.time.get_ticks() + BONUSTIME*width*height
score = 0
while to_find > 0 and pygame.time.get_ticks() < end + score:
# find the percentage of time left and update the timebar
perc = min(1, (end+score-pygame.time.get_ticks())/(BONUSTIME*width*height))
draw_timebar(perc)
if wait and pygame.time.get_ticks() > wait_until:
# we have waited already, now we take care of the cards
wait = False
pygame.event.set_allowed(MOUSEBUTTONDOWN)
# if we got it right
x1, y1 = flipped_coords[0]
x2, y2 = flipped_coords[1]
if card_list[x1][y1] == card_list[x2][y2]:
to_find -= 1
# this is the old rect pointing to the card
# that was most recently turned up
pygame.draw.rect(screen, BACKGROUND_COLOR, rect)
pygame.display.update(rect)
# erase the oldest facing up card from the game table
xc, yc = board_to_pixels((x1, y1))
rect = pygame.Rect(xc, yc, CARD_PIXEL_WIDTH, CARD_PIXEL_WIDTH)
pygame.draw.rect(screen, BACKGROUND_COLOR, rect)
pygame.display.update(rect)
# flag these two cards as found
found_cards.append((x1, y1))
found_cards.append((x2, y2))
score += BONUSTIME
# if we got it wrong
else:
# cover both cards again
screen.blit(cardback, (xc, yc))
pygame.display.update(rect)
xc, yc = board_to_pixels((x1, y1))
rect = pygame.Rect(xc, yc, CARD_PIXEL_WIDTH, CARD_PIXEL_WIDTH)
screen.blit(cardback, (xc, yc))
pygame.display.update(rect)
score -= PENALTYTIME
is_flipped = False
for ev in pygame.event.get():
if ev.type == QUIT:
pygame.quit()
sys.exit()
elif ev.type == MOUSEBUTTONDOWN:
if wait:
continue
# find the card in which we clicked; ignore clicks in the gaps between cards
x, pad = divmod(ev.pos[0], PIXEL_BORDER+CARD_PIXEL_WIDTH)
if x >= width or pad < PIXEL_BORDER:
continue
y, pad = divmod(ev.pos[1], PIXEL_BORDER+CARD_PIXEL_WIDTH)
if y >= height or pad < PIXEL_BORDER:
continue
# find the top left corner of the clicked card
xc, yc = board_to_pixels((x, y))
rect = pygame.Rect(xc, yc, CARD_PIXEL_WIDTH, CARD_PIXEL_WIDTH)
if (x,y) in found_cards:
continue
elif not is_flipped:
screen.blit(images[card_list[x][y]][0], (xc, yc))
pygame.display.update(rect)
is_flipped = True
flipped_card = [card_list[x][y]]
flipped_coords = [(x, y)]
# there is a card face up
else:
# I just clicked it
if flipped_coords[0] == (x, y):
continue
else:
# set a waiting interval where no events are allowed
wait = True
# turn this new card face up; wait
screen.blit(images[card_list[x][y]][0], (xc, yc))
pygame.display.update(rect)
# disable new clicks
pygame.event.set_blocked(MOUSEBUTTONDOWN)
if flipped_card[0] != card_list[x][y]:
flipped_card.append(card_list[x][y])
wait_until = pygame.time.get_ticks() + 800
else:
wait_until = pygame.time.get_ticks() + 300
flipped_coords.append((x,y))
# add the time left to the score in case we won
score += (end+score)-pygame.time.get_ticks()
# initialize a font to print the results
pygame.font.init()
font = pygame.font.Font(None, 40)
if to_find:
img = font.render("You lost!", True, (0,0,0))
pygame.display.set_caption("You lost!")
else:
img = font.render("You scored {}!".format(score), True, (0,0,0))
pygame.display.set_caption("You won!")
screen = pygame.display.set_mode((img.get_width()+60,
img.get_height()+60))
screen.fill(BACKGROUND_COLOR)
screen.blit(img, (30, 30))
pygame.display.update()
while True:
for ev in pygame.event.get():
if ev.type == QUIT:
pygame.quit()
sys.exit()
- RGS

No comments: