強制終了を組み込んでスロットゲームの修正は、おしまいにしたいと思います。
入力検証(バリデーション)を導入する前は、掛コインに数字以外の文字を入力するとエラーで終了していましたが、ここではシステム的な慣例に従った終了の仕方で対応いたします。
入力検証(バリデーション)用の関数が検証結果を 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 の勉強は、ここで一旦終了になります。ありがとうございました。
コメント