K.Shirakiをフォローする

話題のAIエディタ「cursor」を使ってヘビゲームを作ってみた

開発環境・ツール

今更ながらcursor使ってみました

話題の次世代エディタcursorですがせっかくの機会なので使ってみることにしました
https://www.cursor.com

cursorとは

2023年に登場したAI搭載エディタです
VSCodeのフォークなので多くの人が比較的使い慣れたUIで使うことができると思います
有料ですが2週間まで制限付きでトライアルすることができます

Proプランで$20なので今だと3000円くらいですね

なにを作ろう

やっぱりとりあえず何か作ってみるならゲームっぽいものがいいので、今回は古からあるゲームであるヘビがエサをとって長くなるゲームを作ろうかと思います

さっそく作ってみる

とりあえず相談してみる

Ctrl/Command + L でチャットウィンドウが立ち上がるので、なんでも好きなことを聞くことができます

さっそく聞いてみます

わかるみたいです
なんだがPythonで作ろうとしてくれているみたいですね(ヘビだけに🐍)

でも、今回はJavaScriptで作りたいのでできそうか聞いてみます

canvas要素を使って作るのが一般的なようですが、ブログに対応していないようだったので今回はなしでいきます

ここまでなら普通のChatGPTでもやってくれそうですが、cursorはAI搭載エディタなのでワンクリックでファイルにも出力してくれます

すると、いきなり完成品ができてしまいました

ふ、普通に遊べる…!
なんだか感動を通り越して少し怖いですね

しかし、以下の点が気になりました

  • ヘビの動くスピードが遅い
  • スコアが10ずつ増えていく(1ずつにしたい)
  • 壁に当たってもゲームオーバーにならない
  • カーソルの↑↓を押すたびにスクロールされてしまう
  • ページを読み込むと即ゲーム開始してしまう

これらを修正していきたいと思います

修正:ヘビの動くスピードが遅い

修正:スコアが10ずつ増えていく(1ずつにしたい)

修正:カーソルの↑↓を押すたびにスクロールされてしまう

修正:壁に当たってもゲームオーバーにならない

修正:ページを読み込むと即ゲーム開始してしまう

完成品

完成品がこちらです
実際に遊べます
(カーソルキーで操作するのでPCで見てください)

スコア: 0
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>スネークゲーム</title>
    <style>
        .game-board {
            display: grid;
            grid-template-columns: repeat(20, 20px);
            grid-template-rows: repeat(20, 20px);
            gap: 1px;
            background-color: #eee;
            width: fit-content;
            padding: 10px;
            border-radius: 4px;
        }

        .cell {
            width: 20px;
            height: 20px;
            background-color: white;
        }

        .snake {
            background-color: #4CAF50;
            border-radius: 4px;
        }

        .food {
            background-color: #F44336;
            border-radius: 50%;
        }

        .controls {
            margin-top: 10px;
        }

        button {
            padding: 8px 16px;
            margin-right: 10px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 16px;
        }

        #startButton {
            background-color: #4CAF50;
            color: white;
        }

        #retryButton {
            background-color: #2196F3;
            color: white;
            display: none;
        }
    </style>
</head>

<body>
    <div id="game-board" class="game-board"></div>
    <div id="score">スコア: 0</div>
    <div class="controls">
        <button id="startButton">スタート</button>
        <button id="retryButton">リトライ</button>
    </div>
    <script>
        /**
         * スネークゲームのメインクラス
         */
        class SnakeGame {
            /**
             * ゲームの初期設定を行うコンストラクタ
             */
            constructor() {
                // ゲームの基本設定
                this.boardSize = 20;  // ゲームボードの大きさ(20x20)
                this.board = document.getElementById('game-board');
                this.scoreElement = document.getElementById('score');
                this.startButton = document.getElementById('startButton');
                this.retryButton = document.getElementById('retryButton');
                this.speed = 100;     // ゲームの速度(ミリ秒)
                this.gameInterval = null;
                this.isGameRunning = false;

                // 初期設定の実行
                this.setupInitialState();
                this.setupControls();
                this.setupButtons();
            }

            /**
             * ゲームの初期状態を設定
             */
            setupInitialState() {
                this.score = 0;

                // 蛇の初期位置(中央付近)
                this.snake = [{ x: 10, y: 10 }];
                this.direction = { x: 1, y: 0 };  // 最初は右向きに移動
                this.food = this.generateFood();
                this.setupBoard();
                this.draw();
            }

            /**
             * スタート・リトライボタンの設定
             */
            setupButtons() {
                // スタートボタンのクリックイベント
                this.startButton.addEventListener('click', () => {
                    if (!this.isGameRunning) {
                        this.start();
                        this.startButton.style.display = 'none';
                        this.retryButton.style.display = 'inline-block';
                    }
                });

                // リトライボタンのクリックイベント
                this.retryButton.addEventListener('click', () => {
                    this.reset();
                });
            }

            /**
             * ゲームをリセット
             */
            reset() {
                if (this.gameInterval) {
                    clearInterval(this.gameInterval);
                }
                this.setupInitialState();
                this.scoreElement.textContent = 'スコア: 0';
                this.isGameRunning = false;
                this.startButton.style.display = 'inline-block';
                this.retryButton.style.display = 'none';
            }

            /**
             * ゲームボードのDOM要素を作成
             */
            setupBoard() {
                this.board.innerHTML = '';
                for (let i = 0; i < this.boardSize * this.boardSize; i++) {
                    const cell = document.createElement('div');
                    cell.className = 'cell';
                    this.board.appendChild(cell);
                }
            }

            /**
             * 指定座標のセル要素を取得
             */
            getCellElement(x, y) {
                return this.board.children[y * this.boardSize + x];
            }

            /**
             * 新しいエサの位置を生成
             * 蛇の体と重ならない位置に配置
             */
            generateFood() {
                let position;
                do {
                    position = {
                        x: Math.floor(Math.random() * this.boardSize),
                        y: Math.floor(Math.random() * this.boardSize)
                    };
                } while (this.snake.some(segment =>
                    segment.x === position.x && segment.y === position.y));
                return position;
            }

            /**
             * ゲームの状態を更新
             */
            update() {
                // 蛇の新しい頭の位置を計算
                const head = {
                    x: this.snake[0].x + this.direction.x,
                    y: this.snake[0].y + this.direction.y
                };

                // 壁との衝突判定
                if (head.x < 0 || head.x >= this.boardSize ||
                    head.y < 0 || head.y >= this.boardSize) {
                    this.gameOver();
                    return;
                }

                // 自分自身との衝突判定
                if (this.snake.some(segment => segment.x === head.x && segment.y === head.y)) {
                    this.gameOver();
                    return;
                }

                // 蛇の移動
                this.snake.unshift(head);

                // エサを食べた判定
                if (head.x === this.food.x && head.y === this.food.y) {
                    this.score += 1;
                    this.scoreElement.textContent = `スコア: ${this.score}`;
                    this.food = this.generateFood();
                } else {
                    this.snake.pop();  // エサを食べてない場合は尻尾を削除
                }

                this.draw();
            }

            /**
             * ゲームボードを描画
             */
            draw() {
                // 全セルをリセット
                document.querySelectorAll('.cell').forEach(cell => {
                    cell.className = 'cell';
                });

                // 蛇の描画
                this.snake.forEach(segment => {
                    this.getCellElement(segment.x, segment.y).classList.add('snake');
                });

                // エサの描画
                this.getCellElement(this.food.x, this.food.y).classList.add('food');
            }

            /**
             * キーボード操作の設定
             */
            setupControls() {
                document.addEventListener('keydown', (e) => {
                    // ゲーム実行中のみキー操作を受け付ける
                    if (!this.isGameRunning) return;

                    // 矢印キーでのスクロールを防止
                    if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
                        e.preventDefault();
                    }

                    // 方向の設定
                    const newDirection = {
                        'ArrowUp': { x: 0, y: -1 },
                        'ArrowDown': { x: 0, y: 1 },
                        'ArrowLeft': { x: -1, y: 0 },
                        'ArrowRight': { x: 1, y: 0 }
                    }[e.key];

                    // 180度回転を防止(現在の進行方向と反対には進めない)
                    if (newDirection &&
                        !(this.direction.x + newDirection.x === 0 &&
                            this.direction.y + newDirection.y === 0)) {
                        this.direction = newDirection;
                    }
                });
            }

            /**
             * ゲームオーバー時の処理
             */
            gameOver() {
                alert(`ゲームオーバー!\nスコア: ${this.score}`);
                clearInterval(this.gameInterval);
                this.isGameRunning = false;
                this.startButton.style.display = 'none';
                this.retryButton.style.display = 'inline-block';
            }

            /**
             * ゲーム開始
             */
            start() {
                this.isGameRunning = true;
                this.gameInterval = setInterval(() => this.update(), this.speed);
            }
        }

        // ゲームのインスタンスを作成
        new SnakeGame(); 
    </script>
</body>

</html>

1行も書かずにゲームが作れてしまいました
cursorにはもっと色々な機能があるのですが、完成してしまったので今回はここまでとします

所感

  • コードを1行も書かずともそれっぽいサービスが作れてしまう
  • ただ、バグは混入しうるので読む力は必要(今のところは)
  • コードを書くだけのエンジニアは数年で淘汰されそう…

類似サービス

最近だとGitHub CopilotClineがアツいみたいです

まとめ

日進月歩のAI業界にわくわくしますね!