mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 00:32:40 +00:00
Adventure: Complete v1 daily idle RPG with DM-driven gameplay, equipment shop, treasure system, TwinBee NPC, streak/grudge/party mechanics, flavor text, and scheduled morning/evening/midnight tickers. Holdem CFR: Fix three critical training bugs (fold not forfeiting pot, free calls after raises, training/runtime key mismatch). Add performance optimizations (preflop lookup table, zero-alloc equity, integer keys, raise cap, regret pruning). Enrich abstraction with 12 equity buckets, board texture dimension, and 6-char action history. Replace validation with proper multi-street simulation. Also includes wordle plugin, holdem seed tooling, and schema additions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
194 lines
5.3 KiB
Go
194 lines
5.3 KiB
Go
package plugin
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
)
|
|
|
|
// wordnikRandomWordResponse is the Wordnik randomWord response.
|
|
type wordnikRandomWordResponse struct {
|
|
Word string `json:"word"`
|
|
}
|
|
|
|
// wordnikDefinitionResponse is one definition entry.
|
|
type wordnikDefinitionResponse struct {
|
|
Text string `json:"text"`
|
|
PartOfSpeech string `json:"partOfSpeech"`
|
|
}
|
|
|
|
// wordnikFetchRandomWord fetches a random word of the given length from Wordnik.
|
|
// Returns the uppercased word. Retries up to 5 times to avoid bad words.
|
|
func wordnikFetchRandomWord(apiKey string, client *http.Client, wordLength int) (string, error) {
|
|
for attempt := 0; attempt < 5; attempt++ {
|
|
url := fmt.Sprintf(
|
|
"https://api.wordnik.com/v4/words.json/randomWord?hasDictionaryDef=true&minLength=%d&maxLength=%d&minCorpusCount=5000&minDictionaryCount=3&api_key=%s",
|
|
wordLength, wordLength, apiKey,
|
|
)
|
|
|
|
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return "", fmt.Errorf("wordle: create request: %w", err)
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return "", fmt.Errorf("wordle: fetch random word: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return "", fmt.Errorf("wordle: API returned status %d", resp.StatusCode)
|
|
}
|
|
|
|
var result wordnikRandomWordResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return "", fmt.Errorf("wordle: decode response: %w", err)
|
|
}
|
|
|
|
word := strings.ToUpper(strings.TrimSpace(result.Word))
|
|
|
|
// Reject words with hyphens, spaces, apostrophes.
|
|
if strings.ContainsAny(word, "-' ") {
|
|
slog.Debug("wordle: rejecting word with special chars", "word", word, "attempt", attempt+1)
|
|
continue
|
|
}
|
|
|
|
// Reject if not all alphabetic.
|
|
allAlpha := true
|
|
for _, r := range word {
|
|
if !unicode.IsLetter(r) {
|
|
allAlpha = false
|
|
break
|
|
}
|
|
}
|
|
if !allAlpha {
|
|
slog.Debug("wordle: rejecting non-alpha word", "word", word, "attempt", attempt+1)
|
|
continue
|
|
}
|
|
|
|
// Check definition to reject proper nouns.
|
|
if isProperNoun(apiKey, client, word) {
|
|
slog.Debug("wordle: rejecting proper noun", "word", word, "attempt", attempt+1)
|
|
continue
|
|
}
|
|
|
|
slog.Info("wordle: selected word", "word", word, "length", wordLength, "attempt", attempt+1)
|
|
return word, nil
|
|
}
|
|
|
|
return "", fmt.Errorf("wordle: failed to find suitable word after 5 attempts")
|
|
}
|
|
|
|
// isProperNoun checks if a word's first definition starts with a capital letter.
|
|
func isProperNoun(apiKey string, client *http.Client, word string) bool {
|
|
defs, err := wordnikFetchDefinitions(apiKey, client, strings.ToLower(word), 1)
|
|
if err != nil || len(defs) == 0 {
|
|
return false
|
|
}
|
|
text := strings.TrimSpace(defs[0].Text)
|
|
if text == "" {
|
|
return false
|
|
}
|
|
return unicode.IsUpper([]rune(text)[0])
|
|
}
|
|
|
|
// wordnikValidateWord checks if a word exists by looking up its definitions.
|
|
// Returns valid bool and an error flag. On API errors, returns false with apiErr=true
|
|
// so the caller can show a different message than "not a valid word".
|
|
func wordnikValidateWord(apiKey string, client *http.Client, word string) (valid bool, apiErr bool) {
|
|
if apiKey == "" {
|
|
return true, false // no API key = skip validation
|
|
}
|
|
defs, err := wordnikFetchDefinitions(apiKey, client, strings.ToLower(word), 1)
|
|
if err != nil {
|
|
slog.Warn("wordle: validation API error", "word", word, "err", err)
|
|
return false, true
|
|
}
|
|
return len(defs) > 0, false
|
|
}
|
|
|
|
// wordnikFetchDefinitionText fetches a clean definition string for display.
|
|
func wordnikFetchDefinitionText(apiKey string, client *http.Client, word string) string {
|
|
defs, err := wordnikFetchDefinitions(apiKey, client, strings.ToLower(word), 3)
|
|
if err != nil || len(defs) == 0 {
|
|
return ""
|
|
}
|
|
|
|
// Find the first clean definition.
|
|
for _, d := range defs {
|
|
text := strings.TrimSpace(d.Text)
|
|
if text == "" {
|
|
continue
|
|
}
|
|
// Strip HTML tags if any.
|
|
text = stripHTMLTags(text)
|
|
pos := d.PartOfSpeech
|
|
if pos != "" {
|
|
return fmt.Sprintf("%s (%s): %s", strings.ToLower(word), pos, text)
|
|
}
|
|
return fmt.Sprintf("%s: %s", strings.ToLower(word), text)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// wordnikFetchDefinitions fetches definitions from Wordnik.
|
|
func wordnikFetchDefinitions(apiKey string, client *http.Client, word string, limit int) ([]wordnikDefinitionResponse, error) {
|
|
url := fmt.Sprintf(
|
|
"https://api.wordnik.com/v4/word.json/%s/definitions?limit=%d&sourceDictionaries=all&api_key=%s",
|
|
word, limit, apiKey,
|
|
)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusNotFound {
|
|
return nil, nil // word not found
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("status %d", resp.StatusCode)
|
|
}
|
|
|
|
var defs []wordnikDefinitionResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&defs); err != nil {
|
|
return nil, err
|
|
}
|
|
return defs, nil
|
|
}
|
|
|
|
// stripHTMLTags removes HTML tags from a string.
|
|
func stripHTMLTags(s string) string {
|
|
var b strings.Builder
|
|
inTag := false
|
|
for _, r := range s {
|
|
if r == '<' {
|
|
inTag = true
|
|
continue
|
|
}
|
|
if r == '>' {
|
|
inTag = false
|
|
continue
|
|
}
|
|
if !inTag {
|
|
b.WriteRune(r)
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|