ブロー判定バグ
「いちばんやさしいPython入門教室」15章のヒットアンドブローゲームのブロー判定にバグがあります.
テキストの
blow = 0
for j in range(4):
for i in range(4):
if (int(b[j])==a[i]) and (int(b[i])!=a[i]) and(int(b[j])!=a[j]):
blow = blow + 1
break
だと,randomで作った数字が
[0,1,6,0]
に対して
[6,6,1,0]
と予測した場合に,
hit:1 blow:3
と判断されます.正しくは,
hit:1 blow:2
バグの中身
添付のようなコードに書き換えて,おかしい様子を表示させてみました.
def print_check(txt, mark, i, j):
global a,b,mark_a,mark_b
print(txt, i,j)
mark_a[i]=mark
mark_b[j]=mark
print('answer: [{0:s}{4:d}, {1:s}{5:d}, {2:s}{6:d}, {3:s}{7:d}]'.format(*mark_a,*a))
print(' trial: [{0:s}{4:d}, {1:s}{5:d}, {2:s}{6:d}, {3:s}{7:d}]'.format(*mark_b,*b))
def naive_check_text(a, b):
global mark_a,mark_b
blow, hit = 0, 0
for i in range(4):
if a[i] == b[i]:
print_check('hit:','o',i,i)
hit += 1
for j in range(4):
for i in range(4):
if (b[j] == a[i]) and (a[i]!=b[i]) and (a[j]!=b[j]):
print_check('blow:','x',i,j)
blow = blow+1
break
return hit, blow
a = [0, 1, 6, 0]
b=[6,6,1,0]
mark_a=[' ',' ',' ',' ']
mark_b=[' ',' ',' ',' ']
print("\ncheck by text")
print(naive_check_text(a, b))
> python for_rev_check.py
check by text
hit: 3 3
answer: [ 0, 1, 6, o0]
trial: [ 6, 6, 1, o0]
blow: 2 0
answer: [ 0, 1, x6, o0]
trial: [x6, 6, 1, o0]
blow: 2 1
answer: [ 0, 1, x6, o0]
trial: [x6, x6, 1, o0]
blow: 1 2
answer: [ 0, x1, x6, o0]
trial: [x6, x6, x1, o0]
(1, 3)
となり,'o'で表記しているhit pairと,'x'で表記しているblow pairがおかしくなっている様子がわかると思います2, 3.
仕様の変更??
というわけで,ゲームをしているときのヒントが,とんでもなく複雑でなかなか終わりません.解決策としては,
- 重複した数字を選ばないようにする,か,
- はたまた,より厳密にblowを判断させるか...
です.でも,1.の解決策はゲームのルールを変えるある種の仕様変更です.バグを「それは仕様です」と強弁する漫画がよくありますが,それと同じです.正しいプログラマ(?)は2.の道を進もうとしますが...
解決策
初めに自分で考えたやつは,意外と難しかったんです.
「もっと楽なやり方があれば教えてください」にtkuro-pさんに回答いただいたコードの改良版です3.
1 def naive_check_revised(answer, trial):
2 global mark_a,mark_b
3 blow, hit = 0, 0
4 answer = answer[:] # 浅いコピー
5 trial = trial[:] # 浅いコピー
6 for i in range(len(trial)):
7 if trial[i] == answer[i]:
8 answer[i] = trial[i] = None
9 print_check('hit:','o',i,i)
10 hit +=1
11 for i in range(len(answer)):
12 for j in range(len(trial)):
13 # print(i,j,answer[i],trial[j])
14 if (answer[i] == None) or (trial[j] == None):
15 continue
16 if answer[i] == trial[j] :
17 answer[i] = trial[j] = None
18 blow+=1
19 print_check('blow:','x',i,j)
20 return hit, blow
そんなに難しくないので,追いかけてください.
continueはそれ以降の処理をパスして,次の'j'で続けるという命令です.使っているテクニックは,
- 浅いコピー(shallow copy)でダミーのデータを作っておいて(L4, 5),
- 一度使った数字はNoneに書き換えて(L8, 17)
- 以降のLoopでは使わない(continue) (L14, 15)
ことです.結果は,
check by revised
hit: 3 3
answer: [ 0, 1, 6, o0]
trial: [ 6, 6, 1, o0]
blow: 1 2
answer: [ 0, x1, 6, o0]
trial: [ 6, 6, x1, o0]
blow: 2 0
answer: [ 0, x1, x6, o0]
trial: [x6, 6, x1, o0]
(1, 2)
と,ちゃんと予想通りになります.追いかける時のコツは,
- L13のコメントアウトしているprintをアンコメントして生き返らせる
- 変数名を意図を表すより具体的な英語(answer, trial)に変え,
- i,j-loopの順序を普通に戻す
こういう簡単な書き換えで,読みやすく,デバッグしやすくなります.
print関連の,print_check呼び出しやglobalの行(L2)を外すとtextのcodeに置き換えて使えます.
参照
Footnotes
1 「いちばんやさしいPython入門教室」大澤文孝著,(ソーテック社出版,2017).
3 「いちばんやさしいPython入門教室」修正(hit and blow判定のバグ修正)記事
- source ~/Desktop/lecture_24s/comp_a24/d7_11_python/d9_hit_and_blow_rev_23.org