EY-Office ブログ

Roo Codeにいつものジャンケンアプリを作ってもらった、まあまあですね

Roo Code(旧 Roo Cline)のコード生成能力が凄いという噂がネットで広まっているので使ってみる事にしました。作るアプリはいつものReactのジャンケンアプリです。ただしReact Server Componentsを使ってジャンケンの結果をデータベースに書いてもらいます。

また、AIモデルはGemini 2.5 ProClaude 3.7 Sonnetの2つを使ってみました。

AI-code-generate Bing Image Creatorが生成した画像を使っています

プロンプト

# ジャンケンを行うReactアプリを作って下さい

## 画面仕様

- 人間はグー、チョキ、パーのボタンを押し、コンピューターは乱数でジャンケンの手を作り対戦します
- 勝敗(勝ち、負け、引き分け)はコンピューターが判定します
- 対戦の結果(人間の手、コンピューターの手、勝敗)を表形式で表示します
   - 人間が勝った場合は表示は青色にしてください
   - 人間が負けた場合は表示は赤色にしてください
   - 引き分けの場合は黒色にしてください
- 対戦の結果はデータベースに保存してください
   - データベースの扱いはPrisma ORMを使ってください
   - データベースはsqlite3を使ってください

## 環境
- フレームワークはNext.js
- React Server componetsを使ってください
- TypeScript を使ってください
- スタイリングはTailwind CSS を使ってください

Gemini 2.5 Pro

Gemini 2.5 Proの生成したReactアプリはちゃんと動作し画面は以下のようになりました。表の罫線にも赤や青の色が付いてしまいっていますが、美しいですね。
日時は指定していませんが、なぜか表に表示されているのはなぜでしょうか? 以前試したときの記憶でしょうか? それとも表には日時があるのが普通だからでしょうか?

さて生成されたコードは

最初に生成されたコードはpage.tsxに全て書かれていたので以下の指示をしてコンポーネントを分けてもらいました。

このコードを以下のコンポーネントに分割してください
- 結果表示のコンポーネント
- ジャンケンボタンのコンポーネント
  • src/app/page.tsx
import { revalidatePath } from 'next/cache';
import { prisma } from '@/lib/prisma';
import ResultsTable from '@/components/ResultsTable'; // Import ResultsTable
import JankenButtons from '@/components/JankenButtons'; // Import JankenButtons

type Hand = 'グー' | 'チョキ' | 'パー';
type Result = '勝ち' | '負け' | '引き分け'; // Keep Result type for determineWinner

const hands: Hand[] = ['グー', 'チョキ', 'パー'];

// Function to determine the winner (remains here as it's used by the server action)
function determineWinner(playerHand: Hand, computerHand: Hand): Result {
  if (playerHand === computerHand) {
    return '引き分け';
  }
  if (
    (playerHand === 'グー' && computerHand === 'チョキ') ||
    (playerHand === 'チョキ' && computerHand === 'パー') ||
    (playerHand === 'パー' && computerHand === 'グー')
  ) {
    return '勝ち';
  }
  return '負け';
}

// Server Action to play Janken (remains here)
async function playJanken(formData: FormData) {
  'use server';

  const playerHand = formData.get('playerHand') as Hand | null;

  if (!playerHand || !hands.includes(playerHand)) {
    console.error('Invalid player hand');
    return; // Or handle the error appropriately
  }

  const computerHand = hands[Math.floor(Math.random() * hands.length)];
  const result = determineWinner(playerHand, computerHand);

  try {
    await prisma.gameResult.create({
      data: {
        playerHand,
        computerHand,
        result,
      },
    });
    // Revalidate the path to update the results list
    revalidatePath('/');
  } catch (error) {
    console.error('Failed to save game result:', error);
    // Handle database error
  }
}

// Main Page Component (Server Component) - Refactored
export default async function Home() {
  // Fetch game results (remains here)
  const gameResults = await prisma.gameResult.findMany({
    orderBy: {
      createdAt: 'desc',
    },
  });

  return (
    <div className="container mx-auto p-8">
      <h1 className="text-3xl font-bold mb-6 text-center">ジャンケンポン!</h1>

      {/* Use JankenButtons component, passing the server action */}
      <JankenButtons playJankenAction={playJanken} />

      {/* Use ResultsTable component, passing the game results */}
      <ResultsTable gameResults={gameResults} />
    </div>
  );
}
  • src/components/JankenButtons.tsx
type Hand = 'グー' | 'チョキ' | 'パー';

const hands: Hand[] = ['グー', 'チョキ', 'パー'];

interface JankenButtonsProps {
  playJankenAction: (formData: FormData) => Promise<void>;
}

export default function JankenButtons({ playJankenAction }: JankenButtonsProps) {
  return (
    <form action={playJankenAction} className="mb-8 flex justify-center space-x-4">
      {hands.map((hand) => (
        <button
          key={hand}
          type="submit"
          name="playerHand"
          value={hand}
          className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-6 rounded text-xl"
        >
          {hand}
        </button>
      ))}
    </form>
  );
}
  • src/components/ResultsTable.tsx
import type { GameResult as GameResultType } from '@prisma/client';

type Result = '勝ち' | '負け' | '引き分け';

interface ResultsTableProps {
  gameResults: GameResultType[];
}

const getResultColor = (result: Result): string => {
  switch (result) {
    case '勝ち':
      return 'text-blue-600';
    case '負け':
      return 'text-red-600';
    case '引き分け':
    default:
      return 'text-black';
  }
};

export default function ResultsTable({ gameResults }: ResultsTableProps) {
  return (
    <div>
      <h2 className="text-2xl font-semibold mb-4">対戦結果</h2>
      <div className="overflow-x-auto">
        <table className="min-w-full bg-white border border-gray-300">
          <thead>
            <tr className="bg-gray-100">
              <th className="py-2 px-4 border-b text-left">あなたの手</th>
              <th className="py-2 px-4 border-b text-left">コンピューターの手</th>
              <th className="py-2 px-4 border-b text-left">勝敗</th>
              <th className="py-2 px-4 border-b text-left">日時</th>
            </tr>
          </thead>
          <tbody>
            {gameResults.map((game) => (
              <tr key={game.id} className={`border-b ${getResultColor(game.result as Result)}`}>
                <td className="py-2 px-4">{game.playerHand}</td>
                <td className="py-2 px-4">{game.computerHand}</td>
                <td className="py-2 px-4 font-semibold">{game.result}</td>
                <td className="py-2 px-4">{new Date(game.createdAt).toLocaleString('ja-JP')}</td>
              </tr>
            ))}
            {gameResults.length === 0 && (
              <tr>
                <td colSpan={4} className="py-4 px-4 text-center text-gray-500">
                  まだ対戦結果はありません。
                </td>
              </tr>
            )}
          </tbody>
        </table>
      </div>
    </div>
  );
}

コメント

  • 全てのコンポーネントがサーバー・コンポーネントです。やられました😅
    • React19で入ったActionsを使ってますね
    • 私が書いたコードではジャンケンボタンのコンポーネントはクライアント・コンポーネントでジャンケンの判定・DB格納等はServer Function呼出しを行っていました
  • DB操作やジャンケンの判定等もメインのモジュールの書かれていますね
  • Hand, Result等の型が複数のファイルに書かれてますね

その後、リファクタリングを頼むと、これらの問題は解決されてました。😁

Claude 3.7

Claude 3.7の生成したコードは動作しましたが、表示が変でした。😅
直ぐに原因が想像できたので直しました、画面は以下のようにでGeminiに比べて少し貧弱ですね。日時が表示されていたり、罫線にも色が付いているのはGeminiと同じです。

さて生成されたコードは

生成されたコードは、Geminiに比べると素晴らしい構成になっていました。

  • app/page.tsx
import JankenGameClient from './components/JankenGameClient';
import { getGameResults } from './actions/game-actions';

export default async function Home() {
  // Fetch initial game results
  const initialResults = await getGameResults();

  return (
    <main className="container mx-auto px-4 py-8 max-w-4xl">
      <h1 className="text-3xl font-bold mb-6 text-center">じゃんけんゲーム</h1>

      <div className="bg-white p-6 rounded-lg shadow-md">
        <JankenGameClient initialResults={initialResults} />
      </div>
    </main>
  );
}
  • components/JankenGameClient.tsx
'use client';

import { useState } from 'react';
import { Suspense } from 'react';
import { GameResultRecord } from '../types';
import JankenButton from './JankenButton';
import ResultsTable from './ResultsTable';
import { getGameResults } from '../actions/game-actions';

interface JankenGameClientProps {
  initialResults: GameResultRecord[];
}

export default function JankenGameClient({ initialResults }: JankenGameClientProps) {
  const [results, setResults] = useState<GameResultRecord[]>(initialResults);

  // Function to refresh results after a game is played
  const refreshResults = async () => {
    const newResults = await getGameResults();
    setResults(newResults);
  };

  return (
    <>
      <JankenButton onGamePlayed={refreshResults} />

      <Suspense fallback={<div>結果を読み込み中...</div>}>
        <ResultsTable results={results} />
      </Suspense>
    </>
  );
}
  • components/JankenButton.tsx
'use client';

import { useState } from 'react';
import { Hand } from '../types';
import { getHandName } from '../lib/game-utils';
import { playGame } from '../actions/game-actions';

interface JankenButtonProps {
  onGamePlayed: () => void;
}

export default function JankenButton({ onGamePlayed }: JankenButtonProps) {
  const [isLoading, setIsLoading] = useState(false);
  const hands: Hand[] = ['rock', 'paper', 'scissors'];

  const handlePlay = async (hand: Hand) => {
    try {
      setIsLoading(true);
      await playGame(hand);
      onGamePlayed();
    } catch (error) {
      console.error('Error playing game:', error);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div className="my-6">
      <h2 className="text-xl font-bold mb-4">あなたの手を選んでください</h2>
      <div className="flex flex-wrap gap-4">
        {hands.map((hand) => (
          <button
            key={hand}
            onClick={() => handlePlay(hand)}
            disabled={isLoading}
            className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
          >
            {getHandName(hand)}
          </button>
        ))}
      </div>
    </div>
  );
}
  • components/ResultsTable.tsx
import { GameResultRecord } from '../types';
import { getHandName, getResultName } from '../lib/game-utils';

interface ResultsTableProps {
  results: GameResultRecord[];
}

export default function ResultsTable({ results }: ResultsTableProps) {
  // Function to determine the text color based on the result
  const getResultColor = (result: string) => {
    switch (result) {
      case 'win':
        return 'text-blue-600';
      case 'lose':
        return 'text-red-600';
      default:
        return 'text-black';
    }
  };

  return (
    <div className="mt-8">
      <h2 className="text-xl font-bold mb-4">対戦結果</h2>
      <div className="overflow-x-auto">
        <table className="min-w-full bg-white border border-gray-300">
          <thead>
            <tr className="bg-gray-100">
              <th className="py-2 px-4 border-b">プレイヤーの手</th>
              <th className="py-2 px-4 border-b">コンピューターの手</th>
              <th className="py-2 px-4 border-b">結果</th>
              <th className="py-2 px-4 border-b">日時</th>
            </tr>
          </thead>
          <tbody>
            {results.length === 0 ? (
              <tr>
                <td colSpan={4} className="py-4 px-4 text-center">
                  まだ対戦結果はありません。
                </td>
              </tr>
            ) : (
              results.map((result) => (
                <tr key={result.id} className={getResultColor(result.result)}>
                  <td className="py-2 px-4 border-b">{getHandName(result.playerHand)}</td>
                  <td className="py-2 px-4 border-b">{getHandName(result.computerHand)}</td>
                  <td className="py-2 px-4 border-b font-medium">{getResultName(result.result)}</td>
                  <td className="py-2 px-4 border-b">
                    {new Date(result.createdAt).toLocaleString('ja-JP')}
                  </td>
                </tr>
              ))
            )}
          </tbody>
        </table>
      </div>
    </div>
  );
}
  • types/index.ts
// Hand options for Rock-Paper-Scissors
export type Hand = 'rock' | 'paper' | 'scissors';

// Result of the game
export type Result = 'win' | 'lose' | 'draw';

// Game result record
export interface GameResultRecord {
  id: number;
  playerHand: Hand;
  computerHand: Hand;
  result: Result;
  createdAt: Date;
}
  • app/lib/game-utils.ts
import { Hand, Result } from '../types';

/**
 * Generate a random hand for the computer
 * @returns A random hand (rock, paper, or scissors)
 */
export function getComputerHand(): Hand {
  const hands: Hand[] = ['rock', 'paper', 'scissors'];
  const randomIndex = Math.floor(Math.random() * hands.length);
  return hands[randomIndex];
}

/**
 * Determine the result of the game
 * @param playerHand The player's hand
 * @param computerHand The computer's hand
 * @returns The result (win, lose, or draw)
 */
export function determineResult(playerHand: Hand, computerHand: Hand): Result {
  if (playerHand === computerHand) {
    return 'draw';
  }

  if (
    (playerHand === 'rock' && computerHand === 'scissors') ||
    (playerHand === 'paper' && computerHand === 'rock') ||
    (playerHand === 'scissors' && computerHand === 'paper')
  ) {
    return 'win';
  }

  return 'lose';
}

/**
 * Get the Japanese name for a hand
 * @param hand The hand to get the name for
 * @returns The Japanese name for the hand
 */
export function getHandName(hand: Hand): string {
  const handNames: Record<Hand, string> = {
    rock: 'グー',
    paper: 'パー',
    scissors: 'チョキ',
  };
  return handNames[hand];
}

/**
 * Get the Japanese name for a result
 * @param result The result to get the name for
 * @returns The Japanese name for the result
 */
export function getResultName(result: Result): string {
  const resultNames: Record<Result, string> = {
    win: '勝ち',
    lose: '負け',
    draw: '引き分け',
  };
  return resultNames[result];
}
  • app/actions/game-actions.ts
'use server';

import { prisma } from '../lib/prisma';
import { Hand, GameResultRecord } from '../types';
import { getComputerHand, determineResult } from '../lib/game-utils';

/**
 * Play a round of Rock-Paper-Scissors
 * @param playerHand The player's hand
 * @returns The game result
 */
export async function playGame(playerHand: Hand): Promise<GameResultRecord> {
  // Generate computer's hand
  const computerHand = getComputerHand();

  // Determine the result
  const result = determineResult(playerHand, computerHand);

  // Save the result to the database
  const gameResult = await prisma.gameResult.create({
    data: {
      playerHand,
      computerHand,
      result,
    },
  });

  return gameResult as GameResultRecord;
}

/**
 * Get all game results from the database
 * @returns An array of game results
 */
export async function getGameResults(): Promise<GameResultRecord[]> {
  const results = await prisma.gameResult.findMany({
    orderBy: {
      createdAt: 'desc',
    },
  });

  return results as GameResultRecord[];
}

コメント

  • 最初に表示が変だったのは、生成されたコードのglobal.cssがTailwind CSSバージョン4.0用ではなく3.X用だったからです。4.0がリーリースされたのは2025年1月22日なのでClaude 3.7は知らなのかも
  • 繰り返しますが、最初から適当なコンポーネント分割や型定義、ライブラリー、Actions(Server Function)に分かれています
  • ジャンケンのメイン・コンポーネントJankenGameClientは、なぜかジャンケンの結果をStateで管理しています❓
  • さらにJankenGameClient内ではSuspenseを使っていますが、Suspense内のResultsTableコンポーネントはPromiseをThrowしないので変ですね❓

まとめ

Roo Codeで2,3百行のコードが16行のプロンプトから生成でき、ちゃんと動いたのは素晴らしいですね。
ただしClaude 3.7は最近リリースされたTailwind CSS 4.0の事を知らなかったのか、ネット上には3.Xの情報が多いのでだまされたしまったのか、表示は失敗していました。

生成されたコードを見ると、Gemini 2.5 Proの方はコンポーネントやライブラリー等に分割されておらず、このままで使う気はしないものでした。ただし、Gemini 2.5 Proにリファクタリングを依頼するとClaude 3.7と同等なコンポーネントやライブラリー構成になりましたので。Gemini 2.5 Proは使う方は、必ずリファクタリングした方が良いと思います。

Claude 3.7の生成したコードはもっともらしいけど不思議なコードでした。今回はReact Server Componentsという新し目の技術を使ったので、まだ学習が足りないのでしょうか?

ちなみにコストですが、現在Gemini 2.5 Proは無料です。Claude 3.7の方は¥95くらいでした。😁

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ