Files
gogobee/internal/dreamclient/client.go
prosolis 3d6bc1a066 Add WOTD language rotation, fancy word detection, arena improvements, and death consolidation
- Rotate WOTD between Portuguese, French, and English daily with cognate filtering
- Replace LLM-based fancy word detection with DreamDict frequency data (batch API + cache)
- Track fancy word usage per user and add Wordsmith archetype
- Restore full arena combat flavor text and add player miss actions
- Consolidate death logic into Kill() method with 2-hour lockout
- Fix arena death XP grant missing level-up check
- Fix LLM wotd_used classification by injecting actual WOTD into prompt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 18:47:49 -07:00

236 lines
6.8 KiB
Go

package dreamclient
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
// Client is an HTTP client for the DreamDict dictionary service.
type Client struct {
baseURL string
httpClient *http.Client
}
// New creates a new DreamDict client.
func New(baseURL string) *Client {
return &Client{
baseURL: baseURL,
httpClient: &http.Client{Timeout: 3 * time.Second},
}
}
// Definition is a single definition entry from DreamDict.
type Definition struct {
POS string `json:"pos"`
Gloss string `json:"gloss"`
Source string `json:"source"`
Priority int `json:"priority"`
}
// HealthResponse is the response from /health.
type HealthResponse struct {
Status string `json:"status"`
DBPath string `json:"db_path"`
WordCounts map[string]int `json:"word_counts"`
DefCounts map[string]int `json:"def_counts"`
ImportedAt string `json:"imported_at"`
SchemaVersion string `json:"schema_version"`
}
// IsValidWord checks if a word exists in a language's word list.
func (c *Client) IsValidWord(word, lang string) (bool, error) {
u := c.baseURL + "/valid?" + url.Values{"word": {word}, "lang": {lang}}.Encode()
resp, err := c.httpClient.Get(u)
if err != nil {
return false, fmt.Errorf("dreamdict: valid: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return false, fmt.Errorf("dreamdict: valid: status %d", resp.StatusCode)
}
var result struct {
Valid bool `json:"valid"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return false, fmt.Errorf("dreamdict: valid: decode: %w", err)
}
return result.Valid, nil
}
// RandomWord returns a random word for a language with optional filters.
// pos can be empty. min/max of 0 means no filter.
func (c *Client) RandomWord(lang, pos string, min, max int) (string, error) {
params := url.Values{"lang": {lang}}
if pos != "" {
params.Set("pos", pos)
}
if min > 0 {
params.Set("min", strconv.Itoa(min))
}
if max > 0 {
params.Set("max", strconv.Itoa(max))
}
u := c.baseURL + "/random?" + params.Encode()
resp, err := c.httpClient.Get(u)
if err != nil {
return "", fmt.Errorf("dreamdict: random: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return "", fmt.Errorf("dreamdict: random: no match")
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("dreamdict: random: status %d", resp.StatusCode)
}
var result struct {
Word string `json:"word"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", fmt.Errorf("dreamdict: random: decode: %w", err)
}
return result.Word, nil
}
// Define returns all definitions for a word in a language.
func (c *Client) Define(word, lang string) ([]Definition, error) {
u := c.baseURL + "/define?" + url.Values{"word": {word}, "lang": {lang}}.Encode()
resp, err := c.httpClient.Get(u)
if err != nil {
return nil, fmt.Errorf("dreamdict: define: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("dreamdict: define: status %d", resp.StatusCode)
}
var result struct {
Definitions []Definition `json:"definitions"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("dreamdict: define: decode: %w", err)
}
return result.Definitions, nil
}
// Synonyms returns synonyms for a word in a language.
func (c *Client) Synonyms(word, lang string) ([]string, error) {
u := c.baseURL + "/synonyms?" + url.Values{"word": {word}, "lang": {lang}}.Encode()
resp, err := c.httpClient.Get(u)
if err != nil {
return nil, fmt.Errorf("dreamdict: synonyms: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("dreamdict: synonyms: status %d", resp.StatusCode)
}
var result struct {
Synonyms []string `json:"synonyms"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("dreamdict: synonyms: decode: %w", err)
}
return result.Synonyms, nil
}
// Translate returns translations of a word from one language to another.
func (c *Client) Translate(word, from, to string) ([]string, error) {
u := c.baseURL + "/translate?" + url.Values{"word": {word}, "from": {from}, "to": {to}}.Encode()
resp, err := c.httpClient.Get(u)
if err != nil {
return nil, fmt.Errorf("dreamdict: translate: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("dreamdict: translate: status %d", resp.StatusCode)
}
var result struct {
Translations []string `json:"translations"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("dreamdict: translate: decode: %w", err)
}
return result.Translations, nil
}
// Frequency returns the frequency score for a word in a language.
// Higher values indicate more common words. Returns 0 if unknown.
func (c *Client) Frequency(word, lang string) (int, error) {
u := c.baseURL + "/frequency?" + url.Values{"word": {word}, "lang": {lang}}.Encode()
resp, err := c.httpClient.Get(u)
if err != nil {
return 0, fmt.Errorf("dreamdict: frequency: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return 0, fmt.Errorf("dreamdict: frequency: status %d", resp.StatusCode)
}
var result struct {
Frequency int `json:"frequency"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return 0, fmt.Errorf("dreamdict: frequency: decode: %w", err)
}
return result.Frequency, nil
}
// FrequencyBatch returns frequency scores for multiple words in a single request.
func (c *Client) FrequencyBatch(words []string, lang string) (map[string]int, error) {
u := c.baseURL + "/frequency/batch?" + url.Values{
"words": {strings.Join(words, ",")},
"lang": {lang},
}.Encode()
resp, err := c.httpClient.Get(u)
if err != nil {
return nil, fmt.Errorf("dreamdict: frequency batch: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("dreamdict: frequency batch: status %d", resp.StatusCode)
}
var result struct {
Frequencies map[string]int `json:"frequencies"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("dreamdict: frequency batch: decode: %w", err)
}
return result.Frequencies, nil
}
// Health checks if the DreamDict service is reachable and returns stats.
func (c *Client) Health() (*HealthResponse, error) {
resp, err := c.httpClient.Get(c.baseURL + "/health")
if err != nil {
return nil, fmt.Errorf("dreamdict: health: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("dreamdict: health: status %d", resp.StatusCode)
}
var result HealthResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("dreamdict: health: decode: %w", err)
}
return &result, nil
}