Skip to content

Commit 3c8d337

Browse files
authored
Merge pull request #15 from edge2992/feature/llm-vs-human
feat: LLM vs Human online play with Claude bot
2 parents e8b26d8 + 2112b9f commit 3c8d337

24 files changed

Lines changed: 3798 additions & 42 deletions

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
yatzcli
2-
yatz
2+
/yatz

Makefile

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
.PHONY: build test vet clean
2+
3+
BINARY := yatz
4+
BUILD_DIR := ./cmd/yatz
5+
6+
build:
7+
go build -o $(BINARY) $(BUILD_DIR)
8+
9+
test:
10+
go test ./...
11+
12+
vet:
13+
go vet ./...
14+
15+
clean:
16+
rm -f $(BINARY)

bot/bot.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package bot
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
)
8+
9+
type Bot struct {
10+
addr string
11+
name string
12+
strategy string
13+
model string
14+
}
15+
16+
func New(addr, name, strategy, model string) *Bot {
17+
return &Bot{
18+
addr: addr,
19+
name: name,
20+
strategy: strategy,
21+
model: model,
22+
}
23+
}
24+
25+
func (b *Bot) Run() error {
26+
claudePath, err := exec.LookPath("claude")
27+
if err != nil {
28+
return fmt.Errorf("claude CLI is required. Install it first: %w", err)
29+
}
30+
31+
selfPath, err := os.Executable()
32+
if err != nil {
33+
return fmt.Errorf("resolve executable path: %w", err)
34+
}
35+
36+
configFile, err := os.CreateTemp("", "yatz-mcp-*.json")
37+
if err != nil {
38+
return fmt.Errorf("create temp config: %w", err)
39+
}
40+
defer os.Remove(configFile.Name())
41+
42+
if _, err := configFile.WriteString(BuildMCPConfig(selfPath)); err != nil {
43+
configFile.Close()
44+
return fmt.Errorf("write MCP config: %w", err)
45+
}
46+
configFile.Close()
47+
48+
prompt := BuildPrompt(b.addr, b.name, b.strategy)
49+
systemPrompt := BuildSystemPrompt(b.strategy)
50+
51+
args := []string{"-p",
52+
"--mcp-config", configFile.Name(),
53+
"--allowedTools", "mcp__yatzcli__*",
54+
"--system-prompt", systemPrompt,
55+
}
56+
if b.model != "" {
57+
args = append(args, "--model", b.model)
58+
}
59+
args = append(args, prompt)
60+
61+
cmd := exec.Command(claudePath, args...)
62+
cmd.Stdout = os.Stdout
63+
cmd.Stderr = os.Stderr
64+
65+
if err := cmd.Run(); err != nil {
66+
return fmt.Errorf("claude exited with error: %w", err)
67+
}
68+
return nil
69+
}

bot/prompt.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package bot
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
)
7+
8+
func BuildMCPConfig(yatzBinaryPath string) string {
9+
config := map[string]interface{}{
10+
"mcpServers": map[string]interface{}{
11+
"yatzcli": map[string]interface{}{
12+
"command": yatzBinaryPath,
13+
"args": []string{"mcp"},
14+
},
15+
},
16+
}
17+
b, _ := json.MarshalIndent(config, "", " ")
18+
return string(b)
19+
}
20+
21+
func BuildSystemPrompt(strategy string) string {
22+
return fmt.Sprintf(`ヤッツィー対戦プレイヤー。考えすぎず素早くプレイすること。
23+
24+
戦略: %s`, strategy)
25+
}
26+
27+
func BuildPrompt(addr, name, strategy string) string {
28+
return fmt.Sprintf(`ヤッツィー対戦に参加してプレイせよ。説明や分析は不要。ツールを呼ぶだけでよい。
29+
30+
手順:
31+
1. join_game で %s に接続(名前: %s)
32+
2. 自分のターンでなければ wait_for_turn
33+
3. 自分のターン: まず roll_dice → ダイスを見て hold_dice か score を選ぶ
34+
4. score の返り値が次ターンの状態。score 後に wait_for_turn を呼ぶな(デッドロックする)
35+
5. Phase が "Finished" なら終了。そうでなければ 3 に戻る
36+
37+
ルール:
38+
- 毎ターン最初に必ず roll_dice を呼ぶこと
39+
- hold_dice でキープするダイスのインデックス(0-4)を指定、残りを振り直す
40+
- 最大3回ロール(roll_dice 1回 + hold_dice 最大2回)
41+
- score でカテゴリを選んで得点確定
42+
43+
戦略: %s
44+
45+
score 後に send_chat で一言コメント(日本語、10文字以内)。`, addr, name, strategy)
46+
}

bot/prompt_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package bot
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestBuildMCPConfig(t *testing.T) {
12+
config := BuildMCPConfig("/usr/local/bin/yatz")
13+
14+
var parsed map[string]interface{}
15+
err := json.Unmarshal([]byte(config), &parsed)
16+
require.NoError(t, err)
17+
18+
servers := parsed["mcpServers"].(map[string]interface{})
19+
yatzcli := servers["yatzcli"].(map[string]interface{})
20+
assert.Equal(t, "/usr/local/bin/yatz", yatzcli["command"])
21+
22+
args := yatzcli["args"].([]interface{})
23+
assert.Equal(t, []interface{}{"mcp"}, args)
24+
}
25+
26+
func TestBuildPrompt(t *testing.T) {
27+
prompt := BuildPrompt("localhost:9876", "TestBot", "test strategy")
28+
29+
assert.Contains(t, prompt, "localhost:9876")
30+
assert.Contains(t, prompt, "TestBot")
31+
assert.Contains(t, prompt, "test strategy")
32+
assert.Contains(t, prompt, "join_game")
33+
assert.Contains(t, prompt, "wait_for_turn")
34+
assert.Contains(t, prompt, "roll_dice")
35+
assert.Contains(t, prompt, "score")
36+
assert.Contains(t, prompt, "send_chat")
37+
// Critical: instruction not to call wait_for_turn after score
38+
assert.Contains(t, prompt, "score 後に wait_for_turn を呼ぶな")
39+
}
40+
41+
func TestBuildSystemPrompt(t *testing.T) {
42+
prompt := BuildSystemPrompt("my strategy")
43+
assert.Contains(t, prompt, "ヤッツィー")
44+
assert.Contains(t, prompt, "my strategy")
45+
}

bot/strategy.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package bot
2+
3+
const DefaultStrategy = `- 上段ボーナス(63点以上)を最優先で狙う
4+
- 同じ数字が3つ以上あればキープしてヤッツィーやフルハウスを狙う
5+
- ストレート(1-2-3-4, 2-3-4-5, 3-4-5-6)が見えたらキープ
6+
- 3回目のロールでは最も得点が高いカテゴリにスコアする
7+
- 捨てカテゴリ(0点で埋める)は ones を優先
8+
- 終盤は残りカテゴリの期待値を考慮する`

0 commit comments

Comments
 (0)