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
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.

As 1.16 has already been released, to avoid OOS the test is checking that the
current behavior's known bug is preserved. For the 1.17 branch, the five lines
labelled `preserving known bug` will be changed to test the reverse.

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, and
the main reason for using separate files is Git's fuzzy-patch matching when
merging and rebasing. Having large blocks of identical text in
`wml_unit_test_macros.cfg` can mean the wrong section gets patched.

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 will change in 1.17's
equivalent of 1.16's 7b39b65. That's sufficient to prevent any accidental
copy of the 1.17 fix to 1.16, and my thoughts on testing the others are:
* [firststrike]'s test is in 7b39b65. It crashed, so is fixed in 1.16.
* [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 17, 2022
1 parent 1fb6ca6 commit 0d8353c
Show file tree
Hide file tree
Showing 6 changed files with 372 additions and 0 deletions.
43 changes: 43 additions & 0 deletions data/test/macros/start_position_common_keep_a_b.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#textdomain wesnoth-test

##
# 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
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
# [known bug in 1.16] Dave doesn't drain 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_no_drain, "[preserving known bug] 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.
# [known bug in 1.16] Making Bob unpoisonable should not affect Alice.
# [known bug in 1.16] Making Bob poisoned before combat starts should 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 (known bug in 1.16.x: Bob's unpoisonable status disables the poison special, even though it should affect Alice)
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, 0.0, 0.01, "[preserving known bug] 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 (known bug in 1.16.x: it does)
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, 0.0, 0.01, "[preserving known bug] Alice should be at risk of poisoning when attacking Affected Bob")
>>
[/lua]
{SUCCEED}
[/event]
)}
77 changes: 77 additions & 0 deletions data/test/scenarios/reflexive_slow.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#textdomain wesnoth-test

#####
# API(s) being tested: [slow]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-slow (Alice gets slowed if Bob hits her).
# Simulate various combats using melee weapons.
##
# Expected end state:
# Normal combat can slow Alice.
# Making Alice unslowable works.
# [known bug in 1.16] Making Bob unslowable should not affect Alice.
# [known bug in 1.16] Making Bob slowed before combat starts should not affect Alice.
#####
{COMMON_KEEP_A_B_UNIT_TEST reflexive_slow (
[event]
name=start

[object]
[filter]
id=alice
[/filter]
[effect]
apply_to=attack
[set_specials]
mode=append
[slow]
id="reflexive_slow"
name=_ "reflexive_slow"
description=_ "When Alice attacks, her opponent’s weapons get the <i>slow</i> special."
apply_to=opponent
[/slow]
[/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.slowed, expected_chance, 0.01, "Alice should be at risk of slowing")
-- Test the "unslowable" status on Alice (this works correctly, even in 1.16.x)
local immune_alice = alice:clone()
immune_alice.status.unslowable = 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.slowed, 0.0, 0.01, "Immune Alice should be unslowable")
-- Test the "unslowable" status on Bob (known bug in 1.16.x: Bob's unslowable status disables the slow special, even though it should affect Alice)
local immune_bob = bob:clone()
immune_bob.status.unslowable = 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.slowed, 0.0, 0.01, "[preserving known bug] Alice should be at risk of slowing when attacking Immune Bob")

-- Test that Bob already being slowed before combat starts doesn't affect the stats (known bug in 1.16.x: it does)
local affected_bob = bob:clone()
affected_bob.status.slowed = 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.slowed, 0.0, 0.01, "[preserving known bug] Alice should be at risk of slowing when attacking Affected Bob")
>>
[/lua]
{SUCCEED}
[/event]
)}
3 changes: 3 additions & 0 deletions wml_test_schedule
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,9 @@
0 backstab_without_enemy_behind
0 backstab_with_statue_behind
0 backstab_with_ally_behind
0 reflexive_drains
0 reflexive_poison
0 reflexive_slow
0 swarm_disables_upgrades
0 swarm_disables_upgrades_with_abilities
0 swarm_disables_upgrades_with_abilities_fail
Expand Down

0 comments on commit 0d8353c

Please sign in to comment.