From 5005fb20c65dd7802866293400644d2ce7e75a7e Mon Sep 17 00:00:00 2001 From: Cruix Date: Thu, 15 Feb 2018 12:34:08 -0800 Subject: [PATCH] Added chemical reaction unit tests --- code/modules/reagents/chemistry/holder.dm | 147 +++++++++++------- code/modules/reagents/chemistry/recipes.dm | 2 +- code/modules/unit_tests/_unit_tests.dm | 10 ++ code/modules/unit_tests/reagent_id_typos.dm | 14 ++ .../unit_tests/reagent_recipe_collisions.dm | 67 ++++++++ 5 files changed, 179 insertions(+), 61 deletions(-) create mode 100644 code/modules/unit_tests/reagent_id_typos.dm create mode 100644 code/modules/unit_tests/reagent_recipe_collisions.dm diff --git a/code/modules/reagents/chemistry/holder.dm b/code/modules/reagents/chemistry/holder.dm index 71c7f9fa0d..ff16d62609 100644 --- a/code/modules/reagents/chemistry/holder.dm +++ b/code/modules/reagents/chemistry/holder.dm @@ -1,4 +1,45 @@ +/proc/build_chemical_reagent_list() + //Chemical Reagents - Initialises all /datum/reagent into a list indexed by reagent id + + if(GLOB.chemical_reagents_list) + return + + var/paths = subtypesof(/datum/reagent) + GLOB.chemical_reagents_list = list() + + for(var/path in paths) + var/datum/reagent/D = new path() + GLOB.chemical_reagents_list[D.id] = D + +/proc/build_chemical_reactions_list() + //Chemical Reactions - Initialises all /datum/chemical_reaction into a list + // It is filtered into multiple lists within a list. + // For example: + // chemical_reaction_list["plasma"] is a list of all reactions relating to plasma + + if(GLOB.chemical_reactions_list) + return + + var/paths = subtypesof(/datum/chemical_reaction) + GLOB.chemical_reactions_list = list() + + for(var/path in paths) + + var/datum/chemical_reaction/D = new path() + var/list/reaction_ids = list() + + if(D.required_reagents && D.required_reagents.len) + for(var/reaction in D.required_reagents) + reaction_ids += reaction + + // Create filters based on each reagent id in the required reagents list + for(var/id in reaction_ids) + if(!GLOB.chemical_reactions_list[id]) + GLOB.chemical_reactions_list[id] = list() + GLOB.chemical_reactions_list[id] += D + break // Don't bother adding ourselves to other reagent ids, it is redundant +. /////////////////////////////////////////////////////////////////////////////////// /datum/reagents @@ -15,39 +56,11 @@ /datum/reagents/New(maximum=100) maximum_volume = maximum - //I dislike having these here but map-objects are initialised before world/New() is called. >_> if(!GLOB.chemical_reagents_list) - //Chemical Reagents - Initialises all /datum/reagent into a list indexed by reagent id - var/paths = subtypesof(/datum/reagent) - GLOB.chemical_reagents_list = list() - for(var/path in paths) - var/datum/reagent/D = new path() - GLOB.chemical_reagents_list[D.id] = D + build_chemical_reagent_list() if(!GLOB.chemical_reactions_list) - //Chemical Reactions - Initialises all /datum/chemical_reaction into a list - // It is filtered into multiple lists within a list. - // For example: - // chemical_reaction_list["plasma"] is a list of all reactions relating to plasma - - var/paths = subtypesof(/datum/chemical_reaction) - GLOB.chemical_reactions_list = list() - - for(var/path in paths) - - var/datum/chemical_reaction/D = new path() - var/list/reaction_ids = list() - - if(D.required_reagents && D.required_reagents.len) - for(var/reaction in D.required_reagents) - reaction_ids += reaction - - // Create filters based on each reagent id in the required reagents list - for(var/id in reaction_ids) - if(!GLOB.chemical_reactions_list[id]) - GLOB.chemical_reactions_list[id] = list() - GLOB.chemical_reactions_list[id] += D - break // Don't bother adding ourselves to other reagent ids, it is redundant. + build_chemical_reactions_list() /datum/reagents/Destroy() . = ..() @@ -326,6 +339,7 @@ var/reaction_occurred = 0 do + var/list/possible_reactions = list() reaction_occurred = 0 for(var/reagent in cached_reagents) var/datum/reagent/R = reagent @@ -342,18 +356,14 @@ var/total_matching_catalysts= 0 var/matching_container = 0 var/matching_other = 0 - var/list/multipliers = new/list() var/required_temp = C.required_temp var/is_cold_recipe = C.is_cold_recipe var/meets_temp_requirement = 0 - var/list/cached_results = C.results - for(var/B in cached_required_reagents) if(!has_reagent(B, cached_required_reagents[B])) break total_matching_reagents++ - multipliers += round(get_reagent_amount(B) / cached_required_reagents[B]) for(var/B in cached_required_catalysts) if(!has_reagent(B, cached_required_catalysts[B])) break @@ -385,37 +395,54 @@ meets_temp_requirement = 1 if(total_matching_reagents == total_required_reagents && total_matching_catalysts == total_required_catalysts && matching_container && matching_other && meets_temp_requirement) - var/multiplier = min(multipliers) - for(var/B in cached_required_reagents) - remove_reagent(B, (multiplier * cached_required_reagents[B]), safety = 1) + possible_reactions += C - for(var/P in C.results) - multiplier = max(multiplier, 1) //this shouldnt happen ... - SSblackbox.record_feedback("tally", "chemical_reaction", cached_results[P]*multiplier, P) - add_reagent(P, cached_results[P]*multiplier, null, chem_temp) + if(possible_reactions.len) + var/datum/chemical_reaction/selected_reaction = possible_reactions[1] + //select the reaction with the most extreme temperature requirements + for(var/V in possible_reactions) + var/datum/chemical_reaction/competitor = V + if(selected_reaction.is_cold_recipe) //if there are no recipe conflicts, everything in possible_reactions will have this same value for is_cold_reaction. warranty void if assumption not met. + if(competitor.required_temp < selected_reaction.required_temp) + selected_reaction = competitor + else + if(competitor.required_temp > selected_reaction.required_temp) + selected_reaction = competitor + var/list/cached_required_reagents = selected_reaction.required_reagents + var/list/cached_results = selected_reaction.results + var/list/multiplier = INFINITY + for(var/B in cached_required_reagents) + multiplier = min(multiplier, round(get_reagent_amount(B) / cached_required_reagents[B])) - var/list/seen = viewers(4, get_turf(my_atom)) - var/iconhtml = icon2html(cached_my_atom, seen) - if(cached_my_atom) - if(!ismob(cached_my_atom)) // No bubbling mobs - if(C.mix_sound) - playsound(get_turf(cached_my_atom), C.mix_sound, 80, 1) + for(var/B in cached_required_reagents) + remove_reagent(B, (multiplier * cached_required_reagents[B]), safety = 1) - for(var/mob/M in seen) - to_chat(M, "[iconhtml] [C.mix_message]") + for(var/P in selected_reaction.results) + multiplier = max(multiplier, 1) //this shouldnt happen ... + SSblackbox.record_feedback("tally", "chemical_reaction", cached_results[P]*multiplier, P) + add_reagent(P, cached_results[P]*multiplier, null, chem_temp) - if(istype(cached_my_atom, /obj/item/slime_extract)) - var/obj/item/slime_extract/ME2 = my_atom - ME2.Uses-- - if(ME2.Uses <= 0) // give the notification that the slime core is dead - for(var/mob/M in seen) - to_chat(M, "[iconhtml] \The [my_atom]'s power is consumed in the reaction.") - ME2.name = "used slime extract" - ME2.desc = "This extract has been used up." + var/list/seen = viewers(4, get_turf(my_atom)) + var/iconhtml = icon2html(cached_my_atom, seen) + if(cached_my_atom) + if(!ismob(cached_my_atom)) // No bubbling mobs + if(selected_reaction.mix_sound) + playsound(get_turf(cached_my_atom), selected_reaction.mix_sound, 80, 1) - C.on_reaction(src, multiplier) - reaction_occurred = 1 - break + for(var/mob/M in seen) + to_chat(M, "[iconhtml] [selected_reaction.mix_message]") + + if(istype(cached_my_atom, /obj/item/slime_extract)) + var/obj/item/slime_extract/ME2 = my_atom + ME2.Uses-- + if(ME2.Uses <= 0) // give the notification that the slime core is dead + for(var/mob/M in seen) + to_chat(M, "[iconhtml] \The [my_atom]'s power is consumed in the reaction.") + ME2.name = "used slime extract" + ME2.desc = "This extract has been used up." + + selected_reaction.on_reaction(src, multiplier) + reaction_occurred = 1 while(reaction_occurred) update_total() diff --git a/code/modules/reagents/chemistry/recipes.dm b/code/modules/reagents/chemistry/recipes.dm index 9e73f12f10..1a9aeb5597 100644 --- a/code/modules/reagents/chemistry/recipes.dm +++ b/code/modules/reagents/chemistry/recipes.dm @@ -6,7 +6,7 @@ var/list/required_catalysts = new/list() // Both of these variables are mostly going to be used with slime cores - but if you want to, you can use them for other things - var/atom/required_container = null // the container required for the reaction to happen + var/required_container = null // the exact container path required for the reaction to happen var/required_other = 0 // an integer required for the reaction to happen var/secondary = 0 // set to nonzero if secondary reaction diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm index 001952ea65..522e34a10b 100644 --- a/code/modules/unit_tests/_unit_tests.dm +++ b/code/modules/unit_tests/_unit_tests.dm @@ -1,5 +1,15 @@ +<<<<<<< HEAD //include unit test files in this module in this ifdef #ifdef UNIT_TESTS #include "unit_test.dm" #endif +======= +//include unit test files in this module in this ifdef + +#ifdef UNIT_TESTS +#include "unit_test.dm" +#include "reagent_recipe_collisions.dm" +#include "reagent_id_typos.dm" +#endif +>>>>>>> ad7fc74... Added chemical reaction unit tests (#35478) diff --git a/code/modules/unit_tests/reagent_id_typos.dm b/code/modules/unit_tests/reagent_id_typos.dm new file mode 100644 index 0000000000..d6548852fa --- /dev/null +++ b/code/modules/unit_tests/reagent_id_typos.dm @@ -0,0 +1,14 @@ + + +/datum/unit_test/reagent_id_typos + +/datum/unit_test/reagent_id_typos/Run() + build_chemical_reactions_list() + build_chemical_reagent_list() + + for(var/I in GLOB.chemical_reactions_list) + for(var/V in GLOB.chemical_reactions_list[I]) + var/datum/chemical_reaction/R = V + for(var/id in (R.required_reagents + R.required_catalysts)) + if(!GLOB.chemical_reagents_list[id]) + Fail("Unknown chemical id \"[id]\" in recipe [R.type]") diff --git a/code/modules/unit_tests/reagent_recipe_collisions.dm b/code/modules/unit_tests/reagent_recipe_collisions.dm new file mode 100644 index 0000000000..31027c2cd3 --- /dev/null +++ b/code/modules/unit_tests/reagent_recipe_collisions.dm @@ -0,0 +1,67 @@ + + +/datum/unit_test/reagent_recipe_collisions + +/datum/unit_test/reagent_recipe_collisions/Run() + build_chemical_reactions_list() + var/list/reactions = list() + for(var/V in GLOB.chemical_reactions_list) + reactions += GLOB.chemical_reactions_list[V] + for(var/i in 1 to (reactions.len-1)) + for(var/i2 in (i+1) to reactions.len) + var/datum/chemical_reaction/r1 = reactions[i] + var/datum/chemical_reaction/r2 = reactions[i2] + if(recipes_do_conflict(r1, r2)) + Fail("Chemical recipe conflict between [r1.type] and [r2.type]") + +/datum/unit_test/reagent_recipe_collisions/proc/recipes_do_conflict(datum/chemical_reaction/r1, datum/chemical_reaction/r2) + //do the non-list tests first, because they are cheaper + if(r1.required_container != r2.required_container) + return FALSE + if(r1.is_cold_recipe == r2.is_cold_recipe) + if(r1.required_temp != r2.required_temp) + //one reaction requires a more extreme temperature than the other, so there is no conflict + return FALSE + else + var/datum/chemical_reaction/cold_one = r1.is_cold_recipe ? r1 : r2 + var/datum/chemical_reaction/warm_one = r1.is_cold_recipe ? r2 : r1 + if(cold_one.required_temp < warm_one.required_temp) + //the range of temperatures does not overlap, so there is no conflict + return FALSE + + //find the reactions with the shorter and longer required_reagents list + var/datum/chemical_reaction/long_req + var/datum/chemical_reaction/short_req + if(r1.required_reagents.len > r2.required_reagents.len) + long_req = r1 + short_req = r2 + else if(r1.required_reagents.len < r2.required_reagents.len) + long_req = r2 + short_req = r1 + else + //if they are the same length, sort instead by the length of the catalyst list + //this is important if the required_reagents lists are the same + if(r1.required_catalysts.len > r2.required_catalysts.len) + long_req = r1 + short_req = r2 + else + long_req = r2 + short_req = r1 + + + //check if the shorter reaction list is a subset of the longer one + var/list/overlap = r1.required_reagents & r2.required_reagents + if(overlap.len != short_req.required_reagents.len) + //there is at least one reagent in the short list that is not in the long list, so there is no conflict + return FALSE + + //check to see if the shorter reaction's catalyst list is also a subset of the longer reaction's catalyst list + //if the longer reaction's catalyst list is a subset of the shorter ones, that is fine + //if the reaction lists are the same, the short reaction will have the shorter required_catalysts list, so it will register as a conflict + var/list/short_minus_long_catalysts = short_req.required_catalysts - long_req.required_catalysts + if(short_minus_long_catalysts.len) + //there is at least one unique catalyst for the short reaction, so there is no conflict + return FALSE + + //if we got this far, the longer reaction will be impossible to create if the shorter one is earlier in GLOB.chemical_reactions_list, and will require the reagents to be added in a particular order otherwise + return TRUE \ No newline at end of file