【流れのメモ】Claude 3.5 Sonnet で Artifacts を有効化して p5.js のゲームを作ってもらいプレビュー画面上で操作してみたり

以下の内容についての簡単なメモです。

https://x.com/youtoy/status/1804798770892378122

Claude の「Artifacts」が話題になっているのを見かけて、後で試そうと思いつつ保留にしていたのですが、それを試した流れのメモです。

記事の中では、上記のインベーダーゲームを題材に Artifacts を試した内容と、それと別にブロック崩しを題材にして試したものを書いています。

## Artifacts を有効化
Artifacts の有効化は、以下の部分から行いました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/ee97bbd0-edd9-4816-5c10-c5436df51580.png)

上記の部分を押した後に出てくる「Feature Preview」の画面で、「Artifacts」のトグルスイッチを ON にします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/c43ced12-6c2f-9cbf-140c-448bb7baea9e.png)

あとは、プロンプトを入力するだけです。

## p5.js について試した内容1：インベーダーゲーム
とりあえずプレビュー機能を試してみたくて、それが試せる簡素な内容を試してみました。

### 少し失敗した内容
最初に「p5.js でインベーダーゲームを作ってもらう」というのをシンプルに依頼したら、p5.js を使った JavaScript のコードのみしか提示されず、それではプレビューがはたらきませんでした。
HTML を含めた内容を出力してもらう必要があるように思われました。

それで、冒頭に X のポストで掲載していたものでは、プロンプトを「HTML ＋ JavaScript で、p5.js を使ってインベーダーゲームを作って」という、HTML を含むのを明示的に依頼する内容にしました。

### HTML を含めて出力してもらったもの
レスポンスとして返ってきたテキストは、以下のような感じです。
どんな内容なのかという説明と、さらにゲームを発展させたい場合にどうしたら良いかという説明が書かれています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/63834c29-c49b-c99c-1015-4048f7cdf259.png)

そして、この内容が表示された部分の右側に、プレビュー用の部分が表示されました。

そのプレビュー用の部分について、右上にプレビューとコードを切り替えるためのトグルスイッチがあります。それを切り替えてみた結果は、以下の通りです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/6102e982-7ac7-5b54-dec8-398ffeb82b8e.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/bb303fe4-8485-3a8a-dfc9-7a534a90694f.png)

#### 生成されたコード
生成されたコードは以下です。
シンプルなプロンプトに対して、わりとしっかりした内容が出てきているかと思います（※ メジャーなゲームの内容だからというのもあるかとは思いますが）。

```html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Simple Invaders Game</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
    <style>
      body {
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100vh;
        margin: 0;
        background-color: #f0f0f0;
      }
    </style>
  </head>
  <body>
    <script>
      let player;
      let enemies = [];
      let bullets = [];
      let score = 0;

      function setup() {
        createCanvas(400, 400);
        player = new Player();
        for (let i = 0; i < 5; i++) {
          enemies.push(new Enemy(i * 80 + 40, 40));
        }
      }

      function draw() {
        background(0);
        player.show();
        player.move();

        for (let enemy of enemies) {
          enemy.show();
          enemy.move();
        }

        for (let i = bullets.length - 1; i >= 0; i--) {
          bullets[i].show();
          bullets[i].move();
          if (bullets[i].offscreen()) {
            bullets.splice(i, 1);
          } else {
            for (let j = enemies.length - 1; j >= 0; j--) {
              if (bullets[i].hits(enemies[j])) {
                enemies.splice(j, 1);
                bullets.splice(i, 1);
                score += 10;
                break;
              }
            }
          }
        }

        textSize(20);
        fill(255);
        text(`Score: ${score}`, 10, 30);

        if (enemies.length === 0) {
          textSize(32);
          textAlign(CENTER, CENTER);
          text("You Win!", width / 2, height / 2);
          noLoop();
        }
      }

      function keyPressed() {
        if (key === " ") {
          bullets.push(new Bullet(player.x, player.y));
        }
      }

      class Player {
        constructor() {
          this.x = width / 2;
          this.y = height - 20;
        }

        show() {
          fill(0, 255, 0);
          rectMode(CENTER);
          rect(this.x, this.y, 20, 20);
        }

        move() {
          if (keyIsDown(LEFT_ARROW) && this.x > 10) {
            this.x -= 5;
          }
          if (keyIsDown(RIGHT_ARROW) && this.x < width - 10) {
            this.x += 5;
          }
        }
      }

      class Enemy {
        constructor(x, y) {
          this.x = x;
          this.y = y;
          this.xdir = 1;
        }

        show() {
          fill(255, 0, 0);
          rectMode(CENTER);
          rect(this.x, this.y, 30, 30);
        }

        move() {
          this.x += this.xdir;
          if (this.x > width - 15 || this.x < 15) {
            this.xdir *= -1;
            this.y += 20;
          }
        }
      }

      class Bullet {
        constructor(x, y) {
          this.x = x;
          this.y = y;
        }

        show() {
          fill(255);
          ellipse(this.x, this.y, 8, 8);
        }

        move() {
          this.y -= 5;
        }

        offscreen() {
          return this.y < 0;
        }

        hits(enemy) {
          let d = dist(this.x, this.y, enemy.x, enemy.y);
          return d < 20;
        }
      }
    </script>
  </body>
</html>
```

ここで使われている p5.js のライブラリのバージョンがけっこう古かったりしますが（※ 現状の最新版は「1.9.4」）、今回の処理内容には影響はなさそうです。

## p5.js について試した内容2：ブロック崩し
今度はブロック崩しでコードの生成などを試してみました。

### 試してみた内容の一連の流れ
また、以下に掲載している動画は「最初のプロンプト入力 ⇒ 結果の表示 ⇒ 改善してもらうためのプロンプト入力 ⇒ 修正版の表示」という一連の流れを示したものです。

<iframe width="560" height="315" src="https://www.youtube.com/embed/dOxlLf_ElPA?si=VThwFt_wUGH69bQD" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

Claude 3.5 Sonnet のレスポンスもはやく、使った感じはすごく良いです。

### 提示されたコード
ブロック崩しで試した内容は、最初に「HTML ＋ JavaScript で、p5.js を使ってブロック崩しゲームを作って」というプロンプトを入力し、その後に「レベルシステム・スコアを追加して」と入力しました。
追加で依頼した「レベルシステム・スコア」は、1回目の応答の中で提案されたものの中の 2つです。

以下に、最終的に改善版として示されたコードを掲載します。

上に掲載した動画で示したスピード感で以下の内容が出てくるのは、さらなる追加実装を頼んだりするのもそれほどストレスなく行えそうで、いろいろ活用ができそうです。

```html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>ブロック崩しゲーム - レベルとスコア付き</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
    <style>
      body {
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100vh;
        margin: 0;
        background-color: #f0f0f0;
      }
    </style>
  </head>
  <body>
    <script>
      let ball;
      let paddle;
      let blocks = [];
      let score = 0;
      let level = 1;
      let gameState = "playing"; // 'playing', 'levelComplete', 'gameOver'

      function setup() {
        createCanvas(400, 400);
        ball = new Ball(width / 2, height - 50, 10);
        paddle = new Paddle();
        createBlocks();
      }

      function draw() {
        background(220);

        if (gameState === "playing") {
          ball.update();
          paddle.update();
          checkCollisions();
        }

        ball.display();
        paddle.display();

        for (let block of blocks) {
          block.display();
        }

        displayScore();
        displayLevel();

        if (gameState === "levelComplete") {
          displayLevelComplete();
        } else if (gameState === "gameOver") {
          displayGameOver();
        }
      }

      function checkCollisions() {
        // パドルとの衝突
        if (ball.collidesWith(paddle)) {
          ball.bounceOff(paddle);
        }

        // ブロックとの衝突
        for (let i = blocks.length - 1; i >= 0; i--) {
          if (ball.collidesWith(blocks[i])) {
            ball.bounceOff(blocks[i]);
            blocks.splice(i, 1);
            score += 10;
            break;
          }
        }

        // ボールが下に落ちた場合
        if (ball.y + ball.r > height) {
          gameState = "gameOver";
        }

        // すべてのブロックを破壊した場合
        if (blocks.length === 0) {
          levelUp();
        }
      }

      function createBlocks() {
        blocks = [];
        for (let i = 0; i < 5; i++) {
          for (let j = 0; j < 8; j++) {
            blocks.push(new Block(j * 50 + 25, i * 20 + 50, 40, 15));
          }
        }
      }

      function levelUp() {
        level++;
        ball.reset();
        createBlocks();
        ball.speedX *= 1.1;
        ball.speedY *= 1.1;
        gameState = "levelComplete";
        setTimeout(() => {
          gameState = "playing";
        }, 2000);
      }

      function displayScore() {
        fill(0);
        textSize(16);
        textAlign(LEFT);
        text(`Score: ${score}`, 10, 20);
      }

      function displayLevel() {
        fill(0);
        textSize(16);
        textAlign(RIGHT);
        text(`Level: ${level}`, width - 10, 20);
      }

      function displayLevelComplete() {
        fill(0, 128, 0);
        textSize(32);
        textAlign(CENTER, CENTER);
        text(`Level ${level - 1} Complete!`, width / 2, height / 2);
      }

      function displayGameOver() {
        fill(255, 0, 0);
        textSize(32);
        textAlign(CENTER, CENTER);
        text("Game Over", width / 2, height / 2);
        textSize(16);
        text("Click to restart", width / 2, height / 2 + 30);
      }

      function mousePressed() {
        if (gameState === "gameOver") {
          restartGame();
        }
      }

      function restartGame() {
        score = 0;
        level = 1;
        ball.reset();
        createBlocks();
        gameState = "playing";
      }

      class Ball {
        constructor(x, y, r) {
          this.x = x;
          this.y = y;
          this.r = r;
          this.speedX = 3;
          this.speedY = -3;
        }

        update() {
          this.x += this.speedX;
          this.y += this.speedY;

          if (this.x - this.r <= 0 || this.x + this.r >= width) {
            this.speedX *= -1;
          }
          if (this.y - this.r <= 0) {
            this.speedY *= -1;
          }
        }

        display() {
          fill(255, 0, 0);
          ellipse(this.x, this.y, this.r * 2);
        }

        collidesWith(other) {
          if (other instanceof Paddle) {
            return (
              this.y + this.r >= other.y &&
              this.y - this.r <= other.y + other.h &&
              this.x + this.r >= other.x &&
              this.x - this.r <= other.x + other.w
            );
          } else if (other instanceof Block) {
            return (
              this.x + this.r > other.x &&
              this.x - this.r < other.x + other.w &&
              this.y + this.r > other.y &&
              this.y - this.r < other.y + other.h
            );
          }
        }

        bounceOff(other) {
          if (other instanceof Paddle) {
            let relativeX = (this.x - (other.x + other.w / 2)) / (other.w / 2);
            this.speedX = relativeX * 5;
            this.speedY = -Math.abs(this.speedY);
          } else if (other instanceof Block) {
            this.speedY *= -1;
          }
        }

        reset() {
          this.x = width / 2;
          this.y = height - 50;
          this.speedX = 3;
          this.speedY = -3;
        }
      }

      class Paddle {
        constructor() {
          this.w = 80;
          this.h = 15;
          this.x = width / 2 - this.w / 2;
          this.y = height - 30;
        }

        update() {
          this.x = mouseX - this.w / 2;
          this.x = constrain(this.x, 0, width - this.w);
        }

        display() {
          fill(0, 255, 0);
          rect(this.x, this.y, this.w, this.h);
        }
      }

      class Block {
        constructor(x, y, w, h) {
          this.x = x;
          this.y = y;
          this.w = w;
          this.h = h;
        }

        display() {
          fill(0, 0, 255);
          rect(this.x, this.y, this.w, this.h);
        }
      }
    </script>
  </body>
</html>
```

今回、簡単に 2つの内容を試してみましたが、メジャーな題材の場合は動くものを出力するのは、それほど苦労なくできそうな感じがします（その後、改善していくところは今回は試しきれてないですが...）。

## おわりに
今回、自分がよく使っている p5.js の内容でやってみていますが、他にもプレビュー機能が活用できる内容で試してみたいです。

### 余談： 他に試してみたこと
#### Babylon.js を使ったもの
p5.js と種類としては同系統になる描画ライブラリ、Babylon.js を題材にした内容も少し試してみました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr"><a href="https://twitter.com/hashtag/Claude?src=hash&amp;ref_src=twsrc%5Etfw">#Claude</a> の <a href="https://twitter.com/hashtag/Artifacts?src=hash&amp;ref_src=twsrc%5Etfw">#Artifacts</a> で<a href="https://twitter.com/hashtag/BabylonJS?src=hash&amp;ref_src=twsrc%5Etfw">#BabylonJS</a> でのパーティクルを使った描画を作ってもらってみた<br><br>プレビューで WebGL の描画ができていて、自分の操作での視点変更もできた！<br><br>パーティクルの色変更を依頼した部分が、何やらうまく反映されてない感じがあるな（まだコードの細かな部分は見てない） <a href="https://t.co/1mz01OS5WY">pic.twitter.com/1mz01OS5WY</a></p>&mdash; you (@youtoy) <a href="https://twitter.com/youtoy/status/1804811990306885843?ref_src=twsrc%5Etfw">June 23, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

#### 一番最初のお試し
今回記事にした内容を試す前に、ちょっとした挙動の確認として、HTML ＋ CSS を出力させる、という内容を試していました。

https://x.com/youtoy/status/1804796897431085181

こちらは、プロンプトを以下のようにしていました。

1. HTML・CSS・JavaScriptで、名前と数字を入力させる入力フォームを作って
1. CSSフレームワークの Bulma を適用して

### 余談： X で見かけて気になった内容
#### 3Dゲーム的なもの

https://x.com/ammaar/status/1804649903815115053

#### グラフ作成

https://x.com/kensuu/status/1803929068124991495

#### アニメーション付きのスライド作成

https://x.com/SuguruKun_ai/status/1803934101600374971