【Claude 3.7】Go言語でwasmのサンプルプログラムをAIに作らせて動かしてみる

まえがき

前回 は、Go言語でボール落下アニメーションのプログラムを書き、Wasmにコンパイルしてブログ上で動かす所まで作りました。
せっかくなので今話題になっているClaude 3.7を使ってどこまでできるか色々試していこうと思います。

実装内容

新たに実装した機能は下記になります。
・ボール同士が干渉する物理法則の実装
・左ボタンクリックでボールが増える機能
・「ボール削除」ボタンの実装

ちなみに私は1行もコードを書いてません。
「Start」ボタンを押下すると、Wasmファイルがダウンロードされ、プログラムが実行されます。

プロンプト

指示した内容は下記のとおりです。

1つ目のプロンプト

「下記goのwasm用のコードですが、お互いにボールが干渉するように書き換えて下さい」
※ここに 前回 のソースコードを貼り付ける

2つ目のプロンプト

「ボールの色が全部黒色になってしまっているのでrandomcolorの修正が必要そうです。」

ここは一発で修正されたコードが出力されました。

3つ目のプロンプト

「地面の所のどこかでいいので「ボールの削除」ボタンが欲しいです。」

ここは特に問題なしです。

ソースコード

上記プロンプトを経て出力されたソースコードは下記になります。

// main.go
package main

import (
	"math"
	"math/rand/v2"
	"syscall/js"
	"time"
)

type Ball struct {
	X, Y   float64
	VX, VY float64
	Radius float64
	Color  string
	Mass   float64
}

var (
	canvas  js.Value
	ctx     js.Value
	balls   []Ball
	gravity float64 = 0.5 // 重力加速度
	ground  float64 = 300 // 地面の位置
	width   float64 = 500 // キャンバスの幅
	height  float64 = 400 // キャンバスの高さ
)

// 物理演算の更新処理
func updatePhysics() {
	// 重力と地面の衝突を適用
	for i := range balls {
		// 重力の影響を適用
		balls[i].VY += gravity

		// 位置の更新
		balls[i].X += balls[i].VX
		balls[i].Y += balls[i].VY

		// 水平方向の壁との衝突
		if balls[i].X < balls[i].Radius {
			balls[i].X = balls[i].Radius
			balls[i].VX = -balls[i].VX * 0.9 // 水平方向の反発係数
		} else if balls[i].X > width-balls[i].Radius {
			balls[i].X = width - balls[i].Radius
			balls[i].VX = -balls[i].VX * 0.9
		}

		// 地面に衝突した場合
		if balls[i].Y > ground-balls[i].Radius {
			balls[i].Y = ground - balls[i].Radius
			balls[i].VY = -balls[i].VY * 0.7 // 反発係数
			// 水平方向の速度に摩擦を適用
			balls[i].VX *= 0.95
		}
	}

	// ボール同士の衝突判定と応答
	for i := 0; i < len(balls); i++ {
		for j := i + 1; j < len(balls); j++ {
			handleCollision(&balls[i], &balls[j])
		}
	}
}

// ボール同士の衝突を処理する関数
func handleCollision(a, b *Ball) {
	// 2つのボール間の距離を計算
	dx := b.X - a.X
	dy := b.Y - a.Y
	distance := math.Sqrt(dx*dx + dy*dy)

	// 衝突判定: 2つのボールの半径の和より距離が小さい場合
	minDistance := a.Radius + b.Radius
	if distance < minDistance {
		// 衝突している場合、ボールを押し戻して重ならないようにする
		overlap := minDistance - distance
		// 押し戻す方向を計算(正規化)
		if distance > 0 {
			dx /= distance
			dy /= distance
		} else {
			// 完全に重なっている場合のランダムな方向
			angle := rand.Float64() * 2 * math.Pi
			dx = math.Cos(angle)
			dy = math.Sin(angle)
			distance = 0.1 // 非ゼロの小さな値
		}

		// 質量に基づいて押し戻し量を計算
		totalMass := a.Mass + b.Mass
		aRatio := b.Mass / totalMass
		bRatio := a.Mass / totalMass

		// 位置の修正
		a.X -= dx * overlap * aRatio
		a.Y -= dy * overlap * aRatio
		b.X += dx * overlap * bRatio
		b.Y += dy * overlap * bRatio

		// 衝突応答: 運動量保存則に基づく速度の計算
		// 衝突軸に沿った速度成分
		vax := a.VX*dx + a.VY*dy
		vay := a.VX*-dy + a.VY*dx
		vbx := b.VX*dx + b.VY*dy
		vby := b.VX*-dy + b.VY*dx

		// 一次元の弾性衝突
		// 衝突軸に沿った新しい速度(反発係数0.9)
		e := 0.9 // 反発係数
		newVax := (vax*(a.Mass-b.Mass) + 2*b.Mass*vbx) / totalMass * e
		newVbx := (vbx*(b.Mass-a.Mass) + 2*a.Mass*vax) / totalMass * e

		// 新しい速度に更新(衝突軸に直交する成分はそのまま)
		a.VX = newVax*dx - vay*dy
		a.VY = newVax*dy + vay*dx
		b.VX = newVbx*dx - vby*dy
		b.VY = newVbx*dy + vby*dx
	}
}

// 描画処理
func draw() {
	// キャンバスをクリア
	ctx.Call("clearRect", 0, 0, width, height)

	// 地面を描画
	ctx.Set("fillStyle", "#333")
	ctx.Call("fillRect", 0, ground, width, height-ground)

	// クリアボタンを描画
	ctx.Set("fillStyle", "#f44336")
	ctx.Call("fillRect", width-100, ground+10, 90, 30)
	ctx.Set("fillStyle", "white")
	ctx.Set("font", "14px Arial")
	ctx.Set("textAlign", "center")
	ctx.Set("textBaseline", "middle")
	ctx.Call("fillText", "ボールを削除", width-55, ground+25)

	// 各ボールを描画
	for _, ball := range balls {
		ctx.Call("beginPath")
		ctx.Call("arc", ball.X, ball.Y, ball.Radius, 0, 2*math.Pi)
		ctx.Set("fillStyle", ball.Color)
		ctx.Call("fill")
		ctx.Set("strokeStyle", "rgba(0,0,0,0.3)")
		ctx.Set("lineWidth", 2)
		ctx.Call("stroke")
	}
}

// requestAnimationFrame のコールバック関数
func render(this js.Value, args []js.Value) interface{} {
	updatePhysics()
	draw()
	js.Global().Call("requestAnimationFrame", js.FuncOf(render))
	return nil
}

// マウスクリックイベントのハンドラ
func handleClick(this js.Value, args []js.Value) interface{} {
	event := args[0]
	rect := canvas.Call("getBoundingClientRect")
	x := event.Get("clientX").Float() - rect.Get("left").Float()
	y := event.Get("clientY").Float() - rect.Get("top").Float()

	// クリアボタンがクリックされたかチェック
	if x >= width-80 && x <= width-10 && y >= ground+10 && y <= ground+40 {
		// ボールをすべて削除
		balls = []Ball{}
		return nil
	}

	// 地面より下をクリックした場合は何もしない
	if y > ground {
		return nil
	}

	// 新しいボールを追加
	addBall(x, y)
	return nil
}

// 指定位置にボールを追加
func addBall(x, y float64) {
	r := rand.New(rand.NewPCG(uint64(time.Now().UnixNano()), 3))

	radius := 10 + r.Float64()*15 // 10~25pxのランダムな半径

	// 質量は半径の2乗に比例すると仮定
	mass := radius * radius

	balls = append(balls, Ball{
		X:      x,
		Y:      y,
		VX:     (r.Float64()*10 - 5), // -5~5のランダムな初速度
		VY:     (r.Float64()*5 - 10), // -10~-5のランダムな初速度(上向き)
		Radius: radius,
		Color:  randomColor(r),
		Mass:   mass,
	})
}

func main() {
	r := rand.New(rand.NewPCG(uint64(time.Now().UnixNano()), 3))

	// HTML の canvas 要素を取得
	document := js.Global().Get("document")
	canvas = document.Call("getElementById", "myCanvas")
	if canvas.IsUndefined() {
		js.Global().Call("alert", "Canvas要素が見つかりません!")
		return
	}

	// キャンバスのサイズを設定
	width = canvas.Get("width").Float()
	height = canvas.Get("height").Float()
	ground = height * 0.75 // 地面の位置をキャンバスの75%の位置に

	ctx = canvas.Call("getContext", "2d")

	// マウスクリックイベントの登録
	canvas.Call("addEventListener", "click", js.FuncOf(handleClick))

	// ボールを複数追加
	for i := 0; i < 5; i++ {
		xPos := r.Float64()*(width-80) + 40 // 余裕を持たせる
		yPos := r.Float64()*(ground/2) + 20
		radius := 10 + r.Float64()*15 // 10~25pxのランダムな半径

		// 質量は半径の2乗に比例すると仮定
		mass := radius * radius

		balls = append(balls, Ball{
			X:      xPos,
			Y:      yPos,
			VX:     (r.Float64()*10 - 5), // -5~5のランダムな初速度
			VY:     0,                    // 初速度ゼロ
			Radius: radius,
			Color:  randomColor(r),
			Mass:   mass,
		})
	}

	// アニメーション開始
	js.Global().Call("requestAnimationFrame", js.FuncOf(render))

	// 永久ループ
	select {}
}

// ランダムな色を生成する関数
func randomColor(r *rand.Rand) string {
	colors := []string{"#e74c3c", "#3498db", "#2ecc71", "#9b59b6", "#f1c40f", "#1abc9c", "#e67e22", "#34495e"}
	return colors[r.IntN(len(colors))]
}

wasmをコンパイルする環境については 前回 の記事をご覧ください。

ソースコードを修正したらtinygoでコンパイルします。

tinygo build -o main.wasm -target wasm .

いつものように、ローカル環境でWebサーバーを立ち上げて動作確認をしてみましょう。
Pythonで簡易的なWebサーバーを起動します。

python3 -m http.server 8080

サーバー起動後、http://localhost:8080 にアクセスすれば冒頭に設置してあるものと同様のwasmが実行されます。

あとがき

プロンプトの所で書いたようにClaude 3.7はめちゃくちゃ優秀ですが、一回目の出力ではボールの色の設定がうまくいかず、全部黒色で出力される不具合がありました。
ですがその旨を伝えたらすぐに修正されたコードを出力したので、プロンプトだけで修正できたことになります。これは驚異的ですね。

無料プランなのであまり多くの事はできないかもしれませんが使い倒していきたいと思います。

本記事が何かしらの参考になれば幸いです。
記事内容についてご不明な点がございましたら、下記のXアカウントまでお気軽にDMしてください。
もふもふ-Xアカウント


スポンサーリンク

もふもふ

プロフィール

著者
もふもふ
プログラマ。汎用系→ゲームエンジニア→Webエンジニア→QAエンジニア。開発からテストまで一通り経験し、実際に詰まった点や検証結果を技術ブログとしてまとめています。