test: comprehensive tests for player effects, monster, and combat
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -76,3 +76,63 @@ func TestFleeChance(t *testing.T) {
|
|||||||
t.Errorf("Flee success rate suspicious: %d/100", successes)
|
t.Errorf("Flee success rate suspicious: %d/100", successes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMonsterAIBossAoE(t *testing.T) {
|
||||||
|
boss := &entity.Monster{Name: "Boss", HP: 100, IsBoss: true}
|
||||||
|
players := []*entity.Player{entity.NewPlayer("P1", entity.ClassWarrior)}
|
||||||
|
|
||||||
|
// Turn 0 should NOT AoE
|
||||||
|
_, isAoE := MonsterAI(boss, players, 0)
|
||||||
|
if isAoE {
|
||||||
|
t.Error("boss should not AoE on turn 0")
|
||||||
|
}
|
||||||
|
// Turn 3 should AoE
|
||||||
|
_, isAoE = MonsterAI(boss, players, 3)
|
||||||
|
if !isAoE {
|
||||||
|
t.Error("boss should AoE on turn 3")
|
||||||
|
}
|
||||||
|
// Turn 6 should AoE
|
||||||
|
_, isAoE = MonsterAI(boss, players, 6)
|
||||||
|
if !isAoE {
|
||||||
|
t.Error("boss should AoE on turn 6")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMonsterAILowestHP(t *testing.T) {
|
||||||
|
p1 := entity.NewPlayer("Tank", entity.ClassWarrior) // 120 HP
|
||||||
|
p2 := entity.NewPlayer("Mage", entity.ClassMage) // 70 HP
|
||||||
|
p2.HP = 10 // very low
|
||||||
|
|
||||||
|
// Run many times — at least some should target p2 (30% chance)
|
||||||
|
targetedLow := 0
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
m := &entity.Monster{Name: "Orc", HP: 50}
|
||||||
|
idx, _ := MonsterAI(m, []*entity.Player{p1, p2}, 1)
|
||||||
|
if idx == 1 {
|
||||||
|
targetedLow++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Should target low HP player roughly 30% of time
|
||||||
|
if targetedLow < 10 || targetedLow > 60 {
|
||||||
|
t.Errorf("lowest HP targeting out of expected range: %d/100", targetedLow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCalcDamageWithMultiplier(t *testing.T) {
|
||||||
|
// AoE multiplier 0.8: ATK=20, DEF=5, mult=0.8 → base = 20*0.8 - 5 = 11
|
||||||
|
// Range: 11 * 0.85 to 11 * 1.15 = ~9.35 to ~12.65
|
||||||
|
for i := 0; i < 50; i++ {
|
||||||
|
dmg := CalcDamage(20, 5, 0.8)
|
||||||
|
if dmg < 9 || dmg > 13 {
|
||||||
|
t.Errorf("AoE damage %d out of expected range 9-13", dmg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCalcDamageHighDEF(t *testing.T) {
|
||||||
|
// When DEF > ATK*mult, should deal minimum 1 damage
|
||||||
|
dmg := CalcDamage(5, 100, 1.0)
|
||||||
|
if dmg != 1 {
|
||||||
|
t.Errorf("expected minimum damage 1, got %d", dmg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,3 +36,23 @@ func TestMonsterDEFScaling(t *testing.T) {
|
|||||||
t.Errorf("Boss5 DEF should be base 8, got %d", boss.DEF)
|
t.Errorf("Boss5 DEF should be base 8, got %d", boss.DEF)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTickTaunt(t *testing.T) {
|
||||||
|
m := &Monster{Name: "Orc", HP: 50, TauntTarget: true, TauntTurns: 2}
|
||||||
|
m.TickTaunt()
|
||||||
|
if m.TauntTurns != 1 || !m.TauntTarget {
|
||||||
|
t.Error("should still be taunted with 1 turn left")
|
||||||
|
}
|
||||||
|
m.TickTaunt()
|
||||||
|
if m.TauntTurns != 0 || m.TauntTarget {
|
||||||
|
t.Error("taunt should be cleared at 0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMonsterAtMinFloor(t *testing.T) {
|
||||||
|
// Slime at floor 1 (minFloor=1) should have base stats
|
||||||
|
m := NewMonster(MonsterSlime, 1)
|
||||||
|
if m.HP != 20 || m.ATK != 5 || m.DEF != 1 {
|
||||||
|
t.Errorf("Slime at min floor should be base stats, got HP=%d ATK=%d DEF=%d", m.HP, m.ATK, m.DEF)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -63,3 +63,130 @@ func TestPlayerTakeDamage(t *testing.T) {
|
|||||||
t.Error("Player should be dead")
|
t.Error("Player should be dead")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIsOut(t *testing.T) {
|
||||||
|
p := NewPlayer("test", ClassWarrior)
|
||||||
|
if p.IsOut() {
|
||||||
|
t.Error("alive player should not be out")
|
||||||
|
}
|
||||||
|
p.Dead = true
|
||||||
|
if !p.IsOut() {
|
||||||
|
t.Error("dead player should be out")
|
||||||
|
}
|
||||||
|
p.Dead = false
|
||||||
|
p.Fled = true
|
||||||
|
if !p.IsOut() {
|
||||||
|
t.Error("fled player should be out")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRevive(t *testing.T) {
|
||||||
|
p := NewPlayer("test", ClassWarrior) // 120 MaxHP
|
||||||
|
p.TakeDamage(200)
|
||||||
|
if !p.IsDead() {
|
||||||
|
t.Error("should be dead")
|
||||||
|
}
|
||||||
|
p.Revive(0.30)
|
||||||
|
if p.IsDead() {
|
||||||
|
t.Error("should be alive after revive")
|
||||||
|
}
|
||||||
|
if p.HP != 36 { // 120 * 0.30
|
||||||
|
t.Errorf("HP should be 36, got %d", p.HP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHealCap(t *testing.T) {
|
||||||
|
p := NewPlayer("test", ClassWarrior) // 120 HP
|
||||||
|
p.HP = 100
|
||||||
|
p.Heal(50) // should cap at 120
|
||||||
|
if p.HP != 120 {
|
||||||
|
t.Errorf("HP should cap at 120, got %d", p.HP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEffectiveATKWithItems(t *testing.T) {
|
||||||
|
p := NewPlayer("test", ClassWarrior) // base ATK 12
|
||||||
|
p.Inventory = append(p.Inventory, Item{Name: "Sword", Type: ItemWeapon, Bonus: 5})
|
||||||
|
p.Inventory = append(p.Inventory, Item{Name: "Sword2", Type: ItemWeapon, Bonus: 3})
|
||||||
|
if p.EffectiveATK() != 20 { // 12 + 5 + 3
|
||||||
|
t.Errorf("ATK should be 20, got %d", p.EffectiveATK())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEffectiveDEFWithItems(t *testing.T) {
|
||||||
|
p := NewPlayer("test", ClassWarrior) // base DEF 8
|
||||||
|
p.Inventory = append(p.Inventory, Item{Name: "Shield", Type: ItemArmor, Bonus: 4})
|
||||||
|
if p.EffectiveDEF() != 12 { // 8 + 4
|
||||||
|
t.Errorf("DEF should be 12, got %d", p.EffectiveDEF())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatusEffectPoison(t *testing.T) {
|
||||||
|
p := NewPlayer("test", ClassWarrior) // 120 HP
|
||||||
|
p.AddEffect(ActiveEffect{Type: StatusPoison, Duration: 2, Value: 10})
|
||||||
|
if !p.HasEffect(StatusPoison) {
|
||||||
|
t.Error("should have poison")
|
||||||
|
}
|
||||||
|
msgs := p.TickEffects()
|
||||||
|
if len(msgs) != 1 {
|
||||||
|
t.Errorf("expected 1 message, got %d", len(msgs))
|
||||||
|
}
|
||||||
|
if p.HP != 110 {
|
||||||
|
t.Errorf("HP should be 110 after poison tick, got %d", p.HP)
|
||||||
|
}
|
||||||
|
// Poison can't kill
|
||||||
|
p.HP = 5
|
||||||
|
p.TickEffects() // duration expires after this tick
|
||||||
|
if p.HP != 1 {
|
||||||
|
t.Errorf("poison should leave at 1 HP, got %d", p.HP)
|
||||||
|
}
|
||||||
|
if p.IsDead() {
|
||||||
|
t.Error("poison should not kill")
|
||||||
|
}
|
||||||
|
if p.HasEffect(StatusPoison) {
|
||||||
|
t.Error("poison should have expired")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatusEffectBurn(t *testing.T) {
|
||||||
|
p := NewPlayer("test", ClassMage) // 70 HP
|
||||||
|
p.AddEffect(ActiveEffect{Type: StatusBurn, Duration: 1, Value: 100})
|
||||||
|
p.TickEffects()
|
||||||
|
if !p.IsDead() {
|
||||||
|
t.Error("burn should be able to kill")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRelicPoisonImmunity(t *testing.T) {
|
||||||
|
p := NewPlayer("test", ClassWarrior)
|
||||||
|
p.Relics = append(p.Relics, Relic{Name: "Antidote", Effect: RelicPoisonImmunity})
|
||||||
|
p.AddEffect(ActiveEffect{Type: StatusPoison, Duration: 3, Value: 10})
|
||||||
|
if p.HasEffect(StatusPoison) {
|
||||||
|
t.Error("should be immune to poison")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRelicBurnResist(t *testing.T) {
|
||||||
|
p := NewPlayer("test", ClassWarrior)
|
||||||
|
p.Relics = append(p.Relics, Relic{Name: "Flame Guard", Effect: RelicBurnResist})
|
||||||
|
p.AddEffect(ActiveEffect{Type: StatusBurn, Duration: 2, Value: 10})
|
||||||
|
// Burn value should be halved to 5
|
||||||
|
if len(p.Effects) == 0 {
|
||||||
|
t.Fatal("should have burn effect (resisted, not immune)")
|
||||||
|
}
|
||||||
|
if p.Effects[0].Value != 5 {
|
||||||
|
t.Errorf("burn value should be halved to 5, got %d", p.Effects[0].Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEffectOverwrite(t *testing.T) {
|
||||||
|
p := NewPlayer("test", ClassWarrior)
|
||||||
|
p.AddEffect(ActiveEffect{Type: StatusPoison, Duration: 1, Value: 5})
|
||||||
|
p.AddEffect(ActiveEffect{Type: StatusPoison, Duration: 3, Value: 10}) // should overwrite
|
||||||
|
if len(p.Effects) != 1 {
|
||||||
|
t.Errorf("should have 1 effect, got %d", len(p.Effects))
|
||||||
|
}
|
||||||
|
if p.Effects[0].Duration != 3 || p.Effects[0].Value != 10 {
|
||||||
|
t.Error("should have overwritten with new values")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user