※このブログは2026年2月2日に公開された英語ブログ「The Fire-and-Forget Pattern: Scaling Game Analytics with TiDB Cloud and Convex」の拙訳です。
3名の開発者、1つのハッカソン、そしてバイラルしたミームから始まったミッション。これは、マレーシアの若者が直面する経済的困難と現実のB40 (所得の下位40%層) の経験に着想を得た金融教育ゲーム「B40 Life Simulator」の物語です。このプロジェクトは、社会的インパクトと巧妙な技術戦略を両立させています。チームは、リアルタイムのゲームプレイにConvexを、行動分析にTiDB Cloudを組み合わせることで、「高速なユーザーインタラクション」と「複雑なデータ集計」を分離するという重要な課題を解決しました。
彼らのアーキテクチャは、分散SQLがいかに身近なものになったかを証明しています。「Fire-and-forget (投げっぱなし)」の同期パターンを採用することで、ゲームの速度を落とすことなく、プレイヤー行動の生データをTiDB Cloudで深いインサイト (特定の経済的破綻ポイントの特定など) へと変換することに成功しました。これは、あらゆるアプリケーションに強力でスケーラブルな分析機能を短期間で追加したい開発者にとって、完璧な指針と言えます。
彼らのストーリーをチェックして、Cursor x Anthropic ハッカソン・マレーシアでいかにして「Best Use of TiDB」賞を勝ち取ったのか、その舞台裏をご覧ください。
金融リテラシーのゲーム化:B40問題への挑戦
私たちはMelon (Adam)、Rimba (Redzwan)、そしてPekNga (Shafeeq) です。私たちはマレーシア人の開発者3名で構成されており、少しエッジの効いた「DGD (Dark Game Dev)」というチーム名でCursorハッカソンに登録しました。実態は「Wayang Studio」という小さなクリエイティブ・スタジオで、いつか自分たちのゲームを作るという夢について、これまで数え切れないほどの時間を費やして議論してきました。

ハッカソンの説明会のためにモナッシュ大学に足を踏み入れたとき、強烈な緊張感に襲われました。AdamとShafeeqにとって、これが人生で初めてのハッカソンだったからです。
「おい、参加チームがめちゃくちゃ多いぞ。しかも時間は1日しかない。何か形にすることなんてできるのか?」
しかし、私たちには秘密兵器がありました。それはプロンプトエンジニアリングです。私たちはChatGPTに、過去5年間のハッカソン優勝チームを分析させました。AIが導き出したパターンは、「社会的インパクト」「テクノロジーの独創的な活用」「人々が関心を寄せる現実的な問題の解決」でした。そしてAIが提案してくれたアイデアが、私たちの心に深く刺さりました。それが「金融リテラシーに焦点を当てた人生シミュレーター」です。
皆が目にしていた、バイラル化したB40ミームと組み合わさったことで、コンセプトは一気に具体化しました。
「もし…これをゲームにしたらどうだろう? 実際にB40の生活をシミュレートするようなゲームだ」
単にミームを面白がるだけでなく、私たちはそのメッセージを心から重要だと感じていました。マレーシアの若者の60%は、1,000リンギット (約4万円) の急な出費に対処できません。退屈なパンフレットで教えられる金融リテラシーは役に立ちません。統計やインフォグラフィック、ありがちなキャンペーンはいくらでも見てきましたが、数字だけでは理解は深まりませんし、共感も生まれません。
マレーシアで育つ中で、私たちはB40の人々の苦労を目の当たりにしてきました。30,000リンギット (約120万円) ものPTPTN (マレーシアの高等教育基金局) 学生ローンに押しつぶされる新卒者。月給1,800 (約7万円) リンギットで仕事と育児を両立させるひとり親。生活費の支払いを済ませるか、食料を買うか、それとも正気を保つか――そんな不可能な選択を迫られる現実です。
もし、ゲームの中でその経済的な苦境を体験できるとしたら? 現実の世界で手痛い教訓を学ぶ前に、疑似体験を通じて学ぶことができたらどうでしょう。
こうして、AIによるリサーチと1つのミームから「B40 Life Simulator」が誕生しました。(最高のアイデアとは、往々にしてそういうものです。)

技術スタック:パフォーマンスを支えるTiDB CloudとConvexの選択
私たちはある戦略を立てました。このハッカソンには19もの異なるトラックが用意されていたのです。「自分たちのゲームを、できるだけ多くの賞の対象になるように設計したらどうだろう?」と考えた私たちは、オープンエリアから2階のワークショップルームへ移動しました。新しいツールのセッションに参加して学びながら、同時にそれらを自分たちのアイデアに実装していったのです。マルチタスクのレベルは、まさにカオスそのものでした。
中でもTiDBのワークショップは非常に有意義でした。実はTiDBに直接触れるのは今回が初めてだったのですが、学習曲線は予想以上にスムーズなものでした。分散SQLの基礎から、TiDB Cloudのセットアップ方法、そして分析クエリの構築方法までを学ぶことができました。おまけに、TiDBのTシャツまでもらえました。コーディング中に身なりもキマっているのは、モチベーションに繋がりますからね。
最終的に、私たちは19あるトラックのうち10個を自分たちのゲームに統合しました。私たちが何を選び、なぜそれを選んだのかを以下に紹介します。
| レイヤー | テクノロジー | 選定理由 |
| フロントエンド | Next.js, React | 迅速なイテレーションと、慣れ親しんだパターンを活用できる |
| ゲームの状態管理 | Convex | インフラの手動構築なしでリアルタイム同期が可能になる |
| AIロジック | Claude (Anthropic) | ステートフルな対話とメモリ機能を備えている |
| 分析 | TiDB Cloud Starter | セットアップの手間なくSQL分析を利用できる |
| デプロイ | Vercel | コードをプッシュするだけでデプロイできる |
技術スタックが決まったところで、次にアーキテクチャを検討する必要がありました。ストレスフリーなゲームプレイと深い分析の両方を最適化するにはどうすればよいのでしょうか。
アーキテクチャ:関心の分離
私たちは、B40 Life Simulatorを明確な関心の分離に基づいて設計しました。リアルタイムのゲームプレイにはConvexを、分析および調査データにはTiDBを使用しています。このアーキテクチャを採用することで、プレイヤー体験とデータインサイトの両方を最適化することが可能になりました。

核となる知見は、ゲームプレイと分析では求められるパフォーマンス要件が異なるという点にあります。ゲームプレイには低レイテンシ (レスポンス時間100ms未満) が必要ですが、分析には数千ものセッションをまたいだ集計処理が必要です。これら両方を一つのデータベースで実行しようとすると、競合が発生してしまいます。
データの流れは以下の通りです:プレイヤー → Next.js → Convex (ストレスフリーなのゲームプレイ) → TiDB (Fire-and-forget 同期による分析)

実装:Cursor AIと「Fire-and-Forget」同期による本番仕様のTiDB接続レイヤーの構築
Cursorのおかげで、SSLを利用したTiDB接続のセットアップ、パラメータ化されたクエリの記述、そしてトランザクションの適切な処理をスムーズに行うことができました。AIはTiDBのMySQL互換性を理解しており、最適な設定を提案してくれました。
私たちは次のようにプロンプトを入力しました:「分析用にSSLを使用したTiDB接続プールを設定し、クエリ実行、結果の取得、およびトランザクション用のヘルパー関数を作成してください。mysql2/promiseを使用してください。」
わずか数分で、本番環境でもそのまま使えるコネクションプーリングのコードが完成しました。
import mysql from "mysql2/promise";
const tidbConfig = {
host: process.env.TIDB_HOST,
port: parseInt(process.env.TIDB_PORT || "4000"),
user: process.env.TIDB_USER,
password: process.env.TIDB_PASSWORD,
database: process.env.TIDB_DATABASE,
ssl: {
minVersion: "TLSv1.2" as const,
rejectUnauthorized: true,
},
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0,
};
let pool: mysql.Pool | null = null;
export function getPool(): mysql.Pool {
if (!pool) {
pool = mysql.createPool(tidbConfig);
}
return pool;
}
export async function query<T>(sql: string, params?: unknown[]): Promise<T[]> {
const connection = await getPool().getConnection();
try {
const [rows] = await connection.execute(sql, params);
return rows as T[];
} finally {
connection.release();
}
}
シングルトンパターン (コネクションプールが存在しないときのみ生成する) を採用することで、APIリクエストをまたいでコネクションを確実に再利用できるようにし、try/finallyブロックによってコネクションが必ずプールに返却されることを保証しました。これは、ハッカソンの混乱の中でコネクションリークを防ぐために極めて重要でした。
「Fire-and-Forget」同期パターンを採用した理由
私たちはFire-and-Forget同期パターンを実装しました。ゲームの状態は、リアルタイムな反応性を確保するためにConvexで管理し、特定の重要なタイミング (週の完了時やゲームオーバー時など) に、分析用データとしてTiDBへ同期させています。
プレイヤーが1週間を完了すると、Convexのアクションが実行されます。このアクションが現在のゲーム状態を取得しまとめて、TiDBへの書き込みを担当するNext.jsのAPIルートへと送信します。
export const syncWeeklyProgress = action({
args: {
gameId: v.id("games"),
weekCompleted: v.number(),
weekendActivity: v.optional(v.string()),
},
handler: async (ctx, args): Promise<SyncResult> => {
// Get game state from Convex
const game = await ctx.runQuery(api.games.getGame, { gameId: args.gameId });
// Get decisions for this specific week
const allDecisions = await ctx.runQuery(api.games.getAllDecisions, {
gameId: args.gameId,
});
const weekDecisions = allDecisions.filter(d => d.week === args.weekCompleted);
// Prepare snapshot data for TiDB
const snapshotData: WeeklySnapshotData = {
convex_game_id: args.gameId,
player_name: game.playerName || "Anonymous",
persona_id: game.personaId,
week: args.weekCompleted,
money: game.money,
credit_score: game.creditScore,
health: game.health,
stress: game.stress,
objectives_completed: checkObjectives(game.weeklyObjectives),
is_game_over: game.isGameOver,
// ... more fields
};
// Fire-and-forget sync to TiDB via API route
await fetch(`${baseUrl}/api/analytics/sync`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Internal-Key": process.env.INTERNAL_API_KEY,
},
body: JSON.stringify({
syncType: "weekly",
weeklySnapshot: snapshotData,
decisions: decisionsData,
}),
});
return { success: true };
},
});
ここで重要なのは、fetchの呼び出しがTiDBへの書き込み完了を待機しない点です。リクエストを送信したらすぐにプレイヤーへ成功レスポンスを返すことで、UIのレスポンス性能を維持しつつ、分析データが最終的にTiDBへと届くようにしています。
分析クエリ:現実の問いに答える
私たちは、重要な調査課題に対する答えを導き出すための包括的な分析レイヤーを構築しました。例えば、「プレイヤーが失敗する原因は何なのか、そしてそれはいつ起こるのか?」といった問いを明らかにしたいと考えたのです。
export async function getFailurePatterns(): Promise<FailurePattern[]> {
return query(
`SELECT
failure_reason,
COUNT(*) as count,
persona_id,
AVG(weeks_completed) as avg_week_failed
FROM completed_games
WHERE failure_reason IS NOT NULL
GROUP BY failure_reason, persona_id
ORDER BY count DESC`
);
}
// Example insight: "Fresh graduates fail most often in week 2
// due to 'health_crisis' - choosing instant noodles over vegetables"
このクエリは、興味深い事実を明らかにしました。新卒者 (persona_id “graduate”) は、決まって2週目に健康の問題によって失敗しているのです。彼らは栄養よりも貯金を優先してしまい、その結果、医療費の請求が発生して蓄えをすべて失ってしまいます。このインサイトは、ゲームの難易度バランスを調整する上で非常に役立ちました。
もう一つの重要な指標は、生存ファネルです。各週を乗り越えられたプレイヤーは、一体どれくらいいるのでしょうか?
export async function getSurvivalFunnel(): Promise<Array<{
week: number;
started: number;
survived: number;
survival_rate: number;
}>> {
const weekCounts = await query<{ week: number; count: number }>(
`SELECT week, COUNT(DISTINCT convex_game_id) as count
FROM weekly_snapshots
GROUP BY week
ORDER BY week`
);
// Calculate week-over-week retention
return weekCounts.map((wc, index) => ({
week: wc.week,
started: wc.count,
survived: wc.count - (endMap.get(wc.week) || 0),
survival_rate: Math.round(survivalRate),
}));
}
私たちは、第2週目の離脱率が35%に達していることを発見しました。これはゲーム内で最も急激な下落です。このデータは、難易度の上昇曲線が過酷すぎる可能性を示唆していましたが、同時に現実世界の経済的苦境も反映しています。新しい仕事を始めてから (最初の給与が支払われる前の) 2ヶ月目は、しばしば最も困難な時期になるからです。
課題:挫折しかけた瞬間 (そして乗り越えた日々)
ここからが正念場でした。バグが発生し、機能が壊れ、プレッシャーが重くのしかかります。
複数セッションのバグ:最大の悪夢は、ゲームが個別のセッションではなく、1つのグローバルな状態を同期してしまったことでした。一人のプレイヤーが選択を行うと、全員の画面にそれが反映されてしまうのです。最終的にConvexのサブスクリプション処理に原因があることを突き止めました。午前3時に修正できた時の安堵感は、言葉では言い表せません。
「おい、なんで俺のキャラがいきなり破産してるんだ? 何もしてないぞ!」
「…すまん、それ俺だ。タブを間違えた。」
TiDBのSSL接続:TiDB Cloudへの安全なSSL接続の確立には、細心の注意が必要でした。コネクションプーリングの効率を維持しながら、最小バージョンTLS 1.2の使用と適切な証明書の検証を確実に行う必要がありました。
べき等な同期:Fire-and-forgetパターンの同期では、重複して送信されるデータに適切に対処する必要があります。ON DUPLICATE KEY UPDATEと複合ユニークキー (game_id + week) を使用することで、データの整合性を確保しました。
文化的な真正性:Manglish (マレーシアの英語) を、無理やりではなく自然に使うことにもこだわりました。Pak Aliの「常連さんだから、ちょっとだけ負けとくよ、ラー (lah)」といった台詞が、形だけではなく本物らしく感じられるようにしました。
私たちはクアラルンプールのAirbnbで作業を続けました。目の前にはKLタワーがくっきりとそびえ立ち、遠くにはペトロナスツインタワーが輝いていました。徹夜作業をするには、これ以上ないモチベーションでした。
結果:バイラルミームからハッカソン優勝へ
最後の致命的なバグを修正し (そして「味」としていくつかの軽微なバグは残したまま)、午前5時、ついに公開と提出ボタンを押しました。
目は燃えるように熱く、脳は疲れ果てていましたが、どういうわけか満足感に包まれていました。私たちは形にしたのです。本物のゲームを。わずか1日で。たった一つのミームから。
「よし、これで終わりだ。勝てれば最高だけど、そうでなくても、少なくとも俺たちには完成したゲームがある」
2時間だけ泥のように眠り、フラフラになりながら、かすかな希望を胸に結果発表が行われるモナッシュ大学へと向かいました。
数字で見る私たちのゲーム:

会場に戻ると、強烈な疲労が襲ってきました。Rimbaにいたっては、あまりの疲れに、あろうことか皆の目の前で口を大きく開けたまま自分の席で爆睡していました。彼らしいです。
Cursorトラックの結果が発表されました。私たちは勝てませんでした。まあ、仕方がありません。その日はトロフィーを手にすることなく帰路につきましたが、それよりも素晴らしいものを手に入れていました。自分たちが誇りに思えるゲーム、そして、それをさらに拡張していくための計画です。
え、本当に受賞したの?
ハッカソンから1週間後、ついに最終結果が発表されました。その時の私たちは、正直もう何も期待していませんでした。
しかし、自分たちの名前を見つけたのです。それも、2つも
優勝:BEST USE OF TIDB
準優勝:BYTE IN TRACK
ただゲームを作りたかっただけの、3人のゲーム好き。一つのミームから始まったミッション。そして、あらゆる予想に反して、私たちは2つの賞を勝ち取ることができたのです。
ゲームをプレイする: B40 Life Simulator
データベースをそれぞれのワークロードに適合させたことで、このデュアルデータベース・アーキテクチャは真価を発揮しました。Convexが低レイテンシで高頻度の書き込みを処理し、TiDBが全セッションにわたる分析クエリを担います。そしてFire-and-forget同期が、両者を密結合させることなくデータの整合性を保っています。
単なるトロフィー以上に、私たちはB40 Life Simulatorが社会に変化をもたらすと心から信じています。TiDBを通じて収集される分析データは、単なる数字ではありません。それは、人々が極限のプレッシャーの中でどのような経済的決断を下すのかを解き明かすための、貴重なインサイトなのです。
ぜひご自身で体験してください
ハッカソンで制作したプロトタイプは、現在公開中で実際にプレイ可能です。B40の苦境を自ら体験し、不可能な選択を迫られる中で、あなたがどれだけ長く生き残れるか挑戦してみてください。次のステップとして、モバイル向けにUnityでの再構築を計画しています。どなたでも大歓迎ですので、ぜひお試しください!
技術スタック:Next.js 15, React 19, TypeScript, Convex, TiDB Cloud Starter, Claude AI, PixiJS, Tailwind CSS 4, Framer Motion, shadcn/ui, Cursor, Vercel
TiDB Cloud Dedicated
TiDB Cloudのエンタープライズ版。
専用VPC上に構築された専有DBaaSでAWSとGoogle Cloudで利用可能。
TiDB Cloud Starter
TiDB Cloudのライト版。
TiDBの機能をフルマネージド環境で使用でき無料かつお客様の裁量で利用開始。