プログラミング上達講座3：メガテンのコードブレイカー

プログラミング上達講座の3回目です。

メガテンのコードブレイカーを題材にして
プログラムを考えてみましょう。

解説動画はこちら

[![表示されなかったらすみません](https://img.youtube.com/vi/VdvOmHQLxWA/0.jpg)](https://youtu.be/VdvOmHQLxWA)


## コードブレイカーとは

RPG「女神転生」シリーズのミニゲーム`コードブレーカー`

正式名称は「Hit&Blow」という数当てゲームです。

その昔`ヌメロン`という名前でテレビ番組やっていましたね。
`ヌメロン`だとアイテムが使えたりがありましたが
今回はアイテムとか無しです。


### ルール

各桁の数字(0-9までの数字)が全て違う、3桁の数字を当てるゲーム

予想の数字を入力して、桁ごとに下記を判定
`正解と場所と数字が一致する（ヒット）`
`場所が違うがその数字が含まれる（ブロー）`
1回答ごとにHitとBlowの数を言い、全て当たる(3H)まで行う。

例：
正解の数 957
1回目の予想「915」→ 「1H1B」
2回目の予想「234」→ 「0H0B」
3回目の予想「795」→ 「0H3B」
4回目の予想「957」→ 「3H0B」→当たり


### 初級編:コードブレイカーのチェック関数を作る

予想の数値と正解の数字を入力して
HitとBlowの数を返す関数`check_call`を作ってみよう

```python
# 引数：call=予想数字 , solve=正解数字
# 戻り値：(hit , blow)のタプル型(両方数値)
def check_call(call , solve):
    処理
    return  (hit , blow)
```

例：

check_call('564','987') →(0,0)
check_call('564','687') →(0,1)
check_call('564','546') →(1,2)
check_call('564','564') →(3,0)


### 中級編:コードブレイカーのゲームを作ってみよう

3桁の数値をランダムで生成して正解数として
正解数を当てに行くミニゲームを作ってみよう。

inputで数値入力を受け付け
数値とhit , blowを出力し続ける。

3Hになるか、10回当てられなければゲーム終了。

ヒント：
まず正解の数字を1つ作る。
チェックする部分は　初級編の関数を利用する。

正解の数字と入力数字が合うまで
各回の予想とhit&blowの出力する

```python

import random
import itertools

# 数字をチェックする
def check_call(call , solve):
    # 処理を書く
    return hit , blow

# ゲームの開始
    
# 処理を書く

```

### 上級編:正解の数字を導き出すアルゴリズム

正解の数字をあらかじめ決めておき、チェック関数の結果を用いて
正解の数字を予想するプログラムを作成してみよう。

ヒント：
チェック関数をうまく使うと、正解の候補を考える事が出来るよ!!

```python

# 処理を書く
```

## 解答編

### 初級編の解答

```python
# 予想と正解をチェックする関数
def check_call(call , solve):
    hit , blow = 0,0
    for i,n in enumerate(call):
        if solve[i]==n:
            hit+=1
        elif n in solve:
            blow +=1
    return hit , blow

print(check_call('564','987'))
print(check_call('564','687'))
print(check_call('564','546'))
print(check_call('564','564'))
```
(0, 0)
(0, 1)
(1, 2)
(3, 0)

### 中級編の解答

まず最初に正解の数字を作る
(10個の数字を使った3桁でダブりのない数字)

10個の数字を用いた順列を作るには
`itertools.permutations(seq, 3)`

順列の中から1つ選ぶには
`random.choice(リストなど)`

繰り返しはWhile文かfor文を使い
hit==3か10回当てられない場合に抜ける。

```python
import random
import itertools

# 数字をチェックする
def check_call(call , solve):
    hit , blow = 0,0
    for i,n in enumerate(call):
        if solve[i]==n:
            hit+=1
        elif n in solve:
            blow +=1
    return hit , blow

# ゲームの開始
# 正解数値の生成
seq = (0,1,2,3,4,5,6,7,8,9)
nums = [str(a)+str(b)+str(c) for a,b,c in itertools.permutations(seq, 3)]
solve = random.choice(nums)

# 10回繰り返すか、正解で終了
count = 0
while True:
    count+=1
    call = input()
    hit , blow = check_call(call , solve)
    print(count , call , '{0}H{1}B'.format(hit , blow))
    if hit==3:
        print('You Win , Solve = {0}'.format(solve))
        break
    if count==10:
        print('You Lose , Solve = {0}'.format(solve))
        break
```
123
1 123 0H1B
789
2 789 0H1B
567
3 567 0H1B
712
4 712 0H1B
254
5 254 0H0B
710
6 710 1H1B
910
7 910 1H0B
817
8 817 0H1B
709
9 709 0H2B
098
10 098 0H1B
You Lose , Solve = 370


### 上級編の解答

チェック関数を使って予想数字から
正解候補の判定結果を全通り求めておく。

集合をうまく使うと正解候補を絞り込む事が出来る。
Pythonでは`set`が集合のデータ型。

積集合(intersection)を使うと
候補同士の重なる部分を抽出できる。

候補の重なりの中から次の予想を行い
繰り返すと、候補が絞られてゆく

サンプルコードはこちら

```python
# 数字をチェックする
def check_call(call , solve):
    hit , blow = 0,0
    for i,n in enumerate(call):
        if solve[i]==n:
            hit+=1
        elif n in solve:
            blow +=1
    return hit , blow

# 候補を絞り込むための組み合わせの辞書を作る
def all_combination_calc(call):
    result_dict = {}
    for i in list(itertools.permutations((0,1,2,3,4,5,6,7,8,9), 3)):
        num = str(i[0])+str(i[1])+str(i[2])
        hit , blow = check_call(call,num)
        key = '{0}H{1}B'.format(hit , blow)
        if key in result_dict:
            tmp = result_dict[key]
            tmp.append(num)
        else:
            tmp = [num]
        result_dict[key] = tmp
    return result_dict

# 正解の数字を解く
def solve_code_break(solve):
    sets = set([])
    call = random.choice([str(a)+str(b)+str(c) for a,b,c in itertools.permutations(seq, 3)])
    hit , blow = check_call(call , solve)
    key = '{0}H{1}B'.format(hit , blow)
    print(call,key)
    if call==solve:
        print('End')
    else:
        while True:
            conbination_dict = all_combination_calc(call)
            s_choice = conbination_dict[key]
            sets = set(s_choice) if len(sets) ==0 else sets.intersection(set(s_choice))
            call = random.choice(list(sets))
            s_hit , s_blow = check_call(call , solve)
            key = '{0}H{1}B'.format(s_hit , s_blow)
            print(call,key)
            if call==solve:
                break
    print('End')

# 数字当てをする
seq = (0,1,2,3,4,5,6,7,8,9)
s_nums = [str(a)+str(b)+str(c) for a,b,c in itertools.permutations(seq, 3)]
solve = random.choice(s_nums)
print('Solve = ', solve)
solve_code_break(solve)
```
Solve =  736
936 2H0B
931 1H0B
906 1H0B
836 2H0B
236 2H0B
736 3H0B
End

## せっかくなのでシミュレーションしてみる

1000回ほど試行して何回で解けるかシミュレーションしてみる

上級編のコードを改変して集計プログラムに直して試行する。

```python
# 数字をチェックする
def check_call(call , solve):
    hit , blow = 0,0
    for i,n in enumerate(call):
        if solve[i]==n:
            hit+=1
        elif n in solve:
            blow +=1
    return hit , blow

# 候補を絞り込むための組み合わせの辞書を作る
def all_combination_calc(call):
    result_dict = {}
    for i in list(itertools.permutations((0,1,2,3,4,5,6,7,8,9), 3)):
        num = str(i[0])+str(i[1])+str(i[2])
        hit , blow = check_call(call,num)
        key = '{0}H{1}B'.format(hit , blow)
        if key in result_dict:
            tmp = result_dict[key]
            tmp.append(num)
        else:
            tmp = [num]
        result_dict[key] = tmp
    return result_dict

# 正解の数字を解くシミュレーションをする
def code_break_sim():
    seq = (0,1,2,3,4,5,6,7,8,9)
    s_nums = [str(a)+str(b)+str(c) for a,b,c in itertools.permutations(seq, 3)]
    solve = random.choice(s_nums)
    term,sets = 1,set([])
    call = random.choice([str(a)+str(b)+str(c) for a,b,c in itertools.permutations(seq, 3)])
    hit , blow = check_call(call , solve)
    key = '{0}H{1}B'.format(hit , blow)
    if call==solve:
        return term
    else:
        while True:
            conbination_dict = all_combination_calc(call)
            second_choice = conbination_dict[key]
            sets = set(second_choice) if len(sets) ==0 else sets.intersection(set(second_choice))
            call = random.choice(list(sets))
            s_hit , s_blow = check_call(call , solve)
            key = '{0}H{1}B'.format(s_hit , s_blow)
            term+=1
            if call==solve:
                return term

calc_dict = {}
for i in range(1000):
    key = code_break_sim()
    if key in calc_dict:
        calc_dict[key]+=1
    else:
        calc_dict[key]=1
        
for k,v in sorted(calc_dict.items()):
    print(k,v)
```
1 2
2 10
3 63
4 196
5 335
6 277
7 86
8 28
9 3

だいたい5回くらいで解くことができそうです。
７回もあればほぼほぼ解き終わっていますね。


## まとめ

コードブレイカー自体は女神転生のミニゲームなので
だいぶ前のゲームでしたが
プログラム的な考え方ができる
いい題材でした。

ヌメロン懐かしいですよね。
今なら無双できる気がします。

再放送するなら
呼んでくれないかなwww

それでは。

## 作者の情報

乙pyのHP：
http://www.otupy.net/

Youtube：
https://www.youtube.com/channel/UCaT7xpeq8n1G_HcJKKSOXMw

Twitter：
https://twitter.com/otupython