Amazon Q CLI でアプリ（ゲーム）を作ってみよう！

こんにちは。
今回は、日本語にも対応した Amazon Q CLI を使ってアプリを作ってみました。

今回は、[AWS のキャンペーン](https://aws.amazon.com/jp/blogs/news/build-games-with-amazon-q-cli-and-score-a-t-shirt/)に合わせて、ゲームを題材にしていますが Web アプリなどにも応用できますので、ぜひ試してみてください。

## Amazon Q Developer とは
Amazon Q Developer は、ソフトウェア開発のあらゆる工程を支援する AI アシスタントです。AWS の知識を活かし、コードの生成、デバッグ、最適化、セキュリティスキャン、ドキュメント作成などをサポートします。

### Amazon Q CLI とは？
### Amazon Q CLI の特徴とできること

Amazon Q CLI は Amazon Q Developer をCLIで使えるといったもので、AWS 純正の OSS です。
VSCode などの IDE を用いらず、純粋にターミナルやPowerShell のウィンドウ上で利用ができます

## 開発環境の準備
### インストール方法と初期設定

こちらの AWS 公式ドキュメントをご確認ください。
https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-installing.html#command-line-installing-macos

わたしは、[前回の記事](https://qiita.com/hirosys-biz/items/8f556ad21c3ced5d2de0)の記事の通り、Windows 11 に WSL 環境を構築し、そこに Amazon Q CLI を導入しています。Mac などの方は、公式ドキュメント記載の手順で試してみてください。

## 実際に作ってみよう
### 〇✖ゲームを作ってみよう

まずは、簡単に〇✖ゲーム（三目並べ）を作ってもらいます。

```sh
> Web ブラウザで動作する三目並べ（tick-tack-toe）を作ってください
Webブラウザで動作する三目並べ（tic-tac-toe）ゲームを作成しましょう。HTML、CSS、JavaScriptを使って基本
的な三目並べゲームを実装します。
```

この一言でできたものがこちらの3つのファイルです。

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>三目並べ（Tic-Tac-Toe）</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <h1>三目並べ（Tic-Tac-Toe）</h1>
        <div id="status" class="status">先手（X）の番です</div>
        <div class="game-board">
            <div class="cell" data-cell-index="0"></div>
            <div class="cell" data-cell-index="1"></div>
            <div class="cell" data-cell-index="2"></div>
            <div class="cell" data-cell-index="3"></div>
            <div class="cell" data-cell-index="4"></div>
            <div class="cell" data-cell-index="5"></div>
            <div class="cell" data-cell-index="6"></div>
            <div class="cell" data-cell-index="7"></div>
            <div class="cell" data-cell-index="8"></div>
        </div>
        <button id="restart-button">ゲームをリセット</button>
    </div>
    <script src="script.js"></script>
</body>
</html>
```

```css:style.css
* {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

body {
    font-family: 'Arial', sans-serif;
    background-color: #f0f0f0;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
}

.container {
    text-align: center;
    background-color: white;
    border-radius: 10px;
    padding: 20px;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}

h1 {
    color: #333;
    margin-bottom: 20px;
}

.status {
    margin-bottom: 20px;
    font-size: 1.2rem;
    font-weight: bold;
    color: #555;
}

.game-board {
    display: grid;
    grid-template-columns: repeat(3, 100px);
    grid-template-rows: repeat(3, 100px);
    gap: 5px;
    margin: 0 auto 20px;
    max-width: 310px;
}

.cell {
    background-color: #e0e0e0;
    border-radius: 5px;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 3rem;
    font-weight: bold;
    cursor: pointer;
    transition: background-color 0.3s ease;
}

.cell:hover {
    background-color: #d0d0d0;
}

.cell.x {
    color: #e74c3c;
}

.cell.o {
    color: #3498db;
}

#restart-button {
    background-color: #3498db;
    color: white;
    border: none;
    padding: 10px 20px;
    font-size: 1rem;
    border-radius: 5px;
    cursor: pointer;
    transition: background-color 0.3s ease;
}

#restart-button:hover {
    background-color: #2980b9;
}
```

```js:script.js
document.addEventListener('DOMContentLoaded', () => {
    const statusDisplay = document.getElementById('status');
    const cells = document.querySelectorAll('.cell');
    const restartButton = document.getElementById('restart-button');

    let gameActive = true;
    let currentPlayer = 'X';
    let gameState = ['', '', '', '', '', '', '', '', ''];

    const winningConditions = [
        [0, 1, 2],
        [3, 4, 5],
        [6, 7, 8],
        [0, 3, 6],
        [1, 4, 7],
        [2, 5, 8],
        [0, 4, 8],
        [2, 4, 6]
    ];

    const messages = {
        playerTurn: () => `${currentPlayer === 'X' ? '先手（X）' : '後手（O）'}の番です`,
        draw: () => '引き分けです！',
        playerWin: () => `${currentPlayer === 'X' ? '先手（X）' : '後手（O）'}の勝ちです！`
    };

    function handleCellClick(clickedCellEvent) {
        const clickedCell = clickedCellEvent.target;
        const clickedCellIndex = parseInt(clickedCell.getAttribute('data-cell-index'));

        if (gameState[clickedCellIndex] !== '' || !gameActive) {
            return;
        }

        gameState[clickedCellIndex] = currentPlayer;
        clickedCell.textContent = currentPlayer;
        clickedCell.classList.add(currentPlayer.toLowerCase());

        handleResultValidation();
    }

    function handleResultValidation() {
        let roundWon = false;
        for (let i = 0; i < winningConditions.length; i++) {
            const [a, b, c] = winningConditions[i];
            const condition = gameState[a] && gameState[a] === gameState[b] && gameState[a] === gameState[c];
            
            if (condition) {
                roundWon = true;
                break;
            }
        }

        if (roundWon) {
            statusDisplay.textContent = messages.playerWin();
            gameActive = false;
            return;
        }

        const roundDraw = !gameState.includes('');
        if (roundDraw) {
            statusDisplay.textContent = messages.draw();
            gameActive = false;
            return;
        }

        currentPlayer = currentPlayer === 'X' ? 'O' : 'X';
        statusDisplay.textContent = messages.playerTurn();
    }

    function handleRestartGame() {
        gameActive = true;
        currentPlayer = 'X';
        gameState = ['', '', '', '', '', '', '', '', ''];
        statusDisplay.textContent = messages.playerTurn();
        
        cells.forEach(cell => {
            cell.textContent = '';
            cell.classList.remove('x');
            cell.classList.remove('o');
        });
    }

    cells.forEach(cell => cell.addEventListener('click', handleCellClick));
    restartButton.addEventListener('click', handleRestartGame);
});
```

index.html ファイルを Web ブラウザで開くとこのような感じでした。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/abd1d478-64d3-4f7e-bcbe-8e5e69d35756.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/bc2f9632-a42d-4d9c-88b3-46a655fd0cd6.png)

人 vs 人のゲームになったようです。
近くに対戦相手がいればこれでもいいですが、できれば一人で遊びたいですよね？また、〇✖ゲームなので、先手は〇のほうがよかったので、そのように、 Amazon Q に伝えます。

```sh
> 先手を〇、後手を×にしてください。また、1人でも遊べるようにしてください
```

修正後、コード差分が表示されますが長いので割愛します。処理終了後、以下のように応答が返ってきます。

```sh
三目並べゲームを以下のように変更しました：

1. 先手を〇、後手を×に変更
2. 1人でも遊べるようにAI対戦機能を追加

### 主な変更点：

1. 記号の変更
   • 先手が「〇」、後手が「×」になりました
   • 表示テキストもすべて対応するように更新しました

2. AI対戦機能の追加
   • 「コンピュータと対戦」ボタンを追加しました
   • AIは以下の戦略で動作します：
     • 勝利可能な手があれば、その手を選びます
     • プレイヤーが次の手で勝てる場合、それをブロックします
     • 中央が空いていれば、中央を選びます
     • それ以外はランダムな手を選びます
   • AIの思考中は「コンピュータが考え中...」と表示されます

3. UIの改善
   • ボタンのレイアウトを整えました
   • 「コンピュータと対戦」ボタンを緑色にして区別しやすくしました

### 遊び方：

1. ブラウザでindex.htmlファイルを開きます
2. デフォルトでは「コンピュータと対戦」モードになっています
   • プレイヤーは「〇」、コンピュータは「×」です
3. 「2人で対戦」モードに切り替えることもできます
4. 「ゲームをリセット」ボタンでいつでも新しいゲームを始められます

ゲームを開始するには、Windowsエクスプローラーで /mnt/c/Users/****/index.html に移
動して、ダブルクリックしてください。

AIの難易度を調整したり、他の機能を追加したりしたい場合は、お気軽にお知らせください！
```

起動してみると、以下のようになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/ab0fd7e1-639e-4d79-991d-b9df407ce5ef.png)

これだけで、AI と対戦もできる三目並べができました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/3e427fa7-c749-4ea1-80d6-b9e949020216.png)
なんとか勝てた時のスクリーンショットです。

### 自分の作ってみたいゲームのアイディアを伝えて作ってみよう

回転寿司屋さんでお寿司を食べるゲームを作ってみました。

最初に Amazon Q CLI に伝えた内容は以下の通りです。

```sh
> ゲームの仕様をお伝えします。

・制限時間は60秒
・回転ずし屋さんをイメージした背景
・人（プレイヤー）が真ん中でお寿司を食べます。
・お寿司が右から左に流れていきます。
・プレイヤーの前にお寿司が流れてきたら、タイミングよく画面をタップもしくはクリックすることでお寿司を食べます。
・たまにワサビがたっぷり入ったお寿司が流れてきますが、これを食べると5秒間お寿司を食べられなくなります。
・ワサビのお寿司を食べたら画面に表示されているお茶アイコンをクリック・タップするかパソコンならtボタンを押下することでお茶を飲んで食べられなくなる時間を1秒に短縮します。
・点数は食べたお寿司の個数と値段で決まります。高いお寿司ほど出る確率が低いです。
・点数はクッキーに記録され、最高個数、値段、日時を表示できるようにします。
・Amazon CloudFront と Amazon S3 を使って配信します。
```

当初は、お寿司や人といったオブジェクトが 4bit ゲーム機の頃のような色がついただけの箱でしたが、何度かチャットのやり取りをして、改善させた結果、以下のようなゲームが完成しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/7f084860-a313-4b27-a24a-0da8550739a3.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/d60669ce-eea6-48f5-8245-dfa6e240b159.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/ea98a770-abb4-445b-bb36-31c896ec8937.png)

AWS CloudFront と Amazon S3 を用いて配信したいとも伝えているので、画像ファイルや Javascript ファイルなど動作するのに必要なものも自動的に S3 バケットに格納してくれました（もちろん、CloudFront ディストリビューションやバケットポリシーといった設定もしてくれています）

チャットだけでこのようなゲームを完成させ、配信することができてしまいました！

## まとめ

ノーコードでゲームのルーチンや画像まで生成してくれる Amazon Q CLI ですが、これを DynamoDB や Lambda を組み合わせるようにすれば動的な処理のゲームや Web アプリもすぐに作ってくれるでしょう。動作イメージを見せるためのモックアップならすぐに作れると思うので、例えば、アプリ開発の要件定義に活用したりもできそうです。

ーーー
記載されている会社名、製品名、サービス名、ロゴ等は各社の商標または登録商標です。