ラブカカップ

AtQuizer(オンラインクイズコンテスト)

https://github.com/jyogi-web/loveka-2024/

JavaScript

Node.js

Firestore

全国同時開催型クイズ早解きコンテスト

KOU050223

わしじゃよ

さくらんぼ

推しアイデア

Lineをつかうことで手軽に参加できる

作った背景

Lineの手軽さから着想

推し技術

全てLineで済ませることができる(できた!)

プロジェクト詳細

AtQuizer

公式LineQRコード

ゲーム内容

メニュー欄から遊び方を参照して遊ぼう! いくつかの機能をワードを入れることで呼び出して遊びます 開催されているコンテストに早く答えた人からランキングに掲載されます! 上位を目指して

使用技術

  • LineMessagingAPI(技術縛り)
    • LINE Front-end Framework (LIFF)
    • リッチメニュー
    • Webhook
  • Node.js(技術縛り)
    • Express
  • Firestore
  • Vercel(デプロイ先、Nodeが動くのでよし)

処理内容

LINE Front-end Framework (LIFF)

@さくらんぼ&わし

まず、Firebaseを使用してLIFFのデモ版を実装し、動きを確認した

LINE Developersから作成したプロバイダーにLINEログインチャネルを登録した

LINEログインチャネルのメニューから「LIFF」を選択し、「追加」を押した

LIFFアプリ追加のためのフォーム画面に必要事項を入力した。 「Size」ではFullを選択し、全画面にWebアプリの情報が表示されるようにした

「エンドポイントURL」に、表示させたいWebアプリの公開URLを入力した

すべてのフォームを入力したら、「追加」を押して登録を完了した

コンソールに表示している「LIFF URL」をコピーし、 botのトークルームに貼り付けた

「サービス提供者が次の許可をリクエストしています」という表示が出るため、それを許可するとWebアプリがLIFFアプリとして起動する

リッチメニュー

@さくらんぼ

リッチメニューの画像をPhotoshopで作成し、LINE上で表示できるようにした

画像の色は暖色のパステルカラーを用いて、目に優しいデザインにした

メニューは4項目作成しており、それぞれの項目を押すと、定型文が自動で送信され、それにbotが対応する仕組みになっている

「遊び方」をタップすると、あそびかたを尋ねることができる 「クイズを知りたい!」をタップすると、クイズを教えてもらうことができる 「問題を作りたい!」をタップするとクイズ作成をすることができる 「ランキングページを見たい!」をタップするとランキングページを見ることができる

リッチメニュー画像
image

リッチメニュー(ボディ)のソースコード

{ "size":{ "width":1920, "height":1080 }, "selected":false, "name":"メニュー", "chatBarText":"メニュー", "areas":[ { "bounds":{ "x":0, "y":0, "width":960, "height":540 }, "action":{ "type":"message", "text":"あそびかた" } }, { "bounds":{ "x":960, "y":0, "width":960, "height":540 }, "action":{ "type":"message", "text":"クイズ教えて" } }, { "bounds":{ "x":0, "y":540, "width":960, "height":540 }, "action":{ "type":"message", "text":"クイズ作成" } }, { "bounds":{ "x":960, "y":540, "width":960, "height":540 }, "action":{ "type":"message", "text":"ランキング" } } ] }

ランキング

@わし 早く答えた人からランキングに掲載されるシステムにした

正解したらFirestoreにUserNameとTimesstampを保存

web上で早く答えた順になるようにtimestampを昇順にしてからフロントにデータを渡すようにした

async function getRankingData() { const snapshot = await collectionRef.orderBy('timestamp', 'asc').get(); const rankingData = []; snapshot.forEach(doc => { const data = doc.data(); rankingData.push({ id: doc.id, // ドキュメントIDを含める message: data.message, // タイトルフィールドを含める timestamp: data.timestamp, // タイムスタンプフィールドを含める name: data.userName, // ユーザーの名前を含める userid: data.userId }); }); return rankingData; }

正解した人を保存するが同じ問題で複数回回答が保存されないように重複を避ける処理を行う

const RankingData = await getRankingData(); let answered = false; RankingData.forEach((ranking)=> { if (ranking.userid === event.source.userId) { answered = true; } }); if (answered) { return client.replyMessage(event.replyToken, [{ type: 'text', text: '回答済みです!' }]); } else { ・・・

問題が更新されるたびにランキングデータ削除

// ランキングデータ削除処理 const deleteData = await collectionRef.get(); deleteData.forEach(async (doc) => { await doc.ref.delete(); });

画像処理

@KOU

// 画像のピクセル単位での比較 function compareImages(image1, image2, width, height, channels) { let diff = 0; for (let i = 0; i < width * height * channels; i++) { if (image1[i] !== image2[i]) { diff++; } } return diff; }

音声問題

@わし 音声問題を出題できるようにしました。

case 'クイズ教えて': // ランダムにクイズを選択 randomIndex = Math.floor(Math.random() * quizDataArray.length);//quizDataArrayからランダムなクイズデータを取得 quizQuestion = quizDataArray[randomIndex].question;// ランダムに選ばれたクイズの質問を取得 quizAnswer = quizDataArray[randomIndex].answer;// ランダムに選ばれたクイズの答えを取得 quiztype = quizDataArray[randomIndex].type;// ランダムに選ばれたクイズのタイプを取得 if(quiztype == 'audio') { const audioname =quizDataArray[randomIndex].audioUrl;// ランダムに選ばれた音声ファイルの名前を取得 const pestionaudio = 'https://4q79vmt0-3000.asse.devtunnels.ms/audio/' + audioname;// 音声ファイルの完全なURLを生成 // LINE APIを使って音声メッセージとテキストメッセージを返信 return client.replyMessage(event.replyToken, [ { type: 'audio', originalContentUrl: pestionaudio, // 変数を直接渡す duration: 3000 // 音声の長さ(ミリ秒) }, { type: 'text', text: 'なんのポケモンか当ててね☆' } ]); }

Firestore

@KOU

データの形式を書く

基本的にテキストを扱うため手軽なFirestoreを使用

画像なのでもバイナリデータにして保存することで取り扱うことが出来るようにした

クイズデータ image

**画像バイナリデータ ** image

スケジュール処理

@KOU

VercelのCron Jobsを使って定期的にコンテストの開催が終了したものを削除する(毎日午前5時に実行するようにvercel.jsonで定義)

// 定期実行用のエンドポイント app.get('/api/cron', async (req, res) => { // 問題文を現在時間に近い順で取得(過ぎたものは除く) const nextQuizData = await quiz.where('day', '>=', admin.firestore.Timestamp.now()).get(); // 時間が過ぎているものを削除する const deleteQuizData = await quiz.where('day', '<', admin.firestore.Timestamp.now()).get(); deleteQuizData.forEach(async (doc) => { await doc.ref.delete(); }); res.status(200).json({ message: `Cron job executed successfully${nextQuizData}` }); });
"crons": [ { "path": "/api/cron", "schedule": "0 5 * * *" } ]

https://zenn.dev/ryosuke_horie/articles/7374a83566c6b7

次に開催されるコンテストを取得

// 問題文を現在時間に近い順で取得(過ぎたものは除く) const nextQuizData = await quiz.where('day', '>=', admin.firestore.Timestamp.now()).orderBy('day').limit(1).get();

LMM(Gemini)

@KOU

  • 回答の判定が完全一致でないと判定されない問題を解決するために手軽にLLMを使って判定したいと考えた。
  • FirestoreでGoogleにお世話になっているためGeminiを使ってみようと考えた
  • ついでに画像の判定も任せたいと思った

アーキテクチャ図

image

アイデア

なぜ作ろうと思ったのか

  • Lineの手軽さを使いたい
  • 手軽に参加したいものを実装したい

今後実装したいこと

KOU050223

@KOU050223