Pythonでつくるアイテム付きNumer0n

Qiitaへの投稿は2度目になります。よろしくお願いします。

今回は、かつて一世を風靡したあのゲーム「Numer0n」を、Pythonで実装しました。

#1 Numer0nとは？

[Wikipediaから引用](https://ja.wikipedia.org/wiki/Numer0n):
> それぞれのプレイヤーが、0-9までの数字が書かれた10枚のカードのうち3枚を使って、3桁の番号を作成する。カードに重複は無いので「550」「377」といった同じ数字を2つ以上使用した番号は作れない。
先攻のプレイヤーは相手の番号を推理してコールする。相手はコールされた番号と自分の番号を見比べ、コールされた番号がどの程度合っているかを発表する。数字と桁が合っていた場合は「EAT」（イート）、数字は合っているが桁は合っていない場合は「BITE」（バイト）となる。
例として相手の番号が「765」・コールされた番号が「746」であった場合は、3桁のうち「7」は桁の位置が合致しているためEAT、「6」は数字自体は合っているが桁の位置が違うためBITE。EATが1つ・BITEが1つなので、「1EAT-1BITE」となる。
これを先攻・後攻が繰り返して行い、先に相手の番号を完全に当てきった（3桁なら3EATを相手に発表させた）プレイヤーの勝利となる。

今回は、(3桁では満足できない皆様のためにも)桁数を指定できるプログラムを実装しました。

#2 基本プログラム

```python:Numer0n_Basic.py
#Numer0n_Basic
import random

#お題となる番号が決められます
proper = 0
while proper == 0:
    print("Digit?")
    #桁数を決めます
    D = int(input())
    N = []
    #桁数は1桁から10桁まででないといけません
    if 1 <= D <= 10:
        while len(N) < D:
            n = random.randint(0,9)
            if n in N:
                pass
            else:
                N.append(n)
        proper += 1
    else:
        print("Improper.")

#数当てゲーム開始
count = 1
eat = 0
bite = 0

while eat < D:
    print("Call " + str(count) )
    call = input()
    #正しいCallのとき、判定を行います
    if len(call) == D and len(set(call)) == len(call):
        eat = 0
        bite = 0
        for i in range(D):
            for j in range(D):
                if str(N[i]) == call[j]:
                    if i == j:
                        eat += 1
                    else:
                        bite += 1
        if eat == D:
            print(str(D) + " EAT")
            #ONE CALL達成時のみ特別な演出を加えます
            if count == 1:
                print("ONE CALL")
            else:
                print("Numer0n in " + str(count) + " Calls")
        else:
            print(str(eat) + " EAT " + str(bite) + " BITE")
            count += 1
    #Miss Callの場合の対応です
    else:
        print("Again!")
```

桁数`D`を変えることによって、本家で登場した3桁や4桁の他、5桁以上の数字を当てるゲームにも挑戦できます。
基本的に自分が攻めるだけのゲームであるため、「何回で当てられたか」というCallの回数を表示することにより、スコアアタック的要素を加えています。目指せONE CALL!

このプログラムでは、元の数字`N`は`int`型に、入力した数字`call`は文字列型になっており、比較の部分では`N`を文字列型に直して判定を行っています。なんでこんな面倒なことをしているのかの理由は、このあと実装する**「アイテム」**のためです。

#3 アイテム
Numer0nがHit and Blowと一線を画する点は、**アイテム**の存在にあります。

Numer0nでは、攻撃や防御の手助けとなる様々なアイテムが存在します。
今回はそれらのうち、攻撃系アイテムの3つを実装していきます。

##3.1 HIGH & LOW
>相手の全ての桁の数字が、それぞれ「HIGH (5-9)」「LOW (0-4)」のどちらかを知ることができる。相手は左の桁から順に宣言をさせられる（例：「809」ならば「HIGH (8)・LOW (0)・HIGH (9)」）。序盤ではある程度の番号を絞り込むことができ、終盤では番号の確定にも役立たせることができる。(Wikipedia)

全ての桁の数字を絞り込める強力なアイテム。3桁の場合、最初にHIGH & LOWを使うことで、悪くとも候補を720個から100個まで、うまくいけば60個まで絞り込むことができます。

このアイテムをPythonで実装するとこうなります。

```python:high_and_low.py
#HIGH and LOW
N = [int(input()) for _ in range(3)]
HL = ""
for i in range(len(N)):
    if N[i] <= 4:
        HL += 'L '
    else:
        HL += 'H '
print(HL)
```

桁ごとに数字を入力すると、数字のHIGH/LOWを返します。

入力
`5
2
9`

出力
`H L H`

4との大小比較をするだけなので簡単ですね。

##3.2 SLASH
>相手が使っているナンバーの最大数から最小数を引いた「スラッシュナンバー」を訊くことができる。例として「634」ならば、最大数「6」−最小数「3」=スラッシュナンバー「3」となる。
もしスラッシュナンバーが「9」だった場合、最大数「9」・最小数「0」での可能性しかないため、0と9の使用が確定する。また、「2」だった場合は必然的に「012」「123」のような連続した3種類の数字で構成されることがわかる。使い方次第では大勢がわかる他、使わず持っているだけで「9と0の使用」や「連続数字の使用」などへの抑止力として使える。
考えられる数字の使用パターンを「バッサリ斬り捨てる」ことができることから名付けられた。(Wikipedia)

こちらも相手の数字を一気に絞り込めるアイテム。HIGH & LOWよりも使い方が難しいですが、ここぞの場面で使うと一気に使用している数字を確定できます。とくに、引用部分にもあるように「9と0の併用」など、特定の並びに対しては効果抜群。

このアイテムをPythonで実装するとこうなります。

```python:slash.py
#SLASH
N = [int(input()) for _ in range(3)]
SN = max(N) - min(N)
print(SN)
```

桁ごとに数字を入力すると、その数字のスラッシュナンバーをを返します。

入力
`5
2
9`

出力
`7`

これもそうたいしたことはしてないですね。max関数最高！
ちなみに、スラッシュナンバーが7になるのは、「7,0とその間の数字」「8,1とその間の数字」「9,2とその間の数字」というパターンに絞られます。それまでののCallの結果によってはここからより可能性を消していくこともできます。

##3.3 TARGET
>10種類の番号のうち1つを指定して相手に訊くことができる。その番号が相手の組み合わせに含まれている場合は、どの桁に入っているかも判明する。
特に「含まれていることはわかるがどの桁かはわからないBITE状態の数字」を指定すれば、桁のありかを確実に知ることができる。逆に、訊いた数字が含まれていなければ、他の数字を使うことで絞り込みができる。(Wikipedia)

ゲーム中盤から終盤で使われるアイテムです。残り少ない可能性からの絞り込みに有効です。

このアイテムをPythonで実装するとこうなります。

```python:target.py
#TARGET
N = [int(input()) for _ in range(3)]
TN = int(input())
judge = 0
for i in range(len(N)):
    if N[i] == TN:
        print("Place " + str(i+1))
        judge = 1
if judge == 0:
    print("Nothing.")
```

数字を入力、さらに10種類の数字から`TN`(これはTARGET NUMBERの略)をひとつ入力すると、含まれている場合はその位置(左から何番目か)、含まれていない場合はNothing.を返します。

入力
`5
2
9`

ターゲットナンバー1
`2`

出力1
`Place 2`

ターゲットナンバー2
`7`

出力2
`Nothing.`

breakを使ってもいいのですが、高々ループを10回回せばいいだけなので別にいいですね。

#4 アイテムつきNumer0n
上記3つのアイテムを組み込んだ、Numer0nのプログラムがこちらです。

ルールとして、

* アイテムはどれか一つのみが一回だけ使えます。
* アイテムの使用はCallの回数に入りません。例えば、はじめにSLASHを使用し、次のCallで数字を当てられた場合はONE CALLとなります。

```python:Numer0n_Item.py
#Numer0n_Item
import random

#お題となる番号を決めます
proper = 0
while proper == 0:
    print("Digit?")
    #桁数が決められます
    D = int(input())
    N = []
    #桁数は1桁から10桁まででないといけません
    if 1 <= D <= 10:
        while len(N) < D:
            n = random.randint(0,9)
            if n in N:
                pass
            else:
                N.append(n)
        proper += 1
    else:
        print("Improper.")

#数当てゲーム開始
count = 1
eat = 0
bite = 0
#アイテムは合計1回までしか使えません
item = 1
while eat < D:
    print("Call " + str(count) )
    call = input()
    #正しいCallのとき、判定を行います
    if len(call) == D and len(set(call)) == len(call):
        eat = 0
        bite = 0
        for i in range(D):
            for j in range(D):
                if str(N[i]) == call[j]:
                    if i == j:
                        eat += 1
                    else:
                        bite += 1
        if eat == D:
            print(str(D) + " EAT")
            #ONE CALL達成時のみ特別な演出を加えます
            if count == 1:
                print("ONE CALL")
            else:
                print("Numer0n in " + str(count) + " Calls")
        else:
            print(str(eat) + " EAT " + str(bite) + " BITE")
            count += 1
            
    #HIGH AND LOW
    elif call == "HIGH AND LOW":
        if item == 1:
            HL = ""
            for i in range(len(N)):
                if N[i] <= 4:
                    HL += 'L '
                else:
                    HL += 'H '
            print(HL)
            item = 0
        else:
            print("Already used.")
                
    #SLASH
    elif call == "SLASH":
        if item == 1:
            SN = max(N) - min(N)
            print(SN)
            item = 0
        else:
            print("Already used.")
    
    #TARGET
    elif call == "TARGET":
        if item == 1:
            print("TARGET Number?")
            TN = int(input())
            judge = 0
            for i in range(len(N)):
                if N[i] == TN:
                    print("Place " + str(i+1))
                    judge = 1
            if judge == 0:
                print("Nothing.")
            item = 0
        else:
            print("Already used.")
    #Miss Callの場合の対応です
    else:
        print("Again!")
```

`call`を文字列型で受け取ったのは、アイテムの名前を宣言してアイテムを使えるようにしたかったのが主な理由でした。
`TARGET`と`Target`,`target`などの表記ゆれには対応できませんがまあそれは許して。

#5 感想
Qiita記事にもPythonでNumer0nを実装していた方は多かったのですが、アイテムの実装がなかったので自分でやってみました。割とうまくいったのでよかったです。

人対人のゲームにしても面白そうですね。そうすると今回は実装しなかったDOUBLE, SHUFFLE, CHANGEの存在意義も生えそうです。単純にめんどくなりそうなのと、相手に出力を見られたら困るのでそこらへんの実装が苦しそうなのが課題。

CPUが攻めもやるゲームはさすがに僕の実力では厳しそうなのでだれか興味ある人がいたらやってください。

遊んでみた感想、改善点の指摘などのコメントお待ちしております。

それでは。