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


コメント