1 #include <algorithm>
2 #include <cmath>
3 #include <cstdio>
4 #include <cstdlib>
5 #include <map>
6 #include <memory>
7 #include <set>
8 #include <sstream>
9 #include <string>
10 #include <utility>
11 #include <vector>
12
13 #include "calendar.h"
14 #include "catch/catch.hpp"
15 #include "character.h"
16 #include "enums.h"
17 #include "item.h"
18 #include "itype.h"
19 #include "line.h"
20 #include "map.h"
21 #include "map_helpers.h"
22 #include "point.h"
23 #include "test_statistics.h"
24 #include "type_id.h"
25 #include "units.h"
26 #include "value_ptr.h"
27 #include "veh_type.h"
28 #include "vehicle.h"
29 #include "vpart_position.h"
30 #include "vpart_range.h"
31
32 using efficiency_stat = statistics<int>;
33
34 static const efftype_id effect_blind( "blind" );
35
clear_game(const ter_id & terrain)36 static void clear_game( const ter_id &terrain )
37 {
38 // Set to turn 0 to prevent solars from producing power
39 calendar::turn = calendar::turn_zero;
40 clear_creatures();
41 clear_npcs();
42 clear_vehicles();
43
44 Character &player_character = get_player_character();
45 // Move player somewhere safe
46 REQUIRE_FALSE( player_character.in_vehicle );
47 player_character.setpos( tripoint_zero );
48 // Blind the player to avoid needless drawing-related overhead
49 player_character.add_effect( effect_blind, 1_turns, true );
50
51 build_test_map( terrain );
52 }
53
54 // Returns how much fuel did it provide
55 // But contains only fuels actually used by engines
set_vehicle_fuel(vehicle & v,const float veh_fuel_mult)56 static std::map<itype_id, int> set_vehicle_fuel( vehicle &v, const float veh_fuel_mult )
57 {
58 // First we need to find the fuels to set
59 // That is, fuels actually used by some engine
60 std::set<itype_id> actually_used;
61 for( const vpart_reference &vp : v.get_all_parts() ) {
62 vehicle_part &pt = vp.part();
63 if( pt.is_engine() ) {
64 actually_used.insert( pt.info().fuel_type );
65 pt.enabled = true;
66 } else {
67 // Disable all parts that use up power or electric cars become non-deterministic
68 pt.enabled = false;
69 }
70 }
71
72 // We ignore battery when setting fuel because it uses designated "tanks"
73 actually_used.erase( itype_id( "battery" ) );
74
75 // Currently only one liquid fuel supported
76 REQUIRE( actually_used.size() <= 1 );
77 itype_id liquid_fuel = itype_id::NULL_ID();
78 for( const auto &ft : actually_used ) {
79 if( item::find_type( ft )->phase == phase_id::LIQUID ) {
80 liquid_fuel = ft;
81 break;
82 }
83 }
84
85 // Set fuel to a given percentage
86 // Batteries are special cased because they aren't liquid fuel
87 std::map<itype_id, int> ret;
88 const itype_id itype_battery( "battery" );
89 const ammotype ammo_battery( "battery" );
90 for( const vpart_reference &vp : v.get_all_parts() ) {
91 vehicle_part &pt = vp.part();
92
93 if( pt.is_battery() ) {
94 pt.ammo_set( itype_battery, pt.ammo_capacity( ammo_battery ) * veh_fuel_mult );
95 ret[itype_battery] += pt.ammo_capacity( ammo_battery ) * veh_fuel_mult;
96 } else if( pt.is_tank() && !liquid_fuel.is_null() ) {
97 float qty = pt.ammo_capacity( item::find_type( liquid_fuel )->ammo->type ) * veh_fuel_mult;
98 qty *= std::max( item::find_type( liquid_fuel )->stack_size, 1 );
99 qty /= to_milliliter( units::legacy_volume_factor );
100 pt.ammo_set( liquid_fuel, qty );
101 ret[ liquid_fuel ] += qty;
102 } else {
103 pt.ammo_unset();
104 }
105 }
106
107 // We re-add battery because we want it accounted for, just not in the section above
108 actually_used.insert( itype_id( "battery" ) );
109 for( auto iter = ret.begin(); iter != ret.end(); ) {
110 if( iter->second <= 0 || actually_used.count( iter->first ) == 0 ) {
111 iter = ret.erase( iter );
112 } else {
113 ++iter;
114 }
115 }
116 return ret;
117 }
118
119 // Returns the lowest percentage of fuel left
120 // i.e. 1 means no fuel was used, 0 means at least one dry tank
fuel_percentage_left(vehicle & v,const std::map<itype_id,int> & started_with)121 static float fuel_percentage_left( vehicle &v, const std::map<itype_id, int> &started_with )
122 {
123 std::map<itype_id, int> fuel_amount;
124 std::set<itype_id> consumed_fuels;
125 for( const vpart_reference &vp : v.get_all_parts() ) {
126 vehicle_part &pt = vp.part();
127
128 if( ( pt.is_battery() || pt.is_reactor() || pt.is_tank() ) &&
129 !pt.ammo_current().is_null() ) {
130 fuel_amount[ pt.ammo_current() ] += pt.ammo_remaining();
131 }
132
133 if( pt.is_engine() && !pt.info().fuel_type.is_null() ) {
134 consumed_fuels.insert( pt.info().fuel_type );
135 }
136 }
137
138 float left = 1.0f;
139 for( const auto &type : consumed_fuels ) {
140 const auto iter = started_with.find( type );
141 // Weird - we started without this fuel
142 float fuel_amt_at_start = iter != started_with.end() ? iter->second : 0.0f;
143 REQUIRE( fuel_amt_at_start != 0.0f );
144 left = std::min( left, static_cast<float>( fuel_amount[type] ) / fuel_amt_at_start );
145 }
146
147 return left;
148 }
149
150 static const float fuel_level = 0.1f;
151 static const int cycle_limit = 100;
152
153 // Algorithm goes as follows:
154 // Clear map
155 // Spawn a vehicle
156 // Set its fuel up to some percentage - remember exact fuel counts that were set here
157 // Drive it for a while, always moving it back to start point every turn to avoid it going off the bubble
158 // When moving back, record the sum of the tiles moved so far
159 // Repeat that for a set number of turns or until all fuel is drained
160 // Compare saved percentage (set before) to current percentage
161 // Rescale the recorded number of tiles based on fuel percentage left
162 // (i.e. 0% fuel left means no scaling, 50% fuel left means double the effective distance)
163 // Return the rescaled number
test_efficiency(const vproto_id & veh_id,int & expected_mass,const ter_id & terrain,const int reset_velocity_turn,const int target_distance,const bool smooth_stops=false,const bool test_mass=true,const bool in_reverse=false)164 static int test_efficiency( const vproto_id &veh_id, int &expected_mass,
165 const ter_id &terrain,
166 const int reset_velocity_turn, const int target_distance,
167 const bool smooth_stops = false, const bool test_mass = true,
168 const bool in_reverse = false )
169 {
170 int min_dist = target_distance * 0.99;
171 int max_dist = target_distance * 1.01;
172 clear_game( terrain );
173
174 const tripoint map_starting_point( 60, 60, 0 );
175 map &here = get_map();
176 vehicle *veh_ptr = here.add_vehicle( veh_id, map_starting_point, -90_degrees, 0, 0 );
177
178 REQUIRE( veh_ptr != nullptr );
179 if( veh_ptr == nullptr ) {
180 return 0;
181 }
182
183 vehicle &veh = *veh_ptr;
184
185 // Remove all items from cargo to normalize weight.
186 for( const vpart_reference &vp : veh.get_all_parts() ) {
187 veh_ptr->get_items( vp.part_index() ).clear();
188 vp.part().ammo_consume( vp.part().ammo_remaining(), vp.pos() );
189 }
190 for( const vpart_reference &vp : veh.get_avail_parts( "OPENABLE" ) ) {
191 veh.close( vp.part_index() );
192 }
193
194 veh.refresh_insides();
195
196 if( test_mass ) {
197 CHECK( to_gram( veh.total_mass() ) == expected_mass );
198 }
199 expected_mass = to_gram( veh.total_mass() );
200 veh.check_falling_or_floating();
201 REQUIRE( !veh.is_in_water() );
202 const auto &starting_fuel = set_vehicle_fuel( veh, fuel_level );
203 // This is ugly, but improves accuracy: compare the result of fuel approx function
204 // rather than the amount of fuel we actually requested
205 const float starting_fuel_per = fuel_percentage_left( veh, starting_fuel );
206 REQUIRE( std::abs( starting_fuel_per - 1.0f ) < 0.001f );
207
208 const tripoint starting_point = veh.global_pos3();
209 veh.tags.insert( "IN_CONTROL_OVERRIDE" );
210 veh.engine_on = true;
211
212 const int sign = in_reverse ? -1 : 1;
213 const int target_velocity = sign * std::min( 50 * 100, veh.safe_ground_velocity( false ) );
214 veh.cruise_velocity = target_velocity;
215 // If we aren't testing repeated cold starts, start the vehicle at cruising velocity.
216 // Otherwise changing the amount of fuel in the tank perturbs the test results.
217 if( reset_velocity_turn == -1 ) {
218 veh.velocity = target_velocity;
219 }
220 int reset_counter = 0;
221 int tiles_travelled = 0;
222 int cycles_left = cycle_limit;
223 bool accelerating = true;
224 CHECK( veh.safe_velocity() > 0 );
225 while( veh.engine_on && veh.safe_velocity() > 0 && cycles_left > 0 ) {
226 cycles_left--;
227 here.vehmove();
228 veh.idle( true );
229 // If the vehicle starts skidding, the effects become random and test is RUINED
230 REQUIRE( !veh.skidding );
231 for( const tripoint &pos : veh.get_points() ) {
232 REQUIRE( here.ter( pos ) );
233 }
234 // How much it moved
235 tiles_travelled += square_dist( starting_point, veh.global_pos3() );
236 // Bring it back to starting point to prevent it from leaving the map
237 const tripoint displacement = starting_point - veh.global_pos3();
238 here.displace_vehicle( veh, displacement );
239 if( reset_velocity_turn < 0 ) {
240 continue;
241 }
242
243 reset_counter++;
244 if( reset_counter > reset_velocity_turn ) {
245 if( smooth_stops ) {
246 accelerating = !accelerating;
247 veh.cruise_velocity = accelerating ? target_velocity : 0;
248 } else {
249 veh.velocity = 0;
250 veh.last_turn = 0_degrees;
251 veh.of_turn_carry = 0;
252 }
253 reset_counter = 0;
254 }
255 }
256
257 float fuel_left = fuel_percentage_left( veh, starting_fuel );
258 REQUIRE( starting_fuel_per - fuel_left > 0.0001f );
259 const float fuel_percentage_used = fuel_level * ( starting_fuel_per - fuel_left );
260 int adjusted_tiles_travelled = tiles_travelled / fuel_percentage_used;
261 if( target_distance >= 0 ) {
262 CHECK( adjusted_tiles_travelled >= min_dist );
263 CHECK( adjusted_tiles_travelled <= max_dist );
264 }
265
266 return adjusted_tiles_travelled;
267 }
268
find_inner(const std::string & type,int & expected_mass,const std::string & terrain,const int delay,const bool smooth,const bool test_mass=false,const bool in_reverse=false)269 static efficiency_stat find_inner(
270 const std::string &type, int &expected_mass, const std::string &terrain, const int delay,
271 const bool smooth, const bool test_mass = false, const bool in_reverse = false )
272 {
273 efficiency_stat efficiency;
274 for( int i = 0; i < 10; i++ ) {
275 efficiency.add( test_efficiency( vproto_id( type ), expected_mass, ter_id( terrain ),
276 delay, -1, smooth, test_mass, in_reverse ) );
277 }
278 return efficiency;
279 }
280
print_stats(const efficiency_stat & st)281 static void print_stats( const efficiency_stat &st )
282 {
283 if( st.min() == st.max() ) {
284 printf( "All results %d.\n", st.min() );
285 } else {
286 printf( "Min %d, Max %d, Midpoint %f.\n", st.min(), st.max(),
287 ( st.min() + st.max() ) / 2.0 );
288 }
289 }
290
print_efficiency(const std::string & type,int expected_mass,const std::string & terrain,const int delay,const bool smooth)291 static void print_efficiency(
292 const std::string &type, int expected_mass, const std::string &terrain, const int delay,
293 const bool smooth )
294 {
295 printf( "Testing %s on %s with %s: ",
296 type.c_str(), terrain.c_str(), ( delay < 0 ) ? "no resets" : "resets every 5 turns" );
297 print_stats( find_inner( type, expected_mass, terrain, delay, smooth ) );
298 }
299
find_efficiency(const std::string & type)300 static void find_efficiency( const std::string &type )
301 {
302 SECTION( "finding efficiency of " + type ) {
303 print_efficiency( type, 0, "t_pavement", -1, false );
304 print_efficiency( type, 0, "t_dirt", -1, false );
305 print_efficiency( type, 0, "t_pavement", 5, false );
306 print_efficiency( type, 0, "t_dirt", 5, false );
307 }
308 }
309
average_from_stat(const efficiency_stat & st)310 static int average_from_stat( const efficiency_stat &st )
311 {
312 const int ugly_integer = ( st.min() + st.max() ) / 2.0;
313 // Round to 4 most significant places
314 const int magnitude = std::max<int>( 0, std::floor( std::log10( ugly_integer ) ) );
315 const int precision = std::max<int>( 1, std::round( std::pow( 10.0, magnitude - 3 ) ) );
316 return ugly_integer - ugly_integer % precision;
317 }
318
319 // Behold: power of laziness
print_test_strings(const std::string & type,const bool in_reverse=false)320 static int print_test_strings( const std::string &type, const bool in_reverse = false )
321 {
322 std::ostringstream ss;
323 int expected_mass = 0;
324 ss << " test_vehicle( \"" << type << "\", ";
325 const int d_pave = average_from_stat( find_inner( type, expected_mass, "t_pavement", -1,
326 false, false, in_reverse ) );
327 ss << expected_mass << ", " << d_pave << ", ";
328 ss << average_from_stat( find_inner( type, expected_mass, "t_dirt", -1,
329 false, false, in_reverse ) ) << ", ";
330 ss << average_from_stat( find_inner( type, expected_mass, "t_pavement", 5,
331 false, false, in_reverse ) ) << ", ";
332 ss << average_from_stat( find_inner( type, expected_mass, "t_dirt", 5,
333 false, false, in_reverse ) );
334 //ss << average_from_stat( find_inner( type, "t_pavement", 5, true ) ) << ", ";
335 //ss << average_from_stat( find_inner( type, "t_dirt", 5, true ) );
336 if( in_reverse ) {
337 ss << ", 0, 0, true";
338 }
339 ss << " );" << std::endl;
340 printf( "%s", ss.str().c_str() );
341 fflush( stdout );
342 return d_pave;
343 }
344
test_vehicle(const std::string & type,int expected_mass,const int pavement_target,const int dirt_target,const int pavement_target_w_stops,const int dirt_target_w_stops,const int pavement_target_smooth_stops=0,const int dirt_target_smooth_stops=0,const bool in_reverse=false)345 static void test_vehicle(
346 const std::string &type, int expected_mass,
347 const int pavement_target, const int dirt_target,
348 const int pavement_target_w_stops, const int dirt_target_w_stops,
349 const int pavement_target_smooth_stops = 0, const int dirt_target_smooth_stops = 0,
350 const bool in_reverse = false )
351 {
352 SECTION( type + " on pavement" ) {
353 test_efficiency( vproto_id( type ), expected_mass, ter_id( "t_pavement" ), -1,
354 pavement_target, false, true, in_reverse );
355 }
356 SECTION( type + " on dirt" ) {
357 test_efficiency( vproto_id( type ), expected_mass, ter_id( "t_dirt" ), -1,
358 dirt_target, false, true, in_reverse );
359 }
360 SECTION( type + " on pavement, full stop every 5 turns" ) {
361 test_efficiency( vproto_id( type ), expected_mass, ter_id( "t_pavement" ), 5,
362 pavement_target_w_stops, false, true, in_reverse );
363 }
364 SECTION( type + " on dirt, full stop every 5 turns" ) {
365 test_efficiency( vproto_id( type ), expected_mass, ter_id( "t_dirt" ), 5,
366 dirt_target_w_stops, false, true, in_reverse );
367 }
368 if( pavement_target_smooth_stops > 0 ) {
369 SECTION( type + " on pavement, alternating 5 turns of acceleration and 5 turns of decceleration" ) {
370 test_efficiency( vproto_id( type ), expected_mass, ter_id( "t_pavement" ), 5,
371 pavement_target_smooth_stops, true, true, in_reverse );
372 }
373 }
374 if( dirt_target_smooth_stops > 0 ) {
375 SECTION( type + " on dirt, alternating 5 turns of acceleration and 5 turns of decceleration" ) {
376 test_efficiency( vproto_id( type ), expected_mass, ter_id( "t_dirt" ), 5,
377 dirt_target_smooth_stops, true, true, in_reverse );
378 }
379 }
380 }
381
382 static std::vector<std::string> vehs_to_test = {{
383 "beetle",
384 "car",
385 "car_sports",
386 "electric_car",
387 "suv",
388 "motorcycle",
389 "quad_bike",
390 "scooter",
391 "superbike",
392 "ambulance",
393 "fire_engine",
394 "fire_truck",
395 "truck_swat",
396 "tractor_plow",
397 "apc",
398 "humvee",
399 "road_roller",
400 "golf_cart"
401 }
402 };
403
404 /** This isn't a test per se, it executes this code to
405 * determine the current state of vehicle efficiency.
406 **/
407 TEST_CASE( "vehicle_find_efficiency", "[.]" )
408 {
409 for( const std::string &veh : vehs_to_test ) {
410 find_efficiency( veh );
411 }
412 }
413
414 /** This is even less of a test. It generates C++ lines for the actual test below */
415 TEST_CASE( "make_vehicle_efficiency_case", "[.]" )
416 {
417 const float acceptable = 1.25;
418 std::map<std::string, int> forward_distance;
419 for( const std::string &veh : vehs_to_test ) {
420 const int in_forward = print_test_strings( veh );
421 forward_distance[ veh ] = in_forward;
422 }
423 printf( "// in reverse\n" );
424 for( const std::string &veh : vehs_to_test ) {
425 const int in_reverse = print_test_strings( veh, true );
426 CHECK( in_reverse < ( acceptable * forward_distance[ veh ] ) );
427 }
428 }
429
430 // TODO:
431 // Amount of fuel needed to reach safe speed.
432 // Amount of cruising range for a fixed amount of fuel.
433 // Fix test for electric vehicles
434 TEST_CASE( "vehicle_efficiency", "[vehicle] [engine]" )
435 {
436 test_vehicle( "beetle", 816469, 431300, 338700, 95610, 68060 );
437 test_vehicle( "car", 1120618, 617500, 388600, 52730, 25170 );
438 test_vehicle( "car_sports", 1155014, 352600, 267600, 36820, 22360 );
439 test_vehicle( "electric_car", 1047135, 355300, 201600, 22400, 10780 );
440 test_vehicle( "suv", 1320286, 1163000, 630000, 85540, 31840 );
441 test_vehicle( "motorcycle", 162585, 120300, 100900, 63320, 51130 );
442 test_vehicle( "quad_bike", 264845, 116100, 116100, 46770, 46770 );
443 test_vehicle( "scooter", 55441, 235900, 235900, 174700, 174700 );
444 test_vehicle( "superbike", 241585, 109800, 65300, 41990, 24140 );
445 test_vehicle( "ambulance", 1850228, 623000, 511100, 77700, 57910 );
446 test_vehicle( "fire_engine", 2606611, 1895000, 1585000, 337800, 261900 );
447 test_vehicle( "fire_truck", 6441903, 420800, 80000, 19080, 4063 );
448 test_vehicle( "truck_swat", 5994144, 682900, 130200, 29610, 7604 );
449 test_vehicle( "tractor_plow", 723658, 681200, 681200, 132700, 132700 );
450 test_vehicle( "apc", 5802483, 1626000, 1119000, 130800, 85590 );
451 test_vehicle( "humvee", 5503345, 767900, 306900, 25620, 9171 );
452 test_vehicle( "road_roller", 8831620, 602500, 147100, 22760, 6925 );
453 test_vehicle( "golf_cart", 444630, 96000, 69390, 35490, 14200 );
454 // in reverse
455 test_vehicle( "beetle", 816469, 58970, 58870, 44560, 43060, 0, 0, true );
456 test_vehicle( "car", 1120618, 76060, 76060, 44230, 24920, 0, 0, true );
457 test_vehicle( "car_sports", 1155014, 353200, 268000, 35220, 19540, 0, 0, true );
458 test_vehicle( "electric_car", 1047135, 356400, 202300, 22450, 10810, 0, 0, true );
459 test_vehicle( "suv", 1320286, 112000, 111700, 66880, 31640, 0, 0, true );
460 test_vehicle( "motorcycle", 162585, 20070, 19030, 15490, 14890, 0, 0, true );
461 test_vehicle( "quad_bike", 264845, 19650, 19650, 15440, 15440, 0, 0, true );
462 test_vehicle( "scooter", 55441, 58790, 58790, 46320, 46320, 0, 0, true );
463 test_vehicle( "superbike", 241585, 18380, 10570, 13100, 8497, 0, 0, true );
464 test_vehicle( "ambulance", 1850228, 58460, 57740, 42480, 39100, 0, 0, true );
465 test_vehicle( "fire_engine", 2606611, 258000, 257800, 185600, 179400, 0, 0, true );
466 test_vehicle( "fire_truck", 6441903, 58760, 59170, 18580, 3447, 0, 0, true );
467 test_vehicle( "truck_swat", 5994144, 129300, 130100, 29350, 7668, 0, 0, true );
468 test_vehicle( "tractor_plow", 723658, 72490, 72490, 53700, 53700, 0, 0, true );
469 test_vehicle( "apc", 5802483, 381500, 382100, 123600, 82000, 0, 0, true );
470 test_vehicle( "humvee", 5503345, 89940, 89940, 25780, 9086, 0, 0, true );
471 test_vehicle( "road_roller", 8831620, 97490, 97690, 22880, 6606, 0, 0, true );
472 test_vehicle( "golf_cart", 444630, 96150, 28800, 35560, 11150, 0, 0, true );
473 }
474