[Python]すごろくゲーム作ってみた

こんにちは。Yuinaです。

すごろくアプリを作成しましたので、掲載いたします!

単体テスト用のコードも作成したので、よかったらご覧ください。

main.py:

import random
import time
from abc import ABC, abstractmethod
#サイコロの目の値を決める
class Dice():
def __init__(self, min_val=1, max_val=6):
self.min_val = min_val
self.max_val = max_val
self.current_value: int = -1

"""
サイコロの目の値を管理する。
"""
def roll(self):
self.current_value = random.randint(self.min_val, self.max_val)
return self.current_value

def is_twice(self):
return self.current_value == 5

"""
サイコロを振り、出た目と効果を適用した最終的な移動量を返す。
"""
def get_effected_value(self):
"""
3の目が出たら、マスを戻す
"""
if self.current_value == 3:
return -self.current_value
return self.current_value

#継承基底クラス
class PlayerBase(ABC):
def __init__(self, name: str):
#名前の文字数制約
if not (1 <= len(name) <= 10):
#エラーハンドリングで呼び出し元に戻す
raise ValueError("名前は1文字から10文字の間で入力してください。")
#文字数制約をクリアしたもののみ、self.nameに外部からのnameを渡す
self.name = name

#継承メソッド
#このメソッドでは、こういう引数を使うだろう〜という設計を決める
@abstractmethod
def dice_roll(self, game, dice_instance):
pass

## def __dece_role(self):

#PlayerBaseの実装クラス
class Player(PlayerBase):
def __init__(self, name: str):
super().__init__(name)

#人間用
# dice_rollメソッドを宣言する
def dice_roll(self, game, dice: Dice):
print(f"人間: {self.name}の番です。")
input("サイコロを振るにはエンターキーを押してください。")

# dice_instanceから最終的な移動量を取得する
dice.roll()
if dice.is_twice():
game.setPosition(self, dice.get_effected_value())
self.dice_roll(game, dice)
else:
# ゲームクラスに移動を任せる
game.setPosition(self, dice.get_effected_value())
game.countup()

#CPU用
# dice_rollメソッドを宣言する
class CPU(PlayerBase):
def __init__(self, name: str):
super().__init__(name)

def dice_roll(self, game, dice):
print(f"CPU: {self.name}の番です。")
#inputの代わりに待ちを発生させる
time.sleep(1)

# dice_instanceから最終的な移動量を取得する
dice.roll()
if dice.is_twice():
game.setPosition(self, dice.get_effected_value())
self.dice_roll(game, dice)
else:
# ゲームクラスに移動を任せる
game.setPosition(self, dice.get_effected_value())
game.countup()



class Game():
def __init__(self, players, goal: int):
#プレイヤーの人数の制約
if not (2 <= len(players) <= 4):
#エラーハンドリング
#raiseで呼び出し元へ戻す
raise ValueError("プレイヤーの人数は2人から4人の間で入力してください。")
#ゴール値の制約
if not (6 <= goal <= 30):
#エラーハンドリング
#raiseで呼び出し元へ戻す
raise ValueError("ゴールのマスは6から30の間で設定してください。")

#制約の範囲内だった場合、引数で渡されたものをselfに取得する
self.goal = goal
self.count = 0
#playerとpositionはセットで管理したいため、辞書型を使う
self.players = [{'player': p, 'position': 0} for p in players]
self.player_len = len(self.players)

#各プレイヤー・CPUのコマの状態を管理する
def processing(self) -> bool:
for p in self.players:
#対戦中はtrue
if p.get('position') >= self.goal:
return False
return True

#今、サイコロを投げるプレイヤーorCPUを管理する
#countを人数で割る(ex: 4/4=0 → players[0]="Yuki")
#countはcountupメソッドで加算する
def current_player(self):
num = self.count % self.player_len
return self.players[num]['player']

#勝者を管理する
def winner(self):
for p in self.players:
if p.get('position') >= self.goal:
return p.get('player')
#countの加算を行う
def countup(self):
self.count += 1

#コマの進みを管理する
def setPosition(self, player, total_move):
for p in self.players:
if p.get('player') == player:
# 最終的な移動量を使ってコマを進める
p['position'] += total_move

print(f"{p.get('player').name}のコマの状態:{p.get('position')}")

# ゴールを過ぎたらコマを戻す
if self.goal < p.get('position'):
tmp = p['position'] - self.goal
p['position'] = self.goal - tmp
print(f"{tmp}マス戻ります。")
break
#main関数
def main() -> None:
print("すごろくゲームを開始します。")
players = [
Player("Yuki"),
Player("Yui"),
CPU("CPU"),
CPU("CPU2")
]

dice = Dice()
#Gameクラスの引数: players, goal
game = Game(players, 20)

#ゲームが進行中の間、
while game.processing():
#current_playerメソッドを呼び出して、サイコロを振る順番を決める
player = game.current_player()
#サイコロをふる
# dice_rollの引数: game, dice_instance
player.dice_roll(game, dice)

print(f"{game.winner().name}の勝ちです!")

if __name__ == '__main__':
main()

test.py

import unittest
import sys
import io
from main2 import PlayerBase, Player, CPU, Game, Dice

#Playerクラスのテスト
class Test_Player(unittest.TestCase):

"""
プレイヤー名の文字数が制約を満たすかどうか、チェックする
"""
def test_player_name_len_true(self):
#プレイヤーをYukiにする
player = Player("Yuki")
#assertGreaterEqualメソッドでプレイヤー名が1以上かどうかチェック
self.assertGreaterEqual(len(player.name), 1)
#assertLessEqualメソッドでプレイヤー名が10未満かどうかチェック
self.assertLessEqual(len(player.name), 10)

"""
プレイヤー名の文字数が制約を満足さない場合、エラー処理が実行されるかどうかチェックする
"""
def test_player_name_len_false(self):
#エラーハンドリング
with self.assertRaises(ValueError):
#プレイヤー名をNULLとする
Player("")
#エラーハンドリング
with self.assertRaises(ValueError):
#プレイヤー名を10文字以上にする
Player("abcdefghijklmn")

#CPUクラスのテスト
class Test_CPU(unittest.TestCase):

"""
CPU名の文字数が制約を満足さない場合、エラー処理が実行されるかどうかチェックする
"""
def test_cpu_name_len_true(self):
cpu = CPU("CPU")
self.assertGreaterEqual(len(cpu.name), 1)
self.assertLessEqual(len(cpu.name), 10)

def test_player_name_len_false(self):
#エラーハンドリング
with self.assertRaises(ValueError):
#CPU名をNULLとする
CPU("")
#エラーハンドリング
with self.assertRaises(ValueError):
#CPU名を10文字以上にする
CPU("abcdefghijklmn")

#Gameクラスのテスト
class Test_Game(unittest.TestCase):

"""
setUpメソッドで初期化を行う
"""
def setUp(self):
self.players_list = [
Player("Yuki"),
Player("Yui"),
CPU("CPU"),
CPU("CPU2")
]

#Gameクラスをnewする
self.game = Game(self.players_list, 20)

#リソースのクリーンアップをする
def tearDown(self):
sys.stdin = sys.__stdin__
sys.stdout = sys.__stdout__

"""
辞書型のplayersのplayerとpositionの組み合わせが適切かどうか、チェックする
"""
def test_array_length(self):
player_list = [d['player'] for d in self.game.players]
position_list = [d['position'] for d in self.game.players]
#要素数のチェック
self.assertEqual(len(player_list), len(position_list))

"""
対戦人数が制約を満たすかどうか、チェックする
"""
def test_player_num_true(self):
#2名の場合のチェック
#players_2 = [Player("Yuki"), Player("Yui")]
# 準備
players_2 = [Player("Yuki"), Player("Yui")]
players_3 = [Player("Yuki"), Player("Yui"), CPU("CPU")]
players_4 = [Player("Yuki"), Player("Yui"), CPU("CPU"), CPU("CPU2")]

game1 = Game(players_2, 20)
game2 = Game(players_3, 20)
game3 = Game(players_4, 20)
# 発火

#game_2 = Game(players_2, 20)
self.assertGreaterEqual(len(game1.players), 2)
self.assertLessEqual(len(game1.players), 4)
#3名の場合のチェック
#players_list3 = [Player("Yuki"), Player("Yui"), CPU("CPU")]
#game_3 = Game(players_list3, 20)
self.assertGreaterEqual(len(game2.players), 2)
self.assertLessEqual(len(game2.players), 4)
#4名の場合のチェック
self.assertGreaterEqual(len(game3.players), 2)
self.assertLessEqual(len(game3.players), 4)

"""
対戦人数が制約を満たさない場合、エラー処理が実行されるかどうかチェックする
"""
# test_player_num_false メソッドの正しい書き方
def test_player_num_false(self):
players_1 = [Player("Yuki")]
players_5 = [Player("Yuki"), Player("Yui"), CPU("CPU"), CPU("CPU2"), CPU("CPU3")]

with self.assertRaises(ValueError):
Game(players_1, 20)

with self.assertRaises(ValueError):
Game(players_5, 20)

"""
ゴール値が制約を満たしているかどうか、チェックする
"""
def test_goal_scale_true(self):
self.assertGreaterEqual(self.game.goal, 6)
self.assertLessEqual(self.game.goal, 30)

"""
ゴール値が制約を満たしていない場合、エラー処理が実行されるかどうかチェックする
"""
def test_goal_scale_false(self):
#エラーハンドリング
#ゴール値が5の場合のチェック
with self.assertRaises(ValueError):
Game(self.players_list, 5)
#エラーハンドリング
#ゴール値が32の場合のチェック
with self.assertRaises(ValueError):
Game(self.players_list, 32)

"""
positionの初期値が0になっているかどうか、チェックする
"""
def test_position_integrate(self):
for player_data in self.game.players:
self.assertEqual(player_data.get('position'), 0)

"""
勝者がいない場合、NULLと表示されるかどうか、チェックする
"""
def test_winner_null(self):
self.assertIsNone(self.game.winner())

"""
サイコロを振って出た目の数だけ、プレイヤー(CPU)のマスを進められているかどうか、チェックする
"""
def test_correct_position(self):
#現在のプレイヤー(orCPU)の情報を取得する
player = self.game.current_player()
#コマを進める
self.game.setPosition(player, 7)
#適切な数だけコマを進められているかどうか、チェックする
#0(初期値) + 7 = 0
self.assertEqual(self.game.players[0].get('position'), 7)

"""プロンプト上のテキストが適切な形式で表示されているか、チェックする"""
def test_output_cpu(self):
captured_output = io.StringIO()
sys.stdout = captured_output

test_dice = Dice(min_val=3, max_val=3)
cpu = CPU("TestCPU")
cpu.dice_roll(self.game, test_dice)

output = captured_output.getvalue()

self.assertIn("CPU: TestCPUの番です。", output)
#self.assertIn("出た目:3", output)

"""
サイコロを振って3の目が出たら、3コマ戻す処理ができているかどうか、チェックする
"""
def test_minus_dice(self):

#Gameクラスをnewする
game = Game(self.players_list, 20)
#Yukiのpositionの初期値を5とする
game.players[0]['position'] = 5
#サイコロの目を振って出た目を3とする
test_dice = Dice(min_val=3, max_val=3)
#現在のプレイヤー(orCPU)の情報を取得する
player = game.current_player()
#dice_rollメソッドを実行する
sys.stdin = io.StringIO('\n')
player.dice_roll(game, test_dice)
#初期値からサイコロを振って、出た目の数が引かれているかどうか、チェック(5 - 3 = 2)
self.assertEqual(game.players[0]['position'], 2)

"""
ゴール値をはみ出した分、コマを戻す処理ができているかどうか、チェックする
"""
def test_return_dice(self):
#Gameクラスをnewする
game = Game(self.players_list, 20)
#Yukiのpositionの初期値を19とする
game.players[0]['position'] = 19
#サイコロの目を振って出た目を3とする
test_dice = Dice(min_val=4, max_val=4)
#現在のプレイヤー(orCPU)の情報を取得する
player = game.current_player()
#dice_rollメソッドを呼び出し、サイコロを投げる
sys.stdin = io.StringIO('\n')
player.dice_roll(game, test_dice)
#ゴール値をはみ出した分、コマを戻す処理ができているかどうか、チェックする
#19 + 4 = 23
#23 - 20 = 3
#20 - 3 = 17
self.assertEqual(game.players[0]['position'], 17)

class TestDice(unittest.TestCase):

"""サイコロの出目が3の場合、マスを戻すことをテストする。"""
def test_effect_on_three(self):
# 前処理
dice = Dice(min_val=3, max_val=3)
# 実行
dice.roll()
# テスト
# 最終的な結果が-3であることを確認
self.assertEqual(dice.get_effected_value(), -3)

"""サイコロの出目が3以外の場合、そのままの値を返すことをテストする。"""
def test_effect_not_change(self):
# 特殊な効果がない出目として「4」を設定する
# 前処理
dice = Dice(min_val=4, max_val=4)
# 実行
dice.roll()
# テスト
# 最終的な結果が-3であることを確認
self.assertEqual(dice.get_effected_value(), 4)

"""サイコロの出目が5の場合、is_twiceがtrueになること"""
def test_roll_again_on_five(self):
# 前処理
dice = Dice(min_val=5, max_val=5)
# 実行
dice.roll()
# テスト
# 最終的な結果が-3であることを確認
self.assertEqual(dice.is_twice(), True)

if __name__ == '__main__':
unittest.main()

結果:

まだまだスッキリさせられる場所はありそうです。

少しずつ改善して良いコードが書けるように精進します。

ありがとうございました✨

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