Skip to content

Commit

Permalink
Merge pull request #44258 from wapcaplet/w-best-test
Browse files Browse the repository at this point in the history
Add tests for better_pocket and best_pocket
  • Loading branch information
kevingranade authored Oct 4, 2020
2 parents cc0ff8c + dd9fc41 commit ef5a06d
Show file tree
Hide file tree
Showing 4 changed files with 325 additions and 4 deletions.
126 changes: 125 additions & 1 deletion data/mods/TEST_DATA/items.json
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,93 @@
"use_action": { "type": "holster", "holster_prompt": "Store tool or blade", "holster_msg": "You put your %1$s in your %2$s" },
"flags": [ "WAIST", "NO_QUICKDRAW", "WATER_FRIENDLY" ]
},
{
"id": "test_utility_belt",
"type": "ARMOR",
"name": { "str": "utility belt" },
"description": "Holy pockets, Batman! This belt has all kinds of pockets for testing.",
"weight": "3 kg",
"volume": "4 L",
"price": 25000,
"price_postapoc": 9900,
"bashing": 5,
"material": [ "kevlar" ],
"symbol": "[",
"looks_like": "holster",
"color": "yellow",
"covers": [ "TORSO" ],
"coverage": 30,
"encumbrance": 5,
"material_thickness": 4,
"pocket_data": [
{
"//": "Small pouch for a pocket knife, screwdriver, or multi-tool. Just long enough for a test screwdriver, but not long enough for a sonic screwdriver.",
"holster": true,
"rigid": true,
"min_item_volume": "10 ml",
"max_contains_volume": "100 ml",
"max_contains_weight": "1000 g",
"max_item_length": "15 cm",
"moves": 50,
"flag_restriction": [ "BELT_CLIP", "SHEATH_KNIFE" ]
},
{
"//": "Large holster for an axe or Halligan bar",
"holster": true,
"rigid": false,
"min_item_volume": "100 ml",
"max_contains_volume": "2000 ml",
"max_contains_weight": "4000 g",
"max_item_length": "80 cm",
"moves": 200,
"flag_restriction": [ "BELT_CLIP", "SHEATH_KNIFE" ]
},
{
"//": "Flexible water pouch for drinks or other liquid. Integrated straw for fast access.",
"watertight": true,
"rigid": false,
"max_item_volume": "1 ml",
"max_contains_volume": "1 L",
"max_contains_weight": "2 kg",
"moves": 10
},
{
"//": "Pressure-tight canister for emergency oxygen supply or other gas.",
"airtight": true,
"rigid": true,
"max_item_volume": "1 ml",
"max_contains_volume": "1 L",
"max_contains_weight": "1 kg",
"moves": 100
},
{
"//": "Fire-proof pouch for thermite or other combustible materials.",
"rigid": false,
"fire_protection": true,
"max_contains_volume": "500 ml",
"max_contains_weight": "1 kg",
"moves": 150
},
{
"//": "Micro cooler to preserve snacks for long stakeouts.",
"rigid": true,
"spoil_multiplier": 0.5,
"max_contains_volume": "500 ml",
"max_contains_weight": "1 kg",
"moves": 150
},
{
"//": "Cryo-chamber to deep-freeze organic samples. Small, but prevents spoiling indefinitely.",
"rigid": true,
"spoil_multiplier": 0.0,
"max_contains_volume": "100 ml",
"max_contains_weight": "100 g",
"moves": 1000
}
],
"use_action": { "type": "holster", "holster_prompt": "Store tool or blade", "holster_msg": "You put your %1$s in your %2$s" },
"flags": [ "WAIST", "NO_QUICKDRAW", "WATER_FRIENDLY" ]
},
{
"type": "GENERIC",
"id": "test_sheet_metal",
Expand Down Expand Up @@ -983,10 +1070,47 @@
"holster": true,
"max_contains_volume": "20 L",
"max_contains_weight": "20 kg",
"item_restriction": [ "glockmag", "glockbigmag" ]
"item_restriction": [ "test_glockmag", "test_glockbigmag" ]
}
]
},
{
"id": "test_glockmag",
"looks_like": "glock17_17",
"type": "MAGAZINE",
"name": { "str": "Test Glock magazine" },
"description": "Magazine exclusively designed for Test Glocks.",
"weight": "105 g",
"volume": "250 ml",
"price": 2900,
"price_postapoc": 100,
"material": [ "plastic" ],
"symbol": "#",
"color": "light_gray",
"ammo_type": [ "9mm" ],
"capacity": 15,
"flags": [ "MAG_COMPACT" ],
"pocket_data": [ { "pocket_type": "MAGAZINE", "rigid": true, "ammo_restriction": { "9mm": 15 } } ]
},
{
"id": "test_glockbigmag",
"looks_like": "glock17_17",
"type": "MAGAZINE",
"name": { "str": "Test Glock extended magazine" },
"description": "30-round magazine exclusively for use with the Test Glock.",
"weight": "210 g",
"volume": "500 ml",
"price": 4700,
"price_postapoc": 500,
"material": [ "plastic" ],
"symbol": "#",
"color": "light_gray",
"ammo_type": [ "9mm" ],
"capacity": 30,
"reload_time": 140,
"flags": [ "MAG_COMPACT" ],
"pocket_data": [ { "pocket_type": "MAGAZINE", "rigid": true, "ammo_restriction": { "9mm": 30 } } ]
},
{
"id": "test_9mm_ammo",
"type": "AMMO",
Expand Down
8 changes: 6 additions & 2 deletions src/item_contents.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -493,10 +493,14 @@ std::pair<item_location, item_pocket *> item_contents::best_pocket( const item &
// that needs to be something a player explicitly does
continue;
}
if( ( ret.second == nullptr && pocket.can_contain( it ).success() ) ||
( pocket.can_contain( it ).success() && ret.second->better_pocket( pocket, it ) ) ) {
if( !pocket.can_contain( it ).success() ) {
continue;
}
if( ret.second == nullptr || ret.second->better_pocket( pocket, it ) ) {
// this pocket is the new candidate for "best"
ret.first = parent;
ret.second = &pocket;
// check all pockets within to see if they are better
for( item *contained : all_items_top( item_pocket::pocket_type::CONTAINER ) ) {
std::pair<item_location, item_pocket *> internal_pocket =
contained->contents.best_pocket( it, parent, true );
Expand Down
2 changes: 1 addition & 1 deletion tests/iteminfo_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1426,7 +1426,7 @@ TEST_CASE( "gun or other ranged weapon attributes", "[iteminfo][weapon][gun]" )
CHECK( item_info_str( glock, allowed_mags ) ==
"--\n"
"<color_c_white>Compatible magazines</color>:"
" Glock extended magazine and Glock magazine\n" );
" Test Glock extended magazine and Test Glock magazine\n" );

// Rag does not have integral or compatible magazines
REQUIRE_FALSE( rag.magazine_integral() );
Expand Down
193 changes: 193 additions & 0 deletions tests/pocket_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "enums.h"
#include "item.h"
#include "item_pocket.h"
#include "itype.h"
#include "optional.h"
#include "ret_val.h"
#include "type_id.h"
Expand Down Expand Up @@ -990,3 +991,195 @@ TEST_CASE( "sealed containers", "[pocket][seal]" )
}
}

// Better pockets
// --------------
// Pockets are ranked according to item_pocket::better_pocket, which considers player-defined
// favorites, ammo restrictions, spoilage, watertightness, available volume and other factors.
//
// Functions:
// item_pocket::better_pocket
//
TEST_CASE( "when one pocket is better than another", "[pocket][better]" )
{
// TODO:
// settings.is_better_favorite() is top priority
// settings.priority() takes next priority
// has_item_stacks_with(it) is better than one that doesn't stack
// pockets restricted by ammo should try to get filled first
// pockets restricted by flag should try to get filled first
// if remaining volume is equal, lower obtain_cost is better

// A and B: Two generic sets of pocket data for comparison
pocket_data data_a( item_pocket::pocket_type::CONTAINER );
pocket_data data_b( item_pocket::pocket_type::CONTAINER );

// Candidate items to compare pockets with
item liquid( "test_liquid" );
item apple( "test_apple" );
item rock( "test_rock" );

SECTION( "for perishable food, lower spoil_multiplier is better" ) {
REQUIRE( apple.is_comestible() );
REQUIRE( apple.get_comestible()->spoils != 0_seconds );
data_a.spoil_multiplier = 1.0;
data_b.spoil_multiplier = 0.5;
CHECK( item_pocket( &data_a ).better_pocket( item_pocket( &data_b ), apple ) );
}

SECTION( "for solid item, non-watertight pocket is better" ) {
REQUIRE( rock.made_of( phase_id::SOLID ) );
data_a.watertight = true;
data_b.watertight = false;
CHECK( item_pocket( &data_a ).better_pocket( item_pocket( &data_b ), rock ) );
}

SECTION( "rigid pockets are preferable to non-rigid ones" ) {
data_a.rigid = false;
data_b.rigid = true;
CHECK( item_pocket( &data_a ).better_pocket( item_pocket( &data_b ), rock ) );
}

SECTION( "pocket with less remaining volume is better" ) {
data_a.volume_capacity = 2_liter;
data_b.volume_capacity = 1_liter;
CHECK( item_pocket( &data_a ).better_pocket( item_pocket( &data_b ), rock ) );
}
}

// Best pocket
// -----------
// The "best pocket" for an item is the most appropriate pocket for containing it. Only CONTAINER
// type pockets are eligible; other pocket types such as MAGAZINE or MOD cannot be "best".
//
// Functions:
// item::best_pocket
// item_contents::best_pocket
//
static bool has_best_pocket( item &container, const item &thing )
{
item_location loc;
return container.best_pocket( thing, loc ).second != nullptr;
}

TEST_CASE( "best pocket in item contents", "[pocket][item][best]" )
{
item_location loc;

// Waterskins can be best pockets for liquids
SECTION( "item with one watertight pocket has best_pocket for liquid" ) {
// Must have a CONTAINER pocket, first and foremost
item skin( "test_waterskin" );
REQUIRE( skin.has_pockets() );
REQUIRE( skin.contents.has_pocket_type( item_pocket::pocket_type::CONTAINER ) );
// Prerequisite: It can contain water
item liquid( "test_liquid" );
REQUIRE( skin.can_contain( liquid ) );

// Has a best pocket for liquid
CHECK( has_best_pocket( skin, liquid ) );
}

// Test utility belt has pockets best for holding small and large tools, liquids and gases
SECTION( "item with many different pockets can have best_pocket for different items" ) {
// Utility belt has CONTAINER pockets
item util_belt( "test_utility_belt" );
REQUIRE( util_belt.has_pockets() );
REQUIRE( util_belt.contents.has_pocket_type( item_pocket::pocket_type::CONTAINER ) );
// It can contain small and large tools
item screwdriver( "test_screwdriver" );
item halligan( "test_halligan" );
REQUIRE( util_belt.can_contain( screwdriver ) );
REQUIRE( util_belt.can_contain( halligan ) );
// It can contain liquid and gas
item liquid( "test_liquid" );
item gas( "test_gas", 0, item::default_charges_tag{} );
REQUIRE( util_belt.can_contain( liquid ) );
REQUIRE( util_belt.can_contain( gas ) );

// Utility belt has best_pocket for all these things
CHECK( has_best_pocket( util_belt, screwdriver ) );
CHECK( has_best_pocket( util_belt, halligan ) );
CHECK( has_best_pocket( util_belt, liquid ) );
CHECK( has_best_pocket( util_belt, gas ) );
}

// Because best_pocket only works for CONTAINER pockets, a gun item with a MAGAZINE_WELL pocket
// is not best_pocket for a compatible magazine, and a mag item with a MAGAZINE pocket is not
// best_pocket for compatible ammos.
SECTION( "non-container pockets cannot be best_pocket" ) {
// Gun that accepts magazines
item glock( "test_glock" );
REQUIRE( glock.contents.has_pocket_type( item_pocket::pocket_type::MAGAZINE_WELL ) );
// Empty magazine
item glockmag( "test_glockmag", calendar::turn, 0 );
REQUIRE( glockmag.contents.has_pocket_type( item_pocket::pocket_type::MAGAZINE ) );
REQUIRE( glockmag.ammo_remaining() == 0 );
// A single 9mm bullet
item glockammo( "9mm", calendar::turn, 1 );
REQUIRE( glockammo.is_ammo() );
REQUIRE( glockammo.charges == 1 );

// Although gun can contain magazine, and magazine can contain bullet...
REQUIRE( glock.can_contain( glockmag ) );
REQUIRE( glockmag.can_contain( glockammo ) );
// Gun is not best_pocket for magazine, and magazine is not best_pocket for bullet.
CHECK_FALSE( has_best_pocket( glock, glockmag ) );
CHECK_FALSE( has_best_pocket( glockmag, glockammo ) );
}

// sealable pockets may be filled and sealed, to spawn with a "factory seal" in-game.
// While sealed, they clearly cannot be the best pocket for storing anything.
SECTION( "sealed pockets cannot be best_pocket" ) {
// Regular aluminum beverage can and something to fill it with
item can( "test_can_drink" );
REQUIRE( can.has_pockets() );
REQUIRE( can.contents.has_pocket_type( item_pocket::pocket_type::CONTAINER ) );
item liquid( "test_liquid" );
REQUIRE( can.can_contain( liquid ) );

// Before being sealed, it can be best pocket for liquid
CHECK( has_best_pocket( can, liquid ) );
// Fill with liquid, seal it, and ensure success
can.put_in( liquid, item_pocket::pocket_type::CONTAINER );
REQUIRE( can.seal() ); // This must succeed, or next assertion is meaningless
// Now sealed, the can cannot be best_pocket for liquid
CHECK_FALSE( has_best_pocket( can, liquid ) );
}
}

// Character::best_pocket
// - See if wielded item can hold it - start with this as default
// - For each worn item, see if best_pocket is better; if so, use it
// + Return the item_location of the item that has the best pocket
//
// What is the best pocket to put @it into? the pockets in @avoid do not count
// Character::best_pocket( it, avoid )
// NOTE: different syntax than item_contents::best_pocket
// (Second argument is `avoid` item pointer, not parent item location)
TEST_CASE( "character best pocket", "[pocket][character][best]" )
{
// TODO:
// Includes wielded item
// Includes worn items
// Uses best of multiple worn items
// Skips avoided items
}

TEST_CASE( "guns and gunmods", "[pocket][gunmod]" )
{
item m4a1( "m4a1" );
item strap( "shoulder_strap" );
// Guns cannot "contain" gunmods, but gunmods can be inserted into guns
CHECK_FALSE( m4a1.contents.can_contain( strap ).success() );
CHECK( m4a1.contents.insert_item( strap, item_pocket::pocket_type::MOD ).success() );
}

TEST_CASE( "usb drives and software", "[pocket][software]" )
{
item usb( "usb_drive" );
item software( "software_math" );
// USB drives aren't containers, and cannot "contain" software, but software can be inserted
CHECK_FALSE( usb.contents.can_contain( software ).success() );
CHECK( usb.contents.insert_item( software, item_pocket::pocket_type::SOFTWARE ).success() );
}

0 comments on commit ef5a06d

Please sign in to comment.