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