Azureを使ってポケモンクイズ作ってみた

こんにちは。コーディング課3回生のYoneです。

前回は、Microsoft Azureで仮想マシンの作成・削除、SSH接続の方法を解説しました。今回は、その仮想マシン上にPokeAPIを使ったポケモンクイズのWebサイトを構築して、外部からアクセスできるようにします。

前回の記事はこちら↓

はじめに

ここから先は、仮想マシンにSSH接続した状態で行います。sshコマンドで仮想マシンにログインしておいてください。また、Linuxコマンドについての詳細な説明は行いませんので、詳しく知りたい場合は各自で調べてください。

Webサイトを制作するにはいくつか方法がありますが、今回は、PythonのWebアプリケーションフレームワークであるFlaskを使用します。

Flaskについては、以下の動画が参考になります。

Flaskの環境構築

Flaskの環境を構築します。

準備

まず、sudo apt updatesudo apt upgradeでLinuxのシステムを更新しておきます。

Flaskをインストールするためにpipコマンドを使うのですが、Azureではそのまま行うとエラーが出る(「システム管理外の環境でパッケージを直接管理することを制限する」という仕様、らしい)ため、venvを使ってPythonの仮想環境を作成します。

python3 -VでPythonのバージョンを調べて、そのバージョンに合ったvenvモジュールをインストールします。

今回、python3 -Vの結果が「Python 3.12.3」だったので、sudo apt install python3.12-venvで、venvをインストールします。

python3 -m venv .venvで仮想環境を作成すると、.venvディレクトリが作成されます。「.venv」の部分は仮想環境名なので好きに変更しても大丈夫です。直接使用するディレクトリではないので、ディレクトリ名に「.」を付けて隠しディレクトリにしています。(隠しディレクトリは、lsコマンドで確認するには-aオプションが必要です。)

source .venv/bin/activateで仮想環境を有効化します。そうすると、プロンプトの初めに仮想環境名が表示されます。例では「(.venv)」。

この後の作業は仮想環境内で行います。仮想環境を無効化するには、deactivateと入力します。

ちなみに、仮想環境を削除するには、作成したディレクトリ(今回は.venv)を削除すればOKです。

Flaskのインストール

次にFlaskをインストールします。

pip install flask

flaskがインストールされたことを確認するには、以下のコマンドを入力してください。

flask --version

このようにFlaskのバージョン情報が表示されたら、正常にインストールできています。

テストページ

Flaskの環境構築ができたら、まずは簡単なテストページを表示してみましょう。

ディレクトリ構成

以下の図のようにディレクトリを構成してください。

Flaskのディレクトリ構成はこのように、templatesディレクトリ内にHTMLファイルを配置する必要があります。

main.py

from flask import Flask, render_template

app = Flask(__name__)

# ルートディレクトリ
@app.route('/', methods=['GET'])
def home():
    return render_template('index.html')

# サブディレクトリ
@app.route('/test', methods=['GET'])
def test():
    return render_template('index2.html')

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=5000)

index.html

<!DOCTYPE html>
<html lang=”ja”>
    <head>
        <meta charset=”UTF-8″>
        <title>テストページ</title>
    </head>
    <body>
        <h1>テストページ</h1>
        <p>これはテストページです。</p>
    </body>
</html>

index2.html

<!DOCTYPE html>
<html lang=”ja”>
    <head>
        <meta charset=”UTF-8″>
        <title>テストページ2</title>
    </head>
    <body>
        <h1>テストページ2</h1>
        <p>これはテストページ2です。</p>
    </body>
</html>

説明

HTMLについての解説は省略します。

HTTPプロトコルには、GET、POSTなどのメソッドがあり、GETはデータを取得するため、POSTはデータをサーバーに送信するために使われます。

ブラウザにURLを入力してアクセスした場合はGETでアクセスします。ページ内のmain.pyでは、2つのURLパス(「http://・・・/」と「http://・・・/test」)が設定されています。’/’にGETでアクセスするとindex.htmlが表示され、’/test’にGETでアクセスするとindex2.htmlが表示されます。

ファイル作成方法

ファイル作成に関しては、Linux初心者でも使いやすいnanoエディタがおすすめです。

nanoエディタを起動するには、nano <ファイル名>を実行します。指定したファイル名が存在する場合はそのファイルを編集できます。存在しない場合は、新しいファイルが作成されます。

テキストを入力するだけで編集でき、キーボードの上下左右キーでカーソルの移動ができます。

ファイルを保存するには、Ctrl+Oを押してからEnterを押します。ファイル名を変更したい場合は、保存時に名前を入力してEnterを押します。終了するには、Ctrl+Xを押します。保存されていない場合は保存するか確認されます。

実行してみる

先ほどの図のようにディレクトリを配置できたら、main.pyを実行してアクセスできるようにしてみましょう。

python main.py

これが実行できたらブラウザで確認したいところですが、その前に1つ設定しておくことがあります。

このWebサイトは5000番ポートで待ち受けていますが、この仮想マシンを作成するときの受信ポートの規則より、22番ポートのアクセスしか許可されていません。なのでこの設定を変更する必要があります。

受信ポートの規則は、Azureポータル画面の、ホームー>リソースグループー>作成したリソースグループ名(今回はtest_group)ー><仮想マシン名>_nsg(今回はtest_nsg)ー>設定ー>受信セキュリティ規則ー>追加 で設定できます。

「宛先ポート範囲」を5000にして追加すると、5000番ポートへのアクセスができるようになります。

この設定ができたら、ブラウザ上で、http://<仮想マシンのIPアドレス>:5000(/test) にアクセスしてください。今回の例では、http://20.243.80.142:5000 と http://20.243.80.142:5000/test です。

(画面の表示には、「Running on http://127.0.0.1:5000」とありますが、Azure等の手元にない別のマシンでプログラムを実行している場合は、ここにアクセスしてもページは表示されません。127.0.0.1(ループバックアドレス)は、自分自身を表す特別なIPアドレスです。仮想マシンの外部からアクセスするにはグローバルIPアドレスを使用します。サーバーとクライアントが同じPCの場合の場合はループバックアドレスでアクセスできます。)

以下のようにテストページが表示されたら成功です。

ルートディレクトリ(http://20.243.80.142:5000)

サブディレクトリ(http://20.243.80.142:5000/test)

プログラムを停止するには、Ctrl + Cを押してください。

PokeAPIを使ってみる

テストページができたので、次はPokeAPIを使ってポケモンの情報を取得して表示するWebページを作成してみます。

PokeAPIとは?

そもそもAPIとは、クライアントがリクエストを送信すると、そのリクエストに対応するレスポンスを返すものです。

PokeAPIは、ポケモンに関連する情報を提供する無料のAPI(非公式)であり、このAPIを使用することで、ポケモンの名前、タイプ、進化情報、技、アイテム、特性など、ゲームに関連する幅広いデータ(すべて英語)を取得することができます。レスポンスはJSON形式なので、その中から必要な部分を抜き出して使用します。

例えば、以下にブラウザでアクセスすると、ピカチュウに関するデータを取得できます。

https://pokeapi.co/api/v2/pokemon/pikachu

ピカチュウは全国図鑑ナンバーが25番なので、こちらのリンクでも同じ情報を取得できます。

https://pokeapi.co/api/v2/pokemon/25

move(技)セクションにthunderbolt(10万ボルト)がありました。

spritesセクションにはピカチュウの画像のリンクがあります。

ポケモンの情報を表示してみる

PokeAPIを使うには、HTTPリクエストを送信する必要がありますが、PythonのRequestsライブラリを使うことで簡単に実装できます。

pip install requests

ディレクトリの構成を以下の図のようにします。

main.py

from flask import Flask, render_template, request
import requests

app = Flask(__name__)

@app.route('/', methods=['GET', 'POST'])
def index():
    pokemon_data = None
    errorcode = None

    if request.method == 'POST': # POSTでアクセスされたとき
        pokemon_name = request.form.get('pokemon_name').lower() # フォームから送信された文字列を取得
        api_url = f'https://pokeapi.co/api/v2/pokemon/{pokemon_name}'

        response = requests.get(api_url)
        if response.status_code == 200:
            data = response.json()

            pokemon_data = {
                'name': data['name'],
            'height': data['height'],
            'weight': data['weight'],
            'sprites': data['sprites']['front_default']
            }
        else:
            errorcode = response.status_code

    return render_template('index.html', pokemon_data=pokemon_data, errorcode=errorcode)

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=5000)

index.html

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>ポケモン</title>
    </head>
    <body>
        <h1>ポケモンの情報</h1>
        <form method="POST">
            <label for="pokemon_name">ポケモンの名前を入力(英語):</label>
            <input type="text" id="pokemon_name" name="pokemon_name" required>
            <button type="submit">検索</button>
        </form>
        {% if pokemon_data %}
            <ul>
                <li><strong>名前:</strong> {{ pokemon_data.name }}</li>
                <li><strong>たかさ:</strong> {{ pokemon_data.height }}</li>
                <li><strong>おもさ:</strong> {{ pokemon_data.weight }}</li>
            </ul>
            <h2>すがた</h2>
            <img src="{{ pokemon_data.sprites }}" alt="{{ pokemon_data.name }}のすがた">
        {% elif errorcode %}
            <p>データの取得に失敗 ステータスコード: {{ errorcode }}</p>
        {% endif %}
    </body>
</html>

説明

WebページにGETメソッドでアクセスした場合は、検索ボックスしか表示されませんが、検索ボタンを押すことでPOSTメソッドでアクセスし、サーバーに情報を送信することができます。サーバーは送られてきたポケモン名に対応するデータをPokeAPIで取得し、HTMLにデータを埋め込んでレスポンスとして送信します。

PokeAPIのデータはJSON形式で取得できるので、それを辞書形式に変換してから必要なデータをまとめています。HTML上では、Flaskと一緒にインストールされるJinja2というテンプレートエンジンにより、{% <制御構造(if や for など)> %}でPythonの制御構造が記述でき、{{ <変数> }}で変数の値を出力できます。

実行方法はテストページと同じで、main.pyを実行します。

以下は、「pikachu」と入力した例です。

クイズの制作

これまでFlaskやPokeAPIについて、基本的なことを解説しました。これらを組み合わせることでようやくクイズが作れます。

コード

コードに関してですが、1つ1つ説明しているときりがないのと、先ほど説明したことの組み合わせなので、(あと疲れたので)詳細な説明は省きます。

ディレクトリ構成は「ポケモンの情報を表示してみる」セクションでの構成と同じです。

main.py

# app.py
from flask import Flask, render_template, request, jsonify
import requests
import random
from typing import List, Dict
import json

app = Flask(__name__)

# 世代ごとの範囲定義
GENERATION_RANGES = {
    1: (1, 151),
    2: (152, 251),
    3: (252, 386),
    4: (387, 493),
    5: (494, 649),
    6: (650, 721),
    7: (722, 809),
    8: (810, 905),
    9: (906, 1010)
}

# タイプの日本語変換
TYPE_TRANSLATIONS = {
    'normal': 'ノーマル',
    'fire': 'ほのお',
    'water': 'みず',
    'grass': 'くさ',
    'electric': 'でんき',
    'ice': 'こおり',
    'fighting': 'かくとう',
    'poison': 'どく',
    'ground': 'じめん',
    'flying': 'ひこう',
    'psychic': 'エスパー',
    'bug': 'むし',
    'rock': 'いわ',
    'ghost': 'ゴースト',
    'dragon': 'ドラゴン',
    'dark': 'あく',
    'steel': 'はがね',
    'fairy': 'フェアリー'
}

def get_random_pokemon_id(generation: int = None) -> int:
    """指定された世代からランダムなポケモンIDを取得"""
    if generation:
        start, end = GENERATION_RANGES[generation]
    else:
        start, end = 1, 1010
    return random.randint(start, end)

def get_japanese_ability_name(ability_url: str) -> str:
    """特性の日本語名を取得"""
    try:
        ability_response = requests.get(ability_url)
        if ability_response.status_code == 200:
            ability_data = ability_response.json()
            ja_name = next((name['name'] for name in ability_data['names'] 
                          if name['language']['name'] == 'ja'), None)
            if ja_name:
                return ja_name
            
            # 日本語名が見つからない場合は英語名を使用
            en_name = next((name['name'] for name in ability_data['names']
                          if name['language']['name'] == 'en'), None)
            if en_name:
                return en_name
    except requests.exceptions.RequestException as e:
        print(f"Error fetching ability data: {e}")
    
    # データ取得に失敗した場合は特性の取得をスキップ
    return None

def get_pokemon_data(pokemon_id: int) -> Dict:
    """指定されたIDのポケモンデータを取得"""
    try:
        # species情報を取得(日本語名、分類、説明文のため)
        species_response = requests.get(f"https://pokeapi.co/api/v2/pokemon-species/{pokemon_id}")
        if species_response.status_code != 200:
            return None
        
        species_data = species_response.json()
        # 日本語名を取得
        ja_name = next((name['name'] for name in species_data['names'] 
                       if name['language']['name'] == 'ja'), None)
        
        # 分類を取得
        categories = [genus for genus in species_data['genera'] 
                    if genus['language']['name'] == 'ja']
        category = categories[0]['genus'] if categories else None
        
        # 説明文を取得(最新の日本語の説明文を優先)
        descriptions = [entry for entry in species_data['flavor_text_entries'] 
                     if entry['language']['name'] == 'ja']
        if descriptions:
            # 最新の説明文を使用(配列の最後の要素)
            description = descriptions[-1]['flavor_text'].replace('\\n', ' ').replace('\\f', ' ').replace('\n', ' ').replace('\f', ' ')
        else:
            # 日本語の説明文が見つからない場合はそのヒントを表示しない
            description = None
        
        # 基本情報を取得
        pokemon_response = requests.get(f"https://pokeapi.co/api/v2/pokemon/{pokemon_id}")
        if pokemon_response.status_code != 200:
            return None
        
        pokemon_data = pokemon_response.json()
        types = [TYPE_TRANSLATIONS[t['type']['name']] for t in pokemon_data['types']]
        sprite_url = pokemon_data['sprites']['other']['official-artwork']['front_default']
        
        # 特性の日本語名を取得
        abilities = []
        for ability_data in pokemon_data['abilities']:
            ability_name = get_japanese_ability_name(ability_data['ability']['url'])
            if ability_name:  # 特性名が取得できた場合のみ追加
                if ability_data['is_hidden']:
                    abilities.append(f"{ability_name}(夢特性)")
                else:
                    abilities.append(ability_name)
        
        # 世代を判定
        generation = next(gen for gen, (start, end) in GENERATION_RANGES.items() 
                        if start <= pokemon_id <= end)
        
        return {
            'id': pokemon_id,
            'name_ja': ja_name,
            'types': types,
            'generation': generation,
            'sprite_url': sprite_url,
            'height': pokemon_data['height'] / 10,
            'weight': pokemon_data['weight'] / 10,
            'abilities': abilities,
            'category': category,
            'description': description,
            'base_exp': pokemon_data['base_experience']
        }
    
    except requests.exceptions.RequestException as e:
        print(f"Error fetching pokemon data: {e}")
        return None

def generate_hints(pokemon: Dict) -> List[str]:
    """ポケモンに関するヒントを生成"""
    hints = [
        f"このポケモンは第{pokemon['generation']}世代のポケモンです。",
        f"{pokemon['category']}です。",
        f"タイプは{' / '.join(pokemon['types'])}です。",
        f"特性は「{'」「'.join(pokemon['abilities'])}」です。" if pokemon['abilities'] else None,
        f"このポケモンの高さは{pokemon['height']}mです。",
        f"このポケモンの重さは{pokemon['weight']}kgです。",
        f"基礎経験値は{pokemon['base_exp']}です。",
        pokemon['description'],
        f"図鑑番号は{pokemon['id']}番です。",
        f"名前は{len(pokemon['name_ja'])}文字です。",
        f"名前の最初の文字は「{pokemon['name_ja'][0]}」です。"
    ]
    return hints

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/api/start-game', methods=['POST'])
def start_game():
    filters = request.json
    max_attempts = 5
    
    for _ in range(max_attempts):
        try:
            generation = filters.get('generation')
            pokemon_id = get_random_pokemon_id(int(generation) if generation else None)
            pokemon_data = get_pokemon_data(pokemon_id)
            
            if pokemon_data:
                if 'type' in filters and filters['type']:
                    if TYPE_TRANSLATIONS[filters['type']] not in pokemon_data['types']:
                        continue
                
                hints = generate_hints(pokemon_data)
                return jsonify({
                    'hints': hints,
                    'answer': pokemon_data['name_ja'],
                    'sprite_url': pokemon_data['sprite_url']
                })
        
        except Exception as e:
            print(f"Error in start_game: {e}")
            continue
    
    return jsonify({'error': '条件に合うポケモンが見つかりませんでした。条件を変更してお試しください。'}), 400

@app.route('/api/check-answer', methods=['POST'])
def check_answer():
    user_answer = request.json.get('answer')
    correct_answer = request.json.get('correct_answer')
    
    is_correct = user_answer == correct_answer
    return jsonify({
        'correct': is_correct,
        'correct_answer': correct_answer if not is_correct else None
    })

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)

index.html

<!-- templates/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 href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
    </head>
    <body class="bg-gray-100 min-h-screen p-8">
        <div class="max-w-3xl mx-auto bg-white rounded-lg shadow-lg p-6">
            <h1 class="text-3xl font-bold text-center mb-8">ポケモンクイズ</h1>
            
            <!-- フィルター設定 -->
            <div class="mb-8" id="filters">
                <h2 class="text-xl font-semibold mb-4">フィルター設定</h2>
                <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
                    <div>
                        <label class="block mb-2">世代</label>
                        <select id="generation" class="w-full p-2 border rounded">
                            <option value="">指定なし</option>
                            <option value="1">第1世代</option>
                            <option value="2">第2世代</option>
                            <option value="3">第3世代</option>
                            <option value="4">第4世代</option>
                            <option value="5">第5世代</option>
                            <option value="6">第6世代</option>
                            <option value="7">第7世代</option>
                            <option value="8">第8世代</option>
                            <option value="9">第9世代</option>
                        </select>
                    </div>
                    <div>
                        <label class="block mb-2">タイプ</label>
                        <select id="type" class="w-full p-2 border rounded">
                            <option value="">指定なし</option>
                            <option value="normal">ノーマル</option>
                            <option value="fire">ほのお</option>
                            <option value="water">みず</option>
                            <option value="grass">くさ</option>
                            <option value="electric">でんき</option>
                            <option value="ice">こおり</option>
                            <option value="fighting">かくとう</option>
                            <option value="poison">どく</option>
                            <option value="ground">じめん</option>
                            <option value="flying">ひこう</option>
                            <option value="psychic">エスパー</option>
                            <option value="bug">むし</option>
                            <option value="rock">いわ</option>
                            <option value="ghost">ゴースト</option>
                            <option value="dragon">ドラゴン</option>
                            <option value="dark">あく</option>
                            <option value="steel">はがね</option>
                            <option value="fairy">フェアリー</option>
                        </select>
                    </div>
                </div>
                <button id="startGame" class="mt-4 w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600">
                    ゲームスタート
                </button>
            </div>

            <!-- ゲーム画面 -->
            <div id="gameArea" class="hidden">
                <div class="mb-6">
                    <h3 class="text-lg font-semibold mb-2">ヒント</h3>
                    <div id="hints" class="space-y-2"></div>
                    <div class="mt-4 flex space-x-2">
                        <button id="showNextHint" class="flex-1 bg-green-500 text-white py-2 px-4 rounded hover:bg-green-600">
                            次のヒントを表示
                        </button>
                        <button id="giveUp" class="flex-1 bg-red-500 text-white py-2 px-4 rounded hover:bg-red-600">
                            ギブアップ(答えを見る)
                        </button>
                    </div>
                </div>

                <div class="mb-6">
                    <label class="block mb-2">ポケモンの名前を入力してください</label>
                    <input type="text" id="answer" class="w-full p-2 border rounded" placeholder="ポケモンの名前">
                    <button id="checkAnswer" class="mt-4 bg-yellow-500 text-white py-2 px-4 rounded hover:bg-yellow-600">
                        回答する
                    </button>
                </div>

                <!-- 結果表示エリア -->
                <div id="result" class="hidden text-center">
                    <div id="resultMessage" class="text-xl font-bold mb-4"></div>
                    <img id="pokemonImage" class="mx-auto" src="" alt="">
                    <div id="pokemonName" class="text-lg font-semibold mt-2"></div>
                    <button id="playAgain" class="mt-4 bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600">
                        もう一度プレイ
                    </button>
                </div>
            </div>
        </div>

        <script>
            let currentGame = {
                hints: [],
                shownHints: 0,
                answer: '',
                spriteUrl: ''
            };

            document.getElementById('startGame').addEventListener('click', async () => {
                const generation = document.getElementById('generation').value;
                const type = document.getElementById('type').value;
                
                const filters = {};
                if (generation) filters.generation = parseInt(generation);
                if (type) filters.type = type;

                try {
                    const response = await fetch('/api/start-game', {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json'
                        },
                        body: JSON.stringify(filters)
                    });
                    
                    const data = await response.json();
                    if (response.ok) {
                        currentGame = {
                            hints: data.hints,
                            shownHints: 0,
                            answer: data.answer,
                            spriteUrl: data.sprite_url
                        };
                        
                        document.getElementById('filters').classList.add('hidden');
                        document.getElementById('gameArea').classList.remove('hidden');
                        document.getElementById('result').classList.add('hidden');
                        document.getElementById('hints').innerHTML = '';
                        document.getElementById('answer').value = '';
                        document.getElementById('showNextHint').disabled = false;
                        document.getElementById('showNextHint').classList.remove('opacity-50');
                        showNextHint();
                    } else {
                        alert('ゲームの開始に失敗しました。条件を変更してもう一度お試しください。');
                    }
                } catch (error) {
                    console.error('Error:', error);
                    alert('エラーが発生しました。');
                }
            });

            document.getElementById('showNextHint').addEventListener('click', showNextHint);

            document.getElementById('giveUp').addEventListener('click', () => {
                showAnswer(false, true); // 2番目の引数をtrueにしてギブアップを示す
            });

            function showNextHint() {
                if (currentGame.shownHints < currentGame.hints.length) {
                    const hintElement = document.createElement('div');
                    hintElement.className = 'p-2 bg-gray-100 rounded';
                    hintElement.textContent = `ヒント${currentGame.shownHints + 1}: ${currentGame.hints[currentGame.shownHints]}`;
                    document.getElementById('hints').appendChild(hintElement);
                    currentGame.shownHints++;
                }
                
                if (currentGame.shownHints >= currentGame.hints.length) {
                    document.getElementById('showNextHint').disabled = true;
                    document.getElementById('showNextHint').classList.add('opacity-50');
                }
            }

            document.getElementById('checkAnswer').addEventListener('click', async () => {
                const userAnswer = document.getElementById('answer').value.trim();
                
                try {
                    const response = await fetch('/api/check-answer', {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json'
                        },
                        body: JSON.stringify({
                            answer: userAnswer,
                            correct_answer: currentGame.answer
                        })
                    });
                    
                    const data = await response.json();
                    showAnswer(data.correct, false, data.correct_answer);
                    
                } catch (error) {
                    console.error('Error:', error);
                    alert('エラーが発生しました。');
                }
            });

            function showAnswer(isCorrect, isGiveUp = false, correctAnswer = null) {
                const resultElement = document.getElementById('result');
                const resultMessageElement = document.getElementById('resultMessage');
                const pokemonImage = document.getElementById('pokemonImage');
                const pokemonName = document.getElementById('pokemonName');
                
                resultElement.classList.remove('hidden');
                
                if (isCorrect) {
                    // 正解の場合
                    resultMessageElement.textContent = '正解!';
                    resultMessageElement.className = 'text-xl font-bold mb-4 text-green-600';
                    pokemonImage.src = currentGame.spriteUrl;
                    pokemonName.textContent = currentGame.answer;
                } else if (isGiveUp) {
                    // ギブアップの場合
                    resultMessageElement.textContent = '答えは...';
                    resultMessageElement.className = 'text-xl font-bold mb-4 text-blue-600';
                    pokemonImage.src = currentGame.spriteUrl;
                    pokemonName.textContent = currentGame.answer;
                } else {
                    // 不正解の場合
                    resultMessageElement.textContent = '不正解...もう一度チャレンジしてください!';
                    resultMessageElement.className = 'text-xl font-bold mb-4 text-red-600';
                    pokemonImage.src = ''; // 画像をクリア
                    pokemonName.textContent = ''; // 名前をクリア
                }
            }

            document.getElementById('playAgain').addEventListener('click', () => {
                document.getElementById('filters').classList.remove('hidden');
                document.getElementById('gameArea').classList.add('hidden');
                document.getElementById('showNextHint').disabled = false;
                document.getElementById('showNextHint').classList.remove('opacity-50');
                document.getElementById('answer').value = '';
            });

            // Enterキーでの回答送信を有効化
            document.getElementById('answer').addEventListener('keypress', function(event) {
                if (event.key === 'Enter') {
                    document.getElementById('checkAnswer').click();
                }
            });
        </script>
    </body>
</html>

先ほどと同じく、main.pyを実行することで起動できます。

実際の動作

アクセスに成功したら、以下のような画面が表示されます。

ポケモンの世代とタイプを絞り込み、その中でランダムに選ばれたポケモンがクイズの答えになります。

ゲームスタートを押すと、次のような画面になります。

クイズが出題され、回答ができるようになります。「次のヒントを表示」ボタンで1つずつヒントを増やすことができ、正解するかギブアップすると、解答が表示されます。

サービスを継続させるには

main.pyを実行した後、SSH接続を切断すると、Pythonのプログラムも止まってしまいます。これを停止させないようにするには、nohup <実行したいコマンド> &(今回はnohup python main.py &)と入力してください。最後に&をつけることでバックグラウンド実行になり、プログラム実行中に他のコマンドを実行できるようになります。これを停止するには、ps aux | grep <実行したコマンド>(今回はps aux | grep main.py、grepコマンドでは検索文字列に空白を含めれないので「python」は省略)を実行して、停止させたいプロセスのPID(プロセスID)を確認します。次に、kill [PID]で指定したPIDのプロセスを終了します。プロセスが応答しない場合、kill -9 [PID]で強制終了します。

以下は今回の場合の例です。

さいごに

長くなってしまいましたが、今回はFlaskを使ってWebサーバーを構築する方法を解説しました。

今回構築した方法では、Flaskアプリケーションが直接外部と接続されるので、外部からの攻撃を防ぐ十分なセキュリティ機能がありません。また、HTTP通信は暗号化されていないため、データが傍受されるリスクがあります。

次回の記事では、セキュリティ対策として、リバースプロキシや、SSL/TLSの設定を行う予定です。

コメントを残す

CAPTCHA