【Python】Tkinterを使ってマインスイーパー風ゲームの作成

## はじめに
Python初学者のQiita初投稿になります。
拙いコードで見やすいとは言い難いですが、温かい目で見守ってくださると幸いです。
## 動作環境
Windows 11 Enterprise
Pythonのバージョン: 3.10.11
使用IDE：[Thonny](https://thonny.org/)
## コード
早速載せていきます。**ファイルと参考資料はコードより下に添付してあります**
~~~python
import tkinter as tk
import random
import time
import winsound

win_flg = 0
hata_delete_flg = 0
reset_button_flg = 1
hatabtn_cnt = 0
unchis_positions = set()
unchi_sum = 12
buttons = {}
labels = {}
btn_hata = None
hatabuttons = {}
gameover_flg = 0
opened_positions = hatabuttons_set = unchis_positions_set = non_unchi_positions = set()
cvs = None
btn_hata_mode = None
elapsed_time = 0
running = 0
best_time = 0
win_streak = 0
best_time_label = None
game_cnt = 0
win_streak_label = None
flag_rest = 12
flag_rest_label = None
score = 0
score_label = None
best_score = 0
best_score_label = None
value_score = 0
reset_button = None
hatabuttons_cnt = 0

# マスをクリックしたときの関数
def masu_click(y, x):
    global hatabtn_cnt, gameover_flg, flag_rest_label, flag_rest, hatabuttons_cnt
    
    if gameover_flg == 0 and elapsed_time == 0:
        start_stopwatch()
    if hatabtn_cnt == 0:
        if (x, y) in unchis_positions:
            unchi_label = tk.Label(root, image=i_unchi)
            unchi_label.grid(row=y, column=x)
            labels[(x, y)] = unchi_label
            buttons[(x, y)].config(state=tk.DISABLED)
            buttons[(x, y)].grid_forget()
            # 非同期処理
            winsound.PlaySound("bakuha.wav", winsound.SND_FILENAME | winsound.SND_ASYNC)
            if gameover_flg == 0:
                game_over()
        else:
            if gameover_flg == 0:
                open_rensa_0(y, x)
    elif hatabtn_cnt == 1:
        if flag_rest > 0 and win_flg == 0:
            hata_button = tk.Button(root, image=i_hata, command=lambda:break_hatabutton(x, y))
            hata_button.grid(row=y, column=x)
            hatabuttons[(x, y)] = hata_button
            flag_rest -= 1
            flag_rest_label.config(text=f"FLAG : {flag_rest}")
            hatabuttons_cnt += 1
    # クリア判定
    completed_judg()
# 旗ボタンが押されたときに旗をひっこめる関数
def break_hatabutton(x, y):
    global flag_rest_label, flag_rest
    if (x, y) in hatabuttons:
        hatabuttons[(x, y)].grid_forget()
        del hatabuttons[(x, y)]
        flag_rest += 1
        flag_rest_label.config(text=f"FLAG : {flag_rest}")
        
# うんちのマス以外を開けた時に周りのうんちの数を表示して、0のときは周りの8マス開けることを繰り返す関数
def open_rensa_0(y, x):
    global opened_positions
    unchis_cnt = 0
    
    #周囲のうんちを数える処理
    for dy in [-1, 0, 1]:
        for dx in [-1, 0, 1]:
            if dx == 0 and dy == 0:
                continue
            nx, ny = x + dx, y + dy
            if 0 <= nx < 10 and 0 <= ny < 10:
                if (nx, ny) in unchis_positions:
                    unchis_cnt += 1
    if unchis_cnt == 0:
        suuji_label = tk.Label(root, image=i_0)
        suuji_label.grid(row=y, column=x)
        labels[(x, y)] = suuji_label
        buttons[(x, y)].config(state=tk.DISABLED)
        buttons[(x, y)].grid_forget()
        opened_positions.add((x, y))

        # 周囲のマスを再帰的に開く処理
        for dy in [-1, 0, 1]:
            for dx in [-1, 0, 1]:
                if dx == 0 and dy == 0:
                    continue
                nx, ny = x + dx, y + dy
                # 0 <= nx < 10 and 0 <= ny < 10でマスの場外にnxとnyがないかの確認
                if 0 <= nx < 10 and 0 <= ny < 10 and (nx, ny) not in labels:
                    # 関数を再帰的に呼び出す
                    open_rensa_0(ny, nx)
    else:
        suuji_label = tk.Label(root, image=[i_0, i_1, i_2, i_3, i_4, i_5, i_6, i_7, i_8][unchis_cnt])
        suuji_label.grid(row=y, column=x)
        labels[(x, y)] = suuji_label
        buttons[(x, y)].config(state=tk.DISABLED)
        buttons[(x, y)].grid_forget()
        if gameover_flg == 0:
            opened_positions.add((x, y))
# マスとリセットボタンと旗を立てる状態にするボタンを作成する関数
def masu_create():
    global btn_hata, btn_hata_mode, stopwatch_label, start_button, reset_button, best_time_label, game_cnt, win_streak, win_streak_label, flag_rest, flag_rest_label, score, score_label, best_score, best_score_label
    
    # マスを作成する部分
    for y in range(10):
        for x in range(10):
            btn = tk.Button(root, width=5, height=2, relief=tk.RIDGE, command=lambda y=y, x=x: masu_click(y, x))
            btn.grid(row=y, column=x)
            buttons[(x, y)] = btn

    #旗に関するものを作成する部分
    btn_hata = tk.Button(root, image=i_1, width=41, height=36, command=hata_set)
    btn_hata.place(x=500, y=25)
    btn_hata_mode = tk.Label(root, text="MODE:OPEN", bg="gray")
    btn_hata_mode.place(x=485, y=70)
    
    # ゲームのオプション的要素を作成する部分
    stopwatch_label = tk.Label(root, text="TIME: 0 s", bg="gray", font=("Helvetica", 14))
    stopwatch_label.place(x=475, y=150)
    best_time_label = tk.Label(root, text=f"BEST TIME: {best_time} s", bg="gray", font=("Helvetica", 10))
    best_time_label.place(x=475, y=200)
    win_streak_label = tk.Label(root, text=f"WIN STREAK: {win_streak}", bg="gray", font=("Helvetica", 10))
    win_streak_label.place(x=475, y=230)
    flag_rest_label = tk.Label(root, text=f"FLAG : {flag_rest}", bg="gray", font=("Helvetica", 10))
    flag_rest_label.place(x=475, y=270)
    score_label = tk.Label(root, text=f"SCORE : {score}", bg="gray", font=("HElvetica", 10))
    score_label.place(x=475, y=300)
    best_score_label = tk.Label(root, text=f"BEST SCORE : {best_score}", bg="gray", font=("HElvetica", 10))
    best_score_label.place(x=475, y=320)
    
    # うんちの位置をランダムに設定
    while len(unchis_positions) < unchi_sum:
        unchi_x = random.randint(0, 9)
        unchi_y = random.randint(0, 9)
        if (unchi_x, unchi_y) not in unchis_positions:
            unchis_positions.add((unchi_x, unchi_y))
    reset_button = tk.Button(root, text="RESET", command=reset_game)
    reset_button.place(x=500, y=100)

# 旗を出す状態とマスを開ける状態の切り替えを行う関数
def hata_set():
    global hatabtn_cnt, btn_hata, btn_hata_mode
    hatabtn_cnt += 1
    if hatabtn_cnt == 1:
        btn_hata.config(image=i_hata)
        btn_hata_mode["text"] = "MODE:FLAG"
        for (hx, hy) in hatabuttons.keys():
            hatabuttons[(hx, hy)].config(state=tk.NORMAL)
    elif hatabtn_cnt == 2:
        hatabtn_cnt = 0
        btn_hata.config(image=i_1)
        btn_hata_mode["text"] = "MODE:OPEN"
        for (hx, hy) in hatabuttons.keys():
            hatabuttons[(hx, hy)].config(state=tk.DISABLED)

# ストップウォッチで秒数を数えて表示させる関数
def update_stopwatch():
    global elapsed_time
    if running == 1:
        elapsed_time += 1
        if elapsed_time > 999:
            elapsed_time = 999
            stopwatch_label.config(text=f"TIME: {elapsed_time} +s")
        else:
            stopwatch_label.config(text=f"TIME: {elapsed_time} s")
        root.after(1000, update_stopwatch)

# ストップウォッチを開始する関数
def start_stopwatch():
    global running
    running = 1
    update_stopwatch()

def stop_stopwatch():
    global running
    running = 0

# うんちを開けた時に実行される関数
def game_over():
    global gameover_flg, win_streak, win_streak_label
    gameover_flg = 1
    stop_stopwatch()
    time.sleep(1)
    
    for (hx, hy) in list(hatabuttons.keys()):
        hatabuttons[(hx, hy)].config(state=tk.NORMAL)
        break_hatabutton(hx, hy)
        
    # 全てのマスを開ける
    for y in range(10):
        for x in range(10):
            if (x, y) in unchis_positions:
                unchi_label = tk.Label(root, image=i_unchi)
                unchi_label.grid(row=y, column=x)
                labels[(x, y)] = unchi_label
            else:
                open_rensa_0(y, x)
    win_streak = 0
    win_streak_label.config(text=f"WIN STREAK: {win_streak}")
    
# クリアの状態かどうか判定する関数
def completed_judg():
    hatabuttons_set = set(hatabuttons.keys())
    unchis_positions_set = set(unchis_positions)
    
    # 全てのマスを計算
    all_positions = set((x, y) for x in range(10) for y in range(10))
    non_unchi_positions = all_positions - unchis_positions
    if hatabuttons_set == unchis_positions_set or opened_positions == non_unchi_positions:
        stop_stopwatch()
        all_hata = len(hatabuttons_set)
        if all_hata > 0:
            hata_delete_flg = 1
        else:
            hata_delete_flg = 0
        completed_run()

# クリアの処理を行う関数
def completed_run():
    global win_flg, cvs, best_time, elapsed_time, best_time_label, win_streak, win_streak_label, score, best_score, elapsed_time, reset_button_flg, reset_button, hatabuttons_cnt
    if win_flg == 0:
        reset_button_flg = 0
        if reset_button_flg == 0:
            reset_button.config(state=tk.DISABLED)
        cvs = tk.Canvas(root, width=i_win.width()+10, height=i_win.height()+10)
        cvs.place(x=42, y=165)
        cvs.create_image(183, 25, image=i_win)
        win_flg = 1
        if best_time == 0 or elapsed_time < best_time:
            best_time_label.place_forget()
            best_time = elapsed_time
            best_time_label = tk.Label(root, text=f"BEST TIME: {best_time} s", bg="gray", font=("Helvetica", 10))
            best_time_label.place(x=475, y=200)
        win_streak += 1
        win_streak_label.config(text=f"WIN STREAK: {win_streak}")
        
        #スコアの計算
        if hatabuttons_cnt == 0:
            score += 1000
        if elapsed_time <= 60:
            score += 1500
        elif elapsed_time > 60:
            score += 750
        if win_streak == 1:
            score += 100
        elif win_streak == 2:
            score += 200
        # 連勝数が3なら
        elif win_streak == 3:
            score += 300
        elif win_streak == 4:
            score += 400
        elif win_streak >= 5:
            score += 600
        if best_time <= 35:
            score += 300
        else:
            score += 150
        score_output()

# スコア表示のための関数
def score_output():
    global best_score_label, score_label, value_score, score, best_score, win_flg, reset_button_flg
    
    if win_flg == 1:
        if value_score <= score:
            score_label.config(text=f"SCORE : {value_score}")
        else:
            if score > best_score:
                best_score = score
                best_score_label.config(text=f"BEST SCORE : {best_score}")
            reset_button_joutai()
        if value_score <= score:
             value_score += 10
        if reset_button_flg == 0:
            root.after(10, score_output)

# リセットボタンの状態を変えるためのフラグ
def reset_button_joutai():
    global reset_button_flg, reset_button
    
    reset_button_flg = 1
    if reset_button_flg == 1:
        reset_button.config(state=tk.NORMAL)

# リセットボタンが押されたときに実行される関数
def reset_game():
    global gameover_flg, unchis_positions, hatabtn_cnt, buttons, labels, hatabuttons, hatabuttons_set, unchis_positions_set, opened_positions, non_unchi_positions, hata_delete_flg, cvs, win_flg, btn_hata_mode, elapsed_time, running, best_time_label, win_streak, win_streak_label, flag_rest, flag_rest_label, score, score_label,  hatabuttons_cnt, value_score, game_cnt
    
    # 全ての旗を消す(なぜか表示では消えてない)
    if hata_delete_flg == 1:
        for (hx, hy) in list(hatabuttons.keys()):
            break_hatabutton(hx, hy)
    if win_flg == 1:
        cvs.place_forget()
        win_flg = 0
    else:
        win_streak = 0
    win_streak_label.place_forget()
    game_cnt += 1
    
    # ゲームの状態をリセット
    win_flg = 0
    gameover_flg = 0
    hatabtn_cnt = 0
    elapsed_time = 0
    running = 0
    score = 0
    hatabuttons_cnt = 0
    value_score = 0
    flag_rest = 12
    unchis_positions.clear()
    hatabuttons_set.clear()
    unchis_positions_set.clear()
    opened_positions.clear()
    non_unchi_positions.clear()
    stopwatch_label.place_forget()
    best_time_label.place_forget()
    flag_rest_label.place_forget()
    score_label.place_forget()
    
    # すべてのマスのボタンをリセット
    for (x, y) in list(buttons.keys()):
        buttons[(x, y)].grid_forget()
    buttons.clear()

    # 開いたうんちの位置を保存したものをリセット
    for label in list(labels.values()):
        label.grid_forget()
    labels.clear()

    # 旗ボタンの位置を保存したものの中身を全て消す
    hatabuttons.clear()

    # 新しく始めるためにマスを改めて作成する
    masu_create()

root = tk.Tk()
root.title("UnchiSweeper")
root.geometry("700x415")
root.configure(bg="gray")

# 画像の読み込み
i_0 = tk.PhotoImage(file="0_haiiro.png")
i_1 = tk.PhotoImage(file="1_blue.png")
i_2 = tk.PhotoImage(file="2_green.png")
i_3 = tk.PhotoImage(file="3_red.png")
i_4 = tk.PhotoImage(file="4_darkblue.png")
i_5 = tk.PhotoImage(file="5_murasaki.png")
i_6 = tk.PhotoImage(file="6_purple.png")
i_7 = tk.PhotoImage(file="7_tyairo.png")
i_8 = tk.PhotoImage(file="8_pink.png")
i_hata = tk.PhotoImage(file="hata.png")
i_unchi = tk.PhotoImage(file="unchi.png")
i_win = tk.PhotoImage(file="gameclear.png")

masu_create()
root.mainloop()
~~~

##### 実行画面
---
![スクリーンショット 2024-12-23 235020.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3821863/89451bb2-070e-3b07-433c-06a03d349191.png)
---
## 画像と音声ファイル

|i_0|i_1|i_2|i_3|i_4|i_5|
|:--:|:--:|:--:|:--:|:--:|:--:|
|![0_haiiro.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3821863/5331583f-8df6-89a1-cf43-e47706e49c65.png)|![1_blue.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3821863/e0b35516-296a-6fce-8499-09984943f10e.png)|![2_green.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3821863/8b4e14d4-c910-e117-e17e-887c364f63fb.png)|![3_red.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3821863/c227dd8f-e378-aa4f-5737-9b11b928e7c9.png)|![4_darkblue.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3821863/9fec5d87-78de-e9c6-1d08-7c8172058f65.png)|![5_murasaki.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3821863/d47b74cd-5b93-ce86-69cf-0dd9a3c53429.png)
|i_6|i_7|i_8|i_hata|i_unchi|
|![6_purple.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3821863/c3911cf8-0ec6-225d-f082-6909396628db.png)|![7_tyairo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3821863/23cf16ab-e881-7d10-a6f9-61c26575729a.png)|![8_pink.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3821863/54244811-dc04-32eb-301e-d90e8e00b339.png)|![hata.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3821863/122198db-5701-d320-87c2-f072f3dd317d.png)|![unchi.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3821863/7224aa86-8b7d-2fe0-aee8-99167e571c81.png)|![gameclear.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3821863/26397e73-97f4-486f-80f7-529ea43e7c71.png)

###### 音声ファイル
[bakuha.wav](https://otologic.jp/free/se/explosion01.html)
私が使用したものはこちらのリンクの爆発01をwav変換したものですが、パスさえあっていれば大丈夫ですので、お好みの音声ファイル(wav形式)をお使いください。

## 参考資料
https://www.delftstack.com/ja/howto/python-tkinter/how-to-hide-recover-and-delete-tkinter-widgets/
***
https://office54.net/python/tkinter/python-tkinter-button
***
https://office54.net/python/tkinter/after-method-schedule
***

## おわりに
今回はマインスイーパー風のゲームをTkinterで作ってみました。
この記事で1人でもTkinterに触れていただく機会となりましたら幸いです。

classなどを使ってみたいという気もありますが、未だに私のclassについての理解ができていないので今回の使用は控えさせていただきました。ご了承ください。
またTkinterを使用したGUIを作りたいと思っておりますので完成し、気が向き次第投稿します。
最後までお読みいただきありがとうございました。