Pythonでヌメロン(数字当てゲーム)を実装

## はじめに
　過去にテレビ番組としても放送されていた「Numer0n(ヌメロン)」について、一人でもゲームを楽しめるようにシンプルな内容にしてPythonで実装してみました。
　ヌメロンは2人対戦のゲームで、まずお互いに各桁の数字に重複のない3桁の数を設定します。先攻後攻を決め、先攻が3桁の数を予想してコールします。それに対し後攻は、コールされた数の各数字が設定した数に含まれていればBITE、桁も一致していればEATとして「⚪︎EAT、⚪︎BITE」というように答えます。これを両者順番に繰り返して3桁の数を予想し、先に相手が設定した数を当てることができれば勝ちとなります。番組内ではアイテムがあり、2回連続のコールや設定した数字の変更等が可能でしたが、今回はシンプルに数を予測してコールをし、EATとBITEの判定結果に基づいて正解を当てるという内容となっています。
　また、Flaskでwebアプリ化もしたため、HTMLやCSSのコードとともに掲載します。最終的には以下のような画面でゲームを行い、正解するまでの回答数等に基づいてスコアが出力されるようにしました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3840780/2a057784-7e94-7f24-eae4-20ce86b7309d.png)

## Pythonでの実装
　Pythonでの実装はGoogle Colabolatoryで行いました。まずは、3桁の数numberを以下のようにランダムに生成しました。
```py
# ランダムに3桁の数字を生成
import random

while True:
  number = random.randint(102, 987) # 正解の数字(最小102、最大987)
  num = list(str(number)) # 3つの数字を分割したリストに変換
  if len(set(num)) == len(num): # リスト内に数字の重複がない場合生成完了
    print('3桁の数字を当ててください。')
    break
```
正解の数numberをランダムな3桁の数字として生成し、その後各桁に数字の重複がない場合に生成完了とするように記述しました。
　次に、コールされた数に対してEATとBITEの判定を行う関数を作成します。
```py
# 判定の関数化
ans_his = [] # 判定履歴を格納するリスト

def answer(number):
  ans = list(str(number))
  if (len(ans) >= 4) | (len(ans) != len(set(ans))):
    return print('エラー：各桁の数字が重複しない3桁の数字を入力してください。')
    
  EAT = 0
  BITE = 0

  # 回答の数字が正解に含まれていたらBITE、桁も一致していればEATの判定
  for n in range(3):
    if ans[n] == num[n]:
      EAT += 1
    elif ans[n] in num:
      BITE += 1

  # 回答した数字、EAT数、BITE数を判定履歴に追加
  his = f'{number}: {EAT} EAT, {BITE} BITE'
  ans_his.append(his)
  print(*ans_his, sep='\n')

  # スコアの算出
  eats = []
  for i in ans_his:
    i = i[5] # i[5]は判定履歴の中のEATの数
    eats.append(i)

  base_score = 100 // len(ans_his) # 少ない回答数で正解すれば高得点
  teq_score = 50 // (eats.count('2')+1) # 2EATの回答数が少ないほど高得点
  score = base_score + teq_score

  # 正解までの回答数とスコアを表示
  if EAT == 3:
    return print('\n3 EAT！ {}回目で正解!\nスコア：{}'.format(len(ans_his), score))
```
まず、判定結果の履歴を参考にしながら予想をするため、履歴を格納するためのリストを定義します。関数answer()では、はじめにコールされた数字が4桁以上であったり、各桁に重複する数字が含まれていた場合のために、エラーメッセージを設定しました。次に、正解の各桁の数字をリスト化したnumと、コールされた数の各桁の数字をリスト化したansを比較し、EATとBITEの判定結果を履歴に追加します。今回は一人で遊ぶ仕様となっているため、スコアを導入して、正解するまでのステップに対する評価結果を出力するようにしました。例として、今回はより少ない回答数で正解まで辿り着くほど、また大きなヒントとなる2EATと判定された数が少ないほど、正解時のスコアが高得点となるように定義しました。最後に、3EATとなった時に正解までの回答数と算出されたスコアを出力するようにしました。
　では、実際に上記を実行してゲームを開始してみます。まず問題なく3桁の数字が生成されると「3桁の数字を当ててください。」というメッセージが出力されます。そして関数を実行後、以下のようにanswer()の引数に3桁の数を入力して実行します。 
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3840780/8d4cdb19-8acd-c66d-d601-f7046c9c9655.png)

上記のようにEATとBITEの数が返ってきました。引数の数を変更して再度実行すると、上記の判定結果と合わせて2回目の判定結果が出力されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3840780/a1280757-a9fc-feb1-4f89-efb8d8c26c3e.png)

ここで、試しに重複する数字を含む3桁の数を入力すると、以下のようにエラーメッセージが返されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3840780/0a5bd187-a92a-66a6-8dfd-b66bb3eb2909.png)

再び正解を目指して繰り返し実行します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3840780/a3862e71-5d35-975e-932c-c58eed0bf8b4.png)

最終的に6回目で正解することができました。スコアは100を回答数の6で割った商と、50を2EATの数+1で割った商の合計としていたため、16+25=41となりました。なお、正解は変数numberに格納されているため、念の為正解と一致していることを確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3840780/d839a274-24dc-f92f-4dec-1e66c76b31e2.png)

これでヌメロンの実装が完了しました。

## Webアプリ化
　WebアプリはFlaskによって実装し、htmlとcssでページのレイアウト等をデザインしました。フォルダやファイルの構造としては、今回はnum_appというフォルダ内にゲームの内容のコードを記載したnum.pyと、staticおよびtemplatesフォルダを作成し、それぞれのフォルダ内にstyle.cssとindex.htmlを作成しました。
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3840780/b5263b84-f7bb-584c-a250-cc9e7e327ce7.png)

各ファイルのコードは以下の通りです。
```py:num.py
from flask import Flask, render_template, request, session
import random

app = Flask(__name__)
app.secret_key = 'secret_key'

# スタート時の3桁のランダム数字生成
def generate_number():
    while True:
        number = random.randint(102, 987)
        num = list(str(number))
        if len(set(num)) == len(num):
            return num

# 判定の関数
def check_answer(number, answer):
    EAT = 0
    BITE = 0
    for i in range(3):
        if answer[i] == number[i]:
            EAT += 1
        elif answer[i] in number:
            BITE += 1
    return EAT, BITE

@app.route('/')
def index():
    session['number'] = generate_number()  # ランダムな3桁の数字を生成
    session['history'] = []  # 判定履歴のリセット
    session['attempts'] = 0  # 試行回数のリセット
    return render_template('index.html')

@app.route('/guess', methods=['POST'])  # POSTメソッドを許可
def guess():
    user_input = request.form['number']  # ユーザーが入力した数字
    if len(user_input) != 3 or len(set(user_input)) != 3:
        error = "エラー：数字の重複がない3桁の数を入力してください"
        return render_template('index.html', error=error, history=session['history'])

    number = session.get('number')  # 正解の数字を取得
    EAT, BITE = check_answer(number, list(user_input))  # 判定

    session['attempts'] += 1  # 試行回数の更新
    result = f'{user_input}: {EAT} EAT, {BITE} BITE'
    session['history'].append(result)  # 判定結果を履歴に追加

    if EAT == 3:
        two_eat_count = sum('2 EAT' in entry for entry in session['history'])
        score = (100 // session['attempts']) + (50 // (two_eat_count + 1)) 
        return render_template('index.html', success=True, history=session['history'], score=score)

    return render_template('index.html', history=session['history'])

if __name__ == '__main__':
    app.run(debug=True)

```
```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>数字当てゲーム</title>
    <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
    <div class="container">
        <div class="left">
            <h1>0~9を1度だけ使ってできる3桁の数を当ててください</h1>

            <form action="/guess" method="POST">
                <input type="text" name="number" placeholder="半角3桁の数を入力">
                <button type="submit">Enter</button>
            </form>

            {% if error %}
                <p>{{ error }}</p>
            {% endif %}

            {% if success %}
                <h3>3 EAT！ {{ history|length }}回目で正解！</h3>
                <p>Score：{{ score }}</p>
            {% endif %}
        </div>

        <div class="right">
            <h2>Result</h2>
            <ul>
                {% for item in history %}
                    <li>{{ item }}</li>
                {% endfor %}
            </ul>
        </div>
        <a href="/" class="play-again">もう一度プレイ</a>
    </div>

</body>
</html>

```
```css:style.css
body {
    font-family: Arial, sans-serif;
    background-color: #050505;
    margin: 0;
    padding: 2px;
    display: flex;
    justify-content: center;
    align-items: center;
    height: 600px;
}

.container {
    display: flex;
    justify-content: space-between;
    background-color: #4b4a4e;
    padding: 20px;
    border-radius: 8px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
    width: 80%;
    max-width: 800px;
}

.left {
    width: 70%;
}

.right {
    width: 25%;
}

h1 {
    color: white;
    font-size: 22px;
}

h2 {
    color: white;
    font-size: 20px;
}

form {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    margin-bottom: 20px;
}

input[type="text"] {
    padding: 10px;
    font-size: 14px;
    border: 1px solid #080707;
    border-radius: 4px;
    width: 120px;
    margin-bottom: 15px;
    background-color: black;
    color: white;
}

button {
    padding: 10px 15px;
    background-color: #0de7ea;
    color: black;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 16px;
    width: 140px;
}

button:hover {
    background-color: #091bdf;
    color: white;
}

ul {
    list-style-type: none;
    padding: 0;
    margin-bottom: 20px;
    color: white;
}

li {
    font-size: 18px;
    padding: 1px;
    margin-bottom: 5px;
    border-radius: 4px;
}

h3 {
    color: #0bf044;
    font-size: 20px;
}

p {
    color: red;
    font-size: 21px;
}


a {
    color: #00bbff;
    text-align: center;
    display: block;
    margin-top: 15px;
}

a:hover {
    text-decoration: underline;
}

.play-again {
    position: absolute;
    bottom: 150px;
    left: 50%;
    transform: translateX(-50%);
    height: 10px;
}

```

### アプリの起動
 num.pyと同じ階層にhtml、cssファイルが保存されたフォルダを保存して、以下のような構造とします。
 
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3840780/d20a46cb-c19b-b670-ca4f-c6aaa5a00e51.png)

ターミナルで現在のディレクトリをnum.pyが保存されているフォルダ(上記ではnum_app)として、以下の`python num.py`を実行します。
```
'各ファイル・フォルダが保存されているフォルダのパス名' % python num.py
```
以下のように出力されたURLのwebページを開きます。
```
* Serving Flask app 'num'
 * Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:5000
Press CTRL+C to quit
```
ページに移動すると、以下のようなゲーム画面となるため、数字を入力してEnterをクリックすることで回答することができます。また、下部の「もう一度プレイ」をクリックすることで、もう一度最初からゲームをプレイすることができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3840780/b10fd825-fd85-333f-cb1d-eb6ff121cf1a.png)

## 最後に
　今回はPythonでヌメロンの実装を行い、webアプリ化してゲームをプレイできるようにしました。Pythonのみでなく、HTMLやCSSもまだまだ初心者のため、これからさらに色々なwebアプリの作成に取り組みながら勉強したいと思います。