メガテンのコードブレイカーご存知でしょうか?

今回はその、コードブレイカーで
プログラミングを考えていきたいと思います。



解説動画はこちら




コードブレーカーは
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を作ってみよう

def check_call(call , solve):

    処理

    return  (hit , blow)

引数:call=予想数字 , solve=正解数字
戻り値:(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)





・・・
・・





初級編解答:

ポイント:

数字が他の桁に有るかどうかは
in で簡単に判定できる

'検索したい文字' in '文字列'

答え用の変数に0を入れておき
hit , blowがあれば足す。

コードはこんな感じになりました。

# 予想と正解をチェックする関数
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)



中級編:

コードブレイカーのゲームを作ってみよう

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

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

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

ヒント:

まず正解の数字を1つ作る
チェックする部分は初級編の関数を利用する
正解の数字と入力数字が合うまで
各回の予想とhit&blowの出力する





・・・
・・









中級編の解答:

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

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

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

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

コードはこんな感じになりました。
入力に数字を打ち込んであげると
判定結果が返ってきます。

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

ぐう、ダメでしたね。
当てられないとは

0H0Bをうまく使うのが
当てるコツですね。



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

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

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



・・・
・・






上級編の解答:

まず、チェック関数を使って予想数字から
正解数字の判定結果を全通り求めておきます。

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

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

1回目:
正解 XYZ , 予想 123 , 判定結果 1H1B

予想が1H1Bになる数字の組み合わせは
124 , 125 , ・・・・

2回目:
予想が1H1Bになる数字の組み合わせの中から
次の予想をする

1回目と2回目の結果の
積集合を求める

以降繰り返し・・・

どんどん集合を使って候補を絞り込みます。

コードはこんな感じになりました。

# 数字をチェックする
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

# 数字当てをする
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)

# 正解の数字を解く
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)
            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)
            print(call,key)
            if call==solve:
                break
    print('End')

solve_code_break(solve)
Solve =  736
936 2H0B
931 1H0B
906 1H0B
836 2H0B
236 2H0B
736 3H0B
End


せっかくなんで
何回で当てれるものなのか
シミュレーションしてみましょう。

先ほどのプログラムを改良して
解いた時の回数を出すようにして
1000回試行して何回で解けるのか
集計します。

# 正解の数字を解くシミュレーションをする
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回以内には解けそうですね。
多くても7回ほどあれば解答に
たどり着けそうです。

10回以上かかると
やばそうですね。
とほほほ

コードブレイカーは
ヌメロンと言う名前で
テレビ番組用のゲームとして
使われていたこともあったようです。

遊びたい方はみてみてください。

今回はこれまでです
それでは。