1 #include <cstddef>
2 #include <sstream>
3 #include <string>
4 
5 #include "calendar.h"
6 #include "catch/catch.hpp"
7 #include "creature.h"
8 #include "game_constants.h"
9 #include "item.h"
10 #include "monattack.h"
11 #include "monster.h"
12 #include "npc.h"
13 #include "player.h"
14 #include "point.h"
15 #include "type_id.h"
16 
brute_probability(Creature & attacker,Creature & target,const size_t iters)17 static float brute_probability( Creature &attacker, Creature &target, const size_t iters )
18 {
19     // Note: not using deal_melee_attack because it trains dodge, which causes problems here
20     size_t hits = 0;
21     for( size_t i = 0; i < iters; i++ ) {
22         const int spread = attacker.hit_roll() - target.dodge_roll();
23         if( spread > 0 ) {
24             hits++;
25         }
26     }
27 
28     return static_cast<float>( hits ) / iters;
29 }
30 
brute_special_probability(monster & attacker,Creature & target,const size_t iters)31 static float brute_special_probability( monster &attacker, Creature &target, const size_t iters )
32 {
33     size_t hits = 0;
34     for( size_t i = 0; i < iters; i++ ) {
35         if( !mattack::dodge_check( &attacker, &target ) ) {
36             hits++;
37         }
38     }
39 
40     return static_cast<float>( hits ) / iters;
41 }
42 
full_attack_details(const player & dude)43 static std::string full_attack_details( const player &dude )
44 {
45     std::stringstream ss;
46     ss << "Details for " << dude.disp_name() << std::endl;
47     ss << "get_hit() == " << dude.get_hit() << std::endl;
48     ss << "get_melee_hit_base() == " << dude.get_melee_hit_base() << std::endl;
49     ss << "get_hit_weapon() == " << dude.get_hit_weapon( dude.weapon ) << std::endl;
50     return ss.str();
51 }
52 
percent_string(const float f)53 static inline std::string percent_string( const float f )
54 {
55     // Using stringstream for prettier precision printing
56     std::stringstream ss;
57     ss << 100.0f * f << "%";
58     return ss.str();
59 }
60 
check_near(float prob,const float expected,const float tolerance)61 static void check_near( float prob, const float expected, const float tolerance )
62 {
63     const float low = expected - tolerance;
64     const float high = expected + tolerance;
65     THEN( "The chance to hit is between " + percent_string( low ) +
66           " and " + percent_string( high ) ) {
67         REQUIRE( prob > low );
68         REQUIRE( prob < high );
69     }
70 }
71 
72 static const int num_iters = 10000;
73 
74 static constexpr tripoint dude_pos( HALF_MAPSIZE_X, HALF_MAPSIZE_Y, 0 );
75 
76 TEST_CASE( "Character attacking a zombie", "[.melee]" )
77 {
78     monster zed( mtype_id( "mon_zombie" ) );
79     INFO( "Zombie has get_dodge() == " + std::to_string( zed.get_dodge() ) );
80 
81     SECTION( "8/8/8/8, no skills, unarmed" ) {
82         standard_npc dude( "TestCharacter", dude_pos, {}, 0, 8, 8, 8, 8 );
83         const float prob = brute_probability( dude, zed, num_iters );
84         INFO( full_attack_details( dude ) );
85         check_near( prob, 0.6f, 0.1f );
86     }
87 
88     SECTION( "8/8/8/8, 3 all skills, two-by-four" ) {
89         standard_npc dude( "TestCharacter", dude_pos, {}, 3, 8, 8, 8, 8 );
90         dude.weapon = item( "2x4" );
91         const float prob = brute_probability( dude, zed, num_iters );
92         INFO( full_attack_details( dude ) );
93         check_near( prob, 0.8f, 0.05f );
94     }
95 
96     SECTION( "10/10/10/10, 8 all skills, katana" ) {
97         standard_npc dude( "TestCharacter", dude_pos, {}, 8, 10, 10, 10, 10 );
98         dude.weapon = item( "katana" );
99         const float prob = brute_probability( dude, zed, num_iters );
100         INFO( full_attack_details( dude ) );
101         check_near( prob, 0.975f, 0.025f );
102     }
103 }
104 
105 TEST_CASE( "Character attacking a manhack", "[.melee]" )
106 {
107     monster manhack( mtype_id( "mon_manhack" ) );
108     INFO( "Manhack has get_dodge() == " + std::to_string( manhack.get_dodge() ) );
109 
110     SECTION( "8/8/8/8, no skills, unarmed" ) {
111         standard_npc dude( "TestCharacter", dude_pos, {}, 0, 8, 8, 8, 8 );
112         const float prob = brute_probability( dude, manhack, num_iters );
113         INFO( full_attack_details( dude ) );
114         check_near( prob, 0.2f, 0.05f );
115     }
116 
117     SECTION( "8/8/8/8, 3 all skills, two-by-four" ) {
118         standard_npc dude( "TestCharacter", dude_pos, {}, 3, 8, 8, 8, 8 );
119         dude.weapon = item( "2x4" );
120         const float prob = brute_probability( dude, manhack, num_iters );
121         INFO( full_attack_details( dude ) );
122         check_near( prob, 0.4f, 0.05f );
123     }
124 
125     SECTION( "10/10/10/10, 8 all skills, katana" ) {
126         standard_npc dude( "TestCharacter", dude_pos, {}, 8, 10, 10, 10, 10 );
127         dude.weapon = item( "katana" );
128         const float prob = brute_probability( dude, manhack, num_iters );
129         INFO( full_attack_details( dude ) );
130         check_near( prob, 0.7f, 0.05f );
131     }
132 }
133 
134 TEST_CASE( "Zombie attacking a character", "[.melee]" )
135 {
136     monster zed( mtype_id( "mon_zombie" ) );
137     INFO( "Zombie has get_hit() == " + std::to_string( zed.get_hit() ) );
138 
139     SECTION( "8/8/8/8, no skills, unencumbered" ) {
140         standard_npc dude( "TestCharacter", dude_pos, {}, 0, 8, 8, 8, 8 );
141         const float prob = brute_probability( zed, dude, num_iters );
142         INFO( "Has get_dodge() == " + std::to_string( dude.get_dodge() ) );
143         THEN( "Character has no significant dodge bonus or penalty" ) {
144             REQUIRE( dude.get_dodge_bonus() < 0.5f );
145             REQUIRE( dude.get_dodge_bonus() > -0.5f );
146         }
147 
148         THEN( "Character's dodge skill is roughly equal to zombie's attack skill" ) {
149             REQUIRE( dude.get_dodge() < zed.get_hit() + 0.5f );
150             REQUIRE( dude.get_dodge() > zed.get_hit() - 0.5f );
151         }
152 
153         check_near( prob, 0.5f, 0.05f );
154     }
155 
156     SECTION( "10/10/10/10, 3 all skills, good cotton armor" ) {
157         standard_npc dude( "TestCharacter", dude_pos,
158         { "hoodie", "jeans", "long_underpants", "long_undertop", "longshirt" },
159         3, 10, 10, 10, 10 );
160         const float prob = brute_probability( zed, dude, num_iters );
161         INFO( "Has get_dodge() == " + std::to_string( dude.get_dodge() ) );
162         check_near( prob, 0.2f, 0.05f );
163     }
164 
165     SECTION( "10/10/10/10, 8 all skills, survivor suit" ) {
166         standard_npc dude( "TestCharacter", dude_pos, { "survivor_suit" }, 8, 10, 10, 10, 10 );
167         const float prob = brute_probability( zed, dude, num_iters );
168         INFO( "Has get_dodge() == " + std::to_string( dude.get_dodge() ) );
169         check_near( prob, 0.025f, 0.0125f );
170     }
171 }
172 
173 TEST_CASE( "Manhack attacking a character", "[.melee]" )
174 {
175     monster manhack( mtype_id( "mon_manhack" ) );
176     INFO( "Manhack has get_hit() == " + std::to_string( manhack.get_hit() ) );
177 
178     SECTION( "8/8/8/8, no skills, unencumbered" ) {
179         standard_npc dude( "TestCharacter", dude_pos, {}, 0, 8, 8, 8, 8 );
180         const float prob = brute_probability( manhack, dude, num_iters );
181         INFO( "Has get_dodge() == " + std::to_string( dude.get_dodge() ) );
182         THEN( "Character has no significant dodge bonus or penalty" ) {
183             REQUIRE( dude.get_dodge_bonus() < 0.5f );
184             REQUIRE( dude.get_dodge_bonus() > -0.5f );
185         }
186 
187         check_near( prob, 0.9f, 0.05f );
188     }
189 
190     SECTION( "10/10/10/10, 3 all skills, good cotton armor" ) {
191         standard_npc dude( "TestCharacter", dude_pos,
192         { "hoodie", "jeans", "long_underpants", "long_undertop", "longshirt" },
193         3, 10, 10, 10, 10 );
194         const float prob = brute_probability( manhack, dude, num_iters );
195         INFO( "Has get_dodge() == " + std::to_string( dude.get_dodge() ) );
196         check_near( prob, 0.6f, 0.05f );
197     }
198 
199     SECTION( "10/10/10/10, 8 all skills, survivor suit" ) {
200         standard_npc dude( "TestCharacter", dude_pos, { "survivor_suit" }, 8, 10, 10, 10, 10 );
201         const float prob = brute_probability( manhack, dude, num_iters );
202         INFO( "Has get_dodge() == " + std::to_string( dude.get_dodge() ) );
203         check_near( prob, 0.25f, 0.05f );
204     }
205 }
206 
207 TEST_CASE( "Hulk smashing a character", "[.], [melee], [monattack]" )
208 {
209     monster zed( mtype_id( "mon_zombie_hulk" ) );
210     INFO( "Hulk has get_hit() == " + std::to_string( zed.get_hit() ) );
211 
212     SECTION( "8/8/8/8, no skills, unencumbered" ) {
213         standard_npc dude( "TestCharacter", dude_pos, {}, 0, 8, 8, 8, 8 );
214         const float prob = brute_special_probability( zed, dude, num_iters );
215         INFO( "Has get_dodge() == " + std::to_string( dude.get_dodge() ) );
216         THEN( "Character has no significant dodge bonus or penalty" ) {
217             REQUIRE( dude.get_dodge_bonus() < 0.5f );
218             REQUIRE( dude.get_dodge_bonus() > -0.5f );
219         }
220 
221         check_near( prob, 0.95f, 0.05f );
222     }
223 
224     SECTION( "10/10/10/10, 3 all skills, good cotton armor" ) {
225         standard_npc dude( "TestCharacter", dude_pos,
226         { "hoodie", "jeans", "long_underpants", "long_undertop", "longshirt" },
227         3, 10, 10, 10, 10 );
228         const float prob = brute_special_probability( zed, dude, num_iters );
229         INFO( "Has get_dodge() == " + std::to_string( dude.get_dodge() ) );
230         check_near( prob, 0.75f, 0.05f );
231     }
232 
233     SECTION( "10/10/10/10, 8 all skills, survivor suit" ) {
234         standard_npc dude( "TestCharacter", dude_pos, { "survivor_suit" }, 8, 10, 10, 10, 10 );
235         const float prob = brute_special_probability( zed, dude, num_iters );
236         INFO( "Has get_dodge() == " + std::to_string( dude.get_dodge() ) );
237         check_near( prob, 0.2f, 0.05f );
238     }
239 }
240 
241 TEST_CASE( "Charcter can dodge" )
242 {
243     standard_npc dude( "TestCharacter", dude_pos, {}, 0, 8, 8, 8, 8 );
244     monster zed( mtype_id( "mon_zombie" ) );
245 
246     dude.clear_effects();
247     REQUIRE( dude.get_dodge() > 0.0 );
248 
249     const int dodges_left = dude.dodges_left;
250     for( int i = 0; i < 10000; ++i ) {
251         dude.deal_melee_attack( &zed, 1 );
252         if( dodges_left < dude.dodges_left ) {
253             CHECK( dodges_left < dude.dodges_left );
254             break;
255         }
256     }
257 }
258 
259 TEST_CASE( "Incapacited character can't dodge" )
260 {
261     standard_npc dude( "TestCharacter", dude_pos, {}, 0, 8, 8, 8, 8 );
262     monster zed( mtype_id( "mon_zombie" ) );
263 
264     dude.clear_effects();
265     dude.add_effect( efftype_id( "sleep" ), 1_hours );
266     REQUIRE( dude.get_dodge() == 0.0 );
267 
268     const int dodges_left = dude.dodges_left;
269     for( int i = 0; i < 10000; ++i ) {
270         dude.deal_melee_attack( &zed, 1 );
271         CHECK( dodges_left == dude.dodges_left );
272     }
273 }
274