File: uncraft_test.cpp

package info (click to toggle)
cataclysm-dda 0.H-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 710,808 kB
  • sloc: cpp: 524,019; python: 11,580; sh: 1,228; makefile: 1,169; xml: 507; javascript: 150; sql: 56; exp: 41; perl: 37
file content (212 lines) | stat: -rw-r--r-- 9,656 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
#include "calendar.h"
#include "cata_catch.h"
#include "character.h"
#include "item.h"
#include "item_location.h"
#include "map.h"
#include "map_helpers.h"
#include "player_helpers.h"
#include "recipe_dictionary.h"

// Tests for disassembling items from an "uncraft" recipe.
//
// Covers the Character::complete_disassemble function, including:
//
// - Uncraft recipe "difficulty" affects component recovery success using character skill
// - Damage to the original item reduces the chance of successful component recovery

static const itype_id itype_cotton_patchwork( "cotton_patchwork" );
static const itype_id itype_debug_backpack( "debug_backpack" );
static const itype_id itype_string_6( "string_6" );
static const itype_id itype_test_knotted_string_ball( "test_knotted_string_ball" );
static const itype_id itype_test_multitool( "test_multitool" );
static const itype_id itype_test_rag_bundle( "test_rag_bundle" );

static time_point midday = calendar::turn_zero + 12_hours;

// Prepare for crafting with storage, tools, and light, and return the player character
static Character &setup_uncraft_character()
{
    Character &they = get_player_character();
    clear_avatar();
    clear_map();
    set_time( midday );
    // Backpack for storage, and multi-tool for qualities
    they.worn.wear_item( they, item( itype_debug_backpack ), false, false );
    they.i_add( item( itype_test_multitool ) );
    return they;
}

// Repeatedly disassemble an item of a given type according to the given recipe, and return
// a tally of all of the yielded item types.
static std::map<itype_id, int> repeat_uncraft( Character &they, const itype_id &dis_itype,
        const recipe &dis_recip, int repeat = 1, int damage = 0 )
{
    // Accumulate all items from recipe disassembly
    std::map<itype_id, int> yield_items;

    // Locations for real item and disassembly item
    item_location it_loc;
    item_location it_dis_loc;

    for( int rep = 0; rep < repeat; ++rep ) {
        // Add another instance of the disassembly item
        item_location it = they.i_add( item( dis_itype ) );
        if( damage > 0 ) {
            it->set_damage( damage );
        }
        it_dis_loc = they.create_in_progress_disassembly( it );

        // Clear away bits
        clear_map();
        // Get lit
        set_time( midday );
        // Disassemble it
        they.complete_disassemble( it_dis_loc, dis_recip );

        // Count how many of each type of item are here
        for( item &it : get_map().i_at( they.pos() ) ) {
            if( yield_items.count( it.typeId() ) == 0 ) {
                yield_items[it.typeId()] = it.count();
            } else {
                yield_items[it.typeId()] += it.count();
            }
        }
    }

    return yield_items;
}

// Return the number of part_itype items yielded by character with the given skill level
// when disassembling whole_itype items the given number of times.
static int uncraft_yield( Character &they, const itype_id whole_itype, const itype_id part_itype,
                          const int difficulty, const int skill_level )
{
    recipe uncraft_recipe = recipe_dictionary::get_uncraft( whole_itype );
    uncraft_recipe.difficulty = difficulty;
    they.set_skill_level( uncraft_recipe.skill_used, skill_level );

    std::map<itype_id, int> yield = repeat_uncraft( they, whole_itype, uncraft_recipe, 1 );
    return yield[part_itype];
}

// When a "recipe" or "uncraft" has a "difficulty" and "skill_used", recovery of components when
// disassembling from that recipe/uncraft may fail, depending on the skill level of the character
// doing disassembly.
//
// For each of the recipe or uncraft "components", two dice rolls are made:
//
//           Difficulty roll:  #dice = difficulty  #sides = 24
//      Character skill roll:  #dice = 4*SKILL+2   #sides = INT+16
//
// For example, if recipe difficulty is 1, character skill is 0, and INT is the normal 8, the
// character is at an advantage in the dice rolls:
//
//           Difficulty roll:  1 d 24  (difficulty# dice, 24 sides)
//      Character skill roll:  2 d 24  (4*0+2 dice, 8+16 sides)
//
// For a more difficult recipe, say difficulty 4, with character still having 0 skill, and 8 INT,
// they are at a disadvantage:
//
//           Difficulty roll:  4 d 24  (difficulty# dice, 24 sides)
//      Character skill roll:  2 d 24  (4*0+2 dice, 8+16 sides)
//
// The character can increase their dice by increasing skill level; each level gives 4 additional
// dice to the character, so if they can increase their skill to 1, then attempt the same recipe,
// they will be at a slight advantage again:
//
//           Difficulty roll:  4 d 24  (difficulty# dice, 24 sides)
//      Character skill roll:  6 d 24  (4*1+2 dice, 8+16 sides)
//
// For each component, if the character skill roll *exceeds* the difficulty roll, recovery succeeds.
// Recipes with 0 difficulty do not require any skill to succeed; all components are recovered.
//
// Recovery may still fail if the original item was damaged; for that, see the [damage] test.
//
TEST_CASE( "uncraft_difficulty_and_character_skill", "[uncraft][difficulty][skill]" )
{
    Character &they = setup_uncraft_character();

    // The knotted string ball yields 1,000 short strings when successfully desconstructed.
    const itype_id decon_it = itype_test_knotted_string_ball;
    const itype_id part_it = itype_string_6;

    std::map<itype_id, int> yield;

    SECTION( "uncraft recipe with difficulty 0" ) {
        // Full yield at any skill level
        CHECK( uncraft_yield( they, decon_it, part_it, 0, 0 ) == 1000 );
        CHECK( uncraft_yield( they, decon_it, part_it, 0, 1 ) == 1000 );
        CHECK( uncraft_yield( they, decon_it, part_it, 0, 2 ) == 1000 );
        CHECK( uncraft_yield( they, decon_it, part_it, 0, 3 ) == 1000 );
        CHECK( uncraft_yield( they, decon_it, part_it, 0, 4 ) == 1000 );
    }

    constexpr int margin = 55;

    SECTION( "uncraft recipe with difficulty 1" ) {
        CHECK( uncraft_yield( they, decon_it, part_it, 1, 0 ) == Approx( 830 ).margin( margin ) );
        CHECK( uncraft_yield( they, decon_it, part_it, 1, 1 ) == Approx( 1000 ).margin( margin ) );
        CHECK( uncraft_yield( they, decon_it, part_it, 1, 2 ) == Approx( 1000 ).margin( margin ) );
        CHECK( uncraft_yield( they, decon_it, part_it, 1, 3 ) == Approx( 1000 ).margin( margin ) );
        CHECK( uncraft_yield( they, decon_it, part_it, 1, 4 ) == Approx( 1000 ).margin( margin ) );
    }

    SECTION( "uncraft recipe with difficulty 5" ) {
        CHECK( uncraft_yield( they, decon_it, part_it, 5, 0 ) == Approx( 20 ).margin( margin ) );
        CHECK( uncraft_yield( they, decon_it, part_it, 5, 1 ) == Approx( 700 ).margin( margin ) );
        CHECK( uncraft_yield( they, decon_it, part_it, 5, 2 ) == Approx( 990 ).margin( margin ) );
        CHECK( uncraft_yield( they, decon_it, part_it, 5, 3 ) == Approx( 1000 ).margin( margin ) );
        CHECK( uncraft_yield( they, decon_it, part_it, 5, 4 ) == Approx( 1000 ).margin( margin ) );
    }

    SECTION( "uncraft recipe with difficulty 10" ) {
        CHECK( uncraft_yield( they, decon_it, part_it, 10, 0 ) == Approx( 0 ).margin( margin ) );
        CHECK( uncraft_yield( they, decon_it, part_it, 10, 1 ) == Approx( 30 ).margin( margin ) );
        CHECK( uncraft_yield( they, decon_it, part_it, 10, 2 ) == Approx( 500 ).margin( margin ) );
        CHECK( uncraft_yield( they, decon_it, part_it, 10, 3 ) == Approx( 930 ).margin( margin ) );
        CHECK( uncraft_yield( they, decon_it, part_it, 10, 4 ) == Approx( 1000 ).margin( margin ) );
    }
}

// Item damage_level (0-5) affects chance of successfully recovering components from disassembly.
TEST_CASE( "uncraft_from_a_damaged_item", "[uncraft][damage]" )
{
    Character &they = setup_uncraft_character();

    recipe uncraft_rags = recipe_dictionary::get_uncraft( itype_test_rag_bundle );
    REQUIRE( uncraft_rags.difficulty == 0 ); // 0 difficulty makes sure it can't fail because of skills

    struct uncraft_test_preset {
        const int damage;       // value to assign as item's damage
        const int damage_level; // expected item damage level
        const int yield;        // expected approximate yield
        const int margin;       // expected approximate yield margin
    };

    // yield is based on pow( 0.8, damage_level ) roll, e.g. level 2 will get 0.8^2 = ~64% yield
    // uncraft repeated 10 times and the bundle yields 100 each = 1000 yield total for intact item
    // margin value of 55 derived from running 1000 iterations of this, add 5 for rng outliers
    const int margin = 60;
    const std::vector<uncraft_test_preset> presets = {
        {    0, 0, 1000,      0 }, // expect full yield from undamaged
        {  999, 1,  800, margin }, // ~80% from damage level 1
        { 1999, 2,  640, margin }, // ~64% from damage level 2
        { 2999, 3,  512, margin }, // ~51% from damage level 3
        { 3999, 4,  410, margin }, // ~41% from damage level 4
        { 4000, 5,  328, margin }, // ~33% from damage level 5 (XX)
    };

    for( const uncraft_test_preset &preset : presets ) {
        CAPTURE( preset.damage, preset.damage_level, preset.yield, preset.margin );

        item test_item( itype_test_rag_bundle, calendar::turn );
        test_item.set_damage( preset.damage );
        CHECK( test_item.damage_level() == preset.damage_level );

        const int yield = repeat_uncraft( they, itype_test_rag_bundle, uncraft_rags, 10, preset.damage )
                          .at( itype_cotton_patchwork );
        CHECK( yield == Approx( preset.yield ).margin( preset.margin ) );
    }
}