Skip to content

Commit

Permalink
Unit tests for [drains], [poison] and [slow], with apply_to=opponent
Browse files Browse the repository at this point in the history
Slightly different to PR wesnoth#6582, which was the 1.16 version of this. The five
lines that were labelled `preserving known bug` are changed to test that it's
been fixed.

Here `apply_to=opponent` means that the weapon special gives the opponent the
ability, the unit that should get poisoned or slowed is the unit that has the
weapon special.

There's a known bug in 1.16, that `apply_to=opponent` check the wrong unit to
see it it's `unpoisonable`, `undrainable` etc. It also checks the wrong unit to
see if it's already poisoned or slowed, so a battle between two units that both
have reverse-poison results in at most one being poisoned.

Most of the credit for this is Newfrenchy's, as he's already written a fix
and a WML based test. This commit uses a Lua test instead to test more
combinations of statuses.

This adds a `COMMON_KEEP_A_B_UNIT_TEST` macro, which is a counterpart to the
`GENERIC_UNIT_TEST` macro that starts the leaders next to each other, ready
to attack. The `A_B` is because I'm planning a multiple-side variant too.

There's no test for [petrify], as simulate_combat doesn't provide a stat for it.

This tests only 3 of the 6 abilities whose behavior changed in 650f704.
My thoughts on testing the others are:
* [firststrike]'s test is in 650f704.
* [drains], [poison] and [slow] are tested here.
* [petrify] ends combat, it's also not exposed in simulate_combat's stats.
* [plague] triggers after combat ends.
  • Loading branch information
stevecotton committed Apr 14, 2022
1 parent 6dc52f0 commit 566618c
Show file tree
Hide file tree
Showing 6 changed files with 385 additions and 9 deletions.
65 changes: 56 additions & 9 deletions data/test/macros/wml_unit_test_macros.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@
# Starting state:
# Side 1 leader Alice (Elvish Archer)
# Side 2 leader Bob (Orcish Grunt)
#
# Both leaders are on keeps separated by a few hexes, close enough for either
# of them to move next to the other leader in a single turn for an attack.
#
# Both keeps have space to recruit a single unit.
##
#define GENERIC_UNIT_TEST NAME CONTENT
#arg SIDE1_CONTROLLER
Expand Down Expand Up @@ -109,6 +114,48 @@ Elvish Archer#endarg
[/test]
#enddef

##
# Starting state:
# Side 1 leader Alice (Elvish Archer)
# Side 2 leader Bob (Orcish Grunt)
#
# Both leaders are on a single keep, adjacent to each other.
# There is no free castle hex to recruit onto.
##
#define COMMON_KEEP_A_B_UNIT_TEST NAME CONTENT
[test]
name=_ "Unit Test " + {NAME}
map_file=test/maps/2p_single_castle.map
turns=unlimited
id={NAME}
random_start_time=no
is_unit_test=yes

{DAWN}

[side]
side=1
controller=human
name=_ "Alice"
type=Elvish Archer
id=alice
fog=no
team_name=West
[/side]
[side]
side=2
controller=human
name=_ "Bob"
type=Orcish Grunt
id=bob
fog=no
team_name=East
[/side]

{CONTENT}
[/test]
#enddef

#define FAIL
{RETURN ([false][/false])}
#enddef
Expand All @@ -118,13 +165,13 @@ Elvish Archer#endarg
#enddef

#define FAIL_IF_NOT FLAG NOT_EQUALS
[if]
[variable]
name={FLAG}
not_equals={NOT_EQUALS}
[/variable]
[then]
{FAIL}
[/then]
[/if]
[if]
[variable]
name={FLAG}
not_equals={NOT_EQUALS}
[/variable]
[then]
{FAIL}
[/then]
[/if]
#enddef
7 changes: 7 additions & 0 deletions data/test/maps/2p_single_castle.map
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Gg, Gg, Gg, Gg, Gg, Gg, Gg, Gg, Gg, Gg
Gg, Gg, Gg, Gg, Gg, Gg, Gg, Gg, Gg, Gg
Gg, Gg, Gg, Gg, Gg, Gg, Gg, Gg, Gg, Gg
Gg, Gg, Gg, Gg, 1 Ke, 2 Ke, Gg, Gg, Gg, Gg
Gg, Gg, Gg, Gg, Gg, Gg, Gg, Gg, Gg, Gg
Gg, Gg, Gg, Gg, Gg, Gg, Gg, Gg, Gg, Gg
Gg, Gg, Gg, Gg, Gg, Gg, Gg, Gg, Gg, Gg
165 changes: 165 additions & 0 deletions data/test/scenarios/reflexive_drains.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
#textdomain wesnoth-test

#####
# API(s) being tested: [drains]
##
# Actions:
# This uses a "common keep" map, with Alice and Bob already in position to attack any of the other units.
# In this test they're all Orcish Grunts, so the all have 9x2 melee attacks.
# Set everyone to 20 hp, so that drains will show but no-one will die.
# Give Alice drains.
# Give Bob reflexive drains.
# Give Dave the undrainable trait.
# Simulate various combats and check the results.
##
# Expected end state:
# Alice drains Bob and Charlie
# Charlie drains when attacking Bob
# Dave drains in the Bob v Dave fight
#####
[test]
name = _ "Unit Test reflexive_drains"
map_file=test/maps/4p_single_castle.map
turns = unlimited
id = reflexive_drains
is_unit_test = yes

{DAWN}

[side]
side=1
controller=human
[leader]
name = _ "Alice"
type = Orcish Grunt
id=alice
[/leader]
[/side]
[side]
side=2
controller=human
[leader]
name = _ "Bob"
type = Orcish Grunt
id=bob
[/leader]
[/side]
[side]
side=3
controller=human
[leader]
name = _ "Charlie"
type = Orcish Grunt
id=charlie
[/leader]
[/side]
[side]
side=4
controller=human
[leader]
name = _ "Dave"
type = Orcish Grunt
id=dave
[/leader]
[/side]

[event]
name=start

[object]
[filter]
id=alice
[/filter]
[effect]
apply_to=attack
[set_specials]
mode=append
{WEAPON_SPECIAL_DRAIN}
[/set_specials]
[/effect]
[/object]

[object]
[filter]
id=bob
[/filter]
[effect]
apply_to=attack
[set_specials]
mode=append
[drains]
id=drains
name= _ "drains"
description= _ "Reverse drains, gives the drain ability to the opponent."
apply_to=opponent
[/drains]
[/set_specials]
[/effect]
[/object]

[object]
[filter]
id=dave
[/filter]
[effect]
apply_to=status
add=undrainable
[/effect]
[/object]

[lua]
code=<<
local alice = wesnoth.units.find({id="alice"})[1]
local bob = wesnoth.units.find({id="bob"})[1]
local charlie = wesnoth.units.find({id="charlie"})[1]
local dave = wesnoth.units.find({id="dave"})[1]

alice.hitpoints = 20
bob.hitpoints = 20
charlie.hitpoints = 20
dave.hitpoints = 20

-- Everybody's an orcish grunt, and they're all on 60% terrain. The chance of {0,1,2} strikes hitting is:
local hit_distribution = {}
hit_distribution[0] = 0.6 ^ 2
hit_distribution[1] = 0.4 * 0.6 + 0.6 * 0.4
hit_distribution[2] = 0.4 ^ 2
-- Strikes do 9 damage each, and therefore drain heals 4 hp. Starting with 20 hp, each combination of getting
-- hit i times and healing j times leads to a unique amount of hp after the fight.
local expected_no_drain = {}
local expected_with_drain = {}
for i, i_chance in pairs(hit_distribution) do
expected_no_drain[20 - 9 * i] = i_chance
for j, j_chance in pairs(hit_distribution) do
expected_with_drain[20 - 9 * i + 4 * j] = i_chance * j_chance
end
end

function check_results(stats, expectation, log_message)
for i,i_chance in pairs(expectation) do
unit_test.assert_approx_equal(stats[i], i_chance, 0.001, log_message .. " expectation for " .. i .. " hp")
end
end

-- Alice has drain, Bob has reflexive drain, Dave is undrainable
local att_stats, def_stats = wesnoth.simulate_combat(alice, bob)
check_results(att_stats.hp_chance, expected_with_drain, "Alice v Bob att_stats")
check_results(def_stats.hp_chance, expected_no_drain, "Alice v Bob def_stats")
att_stats, def_stats = wesnoth.simulate_combat(alice, charlie)
check_results(att_stats.hp_chance, expected_with_drain, "Alice v Charlie att_stats")
check_results(def_stats.hp_chance, expected_no_drain, "Alice v Charlie def_stats")
att_stats, def_stats = wesnoth.simulate_combat(alice, dave)
check_results(att_stats.hp_chance, expected_no_drain, "Alice v Dave att_stats")
check_results(def_stats.hp_chance, expected_no_drain, "Alice v Dave def_stats")
att_stats, def_stats = wesnoth.simulate_combat(bob, charlie)
check_results(att_stats.hp_chance, expected_no_drain, "Bob v Charlie att_stats")
check_results(def_stats.hp_chance, expected_with_drain, "Bob v Charlie def_stats")
att_stats, def_stats = wesnoth.simulate_combat(bob, dave)
check_results(att_stats.hp_chance, expected_no_drain, "Bob v Dave att_stats")
check_results(def_stats.hp_chance, expected_with_drain, "Bob v Dave def_stats")
>>
[/lua]

{SUCCEED}
[/event]
[/test]
77 changes: 77 additions & 0 deletions data/test/scenarios/reflexive_poison.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#textdomain wesnoth-test

#####
# API(s) being tested: [poison]apply_to=opponent
##
# Actions:
# This uses a "common keep" map, so Alice and Bob are already in position to attack.
# Give Alice a weapon that does reverse-poison (Alice gets poisoned if Bob hits her).
# Simulate various combats using melee weapons.
##
# Expected end state:
# Normal combat can poison Alice.
# Making Alice unpoisonable works.
# Making Bob unpoisonable does not affect Alice.
# Making Bob poisoned before combat starts does not affect Alice.
#####
{COMMON_KEEP_A_B_UNIT_TEST reflexive_poison (
[event]
name=start

[object]
[filter]
id=alice
[/filter]
[effect]
apply_to=attack
[set_specials]
mode=append
[poison]
id="reflexive_poison"
name=_ "reflexive_poison"
description=_ "When Alice attacks, her opponent’s weapons get the <i>poison</i> special."
apply_to=opponent
[/poison]
[/set_specials]
[/effect]
[/object]

[lua]
code=<<
local alice = wesnoth.units.find({id="alice"})[1]
local bob = wesnoth.units.find({id="bob"})[1]

-- Alice attacks with her sword, so Bob can counterattack. They're both on keeps, so 60% defense, and Bob gets 2 swings.
local expected_chance = 1.0 - 0.6 ^ 2
-- Test that the weapon special works, before adding any complications about status conditions
local att_stats, def_stats = wesnoth.simulate_combat(alice, 1, bob)
unit_test.assert_approx_equal(att_stats.untouched, 1.0 - expected_chance, 0.01, "Test setup failed - Alice should be at risk of getting hit")
unit_test.assert_approx_equal(att_stats.poisoned, expected_chance, 0.01, "Alice should be at risk of poisoning")
-- Test the "unpoisonable" status on Alice (this works correctly, even in 1.16.x)
local immune_alice = alice:clone()
immune_alice.status.unpoisonable = true
att_stats, def_stats = wesnoth.simulate_combat(immune_alice, 1, bob)
unit_test.assert_approx_equal(att_stats.untouched, 1.0 - expected_chance, 0.01, "Test setup failed - Alice should be at risk of getting hit")
unit_test.assert_approx_equal(att_stats.poisoned, 0.0, 0.01, "Immune Alice should be unpoisonable")
-- Test the "unpoisonable" status on Bob
local immune_bob = bob:clone()
immune_bob.status.unpoisonable = true
att_stats, def_stats = wesnoth.simulate_combat(alice, 1, immune_bob)
unit_test.assert_approx_equal(att_stats.untouched, 1.0 - expected_chance, 0.01, "Test setup failed - Alice should be at risk of getting hit")
unit_test.assert_approx_equal(att_stats.poisoned, expected_chance, 0.01, "Alice should be at risk of poisoning when attacking Immune Bob")
-- Test that Bob already being poisoned before combat starts doesn't affect the stats
local affected_bob = bob:clone()
affected_bob.status.poisoned = true
att_stats, def_stats = wesnoth.simulate_combat(alice, 1, affected_bob)
unit_test.assert_approx_equal(att_stats.untouched, 1.0 - expected_chance, 0.01, "Test setup failed - Alice should be at risk of getting hit")
unit_test.assert_approx_equal(att_stats.poisoned, expected_chance, 0.01, "Alice should be at risk of poisoning when attacking Affected Bob")
>>
[/lua]

{SUCCEED}
[/event]
)}

0 comments on commit 566618c

Please sign in to comment.