GitHub Copilot Chat だけでガチャプログラムを作ってみた
-
2024年1月30日
皆様こんにちは。
サーバーエンジニアのOと申します。
弊社では GitHub Copilot および GitHub Copilot Chat の導入が順次進んでおり、業務において自由に活用できます。
この記事の執筆は11月上旬なのですが、Copilot Workspace が発表されるなどエンジニアとってコード生成AIを使いこなすスキルは近いうちに必須となるでしょう。
この記事では、GitHub Copilot Chatを使ってプログラムコードを書かずにガチャプログラムを作ってみたいと思います。
最初に結論
充分に実用的なプログラムが作れます。
10分程度で基本的なガチャ抽選ロジックが自動テスト込みで書けました。
人間相手ではないので、回答が気に入らなかったらどんどん後出しで条件を追加し、修正させれば良いです。
GitHub Copilot Chat とは
GitHub Copilot と名前は似ていますが使用感は全く別ものです。
GitHub Copilot:コーディング中に続きのコードが自動的に提案される。
GitHub Copilot Chat:ChatGPTの様に自然言語による指示にてコードの生成、あるいは質問に回答してくれる。

図1. GitHub Copilot Chatの使用例
Step1. まずは適当にお願いしてみる
(Q)
ガチャの抽選プログラムのサンプルを作成してください
(A)
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
// Step 1: Create a list of gacha items
items := []string{"Item A", "Item B", "Item C", "Item D", "Item E"}
// Seed the random number generator
rand.Seed(time.Now().UnixNano())
// Step 2: Use rand.Intn to select an item randomly
selectedItem := items[rand.Intn(len(items))]
// Step 3: Display the selected item
fmt.Printf("You got: %s\n", selectedItem)
}
うーん… 確かに抽選はしているようですが、ガチャっぽくはないですね。
Step2. 自動的に改善してもらう
(Q)
上記サンプルで改善する部分はありますか
(A)
package main
import (
"fmt"
"math/rand"
"time"
)
type Item struct {
Name string
Rarity int
Category string
}
func main() {
// Step 1: Create a list of gacha items with rarity
items := []Item {
{"Item A", 1, "Common"},
{"Item B", 2, "Uncommon"},
{"Item C", 3, "Rare"},
{"Item D", 4, "Epic"},
{"Item E", 5, "Legendary"},
}
// Seed the random number generator
rand.Seed(time.Now().UnixNano())
// Step 2: Use rand.Intn to select an item randomly based on rarity
selectedItems := make([]Item, 0, 10)
for i := 0; i < 10; i++ {
for {
index := rand.Intn(len(items))
if rand.Intn(10) < items[index].Rarity {
selectedItems = append(selectedItems, items[index])
break
}
}
}
// Step 3: Display the selected items
for i, item := range selectedItems {
fmt.Printf("Draw %d: You got a(n) %s item: %s\n", i+1, item.Category, item.Name)
}
}
結構変わりました。
それっぽいレアリティー名もついていますし、よくある10連ガチャを想定して連続で実行されるように改修が入っています。
ですが、まだ違和感があるのでそこを修正したいです。
Step3. 細かく修正内容を指摘する
気になる部分は下記です
- main関数にかかれているので一般の関数にしたい。
- Itemの確率指定および抽選部分がこちらの想定と違う。
これを自然言語で指摘します。
(Q)
上記サンプルを下記のように修正してください。
- mainにかかれているので一般の関数として使えるようにしてください。
- RateについてはItemごとに相対値として使えるようにしたいです。
- ItemのRarityという名前がついている部分をRateに変えてください。
例えば Rate=1にたいしてRate=100と設定すると、Rate=100はRate=1の100倍抽選に選ばれやすいということ
- ItemからCategoryを削除してください。
(A)
package main
import (
"fmt"
"math/rand"
"time"
)
type Item struct {
Name string
Rate int
}
func drawGacha(items []Item, draws int) []Item {
// Seed the random number generator
rand.Seed(time.Now().UnixNano())
// Calculate total rate
totalRate := 0
for _, item := range items {
totalRate += item.Rate
}
// Use rand.Intn to select an item randomly based on rate
selectedItems := make([]Item, 0, draws)
for i := 0; i < draws; i++ {
r := rand.Intn(totalRate)
for _, item := range items {
r -= item.Rate
if r < 0 {
selectedItems = append(selectedItems, item) break
}
}
}
return selectedItems
}
func main() {
// Create a list of gacha items with rate
items := []Item{
{"Item A", 1},
{"Item B", 2},
{"Item C", 3},
{"Item D", 4},
{"Item E", 5},
}
// Draw 10 times from the gacha
selectedItems := drawGacha(items, 10)
// Display the selected items
for i, item := range selectedItems {
fmt.Printf("Draw %d: You got: %s\n", i+1, item.Name)
}
}
おおっ!!指摘した事項がちゃんと反映されています。
人間相手に後出しの修正依頼をするのはNGですが、AI相手であれば問題ありません。 むしろ、答えに近づくように何度も細かく指摘すると良さそうです。 とりあえず一旦機能実装はここまでにしておきます。
Step4. 自動テストを書く
意図通りの動作をするかテストをして確かめます。
(Q)
drawGacha関数の自動テストを書いてください。table driven testでお願いします。
(A)
package main
import (
"testing"
)
func TestDrawGacha(t *testing.T) {
// Define test cases
tests := []struct {
name string
items []Item
draws int
expect int
}{
{
name: "Test 1",
items: []Item {
{"Item A", 1},
{"Item B", 2},
{"Item C", 3},
{"Item D", 4},
{"Item E", 5},
},
draws: 10,
expect: 10,
},
{
name: "Test 2",
items: []Item {
{"Item A", 1},
{"Item B", 2},
},
draws: 5,
expect: 5,
},
{
name: "Test 3",
items: []Item {
{"Item A", 1},
},
draws: 1,
expect: 1,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := drawGacha(test.items, test.draws)
if len(result) != test.expect {
t.Errorf("Expected %d, but got %d", test.expect, len(result))
}
})
}
}
ガチャなので毎回抽選結果が異なるため、ここでは抽選される個数のチェックを行っていますね。 ですが、心配なので確率のテストも行います。
(Q)
上記テストは確率を考慮していませんので、これについての自動テストはできますか?
(A)
func TestDrawGacha(t *testing.T) {
// Define test case
test := struct {
name string
items []Item
draws int
expect map[string]float64
}{
name: "Test 1",
items: []Item{
{"Item A", 1},
{"Item B", 2},
{"Item C", 3},
{"Item D", 4},
{"Item E", 5},
},
draws: 1000000,
expect: map[string]float64{
"Item A": 0.06666666666666667,
"Item B": 0.13333333333333333,
"Item C": 0.2,
"Item D": 0.26666666666666666,
"Item E": 0.3333333333333333,
},
}
t.Run(test.name, func(t *testing.T) {
result := drawGacha(test.items, test.draws)
// Count the occurrences of each item
counts := make(map[string]int)
for _, item := range result {
counts[item.Name]++
}
// Check if the proportions are within acceptable range
for name, expectedRate := range test.expect {
actualRate := float64(counts[name]) / float64(test.draws)
if abs(actualRate-expectedRate) > 0.05 {
t.Errorf("Expected rate for %s is %.2f, but got %.2f", name, expectedRate, actualRate)
}
}
})
}
十分な回数ループを回して、確率が一定の範囲になることをテストしています。
自分でもこのようなテストを書くと思います。
まとめ
実運用するには、データの永続化や入力値のバリデーションなどは必要ですが、それらも指示を与えると対応してくれます。
人に指示するレベルの整った文章でなくとも意図通りになることが多かったです。完全な指示でなくとも、とりあえず指示を与えて修正を繰り返す使い方でも十分実用になると感じました。
執筆の1年前はChatGPT3.5が注目を集めていましたが、わずか1年で相当の進化です。
現段階では、慣れた分野であれば自分でコードを書くほうが早いとは思いますが、そうでないものについては生成AIに指示を与えてそれを手直しするほうが早く安全に作れそうです。
今後もAIの能力が上がると考えるとエンジニアがコーディングするケースは減ってくるのではないでしょうか。
これから持つべきスキルとして、
- AIへの指示能力(目的のコードをいかに素早く出力するように誘導できるか)
- 生成されたコードのレビュー能力(不備を見つけAIへのフィードバックを行う能力)
が重要になると思われます。
AIを使ったコーディングへのシフトは思っているよりも早く来るかもしれません。今から生成AIに慣れておくと今後のエンジニアキャリアに役に立つと思います。