Files
gogobee/internal/plugin/adventure_test.go
prosolis 05cf6657f5 Add hospital revival, Robbie bandit, MW/shop fixes, float64 scoring
- St. Guildmore's Memorial Hospital: !hospital command with Nurse Joy,
  procedural itemized billing, same-day revival, 6-hour dead timer,
  hospital ad after death, 2-hour nudge follow-up
- Robbie the Friendly Bandit: automated inventory cleaner, random daily
  visits (8-21 UTC, 40% chance), collects sub-tier gear at €50/item,
  donates to community pot, drops Get Out of Medical Debt Free card
- Fix MW auto-equip: T1 MW no longer replaces better equipped gear
- Fix shop MW block: allow purchasing upgrades over lower-tier MW
- Float64 equipment scoring: advEffectiveTier and advEquipmentScore
  return float64, no mid-calculation truncation (MW 1.25x, Arena 1.5x)
- Fix markdown rendering: convert *asterisk* to _underscore_ italics
  across all flavor text files (shop, blacksmith, rival, hospital)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 17:04:03 -07:00

303 lines
8.7 KiB
Go

package plugin
import "testing"
func TestAdvEquipmentScore_Empty(t *testing.T) {
score := advEquipmentScore(map[EquipmentSlot]*AdvEquipment{})
if score != 0 {
t.Errorf("empty equipment should score 0, got %.2f", score)
}
}
func TestAdvEquipmentScore_WeaponDoubled(t *testing.T) {
equip := map[EquipmentSlot]*AdvEquipment{
SlotWeapon: {Tier: 3, Condition: 100},
SlotArmor: {Tier: 3, Condition: 100},
}
score := advEquipmentScore(equip)
// Weapon: 3*2=6, Armor: 3
if score != 9.0 {
t.Errorf("got %.2f, want 9 (weapon 6 + armor 3)", score)
}
}
func TestAdvEquipmentScore_LowConditionHalved(t *testing.T) {
equip := map[EquipmentSlot]*AdvEquipment{
SlotArmor: {Tier: 4, Condition: 49}, // below 50%
}
score := advEquipmentScore(equip)
// Tier 4, halved = 2
if score != 2.0 {
t.Errorf("got %.2f, want 2 (tier 4 halved for low condition)", score)
}
}
func TestAdvEquipmentScore_FullLoadout(t *testing.T) {
equip := map[EquipmentSlot]*AdvEquipment{
SlotWeapon: {Tier: 5, Condition: 100},
SlotArmor: {Tier: 5, Condition: 100},
SlotHelmet: {Tier: 5, Condition: 100},
SlotBoots: {Tier: 5, Condition: 100},
SlotTool: {Tier: 5, Condition: 100},
}
score := advEquipmentScore(equip)
// Weapon: 5*2=10, others: 5*4=20, total=30
if score != 30.0 {
t.Errorf("got %.2f, want 30", score)
}
}
func TestXpToNextLevel(t *testing.T) {
// Level 1: 100 + 3 = 103
if xp := xpToNextLevel(1); xp != 103 {
t.Errorf("level 1: got %d, want 103", xp)
}
// Level 50: 100 + 150 = 250
if xp := xpToNextLevel(50); xp != 250 {
t.Errorf("level 50: got %d, want 250", xp)
}
// Higher levels should require more XP
for i := 1; i < 50; i++ {
if xpToNextLevel(i) >= xpToNextLevel(i+1) {
t.Errorf("xp for level %d should be less than level %d", i, i+1)
}
}
}
func TestAdvIsEligible_TooLowLevel(t *testing.T) {
char := &AdventureCharacter{CombatLevel: 5}
equip := map[EquipmentSlot]*AdvEquipment{
SlotWeapon: {Tier: 3, Condition: 100},
}
loc := &AdvLocation{Activity: AdvActivityDungeon, MinLevel: 20, MinEquipTier: 0}
bonuses := &AdvBonusSummary{}
eligible, _ := advIsEligible(char, equip, loc, bonuses)
if eligible {
t.Error("should not be eligible with combat level 5 for min level 20")
}
}
func TestAdvIsEligible_TooLowEquipTier(t *testing.T) {
char := &AdventureCharacter{CombatLevel: 30}
equip := map[EquipmentSlot]*AdvEquipment{
SlotWeapon: {Tier: 1, Condition: 100},
SlotArmor: {Tier: 0, Condition: 100}, // this is the bottleneck
}
loc := &AdvLocation{Activity: AdvActivityDungeon, MinLevel: 1, MinEquipTier: 1}
bonuses := &AdvBonusSummary{}
eligible, _ := advIsEligible(char, equip, loc, bonuses)
if eligible {
t.Error("should not be eligible with min equip tier 0 for requirement 1")
}
}
func TestAdvIsEligible_PenaltyZone(t *testing.T) {
char := &AdventureCharacter{CombatLevel: 21} // within 3 of min 20
equip := map[EquipmentSlot]*AdvEquipment{
SlotWeapon: {Tier: 2, Condition: 100},
}
loc := &AdvLocation{Activity: AdvActivityDungeon, MinLevel: 20, MinEquipTier: 0}
bonuses := &AdvBonusSummary{}
eligible, penalty := advIsEligible(char, equip, loc, bonuses)
if !eligible {
t.Error("should be eligible at combat level 21 for min 20")
}
if !penalty {
t.Error("should be in penalty zone (21-20=1, less than 3)")
}
}
func TestAdvIsEligible_NoPenalty(t *testing.T) {
char := &AdventureCharacter{CombatLevel: 25}
equip := map[EquipmentSlot]*AdvEquipment{
SlotWeapon: {Tier: 2, Condition: 100},
}
loc := &AdvLocation{Activity: AdvActivityDungeon, MinLevel: 20, MinEquipTier: 0}
bonuses := &AdvBonusSummary{}
eligible, penalty := advIsEligible(char, equip, loc, bonuses)
if !eligible {
t.Error("should be eligible")
}
if penalty {
t.Error("should NOT be in penalty zone (25-20=5, >= 3)")
}
}
func TestCalculateAdvProbabilities_SumsTo100(t *testing.T) {
char := &AdventureCharacter{CombatLevel: 10}
equip := map[EquipmentSlot]*AdvEquipment{
SlotWeapon: {Tier: 2, Condition: 100},
SlotArmor: {Tier: 2, Condition: 100},
}
loc := &AdvLocation{
Activity: AdvActivityDungeon,
BaseDeathPct: 18,
EmptyPct: 15,
MinLevel: 8,
}
bonuses := &AdvBonusSummary{}
prob := calculateAdvProbabilities(char, equip, loc, bonuses, false)
total := prob.DeathPct + prob.EmptyPct + prob.SuccessPct + prob.ExceptionalPct
if total < 99.9 || total > 100.1 {
t.Errorf("probabilities should sum to 100, got %.2f (death=%.1f empty=%.1f success=%.1f exceptional=%.1f)",
total, prob.DeathPct, prob.EmptyPct, prob.SuccessPct, prob.ExceptionalPct)
}
}
func TestCalculateAdvProbabilities_PenaltyIncreasesRisk(t *testing.T) {
char := &AdventureCharacter{CombatLevel: 10}
equip := map[EquipmentSlot]*AdvEquipment{
SlotWeapon: {Tier: 2, Condition: 100},
}
loc := &AdvLocation{
Activity: AdvActivityDungeon,
BaseDeathPct: 18,
EmptyPct: 15,
MinLevel: 8,
}
bonuses := &AdvBonusSummary{}
noPenalty := calculateAdvProbabilities(char, equip, loc, bonuses, false)
withPenalty := calculateAdvProbabilities(char, equip, loc, bonuses, true)
if withPenalty.DeathPct <= noPenalty.DeathPct {
t.Errorf("penalty zone should increase death %% (%.1f vs %.1f)", withPenalty.DeathPct, noPenalty.DeathPct)
}
}
func TestCalculateAdvProbabilities_BetterGearReducesDeath(t *testing.T) {
char := &AdventureCharacter{CombatLevel: 10}
loc := &AdvLocation{
Activity: AdvActivityDungeon,
BaseDeathPct: 30,
EmptyPct: 15,
MinLevel: 8,
}
bonuses := &AdvBonusSummary{}
weakGear := map[EquipmentSlot]*AdvEquipment{
SlotWeapon: {Tier: 1, Condition: 100},
}
strongGear := map[EquipmentSlot]*AdvEquipment{
SlotWeapon: {Tier: 5, Condition: 100},
SlotArmor: {Tier: 5, Condition: 100},
SlotHelmet: {Tier: 5, Condition: 100},
SlotBoots: {Tier: 5, Condition: 100},
SlotTool: {Tier: 5, Condition: 100},
}
weakProb := calculateAdvProbabilities(char, weakGear, loc, bonuses, false)
strongProb := calculateAdvProbabilities(char, strongGear, loc, bonuses, false)
if strongProb.DeathPct >= weakProb.DeathPct {
t.Errorf("better gear should reduce death %% (strong=%.1f vs weak=%.1f)", strongProb.DeathPct, weakProb.DeathPct)
}
}
func TestAdvEffectiveSkill(t *testing.T) {
char := &AdventureCharacter{
CombatLevel: 10,
MiningSkill: 15,
ForagingSkill: 20,
}
bonuses := &AdvBonusSummary{
CombatBonus: 5,
MiningBonus: 3,
ForagingBonus: 0,
}
if s := advEffectiveSkill(char, AdvActivityDungeon, bonuses); s != 15 {
t.Errorf("dungeon: got %d, want 15 (10+5)", s)
}
if s := advEffectiveSkill(char, AdvActivityMining, bonuses); s != 18 {
t.Errorf("mining: got %d, want 18 (15+3)", s)
}
if s := advEffectiveSkill(char, AdvActivityForaging, bonuses); s != 20 {
t.Errorf("foraging: got %d, want 20 (20+0)", s)
}
}
func TestAdvParseShopCategory(t *testing.T) {
tests := []struct {
input string
want EquipmentSlot
}{
{"weapon", SlotWeapon},
{"sword", SlotWeapon},
{"swords", SlotWeapon},
{"armor", SlotArmor},
{"armour", SlotArmor},
{"helmet", SlotHelmet},
{"helm", SlotHelmet},
{"boots", SlotBoots},
{"boot", SlotBoots},
{"tool", SlotTool},
{"pickaxe", SlotTool},
{" WEAPON ", SlotWeapon}, // trimmed + lowered
{"nonsense", ""},
{"", ""},
}
for _, tt := range tests {
got := advParseShopCategory(tt.input)
if got != tt.want {
t.Errorf("advParseShopCategory(%q) = %q, want %q", tt.input, got, tt.want)
}
}
}
func TestSlotEmoji(t *testing.T) {
if e := slotEmoji(SlotWeapon); e != "⚔️" {
t.Errorf("weapon emoji: got %q", e)
}
if e := slotEmoji(SlotArmor); e != "🛡️" {
t.Errorf("armor emoji: got %q", e)
}
// Default case
if e := slotEmoji("unknown"); e != "📦" {
t.Errorf("unknown slot emoji: got %q, want 📦", e)
}
}
func TestSlotTitle(t *testing.T) {
if s := slotTitle(SlotWeapon); s != "Weapon" {
t.Errorf("got %q, want Weapon", s)
}
if s := slotTitle(SlotBoots); s != "Boots" {
t.Errorf("got %q, want Boots", s)
}
if s := slotTitle(""); s != "" {
t.Errorf("empty slot should return empty, got %q", s)
}
}
func TestComputeAdvBonuses(t *testing.T) {
treasures := []AdvTreasureBonus{
{BonusType: "combat_level", BonusValue: 3},
{BonusType: "mining_skill", BonusValue: 2},
{BonusType: "all_skills", BonusValue: 1},
{BonusType: "death_chance", BonusValue: -2.5},
}
bonuses := computeAdvBonuses(treasures, nil, 0, false)
if bonuses.CombatBonus != 4 { // 3 + 1 from all_skills
t.Errorf("CombatBonus: got %d, want 4", bonuses.CombatBonus)
}
if bonuses.MiningBonus != 3 { // 2 + 1 from all_skills
t.Errorf("MiningBonus: got %d, want 3", bonuses.MiningBonus)
}
if bonuses.ForagingBonus != 1 { // 1 from all_skills
t.Errorf("ForagingBonus: got %d, want 1", bonuses.ForagingBonus)
}
if bonuses.DeathModifier != -2.5 {
t.Errorf("DeathModifier: got %f, want -2.5", bonuses.DeathModifier)
}
}