🔖

WindsurfでWebアプリを作ってみる

に公開

前書き

こんにちは!
オアシステクノロジーズの山本です。

AIを使って簡単なWebアプリをNext.jsで作ってみたいなと思い、Windsurfを使ってみました。
ただ、使ってみた結果としては「うーん微妙」というのが正直な感想です。

これにはいろいろ理由があるのですが、VSCodeの拡張機能としてWindsurfを使ったのが大きいと考えています。
IDEもWindsurfを使うべきでした。。。

特にワークスペースの実装状態の把握させるのが大変で、同じところの実装をぐるぐる回る、
チャットを一度閉じて作業を再開すると、前回までの状態が認識できていないようでした。

皆さんが実施される際には、IDEはWindsurfを使うことをお勧めします。

Windsurfについてのもろもろ

細かい話はいろいろありますが、今回Windsurfを使う理由は「ファイルを一括で修正可能」なことです。
Webアプリを作るとき、ファイルを一つ一つ修正していくのは面倒ですし、影響箇所を見極めることは一定のスキルが必要となります。
Windsurfを使うことで一括で修正できるのであれば、開発が楽になるのではと思い、使ってみることにしました。

こちらのサイトを参考にさせていただきました。今回はVSCodeを使用します、またnpm等のインストールは済んでいる前提で進めます。

https://weel.co.jp/media/innovator/windsurf-ai/#index_id0

作ってみるWebアプリについて

今回は、AIを使って簡単なWebアプリを作ってみたいと思います。
具体的には、Next.jsを使ったブラックジャックのWebアプリをです。

ブラックジャックは、プレイヤーとディーラーが対戦するカードゲームで、21を超えないように手札の合計を競います。
ルールは簡単で、プレイヤーは2枚のカードを引き、さらに1枚引くかどうかを選択します。
ディーラーは、17以上の合計になるまでカードを引き続けます。
プレイヤーが21を超えた場合は負け、ディーラーが21を超えた場合はプレイヤーの勝ちとなります。

Webアプリの作成

早速、Windsurfに丸投げしてみます!特に細かい設定を行わず
「Next.jsでブラックジャックのWebアプリを作ってください。」と投げてみます。

得られた、結果は以下の通りです。ステップ毎に実際に実行していきます。


Next.jsでブラックジャックのWebアプリを作る
ブラックジャックのWebアプリを作るために、以下のステップに従ってください。

ステップ1: Next.jsプロジェクトを作成する
以下のコマンドを実行して、Next.jsプロジェクトを作成します。

bash
CopyInsert in Terminal
npx create-next-app blackjack-app

ステップ2: pagesディレクトリを作成する
pagesディレクトリを作成し、そこにindex.jsファイルを作成します。

bash
CopyInsert
mkdir pages
touch pages/index.js
ステップ3: ブラックジャックのゲームロジックを作成する

pages/index.jsファイルに以下のコードを追加します。

jsx
CopyInsert
import Head from 'next/head';

function Blackjack() {
  const [deck, setDeck] = useState([]);
  const [playerHand, setPlayerHand] = useState([]);
  const [dealerHand, setDealerHand] = useState([]);
  const [result, setResult] = useState('');

  useEffect(() => {
    const createDeck = () => {
      const suits = ['Hearts', 'Diamonds', 'Clubs', 'Spades'];
      const ranks = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A'];
      const deck = [];
      for (const suit of suits) {
        for (const rank of ranks) {
          deck.push({ suit, rank });
        }
      }
      return deck;
    };
    setDeck(createDeck());
  }, []);

  const dealCard = () => {
    const card = deck.pop();
    setPlayerHand([...playerHand, card]);
    setDealerHand([...dealerHand, deck.pop()]);
  };

  const calculateScore = (hand) => {
    let score = 0;
    for (const card of hand) {
      if (card.rank === 'A') {
        score += 11;
      } else if (card.rank === 'J' || card.rank === 'Q' || card.rank === 'K') {
        score += 10;
      } else {
        score += parseInt(card.rank);
      }
    }
    return score;
  };

  const checkResult = () => {
    const playerScore = calculateScore(playerHand);
    const dealerScore = calculateScore(dealerHand);
    if (playerScore > 21) {
      setResult('Player busts! Dealer wins!');
    } else if (dealerScore > 21) {
      setResult('Dealer busts! Player wins!');
    } else if (playerScore > dealerScore) {
      setResult('Player wins!');
    } else if (playerScore < dealerScore) {
      setResult('Dealer wins!');
    } else {
      setResult('Push!');
    }
  };

  const handleHit = () => {
    dealCard();
    checkResult();
  };

  const handleStand = () => {
    checkResult();
  };

  return (
    <div>
      <Head>
        <title>ブラックジャック</title>
      </Head>
      <h1>ブラックジャック</h1>
      <p>Player's hand: {playerHand.map((card) => `${card.rank} of ${card.suit}`).join(', ')}</p>
      <p>Dealer's hand: {dealerHand.map((card) => `${card.rank} of ${card.suit}`).join(', ')}</p>
      <p>Result: {result}</p>
      <button onClick={handleHit}>Hit</button>
      <button onClick={handleStand}>Stand</button>
    </div>
  );
}

export default Blackjack;

ステップ4: アプリを起動する
以下のコマンドを実行して、アプリを起動します。

bash
CopyInsert in Terminal
npm run dev
ブラウザでhttp://localhost:3000にアクセスすると、ブラックジャックのWebアプリが表示されます。

Note: このコードは、ブラックジャックの基本的なルールに基づいて作成されています。より複雑なルールや機能を追加する場合は、コードを修正する必要があります。

ステップ1: Next.jsプロジェクトを作成する

「npx create-next-app blackjack-app」のコマンドを実行しろとのこと。
これを実行すると、Next.jsのプロジェクトが作成されます。

ただ、細かい設定については記載がありません。TypeScriptを使うか、AppRouterを使うか、CSSフレームワークを使うかなど
このあたりはも決めさせてしまいたい。。。

npxコマンド実行後、問い合わせ内容をもとに、以下のように、Windsurfに投げると決めてくれたので、その内容で進めます。

「Would you like your code inside a src/ directory? と問われています、決めて」

最終的には以下となりました。


? Would you like to customize the import alias

> npx create-next-app blackjack-app
Need to install the following packages:
create-next-app@15.3.1
Ok to proceed? (y) y

√ Would you like to use TypeScript? ... No / Yes
√ Would you like to use ESLint? ... No / Yes
√ Would you like to use Tailwind CSS? ... No / Yes
√ Would you like your code inside a `src/` directory? ... No / Yes
√ Would you like to use App Router? (recommended) ... No / Yes
√ Would you like to use Turbopack for `next dev`? ... No / Yes
√ Would you like to customize the import alias (`@/*` by default)? ... No / Yes
Creating a new Next.js app in C:\Users\yamamoto\blackjack-app.

ステップ2: コンポーネントの作成(pagesディレクトリを作成する)

最初のWindsurfからの指示ではpagesディレクトリを作成するとのことです。しかし、先ほどのプロジェクト作成時の
設定を考慮し、再度確認を行います。

「Next.jsでブラックジャックのWebアプリを作ってください。」
と投げてみます。

得られた結果は変わり、コンポーネントを作成するようにとのこと。提示されたファイルを作成していきます。


ステップ2: コンポーネントの作成

componentsディレクトリを作成し、以下のコンポーネントを作成します。

Card.js: カードを表示するコンポーネント
Deck.js: デッキを管理するコンポーネント
Game.js: ゲームのロジックを管理するコンポーネント
Hand.js: プレイヤーの手札を表示するコンポーネント

components/Card.tsx
import React from 'react';

interface CardProps {
  suit: string;
  rank: string;
  reveal: boolean;
}

const Card: React.FC<CardProps> = ({ suit, rank, reveal }) => {
  return (
    <div>
      <span>{rank}</span>
      <span>{suit}</span>
    </div>
  );
};

export default Card;
components/Deck.tsx
import React, { useState } from 'react';
import Card from './Card';

interface DeckProps {
  // なし
}

interface Card {
  suit: string;
  rank: string;
  value: number;
}

const Deck: React.FC<DeckProps> = () => {
  const [cards, setCards] = useState<Card[]>([]);
  const [deck, setDeck] = useState<Card[]>([]);

  const createDeck = () => {
    const suits = ['Hearts', 'Diamonds', 'Clubs', 'Spades'];
    const ranks = ['Ace', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'Jack', 'Queen', 'King'];

    const newDeck: Card[] = [];
    for (const suit of suits) {
      for (const rank of ranks) {
        newDeck.push({ suit, rank, value: getValue(rank) });
      }
    }

    setDeck(newDeck);
  };

  const shuffleDeck = () => {
    const shuffledDeck = [...deck];
    for (let i = shuffledDeck.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [shuffledDeck[i], shuffledDeck[j]] = [shuffledDeck[j], shuffledDeck[i]];
    }
    setDeck(shuffledDeck);
  };

  const dealCard = () => {
    if (deck.length > 0) {
      const topCard = deck[0];
      setCards([...cards, topCard]);
      setDeck(deck.slice(1));
    }
  };

  const getValue = (rank: string): number => {
    switch (rank) {
      case 'Ace':
        return 11;
      case 'Jack':
      case 'Queen':
      case 'King':
        return 10;
      default:
        return parseInt(rank, 10);
    }
  };

  return (
    <div>
      <button onClick={createDeck}>Create Deck</button>
      <button onClick={shuffleDeck}>Shuffle Deck</button>
      <button onClick={dealCard}>Deal Card</button>
      <ul>
        {cards.map((card, index) => (
          <li key={index}>
            <Card suit={card.suit} rank={card.rank} reveal={true} />
          </li>
        ))}
      </ul>
    </div>
  );
};

export default Deck;
components/Game.tsx
import React, { useState } from 'react';
import Deck from './Deck';
import Hand from './Hand';

interface GameProps {
  // なし
}

interface Card {
  suit: string;
  rank: string;
  value: number;
}

const Game: React.FC<GameProps> = () => {
  const [playerHand, setPlayerHand] = useState<Card[]>([]);
  const [dealerHand, setDealerHand] = useState<Card[]>([]);
  const [gameOver, setGameOver] = useState(false);

  const startGame = () => {
    setPlayerHand([]);
    setDealerHand([]);
    setGameOver(false);
  };

  const hit = () => {
    if (playerHand.length < 5) {
      const newCard = { suit: 'Hearts', rank: 'Ace', value: 11 };
      setPlayerHand([...playerHand, newCard]);
    }
  };

  const stand = () => {
    setGameOver(true);
  };

  return (
    <div>
      <Deck />
      <Hand hand={playerHand} isDealer={false} reveal={true} />
      <Hand hand={dealerHand} isDealer={true} reveal={false} />
      <button onClick={startGame}>Start Game</button>
      <button onClick={hit}>Hit</button>
      <button onClick={stand}>Stand</button>
      {gameOver && <p>Game Over!</p>}
    </div>
  );
};

export default Game;
components/Hand.tsx
import React from 'react';
import Card from './Card';

interface HandProps {
  hand: { suit: string; rank: string; value: number }[];
  isDealer: boolean;
  reveal: boolean;
}

const Hand: React.FC<HandProps> = ({ hand, isDealer, reveal }) => {
  const handValue = hand.reduce((acc, card) => acc + card.value, 0);

  return (
    <div>
      <h2>
        {isDealer ? 'Dealer' : 'Player'}'s Hand
      </h2>
      <ul>
        {hand.map((card, index) => (
          <li key={index}>
            <Card suit={card.suit} rank={card.rank} reveal={reveal} />
          </li>
        ))}
      </ul>
      <p>
        Hand Value: {handValue}
      </p>
    </div>
  );
};

export default Hand;

ステップ3: App.tsx Index.tsx の追加 ブラックジャックのゲームロジックを作成する

当初の流れと変わっているので、次は何をすればよいか確認するとApp.tsxとIndex.tsxを作成するようにとのこと。

App.tsx
import React from 'react';
import Game from './components/Game';

const App: React.FC = () => {
  return (
    <div>
      <Game />
    </div>
  );
};

export default App;

index.tsx

import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';

const root = createRoot(document.getElementById('root')!);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

ステップ4: アプリを起動する

はい、これで動作する!とはなりませんでした。
いろいろとエラーが出てきます。。。解決させようと、プロンプトを入れ続けるのですが、
始めに書いたとおり、実装状態の把握させるのが大変で、同じところの実装をぐるぐる回り解決しない。。。

無料プランなので、モデルもBaseModelを使用しており、これも原因かもしれません。。。

結局、ChatGpt(o4 mini high)を使い一から実装し直しました。。。
実装し直すと一発で解決しました。

まとめ

AIコーディング凄い!という記事を書こうと思ったのですが、変な落とし穴にはまってしまったという、なんとも締まらない記事になってしまいました。

しかし、AIコーディングの進化は凄い勢いで進んでいます。
めげずに、引き続き挑戦していきたいと思います。

参考

https://weel.co.jp/media/innovator/windsurf-ai/#index_id0
https://note.com/mikiokubo/n/n5974e27485b2

Discussion