🔢

PySimpleGUIでつくる数当てゲーム

2023/12/31に公開

数当てゲームをPythonで作ろう

今回は、数当てゲーム(Guess the Number)をPythonのPySimpleGUIを使って作ります。

PySimpleGUIは初めて取り扱いましたが、無事完成までもっていくことができたので学習記録として共有したいと思います。

私と同じような初学者の方のお役に立てば幸いです。

数当てゲーム(Guess the Number)の概要

数当てゲームは、その名の通り、ある正解の数を推測して当てるゲームです。

まず、回答者は正解と思われる数を推測します。その数が正解でない場合、正解の数よりも大きいか小さいかがヒントとして与えられます。

そのヒントをもとにして回答者は推測を繰り返していき、正解の数にたどり着いたらゲーム終了です。

今回は、正解の候補は1から100までの整数とします。以下は、正解が37である場合の数当てゲームの進行例です。

  1. 「67」と推測→「正解は67よりも小さいです」
  2. 「45」と推測→「正解は47よりも小さいです」
  3. 「30」と推測→「正解は30よりも大きいです」
  4. 「40」と推測→「正解は40よりも小さいです」
  5. 「36」と推測→「正解は38よりも大きいです」
  6. 「37」と推測→「正解です」

この場合、正解にたどり着くまでに計6回の推測が行われました。

このゲームでは、なるべく少ない試行回数で正解にたどり着くことが一つの目標になります。

ちなみにこの数当てゲームは二分探索になっているので、取りうる範囲の真ん中の数を選んでいくと、平均してかかる試行回数が最も少なくなります。
このやり方に従うと、先ほどの例では、

  1. 「50」と推測→「正解は50よりも小さいです」(正解の範囲は1~49)
  2. 「25」と推測→「正解は25よりも大きいです」(正解の範囲は26~49)
  3. 「37」と推測→「正解です」

といった推測を行うことになります。

二文探索法に関しては、以下の記事でわかりやすく解説されているので、ぜひご覧になってください。

https://qiita.com/Pro_ktmr/items/8946723fe08ba29a977c

NumPyによる乱数の生成

このゲームでは、正解となる数は毎回異なっていないとゲームとしての意味がありません。

pythonではNumPyモジュールを使って乱数を生成することができます。
1から100までの中からランダムに整数を1つ選んで表示させるコードは以下の通りです。

なお、pythonの場合、上限の数字(今回は101)はその範囲に含まれない点に注意してください。

import numpy as np
answer = np.random.randint(1, 101) # 1から100の中から1つ整数を選ぶ
print(answer) # 選ばれた整数を表示する

PySimpleGUIによるゲームの作成

今回は、PySimpleGUIというライブラリを使ってアプリを作っていきます。

PySimpleGUIの基本的な使い方に関しては、こちらの記事を参照させていただきました。

https://zenn.dev/torachi0401/articles/pysimplegui_articles

今回は、

  1. 開始画面
  2. ルール説明画面
  3. ゲーム画面
  4. 終了画面

の4つのウィンドウを作成していきます。

各ウィンドウのコードが独立して動作する形で順を追って説明し、最終的にそれらをまとめて画面遷移ができるようにしたものをお見せします。

開始画面の作成

まず、開始画面となるウィンドウ(start_window)を定義します。

sg.InputText()の部分にユーザー名が入力できるようになっています。

右上の×(ウィンドウを閉じる)、もしくは「OK」を押すとウィンドウが閉じられます。

この際、入力されたユーザー名は変数"name"に格納されます。

ここまでの流れをまとめると以下のようになります。

import PySimpleGUI as sg
import numpy as np

sg.theme("DarkBlue3")

def start_window():
    layout = [
        [sg.Text("Guess the Number")],
        [sg.Text("Enter your name:"), sg.InputText()],
        [sg.Button("OK")]
    ]
    window = sg.Window("Start Screen", layout, resizable=True)

    while True:
        event, values = window.read()
        if event == sg.WIN_CLOSED:
            break
        if event == "OK":
            name = values[0]
            window.close()

    window.close()

start_window()

ルール説明画面の作成

次に、ルール説明画面を作成します。上述した開始画面の構成が理解できればこちらも問題ないかと思います。

なお、ここでは変数nameを「てけ」としていますが、最終的にコードを合体させるときには変数nameがstart_windowから引き継がれるようにします。

またこの時点で、正解の数answerをランダムに選択しておきます。

name = "てけ"

def rule_window(name):
    layout = [
        [sg.Text(f"Hello, {name}! This is a number guessing game. The rule is as follows:")],
        [sg.Text("1. I randomly select an integer between 1 and 100.")],
        [sg.Text("2. You input a guess. If the guess is incorrect, I provide a hint, indicating whether the correct number is higher or lower.")],
        [sg.Text("3. You continue guessing based on the hints until the correct number is guessed.")],
        [sg.Text("4. The game concludes, and I show you the number of attempt(s)")],
        [sg.Button("Game Start")]
    ]
    window = sg.Window("Rule Explanation", layout, resizable=True)

    while True:
        event, values = window.read()
        if event == sg.WIN_CLOSED:
            break
        if event == "Game Start":
            answer = np.random.randint(1, 101)
            window.close()

        window.close()

rule_window(name)

ゲーム画面の作成

いよいよ、ゲーム画面となるウィンドウを作成します。

正解に関して、本来は前のウィンドウでランダムに生成したものを引き継ぎますが、ここでは実験のため「37」と固定しておきます。

推測する数字の入力には、矢印で数字の増減ができるSpin(スピン)を用いています。
ただし手入力も可能であり、デフォルトでは1から100までの整数以外の値も入れられてしまいます(「3000」、「20.5」、「あああ」など)。
そのため、int型に変換できない値が入力された場合には警告を出して試行ができないようにしてあります。

また変数attemptsに試行回数を記録し、最終的に表示できるようにしておきます。

name = "てけ"
answer = 37

def game_window(name, answer):
    warning = ""
    hint = ""
    attempts = 0
    layout = [
        [sg.Text(f"Enter a guess from 1 to 100:"), sg.Spin(values=list(range(1, 101)), initial_value=1, size=(3, 1))],
        [sg.Text(warning, key="-WARNING-")],
        [sg.Button("Submit")],
        [sg.Text(hint, key="-HINT-")]
    ]
    window = sg.Window("Game in Progress", layout)

    while True:
        event, values = window.read()
        if event == sg.WIN_CLOSED:
            break

        if event == "Submit":
            try:
                guess = int(values[0])
                if 1 <= guess <= 100:
                    if guess < answer:
                        hint = f"{guess} is lower than the answer.\n" + hint
                        warning = ""
                        attempts += 1
                    elif guess > answer:
                        hint = f"{guess} is higher than the answer.\n" + hint
                        warning = ""
                        attempts += 1
                    else:
                        attempts += 1
                        window.close()
                        end_window(name, answer, attempts)
                else:
                    warning = "Please enter a guess between 1 and 100"

            except ValueError:
                warning = "Your submission is not a valid integer"

        window["-HINT-"].update(hint)
        window["-WARNING-"].update(warning)

    window.close()

game_window(name, answer)

終了画面の作成

最後に、正解の数を当てたあとに表示する終了画面を作ります。
正解の数と試行回数を表示してゲーム終了です。

name = "てけ"
answer = 37
attempts = 6

def end_window(name, answer, attempts):
    layout = [
        [sg.Text(f"Congratulations, {name}!\n{answer} is the correct number!\nYou tried {attempts} times!")],
        [sg.Button("Quit")]
    ]
    window = sg.Window("Congratulations!", layout, resizable=True)

    while True:
        event, values = window.read()
        if event == sg.WIN_CLOSED:
            break
        if event == "Quit":
            window.close()

    window.close()

end_window(name, answer, attempts)

完成コード

最後に、ここまでPySimpleGUIを用いて作成したウィンドウ間で画面推移ができるようにします。

まとめたコードは以下の通りです。
前のウィンドウを閉じた直後に、変数を引き継ぐ形で次のウィンドウを開くようにしていけばOKです。

import PySimpleGUI as sg
import numpy as np

sg.theme("DarkBlue3")

def start_window():
    layout = [
        [sg.Text("Guess the Number")],
        [sg.Text("Enter your name:"), sg.InputText()],
        [sg.Button("OK")]
    ]
    window = sg.Window("Start Screen", layout, resizable=True)

    while True:
        event, values = window.read()
        if event == sg.WIN_CLOSED:
            break
        if event == "OK":
            name = values[0]
            window.close()
            rule_window(name)

    window.close()

def rule_window(name):
    layout = [
        [sg.Text(f"Hello, {name}! This is a number guessing game. The rule is as follows:")],
        [sg.Text("1. I randomly select an integer between 1 and 100.")],
        [sg.Text("2. You input a guess. If the guess is incorrect, I provide a hint, indicating whether the correct number is higher or lower.")],
        [sg.Text("3. You continue guessing based on the hints until the correct number is guessed.")],
        [sg.Text("4. The game concludes, and I show you the number of attempt(s)")],
        [sg.Button("Game Start")]
    ]
    window = sg.Window("Rule Explanation", layout, resizable=True)

    while True:
        event, values = window.read()
        if event == sg.WIN_CLOSED:
            break
        if event == "Game Start":
            answer = np.random.randint(1, 101)
            window.close()
            game_window(name, answer)

    window.close()

def game_window(name, answer):
    warning = ""
    hint = ""
    attempts = 0
    layout = [
        [sg.Text(f"Enter a guess from 1 to 100:"), sg.Spin(values=list(range(1, 101)), initial_value=1, size=(3, 1))],
        [sg.Text(warning, key="-WARNING-")],
        [sg.Button("Submit")],
        [sg.Text(hint, key="-HINT-")]  # ヒント表示用のTextを追加
    ]
    window = sg.Window("Game in Progress", layout)

    while True:
        event, values = window.read()
        if event == sg.WIN_CLOSED:
            break

        if event == "Submit":
            try:
                guess = int(values[0])
                if 1 <= guess <= 100:
                    if guess < answer:
                        hint = f"{guess} is lower than the answer.\n" + hint
                        warning = ""
                        attempts += 1
                    elif guess > answer:
                        hint = f"{guess} is higher than the answer.\n" + hint
                        warning = ""
                        attempts += 1
                    else:
                        attempts += 1
                        window.close()
                        end_window(name, answer, attempts)
                else:
                    warning = "Please enter a guess between 0 and 100"

            except ValueError:
                warning = "Your submission is not a valid integer"

        window["-HINT-"].update(hint)
        window["-WARNING-"].update(warning)

    window.close()

def end_window(name, answer, attempts):
    layout = [
        [sg.Text(f"Congratulations, {name}!\n{answer} is the correct number!\nYou tried {attempts} times!")],
        [sg.Button("Quit")]
    ]
    window = sg.Window("Congratulations!", layout, resizable=True)

    while True:
        event, values = window.read()
        if event == sg.WIN_CLOSED:
            break
        if event == "Quit":
            window.close()

    window.close()

start_window()

おわりに

PySimpleGUIを使うのは初めてでしたが、かなり操作がしやすく初学者向けであると感じました。

より高度なことができるよう、私も引き続き勉強していきたいと思います。

本記事をご覧いただきありがとうございました!

Discussion