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