diff --git a/bonemarketsolver/__main__.py b/bonemarketsolver/__main__.py index 039391e..954ed26 100644 --- a/bonemarketsolver/__main__.py +++ b/bonemarketsolver/__main__.py @@ -6,6 +6,8 @@ from .objects.bonemarketargumentparser import BoneMarketArgumentParser from .objects.enumaction import EnumAction from .objects.listaction import ListAction from .solve import * +from .read_char import * + parser = BoneMarketArgumentParser( prog='Bone Market Solver', @@ -71,7 +73,7 @@ skeleton_parameters = parser.add_argument_group( skeleton_parameters.add_argument( "-s", "--shadowy", type=int, - required=True, + default=Char.SHADOWY.value, help="the effective level of Shadowy used for selling to buyers", dest='shadowy_level' ) diff --git a/bonemarketsolver/challenge_functions.py b/bonemarketsolver/challenge_functions.py new file mode 100644 index 0000000..534e291 --- /dev/null +++ b/bonemarketsolver/challenge_functions.py @@ -0,0 +1,27 @@ +# This is a constant used to calculate difficulty checks. You almost certainly do not need to change this. +DIFFICULTY_SCALER = 0.6 + +def narrow_challenge(difficulty_level: int, stat: int): + offset = 6 - difficulty_level + stat += offset + + if stat > 9: + return 1 + elif stat < 2: + return .1 + else: + return stat/10 + +def broad_challenge(difficulty_level: int, stat: int): + chance = DIFFICULTY_SCALER*stat/difficulty_level * 100 + chance = chance // 1 + chance /= 100 + + return chance + +def mean_outcome(success: int, failure: int, chance: float): + mean_success = success*chance + mean_failure = failure*(1-chance) + combined_mean_outcome = mean_success + mean_failure + + return int(combined_mean_outcome) diff --git a/bonemarketsolver/custom_char.py.template b/bonemarketsolver/custom_char.py.template new file mode 100644 index 0000000..06b993a --- /dev/null +++ b/bonemarketsolver/custom_char.py.template @@ -0,0 +1,32 @@ +from enum import Enum + +class Char(Enum): + """Character stats""" + + SHADOWY = 300 + + DANGEROUS = 300 + + PERSUASIVE = 300 + + WATCHFUL = 300 + + PLAYER_OF_CHESS = 7 + + ARTISAN_OF_RED_SCIENCE = 7 + + GLASSWORK = 7 + + KATALEPTIC_TOXICOLOGY = 7 + + MITHRIDACY = 7 + + MONSTROUS_ANATOMY = 7 + + SHAPELING_ARTS = 7 + + BIZARRE = 15 + + DREADED = 15 + + RESPECTABLE = 15 diff --git a/bonemarketsolver/data/adjustments.py b/bonemarketsolver/data/adjustments.py index 6d59d01..bfc7500 100644 --- a/bonemarketsolver/data/adjustments.py +++ b/bonemarketsolver/data/adjustments.py @@ -5,26 +5,32 @@ from enum import Enum from .costs import Cost from ..objects.action import Action +from ..read_char import Char +from ..challenge_functions import narrow_challenge, mean_outcome + class Adjustment(Enum): """An action that is taken after all parts have been added to a skeleton.""" CARVE_AWAY_AGE = Action( "Carve away some evidence of age", - cost = Cost.ACTION.value, - antiquity = -2 + cost = Cost.ACTION.value / narrow_challenge(6, Char.MITHRIDACY.value), + antiquity = -2, + implausibility = mean_outcome(0, 2, narrow_challenge(6, Char.MITHRIDACY.value)) ) DISGUISE_AMALGAMY = Action( "Disguise the amalgamy of this piece", - cost = Cost.ACTION.value + 25*Cost.JADE_FRAGMENT.value, - amalgamy = -2 + cost = 25*Cost.JADE_FRAGMENT.value + Cost.ACTION.value / narrow_challenge(6, Char.KATALEPTIC_TOXICOLOGY.value), + amalgamy = -2, + implausibility = mean_outcome(0, 2, narrow_challenge(6, Char.KATALEPTIC_TOXICOLOGY.value)) ) MAKE_LESS_DREADFUL = Action( "Make your skeleton less dreadful", - cost = Cost.ACTION.value, - menace = -2 + cost = Cost.ACTION.value / narrow_challenge(6, Char.KATALEPTIC_TOXICOLOGY.value), + menace = -2, + implausibility = mean_outcome(0, 2, narrow_challenge(6, Char.KATALEPTIC_TOXICOLOGY.value)) ) def __str__(self): diff --git a/bonemarketsolver/data/appendages.py b/bonemarketsolver/data/appendages.py index f235413..4c29475 100644 --- a/bonemarketsolver/data/appendages.py +++ b/bonemarketsolver/data/appendages.py @@ -5,6 +5,9 @@ from enum import Enum from .costs import Cost from ..objects.action import Action +from ..read_char import Char +from ..challenge_functions import narrow_challenge, broad_challenge, mean_outcome + class Appendage(Enum): """An action that is taken once all skulls are added to a skeleton.""" @@ -17,6 +20,7 @@ class Appendage(Enum): amalgamy = 2 ) + # TODO: Difficulty is increased by 2 for each Fin or Tentacle on the skeleton ALBATROSS_WING = Action( "Put an Albatross Wing on your (Skeleton Type)", cost = Cost.ACTION.value + Cost.ALBATROSS_WING.value, @@ -26,6 +30,7 @@ class Appendage(Enum): amalgamy = 1 ) + # TODO: Difficulty is increased by 2 for each Arm, Leg, Wing, and Tentacle that is already attached AMBER_FIN = Action( "Attach the Amber-Crusted Fin to your (Skeleton Type)", cost = Cost.ACTION.value + Cost.AMBER_FIN.value, @@ -36,6 +41,7 @@ class Appendage(Enum): menace = 1 ) + # TODO: Difficulty is increased with Fins on the skeleton BAT_WING = Action( "Add a Bat Wing to your (Skeleton Type)", cost = Cost.ACTION.value + Cost.BAT_WING.value, @@ -51,7 +57,7 @@ class Appendage(Enum): value = 50, tails_needed = -1, tails = 1, - menace = 2 + menace = mean_outcome(2, 1, narrow_challenge(4, Char.MONSTROUS_ANATOMY.value)) ) CRUSTACEAN_PINCER = Action( @@ -80,6 +86,8 @@ class Appendage(Enum): legs = 1 ) + # TODO: Base challenge: Narrow, Monstrous Anatomy 1 + # The difficulty is increased by 2 for each Arm, Leg, Wing, and Tentacle already attached to your skeleton. FIN_BONES = Action( "Put Fins on your (Skeleton Type)", cost = Cost.ACTION.value + Cost.FIN_BONES.value, @@ -94,7 +102,7 @@ class Appendage(Enum): value = 2750, limbs_needed = -1, arms = 1, - antiquity = 2 + antiquity = mean_outcome(2, 1, narrow_challenge(11, Char.MONSTROUS_ANATOMY.value)) ) HELICAL_THIGH = Action( @@ -103,7 +111,7 @@ class Appendage(Enum): value = 300, limbs_needed = -1, legs = 1, - amalgamy = 2 + amalgamy = mean_outcome(2, 1, narrow_challenge(6, Char.SHAPELING_ARTS.value)) ) HUMAN_ARM = Action( @@ -120,7 +128,8 @@ class Appendage(Enum): cost = Cost.ACTION.value + Cost.IVORY_FEMUR.value, value = 6500, limbs_needed = -1, - legs = 1 + legs = 1, + implausibility = mean_outcome(0, 4, narrow_challenge(7, Char.MONSTROUS_ANATOMY.value)) ) IVORY_HUMERUS = Action( @@ -128,9 +137,12 @@ class Appendage(Enum): cost = Cost.ACTION.value + Cost.IVORY_HUMERUS.value, value = 1500, limbs_needed = -1, - arms = 1 + arms = 1, + implausibility = mean_outcome(0, 2, narrow_challenge(6, Char.KATALEPTIC_TOXICOLOGY.value)) ) + # TODO: Base challenge: Narrow, Mithridacy 1 + # Difficulty increases by 2 for each Fin or Tentacle already attached to your skeleton. JURASSIC_THIGH = Action( "Apply a Jurassic Thigh Bone to your (Skeleton Type)", cost = Cost.ACTION.value + Cost.JURASSIC_FEMUR.value, @@ -140,6 +152,8 @@ class Appendage(Enum): antiquity = 1 ) + # TODO: Base challenge: Narrow, Mithridacy 6 + # Difficulty increases by 1 for each limb that is NOT a KNOTTED_HUMERUS KNOTTED_HUMERUS = Action( "Apply a Knotted Humerus to your (Skeleton Type)", cost = Cost.ACTION.value + Cost.KNOTTED_HUMERUS.value, @@ -149,6 +163,8 @@ class Appendage(Enum): amalgamy = 1 ) + # TODO: Base challenge: Monstrous Anatomy 4 + # No failure info on wiki OBSIDIAN_TAIL = Action( "Apply an Obsidian Chitin Tail to your (Skeleton Type)", cost = Cost.ACTION.value + Cost.OBSIDIAN_TAIL.value, @@ -164,9 +180,11 @@ class Appendage(Enum): value = 250, tails_needed = -1, tails = 1, - implausibility = 1 + implausibility = mean_outcome(1, 4, narrow_challenge(4, Char.MITHRIDACY.value)) ) + # TODO: Base challenge: Narrow, Monstrous Anatomy 1 + # Difficulty increases by 2 for each Fin already attached to your skeleton. TERROR_BIRD_WING = Action( "Add the Wing of a Young Terror Bird to your (Skeleton Type)", cost = Cost.ACTION.value + Cost.TERROR_BIRD_WING.value, @@ -183,7 +201,7 @@ class Appendage(Enum): value = 250, tails_needed = -1, tails = 1, - antiquity = 1 + antiquity = mean_outcome(0, 1, narrow_challenge(4, Char.MONSTROUS_ANATOMY.value)) ) UNIDENTIFIED_THIGH = Action( @@ -194,27 +212,19 @@ class Appendage(Enum): legs = 1 ) - WITHERED_TAIL = Action( - "Apply a Withered Tentacle as a tail on your (Skeleton Type)", - cost = Cost.ACTION.value + Cost.WITHERED_TENTACLE.value, - value = 250, - tails_needed = -1, - tails = 1, - antiquity = -1 - ) - WITHERED_TENTACLE = Action( "Put a Withered Tentacle on your (Skeleton Type)", cost = Cost.ACTION.value + Cost.WITHERED_TENTACLE.value, value = 250, limbs_needed = -1, tentacles = 1, - antiquity = -1 + antiquity = -1, + implausibility = mean_outcome(0, 2, narrow_challenge(5, Char.MONSTROUS_ANATOMY.value)) ) REMOVE_TAIL = Action( "Remove the tail from your (Skeleton Type)", - cost = Cost.ACTION.value, + cost = Cost.ACTION.value * 1 / broad_challenge(220, Char.DANGEROUS.value), tails = -1 ) diff --git a/bonemarketsolver/data/embellishments.py b/bonemarketsolver/data/embellishments.py index 138e073..99684a5 100644 --- a/bonemarketsolver/data/embellishments.py +++ b/bonemarketsolver/data/embellishments.py @@ -5,6 +5,21 @@ from enum import Enum from .costs import Cost from ..objects.action import Action +from ..read_char import * +from ..challenge_functions import narrow_challenge + + +def _convincing_history_cost(): + chance = narrow_challenge(6, Char.KATALEPTIC_TOXICOLOGY.value) + + if chance == 1: + cost = 3*Cost.REVISIONIST_NARRATIVE.value + Cost.ACTION.value + else: + actions = 1 / chance + cost = actions * Cost.ACTION.value + cost += Cost.REVISIONIST_NARRATIVE.value * (3 + actions - 1) + return cost + class Embellishment(Enum): """An action is taken after a declaration has been made for a skeleton.""" @@ -17,7 +32,7 @@ class Embellishment(Enum): CONVINCING_HISTORY = Action( "Invest great time and skill in coming up with a convincing history", - cost = Cost.ACTION.value + 3*Cost.REVISIONIST_NARRATIVE.value, + cost = _convincing_history_cost(), implausibility = -5 ) diff --git a/bonemarketsolver/data/skulls.py b/bonemarketsolver/data/skulls.py index d01a763..db46546 100644 --- a/bonemarketsolver/data/skulls.py +++ b/bonemarketsolver/data/skulls.py @@ -5,13 +5,15 @@ from enum import Enum from .costs import Cost from ..objects.action import Action +from ..challenge_functions import narrow_challenge, mean_outcome +from ..read_char import Char class Skull(Enum): """An action that is taken immediately after starting a skeleton.""" BAPTIST_SKULL = Action( "Duplicate the skull of John the Baptist, if you can call that a skull", - cost = Cost.ACTION.value + 500*Cost.BONE_FRAGMENT.value + 10*Cost.PEPPERCAPS.value, + cost = Cost.ACTION.value * 1 / narrow_challenge(6, Char.ARTISAN_OF_RED_SCIENCE.value) + 500*Cost.BONE_FRAGMENT.value + 10*Cost.PEPPERCAPS.value, value = 1250, skulls_needed = -1, skulls = 1, @@ -20,13 +22,14 @@ class Skull(Enum): BRASS_SKULL = Action( "Affix a Bright Brass Skull to your (Skeleton Type)", - cost = Cost.ACTION.value + Cost.BRASS_SKULL.value + 200*Cost.NEVERCOLD_BRASS.value, - value = 6500, + cost = Cost.ACTION.value + Cost.BRASS_SKULL.value + mean_outcome(200*Cost.NEVERCOLD_BRASS.value, 0, narrow_challenge(6, Char.MITHRIDACY.value)), + value = mean_outcome(6500, 6000, narrow_challenge(6, Char.MITHRIDACY.value)), skulls_needed = -1, skulls = 1, - implausibility = 2 + implausibility = mean_outcome(2, 6, narrow_challenge(6, Char.MITHRIDACY.value)), ) + # TODO CORAL_SKULL = Action( "Affix a Skull in Coral to your (Skeleton Type)", cost = Cost.ACTION.value + Cost.CORAL_SKULL.value + Cost.SCINTILLACK.value, @@ -43,7 +46,7 @@ class Skull(Enum): skulls_needed = -1, skulls = 2, amalgamy = 1, - antiquity = 2 + antiquity = mean_outcome(2, 1, narrow_challenge(4, Char.MONSTROUS_ANATOMY.value)) ) # Adds Exhaustion @@ -53,7 +56,8 @@ class Skull(Enum): value = 10000, skulls_needed = -1, skulls = 1, - exhaustion = 2 + exhaustion = 2, + implausibility = mean_outcome(0, 2, narrow_challenge(4, Char.MITHRIDACY.value)), ) EYELESS_SKULL = Action( @@ -72,13 +76,13 @@ class Skull(Enum): skulls_needed = -1, skulls = 1, antiquity = 1, - menace = 2 + menace = mean_outcome(2, 1, narrow_challenge(6, Char.MONSTROUS_ANATOMY.value)) ) # Seeking the Name of Mr. Eaten OWN_SKULL = Action( "Duplicate your own skull and affix it here", - cost = Cost.ACTION.value + 1000*Cost.BONE_FRAGMENT.value, + cost = Cost.ACTION.value * 1 / narrow_challenge(6, Char.ARTISAN_OF_RED_SCIENCE.value) + 1000*Cost.BONE_FRAGMENT.value, value = -250, skulls_needed = -1, skulls = 1 @@ -100,7 +104,7 @@ class Skull(Enum): value = 2500, skulls_needed = -1, skulls = 1, - menace = 2 + menace = mean_outcome(2, 1, narrow_challenge(4, Char.MONSTROUS_ANATOMY.value)) ) RUBBERY_SKULL = Action( @@ -119,19 +123,20 @@ class Skull(Enum): skulls_needed = -1, skulls = 1, antiquity = 1, - menace = 1 + menace = mean_outcome(1, 0, narrow_challenge(6, Char.MONSTROUS_ANATOMY.value)) ) STYGIAN_IVORY = Action( "Use a Carved Ball of Stygian Ivory to cap off your (Skeleton Type)", cost = Cost.ACTION.value + Cost.STYGIAN_IVORY.value, value = 250, - skulls_needed = -1 + skulls_needed = -1, + implausibility = mean_outcome(0, 2, narrow_challenge(6, Char.MITHRIDACY.value)) ) VAKE_SKULL = Action( "Duplicate the Vake's skull and use it to decorate your (Skeleton Type)", - cost = Cost.ACTION.value + 6000*Cost.BONE_FRAGMENT.value, + cost = Cost.ACTION.value + 6000*Cost.BONE_FRAGMENT.value + mean_outcome(0, Cost.ACTION.value + 300*Cost.BONE_FRAGMENT.value, narrow_challenge(6, Char.ARTISAN_OF_RED_SCIENCE.value)), value = 6500, skulls_needed = -1, skulls = 1, diff --git a/bonemarketsolver/default_char.py b/bonemarketsolver/default_char.py new file mode 100644 index 0000000..06b993a --- /dev/null +++ b/bonemarketsolver/default_char.py @@ -0,0 +1,32 @@ +from enum import Enum + +class Char(Enum): + """Character stats""" + + SHADOWY = 300 + + DANGEROUS = 300 + + PERSUASIVE = 300 + + WATCHFUL = 300 + + PLAYER_OF_CHESS = 7 + + ARTISAN_OF_RED_SCIENCE = 7 + + GLASSWORK = 7 + + KATALEPTIC_TOXICOLOGY = 7 + + MITHRIDACY = 7 + + MONSTROUS_ANATOMY = 7 + + SHAPELING_ARTS = 7 + + BIZARRE = 15 + + DREADED = 15 + + RESPECTABLE = 15 diff --git a/bonemarketsolver/read_char.py b/bonemarketsolver/read_char.py new file mode 100644 index 0000000..8ddec3f --- /dev/null +++ b/bonemarketsolver/read_char.py @@ -0,0 +1,5 @@ +try: + from .custom_char import Char +except: + print("Note: custom_char.py does not exist. Using default_char.py") + from .default_char import Char diff --git a/bonemarketsolver/solve.py b/bonemarketsolver/solve.py index 046eeab..c523b8e 100644 --- a/bonemarketsolver/solve.py +++ b/bonemarketsolver/solve.py @@ -22,12 +22,12 @@ from .data.torsos import Torso # This multiplier is applied to the profit margin to avoid losing precision due to rounding. PROFIT_MARGIN_MULTIPLIER = 10000000 +ATTRIBUTE_MULTIPLIER = 10000000 # This is the highest number of attribute to calculate fractional exponents for. MAXIMUM_ATTRIBUTE = 100 -# This is a constant used to calculate difficulty checks. You almost certainly do not need to change this. -DIFFICULTY_SCALER = 0.6 +from .challenge_functions import DIFFICULTY_SCALER def NewIntermediateBoolVar(self, name, expression, domain): @@ -197,21 +197,36 @@ def Solve(shadowy_level, bone_market_fluctuations = None, zoological_mania = Non model.Add(tentacles == cp_model.LinearExpr.ScalProd(actions.values(), [action.value.tentacles for action in actions.keys()])) # Amalgamy calculation + multiplied_amalgamy = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, 'multiplied amalgamy') + model.Add(multiplied_amalgamy == cp_model.LinearExpr.ScalProd(actions.values(), [int(action.value.amalgamy*ATTRIBUTE_MULTIPLIER) for action in actions.keys()])) amalgamy = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, 'amalgamy') - model.Add(amalgamy == cp_model.LinearExpr.ScalProd(actions.values(), [action.value.amalgamy for action in actions.keys()])) + model.AddDivisionEquality(amalgamy, multiplied_amalgamy, ATTRIBUTE_MULTIPLIER) + + del multiplied_amalgamy # Antiquity calculation + multiplied_antiquity = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, 'multiplied antiquity') + model.Add(multiplied_antiquity == cp_model.LinearExpr.ScalProd(actions.values(), [int(action.value.antiquity*ATTRIBUTE_MULTIPLIER) for action in actions.keys()])) antiquity = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, 'antiquity') - model.Add(antiquity == cp_model.LinearExpr.ScalProd(actions.values(), [action.value.antiquity for action in actions.keys()])) + model.AddDivisionEquality(antiquity, multiplied_antiquity, ATTRIBUTE_MULTIPLIER) + + del multiplied_antiquity # Menace calculation + multiplied_menace = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, 'multiplied menace') + model.Add(multiplied_menace == cp_model.LinearExpr.ScalProd(actions.values(), [int(action.value.menace*ATTRIBUTE_MULTIPLIER) for action in actions.keys()])) menace = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, 'menace') - model.Add(menace == cp_model.LinearExpr.ScalProd(actions.values(), [action.value.menace for action in actions.keys()])) + model.AddDivisionEquality(menace, multiplied_menace, ATTRIBUTE_MULTIPLIER) + + del multiplied_menace # Implausibility calculation + multiplied_implausibility = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, 'multiplied implausibility') + model.Add(multiplied_implausibility == cp_model.LinearExpr.ScalProd(actions.values(), [int(action.value.implausibility*ATTRIBUTE_MULTIPLIER) for action in actions.keys()])) implausibility = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, 'implausibility') - model.Add(implausibility == cp_model.LinearExpr.ScalProd(actions.values(), [action.value.implausibility for action in actions.keys()])) + model.AddDivisionEquality(implausibility, multiplied_implausibility, ATTRIBUTE_MULTIPLIER) + del multiplied_implausibility # Counter-church calculation # Calculate amount of Counter-church from Holy Relics of the Thigh of Saint Fiacre @@ -1235,16 +1250,16 @@ def Solve(shadowy_level, bone_market_fluctuations = None, zoological_mania = Non model.Add(net_profit == total_revenue - cost) # This is necessary to preserve some degree of precision after dividing - multiplied_net_profit = model.NewIntVar(cp_model.INT32_MIN*PROFIT_MARGIN_MULTIPLIER, cp_model.INT32_MAX*PROFIT_MARGIN_MULTIPLIER, 'multiplied net profit') + multiplied_net_profit = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, 'multiplied net profit') model.AddMultiplicationEquality(multiplied_net_profit, [net_profit, PROFIT_MARGIN_MULTIPLIER]) - absolute_multiplied_net_profit = model.NewIntVar(0, cp_model.INT32_MAX*PROFIT_MARGIN_MULTIPLIER, 'absolute multiplied net profit') + absolute_multiplied_net_profit = model.NewIntVar(0, cp_model.INT32_MAX, 'absolute multiplied net profit') model.AddAbsEquality(absolute_multiplied_net_profit, multiplied_net_profit) - absolute_profit_margin = model.NewIntVar(cp_model.INT32_MIN*PROFIT_MARGIN_MULTIPLIER, cp_model.INT32_MAX*PROFIT_MARGIN_MULTIPLIER, 'absolute profit margin') + absolute_profit_margin = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, 'absolute profit margin') model.AddDivisionEquality(absolute_profit_margin, absolute_multiplied_net_profit, total_revenue) - profit_margin = model.NewIntVar(cp_model.INT32_MIN*PROFIT_MARGIN_MULTIPLIER, cp_model.INT32_MAX*PROFIT_MARGIN_MULTIPLIER, 'profit margin') + profit_margin = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, 'profit margin') positive_net_profit = model.NewIntermediateBoolVar('positive net profit', net_profit, cp_model.Domain.FromFlatIntervals([0, cp_model.INT_MAX])) model.Add(profit_margin == absolute_profit_margin).OnlyEnforceIf(positive_net_profit)