4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Python] コンソールで遊べる簡単なゲームを作る - Bulls and Cows

Posted at

はじめに

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
ゲームクリア!
ソースコード
bulls-and-cows.py
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 を使うことで、シーケンスをばらばらにできる.

bulls-and-cows.py
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.secretguess を同時にループしている.
Python に組み込まれている zip 関数を使うと実現できる.

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.secretguess どちらも長さ 4 の数列であることが分かっているが、zip 関数が返す要素数はもっとも短いリストに合わせられる.

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

sample.py
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.secretguess の要素を含んでいるかを g in self.secret で判定している.

bulls-and-cows.py
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.secretany(s == g for s in self.secret) と書き換えられる.
Java などで list.contains(value) のような書き方に慣れていると面食らう構文だが、こういうのが python らしい書き方なのだろう.

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?