mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
Add housing/pets/Thom Krooke, wordle overhaul, boost system, backup strategy
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>
This commit is contained in:
442
internal/plugin/adventure_pets_test.go
Normal file
442
internal/plugin/adventure_pets_test.go
Normal file
@@ -0,0 +1,442 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// ── Pet XP & Leveling ──────────────────────────────────────────────────────
|
||||
|
||||
func TestPetXPToNextLevel(t *testing.T) {
|
||||
tests := []struct {
|
||||
level int
|
||||
expected int
|
||||
}{
|
||||
{0, 10},
|
||||
{1, 10},
|
||||
{2, 10},
|
||||
{4, 20},
|
||||
{5, 35},
|
||||
{7, 35},
|
||||
{8, 50},
|
||||
{9, 50},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := petXPToNextLevel(tt.level); got != tt.expected {
|
||||
t.Errorf("petXPToNextLevel(%d) = %d, want %d", tt.level, got, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPetXPToNextLevel_Increasing(t *testing.T) {
|
||||
// XP requirements should never decrease as level increases
|
||||
prev := petXPToNextLevel(0)
|
||||
for level := 1; level < 10; level++ {
|
||||
curr := petXPToNextLevel(level)
|
||||
if curr < prev {
|
||||
t.Errorf("XP for level %d (%d) should not be less than level %d (%d)", level, curr, level-1, prev)
|
||||
}
|
||||
prev = curr
|
||||
}
|
||||
}
|
||||
|
||||
func TestPetGrantXP_LevelsUp(t *testing.T) {
|
||||
char := &AdventureCharacter{
|
||||
PetType: "dog",
|
||||
PetName: "Rex",
|
||||
PetArrived: true,
|
||||
PetLevel: 1,
|
||||
PetXP: 950, // close to needing 1000 (10 * 100 centixp)
|
||||
}
|
||||
leveled := petGrantXP(char)
|
||||
if !leveled {
|
||||
t.Error("should have leveled up")
|
||||
}
|
||||
if char.PetLevel != 2 {
|
||||
t.Errorf("pet level should be 2, got %d", char.PetLevel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPetGrantXP_NoPet(t *testing.T) {
|
||||
char := &AdventureCharacter{}
|
||||
if petGrantXP(char) {
|
||||
t.Error("should not grant XP without a pet")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPetGrantXP_MaxLevel(t *testing.T) {
|
||||
char := &AdventureCharacter{
|
||||
PetType: "cat",
|
||||
PetName: "Luna",
|
||||
PetArrived: true,
|
||||
PetLevel: 10,
|
||||
PetXP: 9999,
|
||||
}
|
||||
if petGrantXP(char) {
|
||||
t.Error("should not level up past max level 10")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPetGrantXP_ChasedAway(t *testing.T) {
|
||||
char := &AdventureCharacter{
|
||||
PetType: "dog",
|
||||
PetName: "Rex",
|
||||
PetArrived: true,
|
||||
PetChasedAway: true,
|
||||
PetLevel: 1,
|
||||
PetXP: 0,
|
||||
}
|
||||
if petGrantXP(char) {
|
||||
t.Error("should not grant XP to chased-away pet")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPetGrantXP_SetsLevel10Date(t *testing.T) {
|
||||
char := &AdventureCharacter{
|
||||
PetType: "dog",
|
||||
PetName: "Rex",
|
||||
PetArrived: true,
|
||||
PetLevel: 9,
|
||||
PetXP: 4999, // needs 5000 (50 * 100) for level 9→10
|
||||
}
|
||||
petGrantXP(char)
|
||||
if char.PetLevel == 10 && char.PetLevel10Date == "" {
|
||||
t.Error("reaching level 10 should set PetLevel10Date")
|
||||
}
|
||||
}
|
||||
|
||||
// ── Pet Combat Probabilities ───────────────────────────────────────────────
|
||||
|
||||
func TestPetAttackChance_ScalesWithLevel(t *testing.T) {
|
||||
prev := petAttackChance(0)
|
||||
for level := 1; level <= 10; level++ {
|
||||
curr := petAttackChance(level)
|
||||
if curr <= prev {
|
||||
t.Errorf("attack chance at level %d (%.3f) should exceed level %d (%.3f)", level, curr, level-1, prev)
|
||||
}
|
||||
prev = curr
|
||||
}
|
||||
}
|
||||
|
||||
func TestPetAttackChance_Level10(t *testing.T) {
|
||||
chance := petAttackChance(10)
|
||||
// 3% + 10*1.5% = 18%
|
||||
if chance < 0.179 || chance > 0.181 {
|
||||
t.Errorf("level 10 attack chance: got %.3f, want 0.18", chance)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPetDeflectChance_ScalesWithLevel(t *testing.T) {
|
||||
prev := petDeflectChance(0, 0)
|
||||
for level := 1; level <= 10; level++ {
|
||||
curr := petDeflectChance(level, 0)
|
||||
if curr <= prev {
|
||||
t.Errorf("deflect chance at level %d (%.3f) should exceed level %d (%.3f)", level, curr, level-1, prev)
|
||||
}
|
||||
prev = curr
|
||||
}
|
||||
}
|
||||
|
||||
func TestPetDeflectChance_ArmorBonus(t *testing.T) {
|
||||
baseChance := petDeflectChance(5, 0)
|
||||
armoredChance := petDeflectChance(5, 3)
|
||||
if armoredChance <= baseChance {
|
||||
t.Errorf("armored deflect (%.3f) should exceed base (%.3f)", armoredChance, baseChance)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPetDitchRecoveryChance_ScalesWithLevel(t *testing.T) {
|
||||
prev := petDitchRecoveryChance(0)
|
||||
for level := 1; level <= 10; level++ {
|
||||
curr := petDitchRecoveryChance(level)
|
||||
if curr <= prev {
|
||||
t.Errorf("ditch chance at level %d (%.3f) should exceed level %d (%.3f)", level, curr, level-1, prev)
|
||||
}
|
||||
prev = curr
|
||||
}
|
||||
}
|
||||
|
||||
func TestPetDitchRecoveryChance_Level10(t *testing.T) {
|
||||
chance := petDitchRecoveryChance(10)
|
||||
// 5% + 10*2% = 25%
|
||||
if chance < 0.249 || chance > 0.251 {
|
||||
t.Errorf("level 10 ditch chance: got %.3f, want 0.25", chance)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Pet Combat Results ─────────────────────────────────────────────────────
|
||||
|
||||
func TestPetRollCombatActions_NoPet(t *testing.T) {
|
||||
char := &AdventureCharacter{}
|
||||
result := petRollCombatActions(char, "Goblin")
|
||||
if result != nil {
|
||||
t.Error("should return nil without a pet")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPetRollDitchRecovery_NoPet(t *testing.T) {
|
||||
char := &AdventureCharacter{}
|
||||
if petRollDitchRecovery(char) {
|
||||
t.Error("should return false without a pet")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPetVictoryText_Dog(t *testing.T) {
|
||||
char := &AdventureCharacter{PetType: "dog", PetName: "Rex", PetArrived: true}
|
||||
text := petVictoryText(char)
|
||||
if text == "" {
|
||||
t.Error("should return victory text for dog")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPetVictoryText_Cat(t *testing.T) {
|
||||
char := &AdventureCharacter{PetType: "cat", PetName: "Luna", PetArrived: true}
|
||||
text := petVictoryText(char)
|
||||
if text == "" {
|
||||
t.Error("should return victory text for cat")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPetVictoryText_NoPet(t *testing.T) {
|
||||
char := &AdventureCharacter{}
|
||||
if petVictoryText(char) != "" {
|
||||
t.Error("should return empty string without pet")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPetDeathText_NoPet(t *testing.T) {
|
||||
char := &AdventureCharacter{}
|
||||
if petDeathText(char) != "" {
|
||||
t.Error("should return empty string without pet")
|
||||
}
|
||||
}
|
||||
|
||||
// ── Pet Arrival Logic ──────────────────────────────────────────────────────
|
||||
|
||||
func TestPetShouldArrive_NoHouse(t *testing.T) {
|
||||
char := &AdventureCharacter{HouseTier: 0}
|
||||
if petShouldArrive(char) {
|
||||
t.Error("should not arrive without house")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPetShouldArrive_BaseHouseOnly(t *testing.T) {
|
||||
char := &AdventureCharacter{HouseTier: 1} // Base house, not yet Livable
|
||||
if petShouldArrive(char) {
|
||||
t.Error("should not arrive with only base house (need tier 2+)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPetShouldArrive_AlreadyArrived(t *testing.T) {
|
||||
char := &AdventureCharacter{HouseTier: 2, PetArrived: true}
|
||||
if petShouldArrive(char) {
|
||||
t.Error("should not trigger if pet already arrived")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPetShouldArrive_ChasedAway(t *testing.T) {
|
||||
char := &AdventureCharacter{HouseTier: 2, PetChasedAway: true}
|
||||
if petShouldArrive(char) {
|
||||
t.Error("should not trigger if pet was chased away (and not reactivated)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPetShouldArrive_Reactivated(t *testing.T) {
|
||||
char := &AdventureCharacter{HouseTier: 2, PetChasedAway: true, PetReactivated: true}
|
||||
// With reactivation, should always return true
|
||||
if !petShouldArrive(char) {
|
||||
t.Error("reactivated pet should always trigger arrival")
|
||||
}
|
||||
}
|
||||
|
||||
// ── HasPet ─────────────────────────────────────────────────────────────────
|
||||
|
||||
func TestHasPet(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
char AdventureCharacter
|
||||
expected bool
|
||||
}{
|
||||
{"no pet", AdventureCharacter{}, false},
|
||||
{"has pet", AdventureCharacter{PetType: "dog", PetName: "Rex", PetArrived: true}, true},
|
||||
{"chased away", AdventureCharacter{PetType: "dog", PetName: "Rex", PetArrived: true, PetChasedAway: true}, false},
|
||||
{"not arrived", AdventureCharacter{PetType: "dog", PetName: "Rex"}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.char.HasPet() != tt.expected {
|
||||
t.Errorf("HasPet() = %v, want %v", tt.char.HasPet(), tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ── Morning Pet Events ─────────────────────────────────────────────────────
|
||||
|
||||
func TestPetMorningEvent_NoPet(t *testing.T) {
|
||||
char := &AdventureCharacter{}
|
||||
if petMorningEvent(char) != "" {
|
||||
t.Error("should return empty string without pet")
|
||||
}
|
||||
}
|
||||
|
||||
// ── Misty Housing Hint ─────────────────────────────────────────────────────
|
||||
|
||||
func TestMistyHousingHint_NoEncounters(t *testing.T) {
|
||||
char := &AdventureCharacter{MistyEncounterCount: 0}
|
||||
if mistyHousingHint(char) != "" {
|
||||
t.Error("should not hint with 0 encounters")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMistyHousingHint_HasHouse(t *testing.T) {
|
||||
char := &AdventureCharacter{MistyEncounterCount: 3, HouseTier: 1}
|
||||
if mistyHousingHint(char) != "" {
|
||||
t.Error("should not hint if player has house")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMistyHousingHint_Donated(t *testing.T) {
|
||||
char := &AdventureCharacter{MistyEncounterCount: 2, MistyDonatedCount: 1}
|
||||
hint := mistyHousingHint(char)
|
||||
if !strings.Contains(hint, "house") {
|
||||
t.Errorf("donated player hint should mention house, got: %q", hint)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMistyHousingHint_NotDonated(t *testing.T) {
|
||||
char := &AdventureCharacter{MistyEncounterCount: 2, MistyDonatedCount: 0}
|
||||
hint := mistyHousingHint(char)
|
||||
if hint != "Thank you for your time." {
|
||||
t.Errorf("non-donor hint should be dismissive, got: %q", hint)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Misty Pet Reactivation ─────────────────────────────────────────────────
|
||||
|
||||
func TestMistyReactivatePet_ChasedAway(t *testing.T) {
|
||||
char := &AdventureCharacter{PetChasedAway: true, PetReactivated: false}
|
||||
mistyReactivatePet(char)
|
||||
if !char.PetReactivated {
|
||||
t.Error("should set PetReactivated")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMistyReactivatePet_NotChasedAway(t *testing.T) {
|
||||
char := &AdventureCharacter{PetChasedAway: false, PetReactivated: false}
|
||||
mistyReactivatePet(char)
|
||||
if char.PetReactivated {
|
||||
t.Error("should not set PetReactivated if pet wasn't chased away")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMistyReactivatePet_AlreadyReactivated(t *testing.T) {
|
||||
char := &AdventureCharacter{PetChasedAway: true, PetReactivated: true}
|
||||
mistyReactivatePet(char)
|
||||
if !char.PetReactivated {
|
||||
t.Error("should remain reactivated")
|
||||
}
|
||||
}
|
||||
|
||||
// ── Pet Supply Shop Unlock ─────────────────────────────────────────────────
|
||||
|
||||
func TestPetCheckSupplyShopUnlock_NotLevel10(t *testing.T) {
|
||||
char := &AdventureCharacter{PetLevel10Date: ""}
|
||||
if petCheckSupplyShopUnlock(char) {
|
||||
t.Error("should not unlock without level 10 date")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPetCheckSupplyShopUnlock_AlreadyUnlocked(t *testing.T) {
|
||||
char := &AdventureCharacter{PetSupplyShopUnlocked: true, PetLevel10Date: "2020-01-01"}
|
||||
if petCheckSupplyShopUnlock(char) {
|
||||
t.Error("should not re-trigger if already unlocked")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPetCheckSupplyShopUnlock_TooSoon(t *testing.T) {
|
||||
// Set date to today — not 7 days ago
|
||||
char := &AdventureCharacter{PetLevel10Date: "2099-01-01"}
|
||||
if petCheckSupplyShopUnlock(char) {
|
||||
t.Error("should not unlock before 7 days have passed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPetCheckSupplyShopUnlock_Ready(t *testing.T) {
|
||||
char := &AdventureCharacter{PetLevel10Date: "2020-01-01"}
|
||||
if !petCheckSupplyShopUnlock(char) {
|
||||
t.Error("should unlock after 7+ days")
|
||||
}
|
||||
}
|
||||
|
||||
// ── Ditch Recovery Text ────────────────────────────────────────────────────
|
||||
|
||||
func TestPetDitchRecoveryGameRoom_NoPet(t *testing.T) {
|
||||
text := petDitchRecoveryGameRoom("Player1", "", false)
|
||||
if !strings.Contains(text, "Player1") {
|
||||
t.Error("should contain player name")
|
||||
}
|
||||
if !strings.Contains(text, "stretcher") {
|
||||
t.Error("should mention stretcher for non-pet version")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPetDitchRecoveryGameRoom_WithPet(t *testing.T) {
|
||||
text := petDitchRecoveryGameRoom("Player1", "Rex", true)
|
||||
if !strings.Contains(text, "Rex") {
|
||||
t.Error("should contain pet name")
|
||||
}
|
||||
if !strings.Contains(text, "intervened") {
|
||||
t.Error("should mention pet intervention")
|
||||
}
|
||||
}
|
||||
|
||||
// ── Flavor Pool Coverage ───────────────────────────────────────────────────
|
||||
|
||||
func TestPetFlavorPools_NonEmpty(t *testing.T) {
|
||||
pools := map[string][]string{
|
||||
"PetDogAttack": PetDogAttack,
|
||||
"PetCatAttack": PetCatAttack,
|
||||
"PetDogDeath": PetDogDeath,
|
||||
"PetCatDeath": PetCatDeath,
|
||||
"PetDogVictory": PetDogVictory,
|
||||
"PetCatVictory": PetCatVictory,
|
||||
"PetDogDeflect": PetDogDeflect,
|
||||
"PetCatDeflect": PetCatDeflect,
|
||||
"PetCatOffering": PetCatOffering,
|
||||
"PetDogSmothering": PetDogSmothering,
|
||||
}
|
||||
for name, pool := range pools {
|
||||
if len(pool) == 0 {
|
||||
t.Errorf("flavor pool %s is empty", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPetAttackFlavor_Placeholders(t *testing.T) {
|
||||
// All attack lines must have {damage}. {enemy} is optional — some lines
|
||||
// intentionally omit the enemy name for voice/style reasons (especially cat).
|
||||
for i, line := range PetDogAttack {
|
||||
if !strings.Contains(line, "{damage}") {
|
||||
t.Errorf("PetDogAttack[%d] missing {damage} placeholder", i)
|
||||
}
|
||||
}
|
||||
for i, line := range PetCatAttack {
|
||||
if !strings.Contains(line, "{damage}") {
|
||||
t.Errorf("PetCatAttack[%d] missing {damage} placeholder", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPetDeflectFlavor_Placeholders(t *testing.T) {
|
||||
for i, line := range PetDogDeflect {
|
||||
if !strings.Contains(line, "{enemy}") {
|
||||
t.Errorf("PetDogDeflect[%d] missing {enemy} placeholder", i)
|
||||
}
|
||||
}
|
||||
for i, line := range PetCatDeflect {
|
||||
if !strings.Contains(line, "{enemy}") {
|
||||
t.Errorf("PetCatDeflect[%d] missing {enemy} placeholder", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user