Python の勉強 スロットゲーム編 〜その18:強制終了(exit)〜

プログラミング

強制終了を組み込んでスロットゲームの修正は、おしまいにしたいと思います。
入力検証(バリデーション)を導入する前は、掛コインに数字以外の文字を入力するとエラーで終了していましたが、ここではシステム的な慣例に従った終了の仕方で対応いたします。

入力検証(バリデーション)用の関数が検証結果を True / False で返却するように、アプリケーションは終了する際に “終了ステータス” という整数値を結果として返却(出力)するようになっていることが多いです。

整数を指定した場合、シェル等は 0 は “正常終了”、0 以外の整数を “異常終了” として扱います。

sys.exit([arg]) – システムパラメータと関数

掛けコイン数の入力で「quit」or「exit」を入力した場合、終了するように修正

今のプログラムでは所持コイン数が 0 枚にならないとゲームが終了しません。「quit」または「exit」を入力された際、ゲームが終了するように修正しましょう。
単に if 構文を挿入するだけでもいいのですが、それだとあまりに不親切なので「quit」「exit」でゲームを終了できることを明示してあげましょう。必要なのは以下のプログラムです。プレーヤが意図して終了するので終了ステータスを「0」にします。

import sys

str_bets = input('掛けコイン数を入力して下さい。※「quit」「exit」でゲームを終了します\n掛けコイン数: ')
if str_bets.lower() in ('quit', 'exit'):
    print('ゲームを終了します。')
    sys.exit(0)

ここで「.lower()」という関数を使用しました。この関数は対象の文字列(str_bets)を全て小文字に変換された文字列を取得できいます。つまり「Quit」や「EXIT」の様に大文字が混ざっていてもゲームが終了するようにした、ということになります。

テストにも反映する

折角なので pytest を使った場合のテスト方法も確認しておきます。「tests/my_slotgame/test_game17.py」をコピーして、同フォルダに「test_game18.py」を作成します。
sys.exit()SystemExit エラーを発生させるため、それを補足するように以下を追記します。ここでは、補足したエラーから終了ステータスを抽出できるように「as e」としています。
※変数 e には補足したエラーが格納される

def test_ask_bets(mock_stdout):
    # 強制終了のテスト
    with unittest.mock.patch('builtins.input', return_value="quit"):  # input() を 'quit' を返却するものに置き換え
        with pytest.raises(SystemExit) as e:
            game018.ask_bets(100)
        assert e.type == SystemExit
        assert e.value.code == 0
    # 強制終了のテスト
    with unittest.mock.patch('builtins.input', return_value="EXIT"):  # input() を 'EXIT' を返却するものに置き換え
        with pytest.raises(SystemExit) as e:
            game018.ask_bets(100)
        assert e.type == SystemExit
        assert e.value.code == 0
    # 通常のゲームのテスト
    with unittest.mock.patch('builtins.input', return_value="100"):  # input() を 'quit' を返却するものに置き換え
        assert game018.ask_bets(100) == 100

最終的にプログラム、テスト用プログラムはそれぞれ以下になります。

プログラム「my_slotgame/game18.py」

"""スロットゲーム

簡単なスロットゲームです。ゲームスタート時に保有しているコインを増やしましょう。
掛けコイン数を入力すると、スロットの結果が表示されます。
絵柄が揃うと、そのパターンに応じた配当倍率でコインを獲得することができます。

>>> show_start_message()
--------------
スロットゲーム
--------------
"""


import random  # "乱数" を生成するために必要なライブラリをメモリ上に読み込む(標準ライブラリ)
import re  # "正規表現" を使用するために必要なライブラリをメモリ上に読み込む(標準ライブラリ)
import sys


import matplotlib.pyplot as plt  # グラフを表示するために必要なライブラリ(サードパーティライブラリ)
from matplotlib import rcParams
rcParams['font.family'] = 'sans-serif'
rcParams['font.sans-serif'] = ['Hackgen35', 'Jetbrains Mono', 'Droid Sans Mono', 'monospace', 'Noto Sans CJK JP']

# スロット(リール)の中身を "定数" で定義する
# 配列のインデックスは 0 から始まるので、0 〜 9 番目に格納されている
# 定数は全て大文字で記載する
REEL_MARK_LIST = ('♠', '♥', '♥', '◆', '◆', '◆', '♣', '♣', '♣', '♣')

# 特別なプレーヤーの名前と、その名前を使用した際のボーナスコイン数
BONUS_PLAYER_AND_COINS = {'king': 100, 'queen': 150}

# スペシャルサンクスの対象者名
# ボーナスコインは一律 50 枚とする
SPECIAL_THANKS = {'akira',      # 弊社社長。詳細は社長が作成最多ブログ記事のプロフィール欄をご確認下さい。
                  'hurry',      # 詳細はプロフィール欄から twitter をご確認下さい。
                  'shachi',     # https://shachi-web.com/
                  }
SPECIAL_THANKS_COIN = 50


# ============================================================
# ゲームに必要な関数を定義する
# ============================================================

def show_start_message():
    """ゲーム開始のメッセージを表示する
    (This function is to be called to show the start message.)
    """
    print('--------------')
    print('スロットゲーム')
    print('--------------')

def ask_player_name():
    """プレーヤーの名前を尋ねる
    (This function is to be called to ask player's name.)

    プレーヤーの名前を入力(input)してもらい、挨拶を表示(print)する
    入力されたプレーヤーの名前を返す(return)

    Returns:
        str: プレーヤーの名前

    >>> import io, sys; sys.stdin = io.StringIO('king')
    >>> ask_player_name()
    あなたの名前を入力して下さい: こんにちは king さん
    'king'
    """
    player_name = input('あなたの名前を入力して下さい: ')
    print(f'こんにちは {player_name} さん')
    return player_name

def ask_bets(player_coin):
    """現在の所持コイン数を表示した後、掛けるコイン数(bets)を尋ねる
    (This function is to be called to ask bets.)

    所持コイン数(player_coin)は数値なので、文字列に含めたい場合は文字列(string)に変換する必要がある ==> str() 関数
    input() 関数で入力した値は文字列(string)になるので整数(integer)に変換する ==> int() 関数

    Args:
        player_coin (int): プレーヤーの所持コイン数

    Returns:
        int: 掛けコイン数
    """
    print('------------------------------')
    print(f'現在の所持コイン数は {player_coin:,} 枚です。')
    while True:
        str_bets = input('掛けコイン数を入力して下さい。※「quit」「exit」でゲームを終了します\n掛けコイン数: ')
        if str_bets.lower() in ('quit', 'exit'):
            print('ゲームを終了します。')
            sys.exit(0)

        (valid, error_message) = is_bets_valid(str_bets, player_coin)
        if valid:
            return int(str_bets)
        else:
            print(error_message)
            continue

def is_bets_valid(str_bets, player_coin):
    """掛けコイン数が有効(valid)な場合は True を返す(return)
    --> The function returns true if the bets is valid.

    Args:
        str_bets (str): 掛けコイン数
        player_coin (int): プレーヤーの所持コイン数

    Returns:
        tuple: [0] 掛けコイン数が有効な場合 True、それ以外は False
               [1] 検証結果が False だった場合のエラーメッセージ

    >>> is_bets_valid('100', 100)
    (True, None)

    >>> is_bets_valid('200', 100)
    (False, '掛けコイン数は半角数字で入力して下さい。')

    >>> is_bets_valid('0', 100)
    (False, '掛けコイン数は 1 枚以上を指定して下さい。')

    >>> is_bets_valid('101', 100)
    (False, '掛けコイン数は所持コイン数以下を指定して下さい。')

    >>> is_bets_valid('100', 'abc')
    Traceback (most recent call last):
        ...
    TypeError: '>' not supported between instances of 'int' and 'str'
    """
    if not re.search(r'^[0-9]+$', str_bets):
        return (False, '掛けコイン数は半角数字で入力して下さい。')

    bets = int(str_bets)
    if bets == 0:
        return (False, '掛けコイン数は 1 枚以上を指定して下さい。')
    elif bets > player_coin:
        return (False, '掛けコイン数は所持コイン数以下を指定して下さい。')
    else:
        return (True, None)

def show_and_get_result():
    """スロットの結果(絵柄)を表示し、結果を取得する
    (This function is to be called to show and get result.)

    Returns:
        str: スロットの結果(絵柄)
    """
    result_list = []
    for _ in range(3):
        index = random.randint(0, 9)
        result = REEL_MARK_LIST[index]
        result_list.append(result)
    result_all = ''.join(result_list)
    print(result_all)
    return result_all

def get_division(marks):
    """絵柄に応じた配当倍率を取得する
    (This function is to be called to get division.)

    Args:
        marks (str): スロットの結果(絵柄)

    Returns:
        int: 配当倍率
    """
    if marks == '♠♠♠':
        print('超大当たり!!')
        return 15
    elif marks == '♥♥♥':
        print('大当たり!')
        return 12
    elif marks == '◆◆◆':
        print('大当たり!')
        return 10
    elif marks == '♣♣♣':
        print('大当たり!')
        return 8
    elif marks[0:2] == '♠♠':
        print('当たり!')
        return 5
    else:
        print('ハズレ...')
        return -1

def calculate_coin(coin, bets, division):
    """掛けたコイン数(bets)と、配当倍率(division)を使用してプレーヤーの所持コイン数を精算する
    (This function is to be called to calculate (players's) coin.)

    Args:
        coin (int): プレーヤーの精算前の所持コイン数
        bets (int): 掛けコイン数
        division (int): 配当倍率

    Returns:
        int: 精算後のプレーヤーの所持コイン数
    """
    coin = coin + bets * division
    return coin

def show_gameover_message(player_name):
    """ゲーム終了のメッセージを表示する
    (This function is to be called to show a game-over message.)

    Args:
        player_name (string): プレーヤー名前
    """
    print('ゲームオーバーです')
    print(f'さようなら {player_name} さん')


# ============================================================
# 実際にゲームを実行する
# ============================================================

def main():
    # プレーヤーの所持コイン数
    player_coin = 123

    show_start_message()
    player_name = ask_player_name()

    if player_name in BONUS_PLAYER_AND_COINS:
        player_coin = player_coin + BONUS_PLAYER_AND_COINS[player_name]
    elif player_name in SPECIAL_THANKS:
        player_coin = player_coin + SPECIAL_THANKS_COIN

    coin_transition = [player_coin]
    while player_coin > 0:
        for _ in range(5):
            bets = ask_bets(player_coin)
            marks = show_and_get_result()
            division = get_division(marks)
            player_coin = calculate_coin(player_coin, bets, division)
            coin_transition.append(player_coin)
            if player_coin <= 0:
                break
        # 所持コインの遷移をグラフで表示する
        fig = plt.figure()                                              # 描画領域を生成
        ax1 = fig.add_subplot(1, 1, 1,                                  # 描画領域で1行1列の1個目のグラフを指定
                              title='スロットゲーム結果',                  # グラフのタイトル
                              xticks=list(range(len(coin_transition))), # x軸メモリ(1,2,3,...)
                              xlabel='回数',                             # x軸ラベル
                              ylabel='所持コイン数')                      # y軸ラベル
        ax1.plot(coin_transition)
        plt.show()

    show_gameover_message(player_name)


if __name__ == '__main__':
    main()

テスト用プログラム「tests/my_slotgame/test_game18.py」

from io import StringIO
import unittest.mock            # print()、input() をテストするために必要
import os, sys
sys.path.append(os.path.abspath(os.path.dirname(os.path.abspath(__file__)) + "/../../"))

import pytest

from my_slotgame import game018 # パッケージ(__ini__.py があるフォルダ)から指定する


def test_is_bets_valid():             # test_xxx_xxxx という名前にする決まり
    assert game018.is_bets_valid('100', 100) == (True, None)
    assert game018.is_bets_valid('200', 100) == (False, '掛けコイン数は半角数字で入力して下さい。')
    assert game018.is_bets_valid('0', 100) == (False, '掛けコイン数は 1 枚以上を指定して下さい。')
    assert game018.is_bets_valid('101', 100) == (False, '掛けコイン数は所持コイン数以下を指定して下さい。')
    with pytest.raises(TypeError):
        game018.is_bets_valid('100', 'abc')

@unittest.mock.patch('sys.stdout', new_callable=StringIO)             # print() の結果をテストする準備
def test_show_start_message(mock_stdout):
    game018.show_start_message()
    assert mock_stdout.getvalue() == '--------------\nスロットゲーム\n--------------\n'

@unittest.mock.patch('sys.stdout', new_callable=StringIO)             # print() の結果をテストする準備
def test_ask_player_name(mock_stdout):
    with unittest.mock.patch('builtins.input', return_value="king"):  # input() を 'king' を返却するものに置き換え
        assert game018.ask_player_name() == 'king'
        assert mock_stdout.getvalue() == 'こんにちは king さん\n'

def test_ask_bets(mock_stdout):
    # 強制終了のテスト
    with unittest.mock.patch('builtins.input', return_value="quit"):  # input() を 'quit' を返却するものに置き換え
        with pytest.raises(SystemExit) as e:
            game018.ask_bets(100)
        assert e.type == SystemExit
        assert e.value.code == 0
    # 強制終了のテスト
    with unittest.mock.patch('builtins.input', return_value="EXIT"):  # input() を 'EXIT' を返却するものに置き換え
        with pytest.raises(SystemExit) as e:
            game018.ask_bets(100)
        assert e.type == SystemExit
        assert e.value.code == 0
    # 通常のゲームのテスト
    with unittest.mock.patch('builtins.input', return_value="100"):  # input() を 'quit' を返却するものに置き換え
        assert game018.ask_bets(100) == 100

スロットゲームを用いた Python の勉強は、ここで一旦終了になります。ありがとうございました。

コメント

タイトルとURLをコピーしました