mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 00:32:40 +00:00
Part 3 (Housing, Thom Krooke & Pets): - Housing system with tiered upgrades, mortgage loans, FRED API rates - Thom Krooke realtor NPC with !thom commands and !thom pay extra principal - Pet system with arrival, naming, leveling, combat, morning defense - Pet ditch recovery scales with level, name validation Wordle overhaul: - Remove daily expiration — puzzles persist until solved/failed - Auto-start new puzzle after solve, fail, or skip - Sequential puzzle IDs instead of date-based - Auto-create puzzle on first guess if none active Adventure fixes: - Double XP/money boost toggle (!adv boost, admin only) - Fix ensureCharacter wiping existing data on query errors - Fix rival RPS format string bug - Fix daily summary empty data (load today+yesterday logs) - Fix holdem DM-to-room reply routing - Fix Robbie payout to 25% of item value - Add fishing to leaderboard and TwinBee calculations - Add Thom to morning menu - Persist mortgage rate across restarts via db.CacheGet/Set Infrastructure: - Daily database backup via VACUUM INTO with 7-day retention - Admin DM notification on backup failure (async) - Daily NPC house balance reset for holdem Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
246 lines
6.0 KiB
Go
246 lines
6.0 KiB
Go
package plugin
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// letterEmoji returns the colored emoji for a LetterResult.
|
|
func letterEmoji(r LetterResult) string {
|
|
switch r {
|
|
case LetterCorrect:
|
|
return "🟩"
|
|
case LetterPresent:
|
|
return "🟨"
|
|
default:
|
|
return "⬛"
|
|
}
|
|
}
|
|
|
|
// renderGrid renders the full emoji grid for the current puzzle state.
|
|
func renderWordleGrid(puzzle *WordlePuzzle) string {
|
|
var sb strings.Builder
|
|
|
|
status := fmt.Sprintf("%d/%d", len(puzzle.Guesses), puzzle.MaxGuesses)
|
|
if puzzle.Solved {
|
|
status += " " + strings.Repeat("🟩", puzzle.WordLength)
|
|
}
|
|
sb.WriteString(fmt.Sprintf("🟩 **Wordle #%d** — %s\n\n", puzzle.PuzzleNumber, status))
|
|
|
|
for _, g := range puzzle.Guesses {
|
|
// Emoji tiles.
|
|
for _, r := range g.Results {
|
|
sb.WriteString(letterEmoji(r))
|
|
}
|
|
// Word + player name.
|
|
sb.WriteString(fmt.Sprintf(" %s (%s)", g.Word, g.PlayerName))
|
|
if g.Word == puzzle.Answer {
|
|
sb.WriteString(" ✅")
|
|
}
|
|
sb.WriteString("\n")
|
|
}
|
|
|
|
// Keyboard view.
|
|
sb.WriteString("\n")
|
|
sb.WriteString(renderKeyboard(puzzle.LetterStates))
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
// renderKeyboard renders the QWERTY keyboard with color-coded letter states.
|
|
func renderKeyboard(states map[rune]LetterResult) string {
|
|
rows := []string{
|
|
"QWERTYUIOP",
|
|
"ASDFGHJKL",
|
|
"ZXCVBNM",
|
|
}
|
|
|
|
var sb strings.Builder
|
|
for _, row := range rows {
|
|
for i, ch := range row {
|
|
if i > 0 {
|
|
sb.WriteString(" ")
|
|
}
|
|
if state, ok := states[ch]; ok {
|
|
sb.WriteString(letterEmoji(state))
|
|
}
|
|
sb.WriteRune(ch)
|
|
}
|
|
sb.WriteString("\n")
|
|
}
|
|
return sb.String()
|
|
}
|
|
|
|
// renderWordleStartAnnouncement renders the puzzle start message.
|
|
func renderWordleStartAnnouncement(puzzleNumber, wordLength int, hint string) string {
|
|
base := fmt.Sprintf(
|
|
"🟩 **Wordle #%d**\nA new %d-letter puzzle is ready! Work together — %d guesses shared.",
|
|
puzzleNumber, wordLength, wordleMaxGuesses(wordLength),
|
|
)
|
|
if hint != "" {
|
|
base += fmt.Sprintf("\n💡 **Hint:** %s", hint)
|
|
}
|
|
base += "\n\nGuess with: `!wordle <word>`"
|
|
return base
|
|
}
|
|
|
|
// renderSolvedAnnouncement renders the solved puzzle message.
|
|
func renderSolvedAnnouncement(puzzle *WordlePuzzle, definition string, payouts []WordlePayout) string {
|
|
var sb strings.Builder
|
|
|
|
// Find the solver.
|
|
lastGuess := puzzle.Guesses[len(puzzle.Guesses)-1]
|
|
|
|
sb.WriteString(fmt.Sprintf("🎉 **Solved in %d/%d!**\n", len(puzzle.Guesses), puzzle.MaxGuesses))
|
|
sb.WriteString(fmt.Sprintf("The word was **%s** — solved by %s on guess %d.\n",
|
|
puzzle.Answer, lastGuess.PlayerName, len(puzzle.Guesses)))
|
|
|
|
if definition != "" {
|
|
sb.WriteString(fmt.Sprintf("\n📖 *%s*\n", definition))
|
|
}
|
|
|
|
// Full grid.
|
|
sb.WriteString("\n")
|
|
for _, g := range puzzle.Guesses {
|
|
for _, r := range g.Results {
|
|
sb.WriteString(letterEmoji(r))
|
|
}
|
|
sb.WriteString(fmt.Sprintf(" %s (%s)", g.Word, g.PlayerName))
|
|
if g.Word == puzzle.Answer {
|
|
sb.WriteString(" ✅")
|
|
}
|
|
sb.WriteString("\n")
|
|
}
|
|
|
|
// Contributors with payouts.
|
|
if len(payouts) > 0 {
|
|
sb.WriteString("\n💰 **Payouts:**\n")
|
|
for _, pay := range payouts {
|
|
bonus := ""
|
|
if pay.Solver {
|
|
bonus = " 🏆 (solver bonus!)"
|
|
}
|
|
sb.WriteString(fmt.Sprintf(" **%s**: +€%d%s\n", pay.Name, pay.Amount, bonus))
|
|
}
|
|
} else {
|
|
sb.WriteString("\n🏆 Today's contributors:\n")
|
|
contributors := wordleContributors(puzzle)
|
|
for _, c := range contributors {
|
|
line := fmt.Sprintf(" %s — %d guess", c.name, c.guesses)
|
|
if c.guesses != 1 {
|
|
line += "es"
|
|
}
|
|
if c.solved {
|
|
line += " 🏆"
|
|
}
|
|
sb.WriteString(line + "\n")
|
|
}
|
|
}
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
// renderFailedAnnouncement renders the failed puzzle message.
|
|
func renderFailedAnnouncement(puzzle *WordlePuzzle, definition string) string {
|
|
var sb strings.Builder
|
|
|
|
sb.WriteString(fmt.Sprintf("💀 **Puzzle failed — %d/%d**\nThe word was **%s**.\n", puzzle.MaxGuesses, puzzle.MaxGuesses, puzzle.Answer))
|
|
|
|
if definition != "" {
|
|
sb.WriteString(fmt.Sprintf("\n📖 *%s*\n", definition))
|
|
}
|
|
|
|
// Full grid.
|
|
sb.WriteString("\n")
|
|
for _, g := range puzzle.Guesses {
|
|
for _, r := range g.Results {
|
|
sb.WriteString(letterEmoji(r))
|
|
}
|
|
sb.WriteString(fmt.Sprintf(" %s (%s)\n", g.Word, g.PlayerName))
|
|
}
|
|
|
|
sb.WriteString("\nBetter luck tomorrow. Contributors:\n")
|
|
contributors := wordleContributors(puzzle)
|
|
for _, c := range contributors {
|
|
line := fmt.Sprintf(" %s — %d guess", c.name, c.guesses)
|
|
if c.guesses != 1 {
|
|
line += "es"
|
|
}
|
|
sb.WriteString(line + "\n")
|
|
}
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
type wordleContributor struct {
|
|
name string
|
|
guesses int
|
|
solved bool
|
|
}
|
|
|
|
// wordleContributors tallies guess counts per player.
|
|
func wordleContributors(puzzle *WordlePuzzle) []wordleContributor {
|
|
type entry struct {
|
|
name string
|
|
guesses int
|
|
solved bool
|
|
}
|
|
seen := map[string]*entry{}
|
|
var order []string
|
|
|
|
for i, g := range puzzle.Guesses {
|
|
key := string(g.PlayerID)
|
|
if e, ok := seen[key]; ok {
|
|
e.guesses++
|
|
if i == len(puzzle.Guesses)-1 && puzzle.Solved {
|
|
e.solved = true
|
|
}
|
|
} else {
|
|
e := &entry{name: g.PlayerName, guesses: 1}
|
|
if i == len(puzzle.Guesses)-1 && puzzle.Solved {
|
|
e.solved = true
|
|
}
|
|
seen[key] = e
|
|
order = append(order, key)
|
|
}
|
|
}
|
|
|
|
result := make([]wordleContributor, 0, len(order))
|
|
for _, key := range order {
|
|
e := seen[key]
|
|
result = append(result, wordleContributor{name: e.name, guesses: e.guesses, solved: e.solved})
|
|
}
|
|
return result
|
|
}
|
|
|
|
// renderLeaderboard renders the all-time Wordle leaderboard.
|
|
func renderWordleLeaderboard(stats []WordlePlayerStat, streak int) string {
|
|
var sb strings.Builder
|
|
sb.WriteString("📊 **Wordle Leaderboard**\n")
|
|
|
|
for i, s := range stats {
|
|
line := fmt.Sprintf(" %d. %s — %d solve", i+1, s.DisplayName, s.PuzzlesSolved)
|
|
if s.PuzzlesSolved != 1 {
|
|
line += "s"
|
|
}
|
|
if s.WinningGuesses > 0 {
|
|
line += fmt.Sprintf(" | %d winning guess", s.WinningGuesses)
|
|
if s.WinningGuesses != 1 {
|
|
line += "es"
|
|
}
|
|
line += " 🏆"
|
|
}
|
|
sb.WriteString(line + "\n")
|
|
}
|
|
|
|
if streak > 0 {
|
|
sb.WriteString(fmt.Sprintf("\nCommunity streak: %d day", streak))
|
|
if streak != 1 {
|
|
sb.WriteString("s")
|
|
}
|
|
sb.WriteString(" solved in a row 🔥\n")
|
|
}
|
|
|
|
return sb.String()
|
|
}
|