数字当てゲームについてのアドバイスをください{Python}
数字当てゲームについてのアドバイスをください{Python}。
Python初学者です。というかプログラミング自体始めたばかりです。
数字当てゲームについての悩みです。詳しいルールについては以下のサイトをご覧ください。
https://45mix.net/hit-and-blow-game/
このコードの改善点と、同じ数字が二つ出てきた場合のにおいてのblowの扱いなどを知りたいです。どうかご教授お願い致します。
ソースコード
import random
import time
decide = 0 # お題の数を決めるために使用します
challenge = 0
targetlist = []
while(decide < 3): # <3 にすることで三桁の数と決めています。判定の都合上リスト形式で、一つの桁ずつに数を入れています
targetlist.append(random.randint(0,9))
decide += 1
print(f'正解の数字は{targetlist}')
while(challenge < 10):#10回の制限あり
hit = 0 #初期化
blow = 0 #初期化
zikkou = 0 #初期化
mynumlist = [] #自分の数のリスト
mynum_kakunou = [] #自分で決めた数をリスト形式に格納するためのリスト
mynum = int(input("好きな数字を入力"))
mynum100,innum100 = mynum % 100, mynum // 100 #mynumは計算用の変数、innumはリスト格納の変数。それぞれ余りを出すか商を出すかで分かれている
mynum10,innum10 = mynum100 % 10,mynum100 // 10
mynum1,innum1 = mynum10 // 1,mynum10 // 1
mynum_kakunou = [innum100,innum10,innum1]#格納するためのリストを作成
for i in mynum_kakunou:#forで要素を先頭から一つずつ取り出して自分の数のリストに格納する
mynumlist.append(i)
print(f'あなたの数字は{mynumlist}')
for m in mynumlist:#総当たりにてかなり無茶苦茶な判定です。それぞれのリストから一つ一つ要素を取り出し判定します
for j in targetlist:
zikkou += 1#判定のための処理を何回実行したか数える
if m == j:#数が当たっていたら
print('TRUE')#ここは鬱陶しいなら削除しても大丈夫
if zikkou == 1:#ここのhitとblowの見分け方は別途記述
hit += 1
elif zikkou == 5:
hit += 1
elif zikkou == 9:
hit += 1
else:
blow += 1
else:#この二行は鬱陶しいなら削除しても大丈夫
print('FALSE')
print(f'HIT:{hit}')
print(f'BLOW:{blow}')
challenge += 1
if hit == 3:#3つとも正解していたら
print(f'おめでとう!正解の数字は{mynum}だ!')
break
if challenge == 10:
print(f'おしい!正解の数字は{mynum}')
break
if blow > 3:#バグ対策用。例えば335などの数字はblowが4になってしまう。
print('無効な数です。3秒後に終了します')
time.sleep(3)
break
当たり判定について
この当たり判定の仕組みですが、自分の数の桁と数字、正解の数の桁と数字がそれぞれ一緒。いわゆるヒットだった場合、100の桁からそれぞれ、1,5,9番目にTRUEが返ってきます。それ以外でTRUEが返ってきた場合は、全てblowで返します。
正解の数字は349
あなたの数字は[3, 4, 9]
TRUE #1
FALSE
FALSE
FALSE
TRUE #5
FALSE
FALSE
FALSE
TRUE #9
HIT:3
BLOW:0
しかしこのプログラムでは424などの連番においては正しく動作しません。この改善点などもどうぞよろしくお願いいたします。
正解の数字は[4, 2, 4]
好きな数字を入力244
あなたの数字は[2, 4, 4]
TRUE
FALSE
TRUE
TRUE
FALSE
TRUE
TRUE
FALSE
HIT:1
BLOW:4
大幅に書き換えてみました。
zip関数を使って同じ桁位置の数を取り出しています。
import random
answer = random.sample(range(0, 10), 3) # 0~9の数で重複しない3桁の数リストを作る
print(f'正解の数は{answer}')
for challenge in range(10): # 10回の制限あり
while True:
guess = input(f"{len(answer)}桁の重複しない数を入力 {challenge+1}回目: ")
if len(set(guess)) == len(answer) and guess.isdigit():
break
guess = list(map(int, guess)) # 予想した数字列を桁毎の数リストに変換
print(f'あなたが入力した数は{guess}')
hit = 0
blow = 0
for g, a in zip(guess, answer): # guessとanswerから1桁ずつgとaに取り出す
if g == a: # 同じ桁で同じ数
hit += 1
elif g in answer: # 違う桁で正解の中に数がある
blow += 1
print(f'HIT:{hit}')
print(f'BLOW:{blow}')
if hit == len(answer): # 全桁ヒットしていたら
print(f'おめでとう!正解の数は{answer}だ!')
break
else: # 正解(break)しなかったとき
print(f'おしい!正解の数は{answer}')
数字当てゲームについての悩みです。詳しいルールについては以下のサイトをご覧ください。
https://45mix.net/hit-and-blow-game/
こちらのサイトに、以下の記述があります。
123、650、721など、好きな数字を書きます。
このとき同じ数字は2つ以上使うことは出来ません。
× 226、× 777、× 007などは使えません。
つまり、「424などの連番」は、そもそもルール違反なので、正しく動作する必要はないのです。
どうしても同じ数字を使えるようにしたいのであれば、collections.Counterを使用して、以下のように判定するのはいかがでしょうか。
from collections import Counter
...
hit = sum(i == j for i, j in zip(targetlist, mynumlist))
blow = (Counter(targetlist) & Counter(mynumlist)).total() - hit
hit
は同じ位置で同じ数字の個数、blow
は位置無関係で同じ数字の個数(=問題回答で共通に含まれる数字の個数)からhit
を引いたもの、という方針で求めています。
実行結果は、たとえば以下のようになります。
正解の数字は[4, 2, 4]
好きな数字を入力244
あなたの数字は[2, 4, 4]
HIT:1
BLOW:2
好きな数字を入力212
あなたの数字は[2, 1, 2]
HIT:0
BLOW:1
最後の例は、考え方によっては0ヒット2ブローにもなりえますが、一度ヒットやブローの判定に使われた数字は除いて考えるのが自然だと思われるので、0ヒット1ブローとなるような判定にしています。
Counter
を使う方法が分かりづらいようであれば、「一度ヒットやブローの判定に使われた数字は除いて考える」を素直に実装して、以下のように書くこともできます。
hit = blow = 0
targetlist_copy = targetlist[:]
mynumlist_copy = mynumlist[:]
for i in range(len(targetlist_copy) - 1, -1, -1):
if targetlist_copy[i] == mynumlist_copy[i]:
hit += 1
del targetlist_copy[i], mynumlist_copy[i]
for m in mynumlist_copy:
if m in targetlist_copy:
blow += 1
targetlist_copy.remove(m)
最初のループでは、先頭から処理するとdel
で削除した際にインデックスがずれるので、末尾から処理しています。
少しオブジェクト指向らしくコーディングしてみました。参考になれば幸いです。
重複は禁止のルールで実装しました。
from dataclasses import dataclass
import random
# 1~9の数値を表すデータクラス
@dataclass(frozen=True)
class Number:
value: int
# バリデーション: 1~9の数値でない場合はエラー
def __post_init__(self):
if self.value < 1 or 9 < self.value:
Exception('数は1以上9以下でなければなりません')
# 3つの数値を表すデータクラス
@dataclass(frozen=True)
class ThreeNumbers:
first: Number
second: Number
third: Number
# バリデーション: 3つの数値は重複している場合はエラー
def __post__init__(self):
if self.first == self.second or self.first == self.third or self.second == self.third:
Exception('3つの数は重複してはいけません')
# printで表示時の形式
def __str__(self):
return f'{self.first.value}{self.second.value}{self.third.value}'
# 他のThreeNumbersと比較して、ヒット数を返す
def hit(self, other: 'ThreeNumbers') -> int:
hit = 0
if self.first == other.first:
hit += 1
if self.second == other.second:
hit += 1
if self.third == other.third:
hit += 1
return hit
# 他のThreeNumbersと比較して、ブロー数を返す
def blow(self, other: 'ThreeNumbers') -> int:
blow = 0
if self.first == other.second or self.first == other.third:
blow += 1
if self.second == other.first or self.second == other.third:
blow += 1
if self.third == other.first or self.third == other.second:
blow += 1
return blow
# Hit and Blowゲームを表すクラス
class HitAndBlow:
LIMIT = 10 # 制限回数
target: ThreeNumbers # ターゲット
hit: int = 0 # ヒット数
blow: int = 0 # ブロー数
# ターゲットを作成する
def _make_target(self):
# 0~9の数値をランダムに3つ選ぶ
numbers = random.sample(range(10), 3)
# ターゲットを作成
self.target = ThreeNumbers(
Number(numbers[0]),
Number(numbers[1]),
Number(numbers[2])
)
# 入力を受け取る
def _input_numbers(self):
numbers = int(input("好きな数字を入力:"))
# 3桁の数値を表すデータクラスに変換
first = Number(numbers // 100)
second = Number((numbers % 100) // 10)
third = Number(numbers % 10)
return ThreeNumbers(first, second, third)
# ゲームを開始する
def start(self):
print('Hit and Blowを開始します')
# ターゲットを作成
self._make_target()
# 10回の制限あり
for i in range(self.LIMIT):
print(f'あと{self.LIMIT - i}回')
# 入力を受け取る
numbers = self._input_numbers()
# HITとBLOWを計算
self.hit = self.target.hit(numbers)
self.blow = self.target.blow(numbers)
print(f'HIT: {self.hit}, BLOW: {self.blow}')
if self.hit == 3:
print(f'おめでとう!正解の数は{self.target}だ!')
return
print(f'おしい!正解の数字は{self.target}')
if __name__ == '__main__':
hit_and_blow = HitAndBlow()
hit_and_blow.start()
オブジェクト指向でクラス分けするなら、Playerクラスも作るとよさそうですね。
import random
class Answer:
"""出題者"""
def __init__(self, digits: int) -> None:
"""digits桁の重複しない数を作って隠し持つ"""
self._number = ''.join(map(str, random.sample(range(0, 10), digits)))
def __str__(self) -> str:
"""表示用文字列を返す"""
return self._number
def __eq__(self, number: str) -> bool:
"""numberが正解ならTrue、違えばFalseを返す"""
return number == self._number
def judge(self, number: str) -> None:
"""numberのhit数,blow数を表示する"""
hit = blow = 0
for g, a in zip(number, self._number):
if g == a: # 同じ桁で同じ数
hit += 1
elif g in self._number: # 違う桁で正解の中に数がある
blow += 1
print(f'{hit} HIT {blow} BLOW')
class Player:
"""回答者"""
def guess_number(self, digits: int) -> str:
"""digits桁の重複しない数を推測する"""
while True:
number = input(f'{digits}桁の重複しない数を入力: ')
if len(set(number)) == digits and number.isdigit():
return number
class HitAndBlow:
"""司会者(数当てゲーム進行係)"""
LIMIT = 10 # 回答制限回数
def play(self, digits: int = 3) -> None:
"""digits桁の数当てゲームを遊ぶ"""
answer = Answer(digits)
player = Player()
print('Hit and Blowを開始します')
for limit in range(self.LIMIT, 0, -1):
print(f'あと{limit}回')
number = player.guess_number(digits)
answer.judge(number)
if number == answer: # answer.__eq__(number)
print(f'おめでとう!正解の数は{answer}だ!') # answer.__str__()
return
print(f'おしい!正解の数字は{answer}') # answer.__str__()
if __name__ == '__main__':
HitAndBlow().play()