PyQt5 を使ってメモリーゲーム(神経衰弱)を作りました

はじめに

前回PyQt5のLayoutについて学習しました。非常に便利なのでそれを使って何かしたいというのが今回の動機です。
ラベルやボタンを勝手に整列して並べてくれるのでカードゲームにはうってつけです。ということでメモリーゲーム(神経衰弱)を作りました。

f:id:touch-sp:20210306173844p:plain

以前にもVisual StudioC#で作ったことがあります。手動でPictureBoxを並べたのが懐かしいです。
touch-sp.hatenablog.com

Pythonスクリプト

import random
import vlc
import winsound
import glob

import sys
from PyQt5.QtCore import Qt, QSize, QTimer, QEventLoop
from PyQt5.QtWidgets import *
from PyQt5.QtGui import QPixmap, QIcon, QImage, QFont

##########
tate = 2
yoko = 4
##########

total_card = tate * yoko

plyaer_none = vlc.MediaListPlayer()
player_ok = vlc.MediaListPlayer()
player_finish = vlc.MediaListPlayer()

mediaList0 = vlc.MediaList(['./pic/dummy.wav'])
mediaList1 = vlc.MediaList(['./pic/ok.wav'])
mediaList2 = vlc.MediaList(['./pic/yattane.wav'])

plyaer_none.set_media_list(mediaList0)
player_ok.set_media_list(mediaList1)
player_finish.set_media_list(mediaList2)

img_path_list = glob.glob('./pic/*.jpg')

class Window(QWidget):

    def __init__(self):
        super().__init__()
        self.initUI()
        self.image_id = []
        self.now_playing = False
        self.input_ready = False
        self.button_enable = []
        self.first_select = -1
        self.second_select = -1

    def initUI(self):

        self.img = [QImage(x) for x in img_path_list]

        ### main ###
        main = QFrame()
        main.setFrameStyle(QFrame.Box | QFrame.Plain)
        
        card_layout = QGridLayout()
        
        self.card_label =[QPushButton() for i in range(total_card)]
        
        for i in range(total_card):
            self.card_label[i].setObjectName(str(i))
            self.card_label[i].setFlat(True)
            self.card_label[i].setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
            self.card_label[i].clicked.connect(self.buttonClicked)  
            card_layout.addWidget(self.card_label[i], i // yoko, i % yoko)
        main.setLayout(card_layout)
        ### main ###
        
        ### footer ###
        footer = QFrame()

        button_layout = QHBoxLayout()

        self.start_button = QPushButton('start')
        self.start_button.setFont(QFont("Times", 24, QFont.Bold))
        self.start_button.clicked.connect(self.game_start)
        self.start_button.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
        
        self.exit_button = QPushButton('exit')
        self.exit_button.setFont(QFont("Times", 24, QFont.Bold))
        self.exit_button.clicked.connect(self.exit)
        self.exit_button.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)

        button_layout.addWidget(QLabel())
        button_layout.addWidget(self.start_button)
        button_layout.addWidget(self.exit_button)
        button_layout.addWidget(QLabel())

        footer.setLayout(button_layout)
        ### footer ###

        vbox = QVBoxLayout()
        vbox.addWidget(main, 6)
        vbox.addWidget(footer, 1)

        self.setLayout(vbox)

        self.show()

    def exit(self):
        self.close()

    def game_start(self):

        if self.now_playing == True:
            return None

        plyaer_none.play()

        self.now_playing = True

        self.button_enable = [True] * total_card
        self.first_select = -1
        self.second_select = -1
        
        self.image_id = random.sample(range(1, len(img_path_list)), int(total_card/2)) * 2
        random.shuffle(self.image_id)

        for i in range(total_card):
            self.set_image_into_button(self.img[0], self.card_label[i])
        
        self.input_ready = True

    def buttonClicked(self):

        if self.input_ready == False:
            return None
        
        sender = self.sender()
        your_select = int(sender.objectName())
        
        if self.button_enable[your_select] == False:
            return None
        
        self.input_ready = False

        if self.first_select == -1:
            self.first_select = your_select
            self.button_enable[self.first_select] = False
            self.set_image_into_button(self.img[self.image_id[self.first_select]], self.card_label[self.first_select])
        else:
            self.second_select = your_select
            self.set_image_into_button(self.img[self.image_id[self.second_select]], self.card_label[self.second_select])

        if self.second_select != -1:
            self.judge()
        
        self.input_ready = True

    def judge(self):
        if self.image_id[self.first_select] == self.image_id[self.second_select]:
            self.button_enable[self.second_select] = False
            player_ok.play()
            if sum(self.button_enable) == 0:
                self.game_finish()
        else:
            winsound.MessageBeep()

            ### wait for 800msec ###
            loop = QEventLoop()
            QTimer.singleShot(800, loop.quit)
            loop.exec_()

            self.button_enable[self.first_select] = True
            self.button_enable[self.second_select] = True
            self.set_image_into_button(self.img[0], self.card_label[self.first_select])
            self.set_image_into_button(self.img[0], self.card_label[self.second_select])

        self.first_select = -1
        self.second_select = -1

    def game_finish(self):
        
        player_finish.play()
        self.now_playing = False

    def set_image_into_button(self, image, button):
        w = button.width()
        h = button.height()
        img = QPixmap.fromImage(image.scaled(w-10, h-10, Qt.KeepAspectRatio, Qt.SmoothTransformation))
        button.setIcon(QIcon(img))
        button.setIconSize(QSize(img.size()))

app = QApplication(sys.argv)
ex =Window()

ex.showFullScreen()
sys.exit(app.exec_())

カードの枚数はスクリプトの10行目あたりで指定しています。総枚数が偶数であればうまくいきます。
ただし総枚数の半分以上の画像を用意する必要があります。

動作環境

音を出すためにVLCメディアプレイヤーを使用しました。あらかじめインストールして下さい。
GitHubには音がないバージョン「memory_game_no_sound.py」もあります。その場合にはVLCメディアプレイヤーは不要です。

WIndows 10
Python 3.8.7
VLCメディアプレイヤー 3.0.12

PyQt5==5.15.3
PyQt5-Qt==5.15.2
PyQt5-sip==12.8.1
python-vlc==3.0.11115

その他

スクリプトと音源とサンプル画像をGitHubに公開しています。
音がないバージョン「memory_game_no_sound.py」もあります。その場合にはVLCメディアプレイヤーは不要です。
実際は子供のためにアニメ画像を使用したのですが著作権の問題で公開できません。シンプルな数字の画像になっています。自分好みの画像に変更して使用して下さい。すべての画像サイズ(厳密には縦横比)を同じにしておいた方が良いです。そうでないとエラーは出ませんが表示がおかしくなります。
子供の教育にそして大人の頭の体操にぜひ。
github.com