8
\$\begingroup\$

EDIT (November 2024): I have uploaded the updated code to this github repo: https://github.com/umbe1987/memory if you want to have a look :)

ORIGINAL POST

So this is my very first time in this site. I have written this very basic Python code with PyQt5 to create a Memory Game. As I am not very familiar with OOP (but I'm trying to be!), I hope somebody could point me to where I could improve my game.

As it stands, the whole thing is working but it lacks of a pretty template (this is something I am going to improve in a second phase). What I am more interested in right now is to understand if my code could be better organized somehow (e.g. by splitting it into different classes/subclasses, instead of the many methods present).

The game works like this:

  1. the user chooses a folder of (JPG) images;
  2. a grid with all the images is formed, but each image is covered by the "back.jpg" image (it should stay in the same folder as the code)
  3. the game starts

Basically I am kind of doing everything using images in a Qgridlayout, which are put in buttons with event listeners. When an "onclick" signal is emitted, the self.status attribute is analyzed and the game is restarted or goes on according to that.

One last thing: the cards (images) are not shuffled right now, I still need to implement this feature (otherwise the game would be completely boring...).

import os, sys, glob, math, random
from PyQt5.QtWidgets import (QMainWindow, QWidget,
 QGridLayout, QPushButton, QApplication,
 QAction, QFileDialog, QMessageBox)
from PyQt5.QtGui import QPixmap, QIcon, QCloseEvent
from PyQt5.QtMultimedia import QSound
from PyQt5 import QtCore
app_dir = os.path.dirname(__file__)
back_card_img = 'back'
sound_folder = 'sound'
sound_success = 'success.wav'
sound_fail = 'fail.wav'
sound_end = 'end.wav'
image_folder = 'img'
class MemoryGame(QMainWindow):
 def __init__(self):
 super().__init__()
 self.back_card = os.path.join(app_dir, back_card_img) # remove the extension from the image name if present
 self.status = {}
 self.cell_width = 100
 self.cell_height = 150
 self.initUI()
 def initUI(self):
 self.statusBar()
 self.sounds = {
 'success': QSound(os.path.join(app_dir, sound_folder, sound_success)),
 'fail': QSound(os.path.join(app_dir, sound_folder, sound_fail)),
 'end': QSound(os.path.join(app_dir, sound_folder, sound_end))
 }
 openFile = QAction('Open', self)
 openFile.setShortcut('Ctrl+O')
 openFile.setStatusTip('Search image folder')
 openFile.triggered.connect(self.showDialog)
 menubar = self.menuBar()
 self.fileMenu = menubar.addMenu('&File')
 self.fileMenu.addAction(openFile)
 self.gridWidget = QWidget(self)
 self.gridLayout = QGridLayout(self.gridWidget)
 self.setCentralWidget(self.gridWidget)
 self.setGeometry(0, 0, 350, 300)
 self.setWindowTitle('Memory Game!')
 self.show()
 # self.showFullScreen()
 def showDialog(self):
 """Seacrh for a folder of images, and create two dictionaries:
 - one with grid_position : image
 - one with grid_position : back_card"""
 if self.status:
 self.status = {}
 folder = str(QFileDialog.getExistingDirectory(self,
 "Select Directory",
 os.path.join(app_dir, image_folder),
 QFileDialog.ShowDirsOnly))
 # if the user selected a folder
 if folder:
 self.list_images(folder)
 # if the selected foldser contains images, limit the number to 16
 if self.n_img > 0:
 if self.n_img > 16:
 del self.images[16:]
 self.fill_dict(self.images) # create the grid_position:image Dict
 self.empty_dict()
 self.init_grid()
 def list_images(self, folder):
 """List the (JPEG) images within the selected folder"""
 extensions = ('.jpg', '.jpeg', '.gif', '.png')
 images = [os.path.join(folder, f) for f in os.listdir(folder) if f.lower().endswith(extensions)]
 n_img = len(images)
 self.images = images
 self.n_img = n_img
 def fill_dict(self, images):
 grid_cell = images * 2
 random.shuffle(grid_cell)
 self.card_dict = {}
 n_cols = math.ceil(math.sqrt(len(grid_cell)))
 n_rows = math.ceil(math.sqrt(len(grid_cell)))
 max_rows = 4
 if n_rows > max_rows:
 n_cols += n_rows - max_rows
 n_rows = max_rows
 positions = [(i, j) for i in range(n_rows) for j in range(n_cols)]
 for p, cell in zip(positions, grid_cell):
 if cell == '':
 continue
 self.card_dict[p] = cell
 def empty_dict(self):
 # copy the card_dict to make a back_dict (with the back_card)
 self.back_dict = dict(self.card_dict)
 for k, v in self.back_dict.items():
 self.back_dict[k] = self.back_card
 def init_grid(self):
 """Initialize the grid according to the number of images
 found in the selected folder."""
 for pos, img in self.back_dict.items():
 btn = QPushButton(self)
 btn.clicked.connect(self.buttonClicked)
 pixmap = QPixmap(self.back_dict[pos])
 scaled = pixmap.scaled(self.cell_width, self.cell_height)
 btn.setIcon(QIcon(scaled))
 btn.setIconSize(scaled.rect().size())
 btn.setFixedSize(scaled.rect().size())
 del (pixmap)
 self.gridLayout.addWidget(btn, *pos)
 def restart(self):
 self.status = {}
 self.init_grid()
 return
 def turn_card(self, btn, location):
 """When a button (image) is clicked, turn the card."""
 pixmap = QPixmap(self.card_dict[location])
 scaled = pixmap.scaled(self.cell_width, self.cell_height)
 btn.setIcon(QIcon(scaled))
 btn.setIconSize(scaled.rect().size())
 btn.setFixedSize(scaled.rect().size())
 del (pixmap)
 n_cards = len(self.status.keys())
 # if game just started, turn the card
 if n_cards == 0:
 self.status[location] = self.card_dict[location]
 return
 # if the number of card is odd
 elif n_cards % 2 != 0:
 # if card already present, keep it...
 if self.card_dict[location] in self.status.values() and location not in self.status.keys():
 self.status[location] = self.card_dict[location]
 if self.status == self.card_dict:
 self.end_game()
 return
 self.sounds['success'].play()
 return
 # if the same card is clicked, do nothing...
 elif location in self.status.keys():
 pass
 # ...otherwise restart the grid
 else:
 ''' wait 1 sec and restart (second argument MUST be a method;
 cannot use time.sleep, because GUI freezes)'''
 QtCore.QTimer.singleShot(1000, self.restart)
 self.sounds['fail'].play()
 return
 self.status[location] = self.card_dict[location]
 def buttonClicked(self):
 button = self.sender()
 idx = self.gridLayout.indexOf(button)
 location = self.gridLayout.getItemPosition(idx)
 self.turn_card(button, location[:2])
 def end_game(self):
 self.sounds['end'].play()
 msgBox = QMessageBox()
 msgBox.setIcon(QMessageBox.Question)
 msgBox.setWindowTitle("!!!You won!!!")
 msgBox.setInformativeText("What's next now?")
 quit = msgBox.addButton('quit', QMessageBox.RejectRole)
 restartBtn = msgBox.addButton('play again', QMessageBox.ActionRole)
 changeBtn = msgBox.addButton('change cards', QMessageBox.ActionRole)
 msgBox.setDefaultButton(restartBtn)
 msgBox.exec_()
 msgBox.deleteLater()
 if msgBox.clickedButton() == quit:
 self.close()
 elif msgBox.clickedButton() == restartBtn:
 self.restart()
 elif msgBox.clickedButton() == changeBtn:
 self.showDialog()
 return
 def closeEvent(self, event):
 reply = QMessageBox.question(self, 'Message',
 "Are you sure to quit?", QMessageBox.Yes |
 QMessageBox.No, QMessageBox.No)
 if reply == QMessageBox.Yes:
 event.accept()
 else:
 event.ignore()
if __name__ == '__main__':
 app = QApplication(sys.argv)
 ex = MemoryGame()
 sys.exit(app.exec_())
asked Oct 8, 2016 at 23:33
\$\endgroup\$
6
  • \$\begingroup\$ On what operating system did you test this code? The only thing I get is a grey box, no dialogs. \$\endgroup\$ Commented May 5, 2017 at 22:11
  • \$\begingroup\$ On ubuntu os. Anyway, as it stands, it is supposed to open as a grey window with a menu. You can select a folder containing images (this version of the code only accepts ".jpg" extension, and it jas to be exactly all lowercase!), and the game starts. You also are supposed to have am image in the folder of the script which has to be named "back.jpg", and which represents the card when its not turned yet. \$\endgroup\$ Commented May 6, 2017 at 10:50
  • \$\begingroup\$ Anyway, the code has been changed since October 2016 (when I posted the question). If I have time I will update my question inserting the latest version. Meanehile, any comment is still appreciated! Cheers! \$\endgroup\$ Commented May 6, 2017 at 10:52
  • \$\begingroup\$ When I run it (Ubuntu 16.04), there's definitely a grey window. However, there is no menu. Since this question hasn't been answered yet, feel free to simply update the code in the question with the current version (assuming the current version works to the best of your knowledge). \$\endgroup\$ Commented May 6, 2017 at 17:35
  • \$\begingroup\$ It should be a grey window with only "File" menu above it. Otherwise, try to press CTRL + O to open the folder explorere and select a folder with images (jpg only supported so far). It should work. Also, I did not have the time to update the code, I will try to do that when I can. \$\endgroup\$ Commented May 25, 2017 at 14:44

1 Answer 1

3
\$\begingroup\$

Some no very important improvements:

 for k, v in self.back_dict.items():
 self.back_dict[k] = self.back_card

may become

 for k in self.back_dict:
 self.back_dict[k] = self.back_card

as you don't use v (note fully omitting any method for self.back_dict, too).


 for p, cell in zip(positions, grid_cell):
 if cell == '':
 continue
 self.card_dict[p] = cell

may be shortened:

 for p, cell in zip(positions, grid_cell):
 if not cell:
 self.card_dict[p] = cell

Note omitting == 0 as value 0 itself is evaluated as True.


app_dir = os.path.dirname(__file__)
back_card_img = 'back'
sound_folder = 'sound'
sound_success = 'success.wav'
sound_fail = 'fail.wav'
sound_end = 'end.wav'
image_folder = 'img'

Those are all constants, so by PEP 8 Style Guide their names should be written in all capital letters:

APP_DIR = os.path.dirname(__file__)
BACK_CARD_IMG = 'back'
SOUND_FOLDER = 'sound'
SOUND_SUCCESS = 'success.wav'
SOUND_FAIL = 'fail.wav'
SOUND_END = 'end.wav'
IMAGE_FOLDER = 'img'
answered Aug 16, 2017 at 19:39
\$\endgroup\$
1
  • \$\begingroup\$ for p, cell in zip(positions, grid_cell):if not cell:self.card_dict[p] = cell does not seem to work. Other suggestions are greta, thanks! \$\endgroup\$ Commented Sep 6, 2017 at 15:28

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.