mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 08:52:42 +00:00
Add adventure plugin, holdem CFR fixes, and wordle plugin
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>
This commit is contained in:
193
internal/plugin/wordle_wordnik.go
Normal file
193
internal/plugin/wordle_wordnik.go
Normal file
@@ -0,0 +1,193 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user