LoginSignup
33
13

More than 1 year has passed since last update.

対戦型ポケモンWordle「VS Pokémon Wordle」を作ってみた

Last updated at Posted at 2022-02-13

アプリの更新履歴

更新履歴

■2022/12/09
第9世代(SV)のポケモンを追加

■2022/03/21
初回起動時に設定画面からユーザー名を登録、その値が保存されるように修正
部屋の作成と入室の処理を分離
部屋作成時に自動でランダムな4桁の部屋コードが反映される機能を追加
制限時間を変更にできるように修正
ハンディキャップとして、プレイヤーごとに制限時間を設定できる機能を追加
  

■2022/03/03
制限時間を追加(制限時間が切れると無回答で相手のターンに)
  
■2022/02/20
「フリック入力 + 入力履歴」の機能を追加
フリック入力の実装についての記事はこちら
  
■2022/02/15
正誤判定の方法を修正

■2022/02/14
設定画面からハイコントラストモードを選べる機能を追加

■2022/02/12
リリース

はじめに

最近話題の5文字の英単語を当てるゲーム「Wordle」。
それをポケモンの名前で遊べるようにアレンジした「ポケモンWordle」。

シンプルだけど意外と奥が深いこのゲームですが、「リアルタイムに友達と対戦できる機能があったらもっと面白そう」ということで作ってみました!

完成品はこちら

wordle.gif

ソースの全容は以下のリンクから確認できます。

プロローグ

先日、兄から「ポケモンWordleで対戦しよう。LINEで。」というメッセージがきました。
さっそく以下のような形で実際に対戦して遊んでみました。

子どもの頃よくやっていた数字当てゲームと今流行りのWordleを掛け合わせたような遊びで、たしかに面白いなと思いました。
ただ、事前に決めたポケモンを記憶しておいて、いちいち相手の予想が当たっているかの判定を自分で作るのがめんどくさいです。
そこで、対戦用のアプリを自分で作ってしまおうということでアプリの開発を始めました。

ちなみに兄の答えは「ハリーセン」でした。そんなマイナーどころ出てこんわ!

実装

flaskを利用してPythonで開発し、herokuにデプロイしています。
また、リアルタイムな対戦ゲームとするためには、双方向通信が必要となります。
双方向通信部分は「Flask-SocketIO」というライブラリを使って実現しました。

「Flask-SocketIO」の基本的な使い方については以下の2つ記事が大変参考になりました。
FlaskでWebSocketを使う
How to use Socket.IO with Flask/Heroku

ポケモンのデータは、こちらからお借りしました。
8世代対応全ポケモンの.jsonファイルを作ってみた


この記事では、その他ポイントをかいつまんで紹介していきたいと思います。

部屋の実装

事前準備として、以下を用意しておきます。
ゲームの情報を管理するclass「GameInfo」
部屋ごとにゲームの情報を管理するdictionary「d_info」
部屋ごとに人数を管理するdictionary「d_user_count」

python
class GameInfo:
    def __init__(self):
        self.p1_id = ""
        self.p1_user_name = ""
        self.p1_poke_name = ""
        self.p2_id = ""
        self.p2_user_name = ""
        self.p2_poke_name = ""
        self.is_in_game = False
        self.correct = [0, 0]

d_info = defaultdict(GameInfo)

#部屋ごとの人数を保持
d_user_count = defaultdict(int)

上述した記事を参考に、入力した値をほかのユーザの画面に表示させることはできました。
ただ、2人ずつ並行して対戦できるようにしたいので、特定のユーザにのみ値を送信したいです。
そこで、「Flask-SocketIO」の「Rooms」という仕組みを活用します。

join_room(room_code)で部屋に入れさせて、
emit('hogehoge', broadcast=True, to=room_code)で同じ部屋の人にのみ送信することができます。

■JOINボタンクリック時の処理

javascript
$(document).on('click', '#btn_join', function () {
    var temp_user_name = $('#txt_user_name').val();
    var temp_room_code = $('#txt_room_code').val();

    socket.emit('join', { user_name: temp_user_name, room_code: temp_room_code });
});
python
@socketio.on('join')
def join(json):
    global d_user_count, d_info
    room_code = json["room_code"]
    user_name = json["user_name"]

    #満室だった場合
    if d_user_count[room_code] >= 2:
        emit('full_error')
        return

    join_room(room_code)
    d_user_count[room_code] += 1

    temp_info = d_info[room_code]

    if d_user_count[room_code] == 1:
        temp_info.p1_id = request.sid
        temp_info.p1_user_name = user_name

    elif d_user_count[room_code] == 2:
        temp_info.p2_id = request.sid
        temp_info.p2_user_name = user_name

    emit('update_info_join', {'room_code': room_code
                             ,'p1_user_name': temp_info.p1_user_name
                             ,'p2_user_name': temp_info.p2_user_name
                             ,'p1_id': temp_info.p1_id
                             ,'p2_id': temp_info.p2_id}, broadcast=True, to=room_code)

    d_info[room_code] = temp_info
javascript
var room_code = '';
var p1_id = '';
var p2_id = '';

socket.on('update_info_join', function (data) {
    room_code = data.room_code;
    p1_id = data.p1_id;
    p2_id = data.p2_id;

    $('#room_info').text('部屋コード:' + room_code);
    $('#player1_name').text(data.p1_user_name);
    $('#player2_name').text(data.p2_user_name);
});

■ENTERボタンクリック時の処理

javascript
$(document).on('click', '#btn', function () {
    var poke_name = $('#txt_poke_name').val();

   //プレイヤー判断
   if (p1_id == socket.id) {
       update_row(poke_name, $('#answer_l'));
       socket.emit('btn_click', { room_code: room_code, is_p1: true, poke_name: poke_name });
   }
   else if (p2_id == socket.id) {
       update_row(poke_name, $('#answer_r'));
       socket.emit('btn_click', { room_code: room_code, is_p1: false, poke_name: poke_name });
   }
});
python
@socketio.on('btn_click')
def btn_click(json):
    global d_info
    room_code = json["room_code"]
    is_p1 = json["is_p1"]
    poke_name = json["poke_name"]

    temp_info = d_info[room_code]
    
    if is_p1:
        temp_info.p1_poke_name = poke_name
        emit('update_answer', {'is_p1': is_p1 }, broadcast=True, include_self=False, to=room_code)
    else:
        temp_info.p2_poke_name = poke_name
        emit('update_answer', {'is_p1': is_p1 }, broadcast=True, include_self=False, to=room_code)

    d_info[room_code] = temp_info
javascript
socket.on('update_answer', function (data) {
    //相手の正解を「?????」に更新
    if (data.is_p1) {
        update_row('?????', $('#answer_l'));
    }
    else {
        update_row('?????', $('#answer_r'));
    }
});

部屋コードの共有

部屋に入室後、赤枠のアイコンからTwitterかLINEで部屋コードを共有できるようにしています。

ただ、単に部屋コードを共有するだけだと、部屋コードをコピペするなり手入力するなりひと手間かかります。
そこで、URLパラメータを利用して、共有されたURLから飛んできた場合は部屋コードが入力された状態で立ち上がるようにしています。

python
@app.route("/", methods=["GET"])
def get_user():
    room_code = ''

    try:
        req = request.args
        room_code = req.get("room_code")
    except:
        return render_template('home.html')

    if room_code != '':
        return render_template('home.html', room_code = room_code)

    else:
        return render_template('home.html')
html
<input id="txt_room_code" value={{ room_code }}>

対戦相手の退室

Webアプリなので、対戦相手がブラウザを閉じて終了する場合があります。
対戦中に相手がぬけたことに気づかず、ただ相手を待つような状態は時間の無駄になってしまいます。
そこで、相手が抜けたことを検知して通知してあげるようにします。
exit.gif

python
@socketio.on('disconnect')
def disconnect():
    global d_user_count, d_info

    room_code = ''
    #強引にroom_codeを取得
    for s in rooms():
        if len(s) < 5:
            room_code = s

    emit('opponent_exit', broadcast=True, to=room_code)

    close_room(room_code)
    d_user_count[room_code] = 0
    d_info[room_code] = GameInfo()
javascript
socket.on('opponent_exit', function (data) {
    $('#opponent_exit_container').removeClass('transparent');
});

rooms()で抜けた人のSessionIDと部屋コードをlist型で取得することができます。
ただ、[SessionID, 部屋コード]の形だったり[部屋コード, SessionID]の形だったり、どちらに部屋コードが入っているのか決まってなさそうだったので、少し強引な方法で取得しています。

おわりに

双方向通信が必要ということで「WebSocket」に初めて触れましたが、実装にかなり苦労しました。
それ以外は大きなつまりもなく、1週間ほどで形にすることができました。
これまでやってきたことが活きる部分が多くあり、改めて自分の成長を感じました。
やっぱり何か形にしてアウトプットすることは重要ですね。

完成後、兄にこのアプリを紹介して一緒に遊んでみたらかなり好評でした。
みなさんもよかったら家族や友人たちと一緒に遊んでみてください!

参考

Flask-SocketIO documentation
FlaskでWebSocketを使う
How to use Socket.IO with Flask/Heroku
お名前.comで購入したドメインをHerokuに設定する
8世代対応全ポケモンの.jsonファイルを作ってみた

33
13
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
33
13