Vanilla JavaScriptで作る投資ゲーム:Cursor × Gemini × Supabaseで実現した個人開発の技術スタック
はじめに
本記事では、個人開発で制作したWebゲーム「株カレ」の技術的な実装について紹介します。Vanilla JavaScript(約7000行)で構築し、Cursorによるリファクタリング、Gemini APIによるLLM統合、Supabase RPC関数によるバックエンド集計など、モダンな開発手法を組み合わせた実装のポイントを解説します。
株カレ:https://kabukare.vercel.app/
匿名記事:https://anond.hatelabo.jp/20260112195152
技術スタック
- Frontend: HTML5, CSS3 (Mobile First), Vanilla JavaScript
- Backend/DB: Supabase (PostgreSQL), RPC Functions
- AI: Google Gemini API (LLM統合)
- 開発ツール: Cursor (AI支援コーディング)
- デプロイ: Vercel
1. Cursorによる大規模コードのリファクタリング
課題:3000行を超えた時点での可読性の低下
開発初期はメモ帳でコーディングしていましたが、script.jsが3000行を超えた時点で以下の問題が発生しました:
- コメントの統一性がない(
★V5.6:などのバージョンタグが散在) - 関数の説明が冗長(「〜という処理を行う」など)
- JSDoc形式が統一されていない
- デッドコード(コメントアウト)が残存
Cursorを使ったリファクタリング戦略
CursorのAI支援機能を活用し、以下の方針でリファクタリングを実施しました:
1.1 コメントの簡素化
Before:
// ★V5.6: ユーザーのあいづちをカテゴリに分類するという処理を行う
function categorizeReply(replyText) {
// ...
}
After:
/**
* プレイヤーのあいづちをカテゴリに分類
* @param {string} replyText - プレイヤーのあいづちテキスト
* @returns {string} カテゴリID(PRAISE, NEUTRAL, QUESTION, APPRECIATION, FAREWELL)
*/
function categorizeReply(replyText) {
// ...
}
1.2 バージョンタグの削除
プロジェクト全体から★V5.6:, ★FIX:, ★NEW:などのバージョンタグを一括削除し、Git履歴に任せる方針に変更しました。
1.3 実行コードの完全保護
リファクタリング時は実行可能なコードは1文字たりとも変更しないという厳格なルールを設定。Cursorに以下のようなプロンプトを指定しました:
# 🚫 STRICT PROHIBITIONS
1. コードの削除・変更は厳禁
2. コメントアウトされた「死にコード」も削除禁止
3. 機能の破壊禁止
リファクタリング結果
- 行数: 約7000行(コメント整理後もほぼ同数)
- 可読性: JSDoc統一により大幅向上
- 保守性: バージョンタグ削除により、コードが唯一の情報源(Single Source of Truth)に
2. Supabase RPC関数によるバックエンド集計
設計思想:フロントエンドは「表示」のみ
ゲーム内のランキング、配当計算、株価更新など、すべての集計・計算処理はSupabaseのRPC関数(PostgreSQL関数)で実装しています。
2.1 RPC関数の活用例
ランキング取得(get_ranking_full)
// script.js:5477
const { data, error } = await supabaseClient.rpc('get_ranking_full', {
p_type: 'SHARES', // ランキングタイプ
p_user_id: currentUserID,
p_ticker: null,
p_limit: 100
});
配当計算(calculate_dividend)
// script.js:3180
const { data: divRes, error } = await supabaseClient.rpc('calculate_dividend', {
target_user_id: currentUserID
});
2.2 セキュリティ:auth.uid()の活用
RPC関数内でauth.uid()を使用することで、フロントエンドからuser_idを渡さずに認証済みユーザーのみが実行できるようにしています:
-- RPC関数内の例
CREATE OR REPLACE FUNCTION perform_chat_action(p_ticker text)
RETURNS json
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
v_user_id uuid := auth.uid(); -- フロントエンドから渡さない
v_user_ap integer;
BEGIN
-- APチェックはRPC側で最新値を取得
SELECT ap INTO v_user_ap FROM profiles WHERE user_id = v_user_id;
-- ...
END;
$$;
2.3 実装済みRPC関数(17個)
- フロントエンドから直接呼び出し: 14個
- バックグラウンドジョブ(Cron): 1個
- トリガー関数: 2個
すべての集計処理をバックエンドで完結させることで、フロントエンドの負荷を軽減し、データの整合性を担保しています。
3. Gemini APIによるLLM統合
3.1 実装概要
ゲーム内のキャラクターと自由に会話できる機能を、Google Gemini APIで実装しました。
LLMチャット開始処理
// script.js:2871
async function startLLMChat(ticker) {
// LLMモードに移行
userState.context.mode = 'LLM_CHAT';
userState.context.llmHistory = {
recentChats: [],
llmConversation: []
};
// 直近3回分の雑談を会話履歴に保存
if (userState.context.recentChats && userState.context.recentChats.length > 0) {
const sameTickerChats = userState.context.recentChats
.filter(chat => chat.ticker === ticker)
.slice(-3);
userState.context.llmHistory.recentChats = sameTickerChats;
}
// 既存の会話履歴があれば読み込む(DBから)
await loadLLMConversationHistory(ticker);
}
LLM API呼び出し
// script.js:5087-5233
if (mode === 'LLM_CHAT') {
if (userState.llmCount <= 0) {
game.showSystemNotification('LLM使用回数が不足しています');
return;
}
// 会話履歴にユーザーメッセージを追加
userState.context.llmHistory.llmConversation.push({
role: 'user',
text: val
});
// 3往復分(6メッセージ)を超えたら古いものを削除
if (userState.context.llmHistory.llmConversation.length > 6) {
userState.context.llmHistory.llmConversation.shift();
}
// Supabase Edge Function経由でGemini APIを呼び出し
const { data, error } = await supabaseClient.functions.invoke('llm-chat', {
body: {
ticker: ticker,
userMessage: val,
conversationHistory: userState.context.llmHistory.llmConversation,
recentChats: userState.context.llmHistory.recentChats
}
});
// 使用回数を減算(RPC関数経由)
const { data: decrementResult } = await supabaseClient.rpc('decrement_llm_usage', {
p_user_id: currentUserID
});
}
3.2 エラーハンドリング
LLM API呼び出し時のエラーを適切に処理しています:
// 503エラー(Service Unavailable / モデル過負荷)
if (e.status === 503 || e.message?.includes('overloaded')) {
errorMessage = 'LLMモデルが過負荷状態です。しばらく時間をおいてから再度お試しください。';
}
// 429エラー(レート制限)
else if (e.isRateLimit || e.status === 429) {
errorMessage = '本日のLLM使用回数の上限に達しました。';
}
3.3 会話履歴の管理
- メモリ効率: 3往復分(6メッセージ)を上限として、古いメッセージを自動削除
-
永続化: Supabaseの
user_activity_logsテーブルに保存 - コンテキスト: 直近3回分の雑談も履歴に含めて、キャラクターの性格を維持
4. Mobile First設計とキーボード対応
4.1 visualViewport APIによるキーボード対応
モバイルデバイスでキーボードが表示された際のUI調整を、visualViewport APIで実装しています:
// script.js:1551-1623
adjustForKeyboard() {
const viewport = window.visualViewport;
const windowHeight = window.innerHeight;
const viewportHeight = viewport.height;
const keyboardHeight = windowHeight - viewportHeight;
if (keyboardHeight > 150) {
// キーボードが表示されている
const inputAreaHeight = inputArea.offsetHeight || 60;
const quickContainerHeight = quickContainer.offsetHeight || 0;
// 入力エリアをキーボードの上に固定
inputArea.style.position = 'fixed';
inputArea.style.bottom = `${keyboardHeight}px`;
// クイックリプライを入力エリアの上に固定
quickContainer.style.position = 'fixed';
quickContainer.style.bottom = `${keyboardHeight + inputAreaHeight}px`;
// チャットコンテナの下部に余白を追加
chatContainer.style.paddingBottom = `${inputAreaHeight + quickContainerHeight + 20}px`;
} else {
// キーボードが非表示
inputArea.style.position = '';
// ...
}
}
4.2 タッチ操作の最適化
クイックリプライの横スクロールを、マウスドラッグとタッチ操作の両方に対応:
// script.js:1077-1120
const wheelHandler = (e) => {
e.preventDefault();
quickContainer.scrollLeft += e.deltaY;
};
const mouseDownHandler = (e) => {
isDragging = true;
startX = e.pageX - quickContainer.offsetLeft;
scrollLeft = quickContainer.scrollLeft;
};
const mouseMoveHandler = (e) => {
if (!isDragging) return;
e.preventDefault();
const x = e.pageX - quickContainer.offsetLeft;
const walk = (x - startX) * 2;
quickContainer.scrollLeft = scrollLeft - walk;
};
5. ゲームバランス調整へのAI活用
ゲーム内の経済バランス(配当率、株価変動率など)の調整に、CursorのAI支援機能を活用しました。
5.1 課題
初期実装では、特定のキャラクター(ファストフード企業)の配当金が強くなりすぎる問題が発生。数学的な知識が限られているため、手動での調整が困難でした。
5.2 AIによる相談プロセス
- 現状の数式を提示: 配当計算ロジックをCursorに説明
- 問題点の特定: AIが「保有金額ベースの逓減がLv3以下に適用されていない」と指摘
- 修正案の提案: ランクレベルベースの逓減倍率を追加する提案
- 実装と検証: 修正を実装し、ゲーム内でテスト
5.3 最終的な配当計算ロジック
-- 基本配当率 × 保有金額ベース逓減(Lv4以上のみ) × ランクレベルベース逓減
-- ランクレベルベース逓減: Lv5以上=0.7倍、Lv4=0.85倍、Lv3以下=1.0倍
このように、AIの力を借りながらも、最終的な判断は開発者が行う形でバランス調整を実施しました。
6. 開発で得られた知見
6.1 Cursorの効果
- リファクタリング効率: 手動では数日かかる作業が数時間で完了
- コード品質: JSDoc統一により、可読性が大幅に向上
- 学習効果: AIの提案を通じて、ベストプラクティスを学べた
6.2 Supabase RPC関数のメリット
- パフォーマンス: フロントエンドでの全件計算を回避
-
セキュリティ:
auth.uid()による認証の一元管理 - 保守性: ビジネスロジックをバックエンドに集約
6.3 LLM統合の課題と対策
- コスト管理: 1日3回の無料枠 + 有料プランでの制限を実装
- レイテンシ: Edge Function経由で呼び出し、タイムアウト処理を実装
- エラーハンドリング: 503/429/500エラーを適切に処理
7. まとめ
個人開発で約7000行のVanilla JavaScriptアプリケーションを構築し、以下の技術を組み合わせました:
- Cursor: 大規模コードのリファクタリング支援
- Supabase RPC: バックエンド集計によるパフォーマンス最適化
- Gemini API: LLM統合による対話機能の実現
-
Mobile First:
visualViewportAPIによるモバイル最適化
特に、CursorによるリファクタリングとSupabase RPC関数によるバックエンド集計は、コードの可読性とパフォーマンスの両面で大きな効果がありました。
個人開発でも、適切なツールと設計思想を組み合わせることで、保守性の高いアプリケーションを構築できることを実感しました。
参考資料
免責事項: 本記事は個人開発の経験に基づく技術的な知見の共有です。プロダクション環境での利用にあたっては、適切なテストとセキュリティ対策を実施してください。


