Skip to content

Commit 69a066e

Browse files
committed
feat(bot): rewrite bot to use single claude -p session with MCP tools
1 parent 85d6ad8 commit 69a066e

2 files changed

Lines changed: 27 additions & 140 deletions

File tree

bot/bot.go

Lines changed: 26 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -4,169 +4,59 @@ import (
44
"fmt"
55
"os"
66
"os/exec"
7-
8-
"github.com/edge2992/yatzcli/engine"
9-
"github.com/edge2992/yatzcli/p2p"
107
)
118

12-
const maxRetries = 3
13-
149
type Bot struct {
15-
client *p2p.RemoteClient
10+
addr string
1611
name string
1712
strategy string
1813
}
1914

20-
func New(addr, name, strategy string) (*Bot, error) {
21-
rc, err := p2p.NewRemoteClient(addr, name)
22-
if err != nil {
23-
return nil, fmt.Errorf("connect to server: %w", err)
24-
}
15+
func New(addr, name, strategy string) *Bot {
2516
return &Bot{
26-
client: rc,
17+
addr: addr,
2718
name: name,
2819
strategy: strategy,
29-
}, nil
20+
}
3021
}
3122

3223
func (b *Bot) Run() error {
33-
defer b.client.Close()
34-
35-
state, err := b.client.GetState()
24+
claudePath, err := exec.LookPath("claude")
3625
if err != nil {
37-
return fmt.Errorf("get initial state: %w", err)
26+
return fmt.Errorf("claude CLI is required. Install it first: %w", err)
3827
}
3928

40-
// If it's not our turn at the start, wait
41-
if state.CurrentPlayer != b.client.PlayerID() {
42-
fmt.Fprintf(os.Stderr, "Waiting for our turn...\n")
43-
s, isGameOver, err := b.client.WaitForTurn()
44-
if err != nil {
45-
return fmt.Errorf("wait for turn: %w", err)
46-
}
47-
if isGameOver {
48-
b.printFinalScore(s)
49-
return nil
50-
}
51-
state = s
29+
selfPath, err := os.Executable()
30+
if err != nil {
31+
return fmt.Errorf("resolve executable path: %w", err)
5232
}
5333

54-
for state.Phase != engine.PhaseFinished {
55-
state, err = b.playTurn(state)
56-
if err != nil {
57-
return err
58-
}
34+
configFile, err := os.CreateTemp("", "yatz-mcp-*.json")
35+
if err != nil {
36+
return fmt.Errorf("create temp config: %w", err)
5937
}
38+
defer os.Remove(configFile.Name())
6039

61-
b.printFinalScore(state)
62-
return nil
63-
}
64-
65-
func (b *Bot) playTurn(state *engine.GameState) (*engine.GameState, error) {
66-
round := state.Round
67-
var lastErr string
68-
69-
for {
70-
resp, err := b.callClaudeWithRetry(state, lastErr)
71-
if err != nil {
72-
return nil, fmt.Errorf("claude call failed: %w", err)
73-
}
74-
75-
switch resp.Action {
76-
case "roll":
77-
newState, err := b.client.Roll()
78-
if err != nil {
79-
lastErr = err.Error()
80-
fmt.Fprintf(os.Stderr, "[Round %d] Roll error: %s\n", round, lastErr)
81-
continue
82-
}
83-
fmt.Fprintf(os.Stdout, "[Round %d] Roll: %v\n", round, newState.Dice)
84-
state = newState
85-
lastErr = ""
86-
87-
case "hold":
88-
newState, err := b.client.Hold(resp.Indices)
89-
if err != nil {
90-
lastErr = err.Error()
91-
fmt.Fprintf(os.Stderr, "[Round %d] Hold error: %s\n", round, lastErr)
92-
continue
93-
}
94-
fmt.Fprintf(os.Stdout, "[Round %d] Hold %v → %v\n", round, resp.Indices, newState.Dice)
95-
state = newState
96-
lastErr = ""
97-
98-
case "score":
99-
cat := engine.Category(resp.Category)
100-
score := engine.CalcScore(cat, state.Dice)
101-
newState, err := b.client.Score(cat)
102-
if err != nil {
103-
lastErr = err.Error()
104-
fmt.Fprintf(os.Stderr, "[Round %d] Score error: %s\n", round, lastErr)
105-
continue
106-
}
107-
fmt.Fprintf(os.Stdout, "[Round %d] Score: %s (%d pts) — %q\n", round, cat, score, resp.Comment)
108-
109-
// Send chat
110-
if resp.Comment != "" {
111-
_ = b.client.SendChat(b.client.PlayerID(), b.name, resp.Comment)
112-
}
113-
114-
return newState, nil
115-
116-
default:
117-
lastErr = fmt.Sprintf("unknown action %q", resp.Action)
118-
fmt.Fprintf(os.Stderr, "[Round %d] Unknown action: %s\n", round, resp.Action)
119-
}
40+
if _, err := configFile.WriteString(BuildMCPConfig(selfPath)); err != nil {
41+
configFile.Close()
42+
return fmt.Errorf("write MCP config: %w", err)
12043
}
121-
}
44+
configFile.Close()
12245

123-
func (b *Bot) callClaudeWithRetry(state *engine.GameState, lastErr string) (*ClaudeResponse, error) {
46+
prompt := BuildPrompt(b.addr, b.name, b.strategy)
12447
systemPrompt := BuildSystemPrompt(b.strategy)
125-
schemaJSON := ResponseSchemaJSON()
126-
127-
for attempt := 0; attempt < maxRetries; attempt++ {
128-
var userPrompt string
129-
if lastErr != "" && attempt == 0 {
130-
userPrompt = BuildRetryPrompt(state, b.client.PlayerID(), lastErr)
131-
} else {
132-
userPrompt = BuildUserPrompt(state, b.client.PlayerID())
133-
}
134-
135-
output, err := callClaude(systemPrompt, schemaJSON, userPrompt)
136-
if err != nil {
137-
fmt.Fprintf(os.Stderr, "claude command failed (attempt %d/%d): %v\n", attempt+1, maxRetries, err)
138-
continue
139-
}
140-
141-
resp, err := ParseResponse(output)
142-
if err != nil {
143-
fmt.Fprintf(os.Stderr, "parse response failed (attempt %d/%d): %v\n", attempt+1, maxRetries, err)
144-
continue
145-
}
14648

147-
return resp, nil
148-
}
149-
return nil, fmt.Errorf("claude failed after %d retries", maxRetries)
150-
}
151-
152-
func callClaude(systemPrompt, schemaJSON, userPrompt string) ([]byte, error) {
153-
cmd := exec.Command("claude", "-p",
154-
"--output-format", "json",
155-
"--json-schema", schemaJSON,
49+
cmd := exec.Command(claudePath, "-p",
50+
"--mcp-config", configFile.Name(),
51+
"--allowedTools", "mcp__yatzcli__*",
15652
"--system-prompt", systemPrompt,
157-
userPrompt,
53+
prompt,
15854
)
55+
cmd.Stdout = os.Stdout
15956
cmd.Stderr = os.Stderr
160-
output, err := cmd.Output()
161-
if err != nil {
162-
return nil, fmt.Errorf("execute claude: %w", err)
163-
}
164-
return output, nil
165-
}
16657

167-
func (b *Bot) printFinalScore(state *engine.GameState) {
168-
fmt.Fprintf(os.Stdout, "\n=== Game Over ===\n")
169-
for _, p := range state.Players {
170-
fmt.Fprintf(os.Stdout, "%s: %d pts\n", p.Name, p.Scorecard.Total())
58+
if err := cmd.Run(); err != nil {
59+
return fmt.Errorf("claude exited with error: %w", err)
17160
}
61+
return nil
17262
}

cmd/yatz/bot.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,7 @@ var botCmd = &cobra.Command{
2626
strategy = string(data)
2727
}
2828

29-
b, err := bot.New(addr, name, strategy)
30-
if err != nil {
31-
return err
32-
}
29+
b := bot.New(addr, name, strategy)
3330
return b.Run()
3431
},
3532
}

0 commit comments

Comments
 (0)