Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ make test-all # All tests with verbose output
make test-coverage # Unit tests with coverage report
make build # Build binary
make lint # Static analysis (go vet)

# AI Battle
yatz battle # Greedy vs Statistical (default)
yatz battle --players "A:llm:personas/aggressive.md,D:llm:personas/defensive.md"
yatz battle --rounds 100 --quiet # 100-game statistics
```

## Testing Workflow
Expand All @@ -27,14 +32,15 @@ make lint # Static analysis (go vet)
## Project Structure

```
cmd/yatz/ Entry point (cobra subcommands: play, mcp, host, join, match)
engine/ Pure game logic (state machine, scoring, dice, AI, GameClient interface)
cli/ Interactive TUI (bubbletea v2)
cmd/yatz/ Entry point (cobra subcommands: play, mcp, host, join, match, battle)
engine/ Pure game logic (state machine, scoring, dice, AI, Strategy, Battle, GameClient interface)
cli/ Interactive TUI (bubbletea v2) + AI battle spectator
mcp/ MCP server for LLM integration (mcp-go, stdio transport)
p2p/ P2P host-authority online play (length-prefixed JSON over TCP)
match/ Matchmaking WebSocket client
lambda/ Serverless matchmaking handler (AWS Lambda + API Gateway + DynamoDB)
bot/ LLM bot integration (MCP config, system prompt, Claude API interaction)
bot/ LLM bot integration (MCP config, system prompt, Claude API, LLM Strategy)
personas/ Markdown-based AI persona definitions for LLM Strategy
```

## Key Design Decisions
Expand All @@ -45,6 +51,9 @@ bot/ LLM bot integration (MCP config, system prompt, Claude API interacti
- **Host-authority model**: Host runs the game engine; guest sends actions over TCP and receives state updates.
- **AI auto-play**: `LocalClient.Score()` triggers AI turns automatically via `runAITurns()`.
- **Scorecard**: `map[Category]*int` where `nil` = unfilled, `*0` = filled with zero.
- **Strategy pattern** (`engine/strategy.go`): `Strategy` interface abstracts AI decision-making. Implementations: `GreedyStrategy` (immediate best score), `StatisticalStrategy` (expected value), `LLMStrategy` (Claude API via `anthropic-sdk-go`).
- **Battle engine** (`engine/battle.go`): `RunBattle()` drives AI-vs-AI games. `OnTurnDone` callback streams results to TUI spectator.
- **LLM API Key**: `LLMStrategy` calls Claude API directly (not via MCP) for speed. Uses `--api-key` flag or `ANTHROPIC_API_KEY` env var.

## Dependencies

Expand All @@ -53,4 +62,5 @@ bot/ LLM bot integration (MCP config, system prompt, Claude API interacti
- `mark3labs/mcp-go` — MCP server
- `gorilla/websocket` — matchmaking client
- `aws-lambda-go`, `aws-sdk-go-v2` — serverless matchmaking
- `anthropic-sdk-go` — Claude API client for LLM Strategy
- `stretchr/testify` — test assertions
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,24 @@ yatz join 192.168.1.10:9876 --name Bob
yatz match --server wss://your-api-gateway-url --name Alice
```

### AI Battle

Watch AI strategies compete against each other:

```bash
# Greedy vs Statistical (default)
yatz battle

# LLM persona battle (requires ANTHROPIC_API_KEY)
yatz battle --players "Attacker:llm:personas/aggressive.md,Defender:llm:personas/defensive.md"

# Run 100 games and compare statistics
yatz battle --rounds 100 --quiet

# Three-way battle with fixed seed
yatz battle --players "G:greedy,S:statistical,L:llm" --seed 42
```

## Commands

| Command | Description |
Expand All @@ -61,6 +79,7 @@ yatz match --server wss://your-api-gateway-url --name Alice
| `yatz host` | Host a P2P game |
| `yatz join <addr>` | Join a P2P game |
| `yatz match` | Find opponent via matchmaking |
| `yatz battle` | Watch AI vs AI battle |

## Controls (TUI)

Expand All @@ -75,6 +94,29 @@ yatz match --server wss://your-api-gateway-url --name Alice
- `p2p/` - P2P host-authority online play
- `match/` - Matchmaking client
- `lambda/` - Serverless matchmaking handler (AWS)
- `bot/` - LLM bot integration (Claude API, LLM Strategy)
- `personas/` - AI persona definitions (Markdown)

## Personas

Create custom AI personas as Markdown files:

```markdown
# My Custom AI
## 性格
Description of personality...

## 戦略
- Strategy point 1
- Strategy point 2

## 口癖
「Catchphrase」
```

Use with: `yatz battle --players "MyAI:llm:path/to/persona.md"`

Built-in personas: `personas/aggressive.md`, `personas/defensive.md`, `personas/gambler.md`

## Development

Expand Down
80 changes: 80 additions & 0 deletions bot/persona.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package bot

import (
"os"
"strings"
)

// Persona represents a loaded character persona from a markdown file.
type Persona struct {
Name string
Personality string
Strategy string
Catchphrase string
Raw string
}

// LoadPersona reads and parses a persona markdown file.
// Format:
//
// # Character Name
// ## 性格
// ...
// ## 戦略
// ...
// ## 口癖
// ...
func LoadPersona(path string) (*Persona, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}

raw := string(data)
p := &Persona{Raw: raw}

lines := strings.Split(raw, "\n")
var currentSection string
var sectionBuf strings.Builder

flushSection := func() {
content := strings.TrimSpace(sectionBuf.String())
switch currentSection {
case "name":
p.Name = content
case "性格", "personality":
p.Personality = content
case "戦略", "strategy":
p.Strategy = content
case "口癖", "catchphrase":
p.Catchphrase = content
}
sectionBuf.Reset()
}

for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "# ") && !strings.HasPrefix(trimmed, "## ") {
flushSection()
currentSection = "name"
sectionBuf.WriteString(strings.TrimPrefix(trimmed, "# "))
continue
}
if strings.HasPrefix(trimmed, "## ") {
flushSection()
currentSection = strings.ToLower(strings.TrimPrefix(trimmed, "## "))
continue
}
if currentSection != "" {
sectionBuf.WriteString(line)
sectionBuf.WriteString("\n")
}
}
flushSection()

if p.Name == "" {
p.Name = "LLM"
}

return p, nil
}
Loading
Loading