@@ -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-
149type 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
3223func (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}
0 commit comments