“Shuffle” é um jogo de memória, em que o jogador tem de encontrar os pares de cartas virando apenas duas de cada vez. Caso as cartas viradas formem um par, o jogador recebe 100 pontos e as cartas são eliminadas do tabuleiro.
Caso as cartas não formem um par, ambas serão viradas para baixo novamente e o número de tentativas falhadas é incrementado.
A penalização que o jogador sofrerá na sua pontuação é definido com base no número de tentativas falhadas.
penalização = P
tentativas falhadas = TF
P = TF * 20
Nenhuma penalização é dada caso o jogador ainda esteja na sua primeira tentativa falhada e o contador de tentativas é reiniciado a cada acerto. A pontuação nunca é inferior a 0.
A qualquer momento o jogador pode usar a tecla 'ESC' para sair do jogo. E caso esteja dentro de um modo de jogo, basta pressionar a tecla 'BACKSPACE' para voltar ao menu principal. Alternativamente, também é possível usar os botões 'Exit' para fechar o jogo, caso esteja no menu principal, e para voltar ao menu, caso esteja num modo de jogo.
Cada vez que o jogador "limpa" o tabuleiro por completo o programa registra a sua pontuação num ficheiro: score.txt. Posteriormente é feita uma comparação entre todas as pontuações registradas no ficheiro e a maior delas é tida como best score.
A pontuação obtida na última vez em que se jogou e o tabuleiro foi "limpo" é tida como last score. Uma música é tocada a cada vez que o jogador consegue limpar o tabuleiro. Caso a pontuação feita supere o best score toca uma música, caso não aconteça, toca outra.O código está distribuído entre 5 ficheiros .py principais:
O restante dos arquivos diz respeito a sons, imagens e fontes usadas no jogo.
Este é o ficheiro principal do jogo. É aquele que deve ser corrido para que o jogo seja aberto.
O main.py contém a funçãogame_menu()
que, assim como o nome indica, apresenta o menu principal ao jogador.
Mas para além de mostrar na tela o menu principal, esta função realiza todos os processos necessários para a execução das tarefas que são atribuídas a cada botão presente na tela.
Para criar os botões que o jogo vai aprensentar, basta acrescentar o texto referente ao game mode pretendido na lista game_mode
:
game_mode = ['4 x 3', '4 x 4', '5 x 4', '6 x 5', '6 x 6','Exit']
Esta lista deve conter SOMENTE strings.
Uma vez que esta lista está criada, é invocada a funçãoget_gm_list()
que vai converter toda essa lista de strings, para uma lista de inteiros:
gm_list = [[4, 3], [4, 4], [5, 4], [6, 5], [6, 6]]
Esta função vai explicada mais à frente.
Sendo esta uma lista de inteiros, o conteúdo do botão 'Exit é ignorado e deixado de fora desta lista. A função chega então a um loop, que irá criar realmente os botões. for i in range (0, len(game_mode)):
# creates the first button
if i == 0:
button_set.append(Button(button_width, button_height, button_x, button_y, game_mode[0]))
# creates the rest of the buttons, except for the last one
elif i > 0 and i < len(game_mode) - 1:
button_set.append(Button(button_width, button_height, button_x, button_set[i - 1].y + button_set[i - 1].height*dist_modifier , game_mode[i]))
# creates the 'EXIT' button, wich is the last one, only if the button text contains 'exit' writen in any way
elif i == len(game_mode) - 1 and (game_mode[i].replace(" ", "")).lower() == 'exit':
button_set.append(Button(button_width, button_height, button_x, button_set[i - 1].y + button_set[i - 1].height*dist_modifier*1.5 , game_mode[i]))
# if the last button doen't contain 'exit' in it, a regular game mode button will be created
else:
button_set.append(Button(button_width, button_height, button_x, button_set[i - 1].y + button_set[i - 1].height*dist_modifier , game_mode[i]))
# if the buttons excede the display dimensions it will rise an error with a possible solution
if button_set[i].y + button_set[i].height>= display_height:
print("-"*30 + " E R R O R " + "-"*30 + "\nThe amount of buttons exceeds the display height")
print("Try either repositioning the button set or creating less buttons\n" + "-"*72)
Como a posição do primeiro botão é fixa, há que ter cuidado na criação de novos botões, pois eles podem exceder as medidas da tela de jogo. E se for este o caso, será impressa na command line a seguinte mensagem de erro:
------------------------------ E R R O R ------------------------------
The amount of buttons exceeds the display height
Try either repositioning the button set or creating less buttons
------------------------------------------------------------------------
Cada botão é uma instância da classe Button (também será melhor explicada adiante).
Sendo umButton
cada um dos botões deve receber os seguintes parâmetros:
- largura;
- altura;
- posição x;
- posição y;
- texto do game mode;
Ao entrar no loop, são criados os botões com base nas strings contidas na lista
game_mode
e os Button's são realmente colocados na listabutton_set
:
button_set.append(Button(button_width, button_height, button_x, button_y, game_mode[<index>]))
A lista button_set
contém, REALMENTE, os botões. Não é apenas uma lista de strings ou inteiros.
game_mode
serão, exatamente, os textos dos botões.
Os botões são criados por ordem, guiados pela posição do primeiro, mantendo uma pequena distância entre si. O botão 'Exit' é o único que é tratado de forma diferente. Ele deve ser o ÚLTIMO da lista
game_mode
e o seu texto deverá conter de alguma forma as letras ['E', 'X', 'I', 'T'], nesta ordem, para que seja atribuída uma distância um pouco maior entre ele e o botão anterior.
Posteriormente, a função trata verificar a colisão do mouse com o botão, e pinta-o de acordo e também imprime a string passada na lista game_mode
no botão:
for button in button_set:
if button.collision(mouse) == True:
button_color = white
else:
button_color = yellow
# draws button text based on the game_mode list elements
button.draw_text(button_color)
button.draw_button(button_color, 2)
Feito isto, o programa verifica se algum botão é clicado. Caso seja, entrará no modo de jogo definido.
Caso o botão seja o 'Exit', o jogo irá encerrar. Caso seja passada na listagame_mode
uma string que não contenha, pelo menos, dois números ou a string 'exit' o jogo irá fechar e apresentar uma mensagem de erro.
Segue o exemplo caso seja passado na lista a string 'pudim':
------------------------------ E R R O R ------------------------------
File "main.py", line 126
'pudim' is not a valid game mode.
Try creating a game mode with the format '4 x 3'.
The game mode you add to the list MUST be a string.
------------------------------------------------------------------------
Esta verificação é feita da seguinte forma:
try:
in_game(gm_list[i][0], gm_list[i][1])
except IndexError:
<imprime o erro referido acima>
Não havendo nenhum erro, a função in_game()
é chamada, e passam-se como parâmetro os valores guardados na lista gm_list
que correspondem ao número de cartas na horizontal e vertical, respectivamente:
gm_list = [[4, 3], [4, 4], [5, 4], [6, 5], [6, 6]]
Este ficheiro contém a função que roda o modo de jogo selecionado:
in_game(cards_hor, cards_vert)
.
Inicialmente, esta função define o tamanho máximo do "tabuleiro" de cartas, tendo em conta o tamanho da janela de jogo:
board_width = display_width - display_width/6
board_height = display_height - display_height/6
Em seguida, é definida a dimensão das cartas, que tem me conta o tamanho do tabuleiro desta vez.
E para que a dimensão das cartas fosse estéticamente agradável, inicialmente adotou-se um método para estabelecer primeiro a largura da carta e depois a altura (em função da largura). Mas isto só funcionava para casos em que o número de cartas horizontais fosse maior que o número de cartar verticais. Quando o caso era inverso, as cartas podiam ultrapassar o limite do tabuleiro, em termos de largura.
Para contornar esse problema, adotou-se a seguinte medida:
if cards_vert > cards_hor:
card_width = (board_width/cards_vert) / 2.5
else:
card_width = (board_width/cards_hor) / 2.5
Isto verifica em que caso nos encontramos - mais cartas na vertical (ou igual número de cartas) ou na horizontal. E assim define a dimensão das cartas, de modo a não se ultrapassar as medidas máximas do tabuleiro.
Tendo estabelecido a largura das cartas, o programa então define a distância que deverá haver entre as cartas, considerando a sua largura:card_dist = card_width/15
.
Desta forma, garantimos que, mesmo não havendo uma distância fixa estabelecida entre as cartas, ela sempre será proporcional à largura das mesmas, o que depende do número de cartas no ecrã.
Procede-se da mesma forma para a altura das cartas:
card_height = card_width*1.5
.
As formas que aparecem nas cartas são passadas numa lista de strings:
shape_list = ['square', 'circle', 'triangle']
Mais à frente será explicado como estas strings influenciam realmente na forma atribuida à carta.
E as cores das cartas são passadas em outra lista.shape_color = [cyan, red, blue, green, orange, pink, yellow, blue_green]
Todas estas variáveis estão definidas no ficheiro colors.py.
Tendo listado as cores e formas possíveis, o programa cria uma lista de cartas que são possíveis criar combinando ambos os parâmetros:possible = []
for j in shape_color:
for i in shape_list:
possible.append((i,j))
A lista possible
passa a guardar as formas e cores como túpulos, seguindo o seguinte formato:
possible = [('square', [0, 255, 255]), ('circle', [0, 255, 255]), ('triangle', [0, 255, 255]]
Como próximo passo, o programa verifica, com base na quantidade de cartas possíveis, é possível criar número de cartas que se pretende no game mode escolhido.
Caso não seja possível, o jogo fecha e a seguinte mensagem de texto é impressa na linha de comandos:---------------------------------- E R R O R ----------------------------------
The game board has too many cards.
You may try either creating new colors/shapes or reducing the amount of cards.
---------------------------------------------------------------------------------
Não ocorrendo nenhum erro, o programa cria uma lista que contém pares de cartas, com base na lista de cartas possíveis:
for i in range (0, (cards_vert * cards_hor)//2):
game_deck.append(possible[i])
game_deck.append(possible[i])
E então baralha a ordem destas cartas:
random.shuffle(game_deck)
Feito isto, é criada então mais uma lista - card_list
-, que vai conter, desta vez, as cartas propriamente ditas, isto é, as instânciações da classe Card
.
try:
card.draw_flip(game_deck[num][0], game_deck[num][1])
except:
print("-"*30," E R R O R " + "-"*30 )
print(f'File "game_mode.py", line {line}\n')
print(f"'{cards_hor} x {cards_vert}' is not a valid game mode")
print("Try making a game mode that will generate a odd number of cards\n" + "-"*72)
exit()
Isto irá sair do jogo e imprimir para a linha de comandos a seguinte mensagem:
------------------------------ E R R O R ------------------------------
File "game_mode.py", line 146
'3 x 3' is not a valid game mode
Try making a game mode that will generate a odd number of cards
------------------------------------------------------------------------
Para entender como a lógica foi estruturada, há que atentar para as seguintes variáveis:
- flipped_cards_num
- flipped_cards_list
- clickable
- card.selected
- flipped_time
- flips
- card_color
- score
flipped_cards_num
trata-se do número de cartas que estão viradas com a face para cima.
flipped_cards_list
é uma lista que contém as cartas que foram viradas.
card.selected
é uma variável associada ao objeto card, que define se a carta encontra-se selecionada ou não
clickable
é uma variável booleana que define se a é possível clicar ou não nas cartas.
flipped_time
conta o tempo que se passou desde que duas cartas foram viradas.
flips
registra o número de vezes que o jogador tentou encontrar um par de cartas sem ter sucesso.
card_color
trata-se da cor da carta.
score
é a pontuação do jogador.
O primeiro passo para desenhar as cartas, é verificar se cada carta está ou não selecionada, através da variável selected
, da classe Card
.
- Caso esteja a colidir, a carta é pintada de branco, senão, deve ser pintada de verde;
- Verificar se é possível clicar nas cartas através da variável
clickable
: Se as carta forem "clickable", for feito um click sobre uma carta e o usuário levantar o botão do rato, o programa avançará para o passo seguinte:- Incrementa o número de cartas viradas em uma unidade:
flipped_cards_num += 1
- Adiciona a carta clicada à lista de cartas viradas:
flipped_cards_list.append(card)
- Passa a carta para "selected":
card.selected = True
- Toca o som da carta a virar:
flip_sound.play()
- Verifica quantas cartas já foram viradas e caso o número seja 2, incrementa o número de tentativas em 1 unidade e torna as cartas não clicáveis:
flips += 1
clickable = False
- Incrementa o número de cartas viradas em uma unidade:
Uma vez que a carta estiver "selected", o programa irá deixar de verificar a colisão afim de desenhar a parte de trás da carta como verde ou branco.
O que irá acontecer agora é desenhar a forma da carta (com a sua respectiva cor) e pintar o outline de branco, caso o rato esteja a colidir com a carta, ou da cor da forma caso não esteja. A partir do momento em que duas cartas estiverem selecionadas, a variávelfliped_card_num
irá assumir o valor 2, que é um gatilho para algumas ações ocorrerem:
- Nenhuma das cartas do tabuleiro será clicável;
- O número de tentativas é incrementado em 1 unidade;
- A variável
fliped_time
aumenta uma unidade a cada segundo;- Assim que esta variável atinge o valor 2 ou superior, a função
card_check()
é chamada, e verifica se as duas últimas cartas acrescentadas à lista de cartas viradas tem cor e forma correspondente.- Caso tenham o score aumenta em 100 unidades, o número de tentativas (
flips
) volta a ser 0 e as cartas são removidas da lista de cartas selecionadas. - Caso contrário, ambas as cartas selecionadas passam a estar "não selecionadas", e o jogador recebe uma penalização na pontuação tendo em conta o número de tentativas que realizou até o momento.
- Caso tenham o score aumenta em 100 unidades, o número de tentativas (
- Independentemente do retorno da função
card_check()
, todas as cartas do tabuleiro passam a ser "clicáveis" novamente:clickable = True
; o contador de tempo volta a zero:flipped_time = 0
e o número de cartas viradas também volta a zero:flipped_cards_num = 0
.
- Assim que esta variável atinge o valor 2 ou superior, a função
Inicialmente, a ideia era usar a função pygame.time.delay()
para fazer o jogo esperar o tempo desejado até virar as cartas para baixo novamente.
card_check()
faz o seu trabalho.
Assim que o jogador limpa todo o tabuleiro, o programa registra a pontuação obtida nesse jogo e compara com a pontuação mais alta até aquele momento. E, dependendo do resultado da comparação, é tocada uma música de vitória diferente.
Há uma música para quando o jogador ultrapassa a melhor pontuação até o momento:win_sound1
e outra para caso isso não aconteça win_sound2
.
Se o jogador desistir a meio do jogo, não é feito registro algum de score.
Este ficheiro contém as duas classes utilizadas ao longo do código: Card
e Button
.
Button
é uma variação da outra classe.
Porque não usar a mesma classe?
Inicialmente esta classe era uma só, mas mais tarde, pelo bem da legibilidade e da praticidade definiu-se uma classe apenas para os botões e outra apenas para as cartas.
Esta é mais uma medida em vista a facilitar a compreensão e manipulação do código no futuro, não só por mim, mas por qualquer pessoa.
Olhando agora para alguns dos métodos das classes, temos:
collision(self, mouse):
, que verifica a colisão do rato com a carta.
draw_card(self, color, stroke = 0)
, que desenha a carta com a cor e com a espessura de outline passados como parâmetro.
button(self, mouse)
, que verifica se a carta foi clicada.
draw_flip(self, shape, shape_color, stroke = 2)
, que é o método mais complexo desta classe Card
.
Esta função é invocada durante o jogo para desenhar a forma da carta, com a sua respectiva cor.
Para isto, basta:
- Passar a forma da carta em formato de string; daí usar-se uma lista com strings, como referido anteriormente.
shape_list = ['square', 'circle', 'triangle']
- Passar a cor da forma;
Posteriormente o programa irá reconhecer o formato e cor passados como parâmetro e desenhá-los quando a função for invocada.
Este ficheiro contém todas as funções do jogo, com exceção da game_menu()
e da in_game()
.
get_gm_list(game_mode)
.
O objetivo desta função é receber uma lista de strings e procurar por, pelo menos, dois números, em cada string e depois retornar a string recebida como uma lista de inteiros.
Exemplo:game_mode = ['4 x 3']
get_gm_list(game_mode) = [4, 3]
Para fazer isto, a função deve percorrer a string com dois loops:
for j in range (0, len(game_mode)):
for i in range (0, len(game_mode[j])):
O primeiro para percorrer a lista passada como parâmetro, e o segundo para percorrer cada uma das strings da lista.
Então verifica-se se o elemento da string que está a ser percorrido naquele momento é um número e adiciona-o a uma lista chamadanumber
:
if game_mode[j][i].isdigit():
number.append(str(game_mode[j][i]))
Caso um elemento da string não seja um digito, o função entrará na seguinte condição:
elif number:
temp_list.append(number)
number = []
Isto faz com que a lista number
seja guardada numa lista chamada temp_list
.
temp_list
vai guardando listas de strings que são digitos.
Então a lista number
volta a estar fazia, para poder guardar mais números.
Ao sair deste loop, entra-se em outro loop onde a lista de strings temp_list
vai dar origem a uma lista de inteiros: mode_list
.
Então, ao invés de se ter uma lista com o formato:
[['4'], ['3'], ['4'], ['4'], ['5'], ['4'], ['6'], ['5'], ['6'], ['6']]
Tem-se uma no formato:
[4, 3, 4, 4, 5, 4, 6, 5, 6, 6]
E como passo final, esta lista dá origem a uma outra lista, que irá conter todos esses inteiros agrupados de dois em dois. Passa-se então a ter uma lista no formato:
[[4, 3], [4, 4], [5, 4], [6, 5], [6, 6]]
E, por fim, esta lista sim pode ser usada passar os parâmetros da função in_game()
Esta função cria um ficheiro de texto e salva nele a pontuação passada por parâmetro quando a função é chamada.
Os ficheiros ficam salvos com o seguinte formato:A get_score()
é usada para abrir o ficheiro de texto que foi criado pela função save_score()
e encontrar nesse ficheiro a última pontuação registrada e a maior dessas pontuações.
get_gm_list()
e salva as pontuações lidas numa lista: score_list
.
A função retorna então o último valor dessa lista e o maior valor também:
return (score_list[-1], max(score_list))