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

JavaScriptを有効にしてください

まえがき

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

実装内容

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

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

プロンプト

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

1つ目のプロンプト

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

2つ目のプロンプト

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

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

3つ目のプロンプト

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

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

ソースコード

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

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
// 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でコンパイルします。

1
tinygo build -o main.wasm -target wasm .

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

1
python3 -m http.server 8080

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

あとがき

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

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

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


スポンサーリンク

共有

もふもふ

プロフィール

著者
もふもふ
プログラマ。汎用系→ゲームエンジニア→Webエンジニア→QAエンジニア