はじめに
Python の勉強のため簡単なゲームを作りましたので備忘録としてまとめておきます.
Bulls and Cows を最短で解くプログラムを期待してこられた方がいらしたら申し訳ありませんが、単純に遊べるだけのものです.
とりとめのない個人的な記事ではありますが、python を勉強するどなたかの参考になりましたならば幸いです.
環境は Python 3.13.1、Windows 11、コマンドプロンプトです.
完成品
|>
の行が入力.
No. Guess | Bulls Cows
4 つの数字を推測し空白区切りで入力してください; e.g. 1 5 0 4
bulls: 値も位置も正しい数字の数
cows : 値は正しいが位置が異なる数字の数
ゲームを終了する場合、quit
|>
4 つの数字を推測し空白区切りで入力してください; e.g. 1 5 0 4
bulls: 値も位置も正しい数字の数
cows : 値は正しいが位置が異なる数字の数
ゲームを終了する場合、quit
|> 0 4 9 8
No. Guess | Bulls Cows
1. 0 1 2 3 | 1 0
2. 0 3 1 2 | 1 0
3. 0 4 5 6 | 2 0
4. 0 4 6 5 | 2 0
5. 0 4 7 8 | 3 0
6. 0 4 7 9 | 2 1
7. 0 4 9 7 | 3 0
8. 0 4 9 8 | 4 0
ゲームクリア!
ソースコード
from random import shuffle
from collections import namedtuple
Result = namedtuple('Result', ['bulls', 'cows'])
class Game:
secret: list[int]
history: list[tuple[list[int],Result]]
def __init__(self):
nums = list(range(10))
shuffle(nums)
self.secret = nums[0:4]
self.history = []
def answer(self, guess) -> bool:
if len(guess) != len(set(guess)):
# 重複があるので受け付けない
return False
bulls = 0
cows = 0
for i,(s,g) in enumerate(zip(self.secret, guess)):
if s == g:
bulls += 1
elif g in self.secret:
cows += 1
result = Result(bulls, cows)
self.history.append((guess,result))
return True
def show(self):
print('No. Guess | Bulls Cows')
for i,h in enumerate(self.history):
guess,result = h
bulls,cows = result
guess_str = ' '.join(str(g) for g in guess)
print(f'{i+1:>2}. {guess_str} | {bulls} {cows}')
def game_over(self):
return self.history and self.history[-1][1].bulls == 4
def process_message(game):
'''
メッセージループの処理
Returns:
処理を終了する場合 True
'''
game.show()
print('4 つの数字を推測し空白区切りで入力してください; e.g. 1 5 0 4')
print('bulls: 値も位置も正しい数字の数')
print('cows : 値は正しいが位置が異なる数字の数')
print('ゲームを終了する場合、quit')
# 内部で input() を使っているのにメッセージループを名乗れるかは知らない.
user_input = input('|> ').split()
if user_input == ['quit']:
return True
if len(user_input) != 4:
print('4 つの数字をスペース区切りで入力してください; e.g. 7 0 8 3 ')
return False
try:
guess = list(map(int, user_input))
except ValueError:
print('数字を入力してください')
return False
if not game.answer(guess):
print(f'推測に重複があります')
if game.game_over():
game.show()
print('ゲームクリア!')
return True
return False
# メインの処理
game = Game()
while(True):
if process_message(game):
break
説明
メッセージループ
メインの処理はコードの一番下に描かれている関数 process_message と、それを呼び出す無限ループである.
process_message で行っていることを大まかに分ければ
- print で入力形式などを表示する
- input で入力を受け取る
- 入力に応じた処理
のみっつで、入力された数字の記憶やクリア判定は Game クラスに任せている.
毎回ルールの説明を書いているので、回答の履歴を毎回描画している(game.show()
).
Game
クラス
内部表現
プレイヤーが当てることになる秘密の数列 secret: list[int]
と、
プレイヤーが行った推測の履歴 history: list[tuple[list[int], Result]]
を持っている.
history
をもう少し説明的に書けば、list[(推測した四つの数字), (Bulls, Cows)]
.
回答とクリア判定
answer(self, guess)
では回答を受け取り、形式に問題がなければ (bulls, cows)
を計算して history
に保存する.
game_over(self)
では最後の回答で bulls
が 4 であるかを調べることによってクリア判定を行っている.
履歴の描画
show(self)
ではプレイヤーが行った推測の履歴を表形式で表示している.
Python の勉強ポイント
この部分を書いて勉強になった点のまとめ
リストのシャッフル
__init__(self)
では secret
を作成するために配列をシャッフルしている.
random.shuffle
を使うことで、シーケンスをばらばらにできる.
from random import shuffle
[中略]
def __init__(self):
nums = list(range(10))
shuffle(nums)
self.secret = nums[0:4]
self.history = []
リストの直積
answer(self, guess)
では (bulls,cows)
を計算するために、self.secret
と guess
を同時にループしている.
Python に組み込まれている zip
関数を使うと実現できる.
list0 = [ 0, 2, 4, 6 ]
list1 = [ 1, 3, 5, 7 ]
zipped = list(zip(list0, list1))
print(zipped) # [(0, 1), (2, 3), (4, 5), (6, 7)]
今回は self.secret
と guess
どちらも長さ 4 の数列であることが分かっているが、zip 関数が返す要素数はもっとも短いリストに合わせられる.
list0 = [ 0, 2, 4, 6, 8, 10 ] # 長さ 6
list1 = [ 1, 3, 5, 7 ] # 長さ 4
zipped = list(zip(list0, list1)) # 長さ min(6, 4) = 4
print(zipped) # [(0, 1), (2, 3), (4, 5), (6, 7)]
名前付きタプル
推測の結果を単に result = (bulls,cows)
のようなタプルにすると、分割代入(bulls,cows = result
) のようにしない場合 bulls = result[0]
のような仕方でアクセスすることになる.
これを、bulls = result.bulls
のようにする場合クラスを使うか、今回のように名前付きタプルを使うかとなる(今回のコードでは分割代入しかしていないので名前付きタプルの恩恵はないけれど).
collections.namedtuple
from collections import namedtuple
# Result という名前で、0 番目の要素の名前が bulls、1 番目の要素のが cows であるような名前付きタプルを作成する.
# 第一引数の 'Sample' は str や repr などで文字列として扱うときに使われる.
# 例として変数名の方を T としているが、分かりやすさのために名前を揃えた方がよい.
T = namedtuple('Sample', ['x', 'y', 'z'])
nt = T(1, 2, 3) # T はクラスのコンストラクタと同じようにして扱う
print(nt) # Sample(x=1, y=2, z=3)
中身はタプルなので、==
で比較する場合などではクラスで定義する場合と少し異なるが、省スペースで定義できるため使い所を考えれば便利なのだと思う.
リストが特定の値を含んでいるかを判定する
answer(self, guess)
では、cows
を求めるため self.secret
が guess
の要素を含んでいるかを g in self.secret
で判定している.
def answer(self, guess) -> bool:
if len(guess) != len(set(guess)):
# 重複があるので受け付けない
return False
bulls = 0
cows = 0
for i,(s,g) in enumerate(zip(self.secret, guess)):
if s == g:
bulls += 1
elif g in self.secret:
cows += 1
result = Result(bulls, cows)
self.history.append((guess,result))
return True
g in self.secret
は any(s == g for s in self.secret)
と書き換えられる.
Java などで list.contains(value)
のような書き方に慣れていると面食らう構文だが、こういうのが python らしい書き方なのだろう.