import functools import enum import os from enum import auto from ortools.sat.python import cp_model # This multiplier is applied to the profit margin to avoid losing precision due to rounding. PROFIT_MARGIN_MULTIPLIER = 10000000 # This is the highest number of attribute to calculate fractional exponents for. MAXIMUM_ATTRIBUTE = 100 # The number of pennies that a single action (the game mechanic) is worth. ACTION_VALUE = 400 # The number of pennies needed to produce a single Survey of the Neath's Bones. SURVEY_VALUE = (ACTION_VALUE * 6) / 25 # This is a constant used to calculate difficulty checks. You almost certainly do not need to change this. DIFFICULTY_SCALER = 0.6 # This is the effective level of Shadowy used for attempting to sell. SHADOWY_LEVEL = 300 # The maximum number of pennies that should be invested in this skeleton. MAXIMUM_COST = cp_model.INT32_MAX # The maximum Exhaustion that this skeleton should generate. MAXIMUM_EXHAUSTION = 4 # Adds a fully-reified implication using an intermediate Boolean variable. def NewIntermediateBoolVar(self, name, expression, domain): intermediate = self.NewBoolVar(name) self.AddLinearExpressionInDomain(expression, domain).OnlyEnforceIf(intermediate) self.AddLinearExpressionInDomain(expression, domain.Complement()).OnlyEnforceIf(intermediate.Not()) return intermediate setattr(cp_model.CpModel, 'NewIntermediateBoolVar', NewIntermediateBoolVar) del NewIntermediateBoolVar # Adds an approximate exponentiation equality using a lookup table. # Set `upto` to a value that is unlikely to come into play. def AddApproximateExponentiationEquality(self, target, var, exp, upto): return self.AddAllowedAssignments([target, var], [(int(base**exp), base) for base in range(upto + 1)]) setattr(cp_model.CpModel, 'AddApproximateExponentiationEquality', AddApproximateExponentiationEquality) del AddApproximateExponentiationEquality # Adds a multiplication equality for any number of terms using intermediate variables. def AddGeneralMultiplicationEquality(self, target, *variables): # This is used for producing unique names for intermediate variables. term_index = 1 def function(a, b): nonlocal term_index intermediate = self.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{} term {}'.format(target.Name(), term_index)) term_index += 1 self.AddMultiplicationEquality(intermediate, [a, b]) return intermediate product = functools.reduce(function, variables) return self.Add(target == product) setattr(cp_model.CpModel, 'AddGeneralMultiplicationEquality', AddGeneralMultiplicationEquality) del AddGeneralMultiplicationEquality # A way to convert a skeleton into revenue. class Buyer(enum.Enum): A_PALAEONTOLOGIST_WITH_HOARDING_PROPENSITIES = auto() A_NAIVE_COLLECTOR = auto() A_FAMILIAR_BOHEMIAN_SCULPTRESS = auto() A_PEDAGOGICALLY_INCLINED_GRANDMOTHER = auto() A_THEOLOGIAN_OF_THE_OLD_SCHOOL = auto() AN_ENTHUSIAST_OF_THE_ANCIENT_WORLD = auto() MRS_PLENTY = auto() A_TENTACLED_SERVANT = auto() AN_INVESTMENT_MINDED_AMBASSADOR = auto() A_TELLER_OF_TERRORS = auto() A_TENTACLED_ENTREPRENEUR = auto() AN_AUTHOR_OF_GOTHIC_TALES = auto() A_ZAILOR_WITH_PARTICULAR_INTERESTS = auto() A_RUBBERY_COLLECTOR = auto() A_CONSTABLE = auto() AN_ENTHUSIAST_IN_SKULLS = auto() A_DREARY_MIDNIGHTER = auto() THE_DUMBWAITER_OF_BALMORAL = auto() # An action that affects a skeleton's qualities. class Action: def __init__(self, name, cost, value = 0, skulls_needed = 0, limbs_needed = 0, tails_needed = 0, skulls = 0, arms = 0, legs = 0, tails = 0, wings = 0, fins = 0, tentacles = 0, amalgamy = 0, antiquity = 0, menace = 0, implausibility = 0, counter_church = 0, exhaustion = 0): self.name = name # Cost in pennies of using this action, including the value of the actions spent self.cost = cost # Approximate Value of Your Skeleton in Pennies self.value = value # Skeleton: Skulls Needed self.skulls_needed = skulls_needed # Skeleton: Limbs Needed self.limbs_needed = limbs_needed # Skeleton: Tails Needed self.tails_needed = tails_needed # Skeleton: Skulls self.skulls = skulls # Skeleton: Arms self.arms = arms # Skeleton: Legs self.legs = legs # Skeleton: Tails self.tails = tails # Skeleton: Wings self.wings = wings # Skeleton: Fins self.fins = fins # Skeleton: Tentacles self.tentacles = tentacles # Skeleton: Amalgamy self.amalgamy = amalgamy # Skeleton: Antiquity self.antiquity = antiquity # Skeleton: Menace self.menace = menace # Skeleton: Self-Evident Implausibility self.implausibility = implausibility # Skeleton: Support for a Counter-church Theology self.counter_church = counter_church # Bone Market Exhaustion self.exhaustion = exhaustion def __str__(self): return str(self.name) # The base of a skeleton. class Torso(Action): def __init__(self, name, torso_style, cost, value = 0, skulls_needed = 0, limbs_needed = 0, tails_needed = 0, skulls = 0, arms = 0, legs = 0, tails = 0, wings = 0, fins = 0, tentacles = 0, amalgamy = 0, antiquity = 0, menace = 0, implausibility = 0, counter_church = 0, exhaustion = 0): super().__init__(name, cost, value, skulls_needed, limbs_needed, tails_needed, skulls, arms, legs, tails, wings, fins, tentacles, amalgamy, antiquity, menace, implausibility, counter_church, exhaustion) # Skeleton: Torso Style self.torso_style = torso_style # Which kind of skeleton is to be declared. class Declaration(enum.Enum): CHIMERA = Action("Declare your (Skeleton Type) a completed Chimera", cost = ACTION_VALUE, implausibility = 3) HUMANOID = Action("Declare your (Skeleton Type) a completed Humanoid", cost = ACTION_VALUE) APE = Action("Declare your (Skeleton Type) a completed Ape", cost = ACTION_VALUE) MONKEY = Action("Declare your (Skeleton Type) a completed Monkey", cost = ACTION_VALUE) BIRD = Action("Declare your (Skeleton Type) a completed Bird", cost = ACTION_VALUE) CURATOR = Action("Declare your (Skeleton Type) a completed Curator", cost = ACTION_VALUE) REPTILE = Action("Declare your (Skeleton Type) a completed Reptile", cost = ACTION_VALUE) AMPHIBIAN = Action("Declare your (Skeleton Type) a completed Amphibian", cost = ACTION_VALUE) FISH = Action("Declare your (Skeleton Type) a completed Fish", cost = ACTION_VALUE) INSECT = Action("Declare your (Skeleton Type) a completed Insect", cost = ACTION_VALUE) SPIDER = Action("Declare your (Skeleton Type) a completed Spider", cost = ACTION_VALUE) def __str__(self): return str(self.value) # Which skeleton attribute is currently boosted. class Fluctuation(enum.Enum): ANTIQUITY = 1 AMALGAMY = 2 def create_data_model(): data = {} data['buyer'] = Buyer.AN_ENTHUSIAST_IN_SKULLS # The current value of Bone Market Fluctuations, which grants various bonuses to certain buyers. data['bone_market_fluctuations'] = Fluctuation.AMALGAMY # The current value of Zoological Mania, which grants a 10% bonus to value for a certain declaration. data['zoological_mania'] = Declaration.AMPHIBIAN data['actions'] = [ # Torso("Supply a skeleton of your own", cost = ACTION_VALUE, torso_style = 10, value = 250, skulls_needed = 1, arms = 2, legs = 2), # Accumulated while trying to get other things Torso("Reassemble your Headless Humanoid", torso_style = 10, cost = ACTION_VALUE, value = 250, skulls_needed = 1, arms = 2, legs = 2), # Ealing Gardens Torso("Build on the Human Ribcage", torso_style = 15, cost = ACTION_VALUE*2 + SURVEY_VALUE*15, value = 1250, skulls_needed = 1, limbs_needed = 4), # Balmoral Woods, (also gives Doubled Skull) Torso("Make something of your Thorned Ribcage", torso_style = 20, cost = 2000 + ACTION_VALUE*14, value = 1250, skulls_needed = 1, limbs_needed = 4, tails_needed = 1, amalgamy = 1, menace = 1), # Combination of Human Ribcage and Thorned Ribcage Torso("Build on the Flourishing Ribcage", torso_style = 40, cost = 2000 + ACTION_VALUE*16 + SURVEY_VALUE*15, value = 1250, skulls_needed = 2, limbs_needed = 6, tails_needed = 1, amalgamy = 2), Torso("Build on the Skeleton with Seven Necks", torso_style = 30, cost = 1150 + ACTION_VALUE*18, value = 6250, skulls_needed = 7, limbs_needed = 2, legs = 2, amalgamy = 2, menace = 1), # Human Ribcage and Betrayer of Measures Torso("Build on the Mammoth Ribcage", torso_style = 50, cost = ACTION_VALUE*18 + SURVEY_VALUE*15, value = 6250, skulls_needed = 1, limbs_needed = 4, tails_needed = 1, antiquity = 2), # Skeleton with Seven Necks, 2 x Severed Chimaerical Head of the Vake, 2 x Counterfeit Head of John the Baptist, Carved Ball of Stygian Ivory, 2 x Plated Skull, 2 x Albatross Wing Torso("Build on the Leviathan Frame", cost = 22150 + ACTION_VALUE*33, torso_style = 70, value = 31250, skulls_needed = 1, limbs_needed = 2, tails = 1, antiquity = 1, menace = 1), # Combination of Skeleton with Seven Necks and Thorned Ribcage Torso("Build on the Ribcage with the Eight Spines", torso_style = 60, cost = 25650 + ACTION_VALUE*32, value = 31250, skulls_needed = 8, limbs_needed = 4, tails_needed = 1, amalgamy = 1, menace = 2), # Expedition Torso("Build on the Prismatic Frame", torso_style = 80, cost = 29250 + ACTION_VALUE*5, value = 31250, skulls_needed = 3, limbs_needed = 3, tails_needed = 3, amalgamy = 2, antiquity = 2), # Upwards Torso("Build on the Five-Pointed Frame", torso_style = 100, cost = 31250 + ACTION_VALUE*10, value = 31250, skulls_needed = 5, limbs_needed = 5, amalgamy = 2, menace = 1), Action("Affix a Bright Brass Skull to your (Skeleton Type)", cost = 6450 + ACTION_VALUE, value = 6500, skulls_needed = -1, skulls = 1, implausibility = 2), # No consistent source # Action("Affix an Eyeless Skull to your (Skeleton Type)", cost = cp_model.INT32_MAX, value = 3000, skulls_needed = -1, skulls = 1, menace = 2), # Feast of the Exceptional Rose, 200 Inklings of Identity, action to send and receive it #Action("Affix a Custom-Engraved Skull to your (Skeleton Type)", cost = 2000 + ACTION_VALUE*2, value = 10000, skulls_needed = -1, skulls = 1, exhaustion = 2), Action("Affix a Horned Skull to your (Skeleton Type)", cost = 1050 + ACTION_VALUE*2, value = 1250, skulls_needed = -1, skulls = 1, antiquity = 1, menace = 2), Action("Affix a Sabre-toothed Skull to your (Skeleton Type)", cost = 6150 + ACTION_VALUE*2, value = 6250, skulls_needed = -1, skulls = 1, antiquity = 1, menace = 1), # Upwards Action("Affix a Pentagrammic Skull to your (Skeleton Type)", cost = ACTION_VALUE*10, value = 1250, skulls_needed = -1, skulls = 1, amalgamy = 2, menace = 1), Action("Affix a Plated Skull to your (Skeleton Type)", cost = 2250 + ACTION_VALUE*2, value = 2500, skulls_needed = -1, skulls = 1, menace = 2), # Flute Street, including travel due to quality cap Action("Affix a Rubbery Skull to your (Skeleton Type)", cost = ACTION_VALUE*26, value = 600, skulls_needed = -1, skulls = 1, amalgamy = 1), # Action("Duplicate your own skull and affix it here", cost = 1000 + ACTION_VALUE, value = -250, skulls_needed = -1, skulls = 1), Action("Duplicate the skull of John the Baptist, if you can call that a skull", cost = 1000 + ACTION_VALUE, value = 1500, skulls_needed = -1, skulls = 1, counter_church = 2), # Persephone, 6 actions (Favours: the Docks) for 2 Esteem of the Guild Action("Affix a Skull in Coral to your (Skeleton Type)", cost = ACTION_VALUE*25/3, value = 1750, skulls_needed = -1, skulls = 1, amalgamy = 2), Action("Duplicate the Vake's skull and use it to decorate your (Skeleton Type)", cost = 6000 + ACTION_VALUE, value = 6500, skulls_needed = -1, skulls = 1, menace = 3), # Action("Cap this with a victim’s skull", cost = ACTION_VALUE, value = 250, skulls_needed = -1, skulls = 1), # Balmoral Woods (also gives Thorned Ribcage) Action("Affix a Doubled Skull to your (Skeleton Type)", cost = 2000 + ACTION_VALUE*14, value = 6250, skulls_needed = -1, skulls = 2, amalgamy = 1, antiquity = 2), Action("Use a Carved Ball of Stygian Ivory to cap off your (Skeleton Type)", cost = 250 + ACTION_VALUE, value = 250, skulls_needed = -1), # 2 pincers at once Action("Apply a Crustacean Pincer to your (Skeleton Type)", cost = 25 + ACTION_VALUE*1.5, limbs_needed = -1, arms = 1, menace = 1), # Accumulated while trying to get other things Action("Apply a Knotted Humerus to your (Skeleton Type)", cost = ACTION_VALUE, value = 150, limbs_needed = -1, arms = 1, amalgamy = 1), # Ealing Gardens, 5 actions (Favours: Bohemians) for 2 Action("Apply an Ivory Humerus to your (Skeleton Type)", cost = ACTION_VALUE*3.5, value = 1500, limbs_needed = -1, arms = 1), # Accumulated while trying to get other things Action("Join a Human Arm to your (Skeleton Type)", cost = ACTION_VALUE, value = 250, limbs_needed = -1, arms = 1, menace = -1), # Anning and Daughters Action("Apply a Fossilised Forelimb to your (Skeleton Type)", cost = 2500 + ACTION_VALUE, value = 2750, limbs_needed = -1, arms = 1, antiquity = 2), # 2 wings at once Action("Add the Wing of a Young Terror Bird to your (Skeleton Type)", cost = 175 + ACTION_VALUE*1.5, value = 250, limbs_needed = -1, wings = 1, antiquity = 1, menace = 1), # 2 wings at once Action("Put an Albatross Wing on your (Skeleton Type)", cost = 1125 + ACTION_VALUE*1.5, value = 1250, limbs_needed = -1, wings = 1, amalgamy = 1), # 2 wings at once Action("Add a Bat Wing to your (Skeleton Type)", cost = 60 + ACTION_VALUE*1.5, value = 1, limbs_needed = -1, wings = 1, menace = -1), # Dumbwaiter of Balmoral, 25 at a time Action("Apply the Femur of a Surface Deer to your (Skeleton Type)", cost = ACTION_VALUE*1.04, value = 10, limbs_needed = -1, legs = 1, menace = -1), # Accumulated while trying to get other things Action("Apply an Unidentified Thigh Bone to your (Skeleton Type)", cost = ACTION_VALUE, value = 100, limbs_needed = -1, legs = 1), # Brawling, 12 at a time Action("Apply a Jurassic Thigh Bone to your (Skeleton Type)", cost = ACTION_VALUE*(11/6), value = 300, limbs_needed = -1, legs = 1, antiquity = 1), # Jericho Locks, 5 actions (Favours: the Church) for 2 # Counter-Church theology from this scales with torso style and is implemented separately Action("Affix Saint Fiacre's Thigh Relic to your (Skeleton Type)", cost = ACTION_VALUE*3.5, value = 1250, limbs_needed = -1, legs = 1), # Palaeontological Discoveries, Plain of Thirsty Grasses Action("Affix the Helical Thighbone to your (Skeleton Type)", cost = ACTION_VALUE + SURVEY_VALUE*(70/9), value = 300, limbs_needed = -1, legs = 1, amalgamy = 2), # Parabolan Orange-Apples, Hedonist, 3cp/action Action("Apply an Ivory Femur to your (Skeleton Type)", cost = 900 + ACTION_VALUE*15.5, value = 6500, limbs_needed = -1, legs = 1), # Hunt and dissect Pinewood Shark, 40 at a time Action("Put Fins on your (Skeleton Type)", cost = ACTION_VALUE*(51/40), value = 50, limbs_needed = -1, fins = 1), # Combination of 10 Fins Action("Attach the Amber-Crusted Fin to your (Skeleton Type)", cost = ACTION_VALUE*(15/4), value = 1500, limbs_needed = -1, fins = 1, amalgamy = 1, menace = 1), # Helicon House, 3 at a time Action("Put a Withered Tentacle on your (Skeleton Type)", cost = 50/3 + ACTION_VALUE*4/3, value = 250, limbs_needed = -1, tentacles = 1, antiquity = -1), # Carpenter's Granddaughter, 2 at a time Action("Apply Plaster Tail Bones to your (Skeleton Type)", cost = ACTION_VALUE*1.5 + SURVEY_VALUE*5, value = 250, tails_needed = -1, tails = 1, implausibility = 1), Action("Apply a Tomb-Lion's Tail to your (Skeleton Type)", cost = 220 + ACTION_VALUE*2, value = 250, tails_needed = -1, tails = 1, antiquity = 1), # Geology of Winewound Action("Apply a Jet Black Stinger to your (Skeleton Type)", cost = ACTION_VALUE*2 + SURVEY_VALUE, value = 50, tails_needed = -1, tails = 1, menace = 2), # No consistent source # Action("Apply an Obsidian Chitin Tail to your (Skeleton Type)", cost = cp_model.INT32_MAX, value = 500, tails_needed = -1, tails = 1, amalgamy = 1), # Helicon House, 3 at a time Action("Apply a Withered Tentacle as a tail on your (Skeleton Type)", cost = 50/3 + ACTION_VALUE*4/3, value = 250, tails_needed = -1, tails = 1, antiquity = -1), # This actually sets Skeleton: Tails Needed to 0 Action("Decide your Tailless Animal needs no tail", cost = ACTION_VALUE, tails_needed = -1), Action("Remove the tail from your (Skeleton Type)", cost = ACTION_VALUE, tails = -1), # Cost from this scales with limbs and is partially implemented separately Action("Add four more joints to your skeleton", cost = 1250 + ACTION_VALUE, limbs_needed = 4, amalgamy = 2), Action("Make your skeleton less dreadful", cost = ACTION_VALUE, menace = -2), Action("Disguise the amalgamy of this piece", cost = 25 + ACTION_VALUE, amalgamy = -2), Action("Carve away some evidence of age", cost = ACTION_VALUE, antiquity = -2) ] return data def Solve(): data = create_data_model() model = cp_model.CpModel() # Any number of any action, except only one torso torsos = {} actions = {} for action in data['actions']: if isinstance(action, Torso): torsos[action] = model.NewBoolVar(action.name) actions[action] = torsos[action] else: actions[action] = model.NewIntVar(0, cp_model.INT32_MAX, action.name) model.Add(cp_model.LinearExpr.Sum(torsos.values()) == 1) # Skeleton must be declared something declarations = {} for declaration in Declaration: declarations[declaration] = model.NewBoolVar(declaration.value.name) actions[declaration.value] = declarations[declaration] model.Add(cp_model.LinearExpr.Sum(declarations.values()) == 1) # Value calculation original_value = model.NewIntVar(0, cp_model.INT32_MAX, 'original value') model.Add(cp_model.LinearExpr.ScalProd(actions.values(), [action.value for action in actions.keys()]) == original_value) multiplied_value = model.NewIntVar(0, cp_model.INT32_MAX*11, "multiplied value") model.Add(multiplied_value == original_value*11).OnlyEnforceIf(declarations[data['zoological_mania']]) model.Add(multiplied_value == original_value*10).OnlyEnforceIf(declarations[data['zoological_mania']].Not()) value = model.NewIntVar(0, cp_model.INT32_MAX, 'value') model.AddDivisionEquality(value, multiplied_value, 10) del original_value, multiplied_value # Torso Style calculation torso_style = model.NewIntVarFromDomain(cp_model.Domain.FromValues([torso.torso_style for torso in torsos.keys()]), 'torso_style') for torso, torso_variable in torsos.items(): model.Add(torso_style == torso.torso_style).OnlyEnforceIf(torso_variable) # Skulls calculation skulls = model.NewIntVar(0, cp_model.INT32_MAX, 'skulls') model.Add(skulls == cp_model.LinearExpr.ScalProd(actions.values(), [action.skulls for action in actions.keys()])) # Arms calculation arms = model.NewIntVar(0, cp_model.INT32_MAX, 'arms') model.Add(arms == cp_model.LinearExpr.ScalProd(actions.values(), [action.arms for action in actions.keys()])) # Legs calculation legs = model.NewIntVar(0, cp_model.INT32_MAX, 'legs') model.Add(legs == cp_model.LinearExpr.ScalProd(actions.values(), [action.legs for action in actions.keys()])) # Tails calculation tails = model.NewIntVar(0, cp_model.INT32_MAX, 'tails') model.Add(tails == cp_model.LinearExpr.ScalProd(actions.values(), [action.tails for action in actions.keys()])) # Wings calculation wings = model.NewIntVar(0, cp_model.INT32_MAX, 'wings') model.Add(wings == cp_model.LinearExpr.ScalProd(actions.values(), [action.wings for action in actions.keys()])) # Fins calculation fins = model.NewIntVar(0, cp_model.INT32_MAX, 'fins') model.Add(fins == cp_model.LinearExpr.ScalProd(actions.values(), [action.fins for action in actions.keys()])) # Tentacles calculation tentacles = model.NewIntVar(0, cp_model.INT32_MAX, 'tentacles') model.Add(tentacles == cp_model.LinearExpr.ScalProd(actions.values(), [action.tentacles for action in actions.keys()])) # Amalgamy calculation amalgamy = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, 'amalgamy') model.Add(amalgamy == cp_model.LinearExpr.ScalProd(actions.values(), [action.amalgamy for action in actions.keys()])) # Antiquity calculation antiquity = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, 'antiquity') model.Add(antiquity == cp_model.LinearExpr.ScalProd(actions.values(), [action.antiquity for action in actions.keys()])) # Menace calculation menace = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, 'menace') model.Add(menace == cp_model.LinearExpr.ScalProd(actions.values(), [action.menace for action in actions.keys()])) # Implausibility calculation implausibility = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, 'implausibility') model.Add(implausibility == cp_model.LinearExpr.ScalProd(actions.values(), [action.implausibility for action in actions.keys()])) # Counter-church calculation # Calculate amount of Counter-church from Holy Relics of the Thigh of Saint Fiacre holy_relic = next(filter(lambda action: action[0].name == "Affix Saint Fiacre's Thigh Relic to your (Skeleton Type)", actions.items()))[1] torso_style_divided_by_ten = model.NewIntVar(0, cp_model.INT32_MAX, 'torso style divided by ten') model.AddDivisionEquality(torso_style_divided_by_ten, torso_style, 10) holy_relic_counter_church = model.NewIntVar(0, cp_model.INT32_MAX, 'holy relic counter-church') model.AddMultiplicationEquality(holy_relic_counter_church, [holy_relic, torso_style_divided_by_ten]) counter_church = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, 'counter-church') model.Add(counter_church == cp_model.LinearExpr.ScalProd(actions.values(), [action.counter_church for action in actions.keys()]) + holy_relic_counter_church) del holy_relic, torso_style_divided_by_ten, holy_relic_counter_church # Exhaustion calculation exhaustion = model.NewIntVar(0, MAXIMUM_EXHAUSTION, 'exhaustion') # Profit intermediate variables primary_revenue = model.NewIntVar(0, cp_model.INT32_MAX, 'primary revenue') secondary_revenue = model.NewIntVar(0, cp_model.INT32_MAX, 'secondary revenue') total_revenue = model.NewIntVar(0, cp_model.INT32_MAX*2, 'total revenue') model.Add(total_revenue == cp_model.LinearExpr.Sum([primary_revenue, secondary_revenue])) # Cost # Calculate value of actions needed to sell the skeleton. difficulty_level = model.NewIntVar(0, cp_model.INT32_MAX, 'difficulty level') non_zero_difficulty_level = model.NewIntVar(1, cp_model.INT32_MAX, 'non-zero difficulty level') model.AddMaxEquality(non_zero_difficulty_level, [difficulty_level, 1]) sale_actions_times_action_value = model.NewIntVar(0, cp_model.INT32_MAX, 'sale actions times action value') model.AddDivisionEquality(sale_actions_times_action_value, model.NewConstant(round(DIFFICULTY_SCALER*SHADOWY_LEVEL*ACTION_VALUE)), non_zero_difficulty_level) abstract_sale_cost = model.NewIntVar(0, cp_model.INT32_MAX, 'abstract sale cost') model.AddDivisionEquality(abstract_sale_cost, ACTION_VALUE**2, sale_actions_times_action_value) sale_cost = model.NewIntVar(0, cp_model.INT32_MAX, 'sale cost') model.AddMaxEquality(sale_cost, [abstract_sale_cost, ACTION_VALUE]) del non_zero_difficulty_level, sale_actions_times_action_value, abstract_sale_cost # Calculate cost of adding joints # This is a partial sum formula. add_joints_amber_cost = model.NewIntVar(0, cp_model.INT32_MAX, 'add joints amber cost') add_joints = next(filter(lambda action: action[0].name == "Add four more joints to your skeleton", actions.items()))[1] base_joints = model.NewIntVar(0, cp_model.INT32_MAX, 'base joints') model.Add(base_joints == cp_model.LinearExpr.ScalProd(torsos.values(), [action.limbs_needed for torso in torsos.keys()])) add_joints_amber_cost_multiple = model.NewIntVar(0, cp_model.INT32_MAX, 'add joints amber cost multiple') add_joints_amber_cost_multiple_first_term = model.NewIntVar(0, cp_model.INT32_MAX, 'add joints amber cost multiple first term') model.AddGeneralMultiplicationEquality(add_joints_amber_cost_multiple_first_term, 250, base_joints, base_joints, add_joints) add_joints_amber_cost_multiple_second_term = model.NewIntVar(0, cp_model.INT32_MAX, 'add joints amber cost multiple second term') model.AddGeneralMultiplicationEquality(add_joints_amber_cost_multiple_second_term, 1000, base_joints, add_joints, add_joints) add_joints_amber_cost_multiple_third_term = model.NewIntVar(0, cp_model.INT32_MAX, 'add joints amber cost multiple third term') model.AddGeneralMultiplicationEquality(add_joints_amber_cost_multiple_third_term, 1000, base_joints, add_joints) add_joints_amber_cost_multiple_fourth_term = model.NewIntVar(0, cp_model.INT32_MAX, 'add joints amber cost multiple fourth term') add_joints_amber_cost_multiple_fourth_term_numerator = model.NewIntVar(0, cp_model.INT32_MAX, 'add joints amber cost multiple fourth term numerator') add_joints_amber_cost_multiple_fourth_term_numerator_first_term = model.NewIntVar(0, cp_model.INT32_MAX, 'add joints amber cost multiple fourth term numerator first term') model.AddGeneralMultiplicationEquality(add_joints_amber_cost_multiple_fourth_term_numerator_first_term, 4000, add_joints, add_joints, add_joints) model.Add(add_joints_amber_cost_multiple_fourth_term_numerator == add_joints_amber_cost_multiple_fourth_term_numerator_first_term + 2000*add_joints) model.AddDivisionEquality(add_joints_amber_cost_multiple_fourth_term, add_joints_amber_cost_multiple_fourth_term_numerator, 3) del add_joints_amber_cost_multiple_fourth_term_numerator, add_joints_amber_cost_multiple_fourth_term_numerator_first_term add_joints_amber_cost_multiple_fifth_term = model.NewIntVar(0, cp_model.INT32_MAX, 'add joints amber cost multiple fifth term') model.AddGeneralMultiplicationEquality(add_joints_amber_cost_multiple_fifth_term, 2000, add_joints, add_joints) model.Add(add_joints_amber_cost_multiple == add_joints_amber_cost_multiple_first_term + add_joints_amber_cost_multiple_second_term - add_joints_amber_cost_multiple_third_term + add_joints_amber_cost_multiple_fourth_term - add_joints_amber_cost_multiple_fifth_term) del add_joints_amber_cost_multiple_first_term, add_joints_amber_cost_multiple_second_term, add_joints_amber_cost_multiple_third_term, add_joints_amber_cost_multiple_fourth_term, add_joints_amber_cost_multiple_fifth_term model.AddMultiplicationEquality(add_joints_amber_cost, [add_joints, add_joints_amber_cost_multiple]) del add_joints, add_joints_amber_cost_multiple cost = model.NewIntVar(0, MAXIMUM_COST, 'cost') model.Add(cp_model.LinearExpr.ScalProd(actions.values(), [int(action.cost) for action in actions.keys()]) + add_joints_amber_cost + sale_cost == cost) del sale_cost, add_joints_amber_cost # Type of skeleton skeleton_in_progress = model.NewIntVar(0, cp_model.INT32_MAX, 'skeleton in progress') # Chimera model.Add(skeleton_in_progress == 100) \ .OnlyEnforceIf(declarations[Declaration.CHIMERA]) # Humanoid model.Add(skeleton_in_progress == 110) \ .OnlyEnforceIf(declarations[Declaration.HUMANOID]) \ .OnlyEnforceIf(model.NewIntermediateBoolVar('humanoid antiquity', antiquity, cp_model.Domain.FromFlatIntervals([cp_model.INT_MIN, 0]))) # Ancient Humanoid (UNCERTAIN) model.Add(skeleton_in_progress == 111) \ .OnlyEnforceIf(declarations[Declaration.HUMANOID]) \ .OnlyEnforceIf(model.NewIntermediateBoolVar('ancient humanoid antiquity', antiquity, cp_model.Domain.FromFlatIntervals([1, 5]))) # Neanderthal model.Add(skeleton_in_progress == 112) \ .OnlyEnforceIf(declarations[Declaration.HUMANOID]) \ .OnlyEnforceIf(model.NewIntermediateBoolVar('neanderthal antiquity', antiquity, cp_model.Domain.FromFlatIntervals([6, cp_model.INT_MAX]))) # Ape (UNCERTAIN) model.Add(skeleton_in_progress == 120) \ .OnlyEnforceIf(declarations[Declaration.APE]) \ .OnlyEnforceIf(model.NewIntermediateBoolVar('ape antiquity', antiquity, cp_model.Domain.FromFlatIntervals([cp_model.INT_MIN, 1]))) # Primordial Ape (UNCERTAIN) model.Add(skeleton_in_progress == 121) \ .OnlyEnforceIf(declarations[Declaration.APE]) \ .OnlyEnforceIf(model.NewIntermediateBoolVar('primordial ape antiquity', antiquity, cp_model.Domain.FromFlatIntervals([2, cp_model.INT_MAX]))) # Monkey model.Add(skeleton_in_progress == 125) \ .OnlyEnforceIf(declarations[Declaration.MONKEY]) \ .OnlyEnforceIf(model.NewIntermediateBoolVar('monkey antiquity', antiquity, cp_model.Domain.FromFlatIntervals([cp_model.INT_MIN, 0]))) # Catarrhine Monkey (UNCERTAIN) model.Add(skeleton_in_progress == 126) \ .OnlyEnforceIf(declarations[Declaration.MONKEY]) \ .OnlyEnforceIf(model.NewIntermediateBoolVar('catarrhine monkey 126 antiquity', antiquity, cp_model.Domain.FromFlatIntervals([1, 8]))) # Catarrhine Monkey model.Add(skeleton_in_progress == 128) \ .OnlyEnforceIf(declarations[Declaration.MONKEY]) \ .OnlyEnforceIf(model.NewIntermediateBoolVar('catarrhine monkey 128 antiquity', antiquity, cp_model.Domain.FromFlatIntervals([9, cp_model.INT_MAX]))) # Crocodile model.Add(skeleton_in_progress == 160) \ .OnlyEnforceIf(declarations[Declaration.REPTILE]) \ .OnlyEnforceIf(model.NewIntermediateBoolVar('crocodile antiquity', antiquity, cp_model.Domain.FromFlatIntervals([cp_model.INT_MIN, 1]))) # Dinosaur model.Add(skeleton_in_progress == 161) \ .OnlyEnforceIf(declarations[Declaration.REPTILE]) \ .OnlyEnforceIf(model.NewIntermediateBoolVar('dinosaur antiquity', antiquity, cp_model.Domain.FromFlatIntervals([2, 4]))) # Mesosaur (UNCERTAIN) model.Add(skeleton_in_progress == 162) \ .OnlyEnforceIf(declarations[Declaration.REPTILE]) \ .OnlyEnforceIf(model.NewIntermediateBoolVar('mesosaur antiquity', antiquity, cp_model.Domain.FromFlatIntervals([5, cp_model.INT_MAX]))) # Toad model.Add(skeleton_in_progress == 170) \ .OnlyEnforceIf(declarations[Declaration.AMPHIBIAN]) \ .OnlyEnforceIf(model.NewIntermediateBoolVar('toad antiquity', antiquity, cp_model.Domain.FromFlatIntervals([cp_model.INT_MIN, 1]))) # Primordial Amphibian model.Add(skeleton_in_progress == 171) \ .OnlyEnforceIf(declarations[Declaration.AMPHIBIAN]) \ .OnlyEnforceIf(model.NewIntermediateBoolVar('primordial amphibian antiquity', antiquity, cp_model.Domain.FromFlatIntervals([2, 4]))) # Temnospondyl model.Add(skeleton_in_progress == 172) \ .OnlyEnforceIf(declarations[Declaration.AMPHIBIAN]) \ .OnlyEnforceIf(model.NewIntermediateBoolVar('temnospondyl antiquity', antiquity, cp_model.Domain.FromFlatIntervals([5, cp_model.INT_MAX]))) # Owl model.Add(skeleton_in_progress == 180) \ .OnlyEnforceIf(declarations[Declaration.BIRD]) \ .OnlyEnforceIf(model.NewIntermediateBoolVar('owl antiquity', antiquity, cp_model.Domain.FromFlatIntervals([cp_model.INT_MIN, 1]))) # Archaeopteryx model.Add(skeleton_in_progress == 181) \ .OnlyEnforceIf(declarations[Declaration.BIRD]) \ .OnlyEnforceIf(model.NewIntermediateBoolVar('archaeopteryx antiquity', antiquity, cp_model.Domain.FromFlatIntervals([2, 4]))) # Ornithomimosaur (UNCERTAIN) model.Add(skeleton_in_progress == 182) \ .OnlyEnforceIf(declarations[Declaration.BIRD]) \ .OnlyEnforceIf(model.NewIntermediateBoolVar('ornithomimosaur antiquity', antiquity, cp_model.Domain.FromFlatIntervals([5, cp_model.INT_MAX]))) # Lamprey model.Add(skeleton_in_progress == 190) \ .OnlyEnforceIf(declarations[Declaration.FISH]) \ .OnlyEnforceIf(model.NewIntermediateBoolVar('lamprey antiquity', antiquity, cp_model.Domain.FromFlatIntervals([cp_model.INT_MIN, 0]))) # Coelacanth (UNCERTAIN) model.Add(skeleton_in_progress == 191) \ .OnlyEnforceIf(declarations[Declaration.FISH]) \ .OnlyEnforceIf(model.NewIntermediateBoolVar('coelacanth antiquity', antiquity, cp_model.Domain.FromFlatIntervals([1, cp_model.INT_MAX]))) # Spider (UNCERTAIN) model.Add(skeleton_in_progress == 200) \ .OnlyEnforceIf(declarations[Declaration.SPIDER]) \ .OnlyEnforceIf(model.NewIntermediateBoolVar('spider antiquity', antiquity, cp_model.Domain.FromFlatIntervals([cp_model.INT_MIN, 1]))) # Primordial Orb-Weaver (UNCERTAIN) model.Add(skeleton_in_progress == 201) \ .OnlyEnforceIf(declarations[Declaration.SPIDER]) \ .OnlyEnforceIf(model.NewIntermediateBoolVar('primordial orb-weaver antiquity', antiquity, cp_model.Domain.FromFlatIntervals([2, 7]))) # Trigonotarbid model.Add(skeleton_in_progress == 203) \ .OnlyEnforceIf(declarations[Declaration.SPIDER]) \ .OnlyEnforceIf(model.NewIntermediateBoolVar('trigonotarbid antiquity', antiquity, cp_model.Domain.FromFlatIntervals([8, cp_model.INT_MAX]))) # Beetle (UNCERTAIN) model.Add(skeleton_in_progress == 210) \ .OnlyEnforceIf(declarations[Declaration.INSECT]) \ .OnlyEnforceIf(model.NewIntermediateBoolVar('beetle antiquity', antiquity, cp_model.Domain.FromFlatIntervals([cp_model.INT_MIN, 1]))) # Primordial Beetle (UNCERTAIN) model.Add(skeleton_in_progress == 211) \ .OnlyEnforceIf(declarations[Declaration.INSECT]) \ .OnlyEnforceIf(model.NewIntermediateBoolVar('primordial beetle antiquity', antiquity, cp_model.Domain.FromFlatIntervals([2, 6]))) # Rhyniognatha model.Add(skeleton_in_progress == 212) \ .OnlyEnforceIf(declarations[Declaration.INSECT]) \ .OnlyEnforceIf(model.NewIntermediateBoolVar('rhyniognatha antiquity', antiquity, cp_model.Domain.FromFlatIntervals([7, cp_model.INT_MAX]))) # Curator model.Add(skeleton_in_progress == 300) \ .OnlyEnforceIf(declarations[Declaration.CURATOR]) # Humanoid requirements model.Add(skulls == 1).OnlyEnforceIf(declarations[Declaration.HUMANOID]) model.Add(legs == 2).OnlyEnforceIf(declarations[Declaration.HUMANOID]) model.Add(arms == 2).OnlyEnforceIf(declarations[Declaration.HUMANOID]) model.Add(torso_style >= 10).OnlyEnforceIf(declarations[Declaration.HUMANOID]) model.Add(torso_style <= 20).OnlyEnforceIf(declarations[Declaration.HUMANOID]) for prohibited_quality in [tails, fins, wings]: model.Add(prohibited_quality == 0).OnlyEnforceIf(declarations[Declaration.HUMANOID]) # Ape requirements model.Add(skulls == 1).OnlyEnforceIf(declarations[Declaration.APE]) model.Add(arms == 4).OnlyEnforceIf(declarations[Declaration.APE]) model.Add(torso_style >= 10).OnlyEnforceIf(declarations[Declaration.APE]) model.Add(torso_style <= 20).OnlyEnforceIf(declarations[Declaration.APE]) for prohibited_quality in [legs, tails, fins, wings]: model.Add(prohibited_quality == 0).OnlyEnforceIf(declarations[Declaration.APE]) # Monkey requirements model.Add(skulls == 1).OnlyEnforceIf(declarations[Declaration.MONKEY]) model.Add(arms == 4).OnlyEnforceIf(declarations[Declaration.MONKEY]) model.Add(tails == 1).OnlyEnforceIf(declarations[Declaration.MONKEY]) model.Add(torso_style >= 10).OnlyEnforceIf(declarations[Declaration.MONKEY]) model.Add(torso_style <= 20).OnlyEnforceIf(declarations[Declaration.MONKEY]) for prohibited_quality in [legs, fins, wings]: model.Add(prohibited_quality == 0).OnlyEnforceIf(declarations[Declaration.MONKEY]) # Bird requirements model.Add(legs == 2).OnlyEnforceIf(declarations[Declaration.BIRD]) model.Add(wings == 2).OnlyEnforceIf(declarations[Declaration.BIRD]) model.Add(torso_style >= 20).OnlyEnforceIf(declarations[Declaration.BIRD]) for prohibited_quality in [arms, fins]: model.Add(prohibited_quality == 0).OnlyEnforceIf(declarations[Declaration.BIRD]) model.Add(tails < 2).OnlyEnforceIf(declarations[Declaration.BIRD]) # Curator requirements model.Add(skulls == 1).OnlyEnforceIf(declarations[Declaration.CURATOR]) model.Add(arms == 2).OnlyEnforceIf(declarations[Declaration.CURATOR]) model.Add(legs == 2).OnlyEnforceIf(declarations[Declaration.CURATOR]) model.Add(wings == 2).OnlyEnforceIf(declarations[Declaration.CURATOR]) for prohibited_quality in [fins, tails]: model.Add(prohibited_quality == 0).OnlyEnforceIf(declarations[Declaration.CURATOR]) # Reptile requirements model.Add(torso_style >= 20).OnlyEnforceIf(declarations[Declaration.REPTILE]) model.Add(tails == 1).OnlyEnforceIf(declarations[Declaration.REPTILE]) model.Add(skulls == 1).OnlyEnforceIf(declarations[Declaration.REPTILE]) for prohibited_quality in [fins, wings, arms]: model.Add(prohibited_quality == 0).OnlyEnforceIf(declarations[Declaration.REPTILE]) model.Add(legs < 5).OnlyEnforceIf(declarations[Declaration.REPTILE]) # Amphibian requirements model.Add(torso_style >= 20).OnlyEnforceIf(declarations[Declaration.AMPHIBIAN]) model.Add(legs == 4).OnlyEnforceIf(declarations[Declaration.AMPHIBIAN]) model.Add(skulls == 1).OnlyEnforceIf(declarations[Declaration.AMPHIBIAN]) for prohibited_quality in [tails, fins, wings, arms]: model.Add(prohibited_quality == 0).OnlyEnforceIf(declarations[Declaration.AMPHIBIAN]) # Fish requirements model.Add(skulls == 1).OnlyEnforceIf(declarations[Declaration.FISH]) model.Add(fins >= 2).OnlyEnforceIf(declarations[Declaration.FISH]) model.Add(tails <= 1).OnlyEnforceIf(declarations[Declaration.FISH]) model.Add(torso_style >= 20).OnlyEnforceIf(declarations[Declaration.FISH]) for prohibited_quality in [arms, legs, wings]: model.Add(prohibited_quality == 0).OnlyEnforceIf(declarations[Declaration.FISH]) # Insect requirements model.Add(skulls == 1).OnlyEnforceIf(declarations[Declaration.INSECT]) model.Add(legs == 6).OnlyEnforceIf(declarations[Declaration.INSECT]) model.Add(torso_style >= 20).OnlyEnforceIf(declarations[Declaration.INSECT]) for prohibited_quality in [arms, fins, tails]: model.Add(prohibited_quality == 0).OnlyEnforceIf(declarations[Declaration.INSECT]) model.Add(wings < 5).OnlyEnforceIf(declarations[Declaration.INSECT]) # Spider requirements model.Add(legs == 8).OnlyEnforceIf(declarations[Declaration.SPIDER]) model.Add(tails <= 1).OnlyEnforceIf(declarations[Declaration.SPIDER]) model.Add(torso_style >= 20).OnlyEnforceIf(declarations[Declaration.SPIDER]) for prohibited_quality in [skulls, arms, wings, fins]: model.Add(prohibited_quality == 0).OnlyEnforceIf(declarations[Declaration.SPIDER]) # Skeleton must be finished for needed_quality in [lambda action: action.skulls_needed, lambda action: action.limbs_needed, lambda action: action.tails_needed]: model.Add(cp_model.LinearExpr.ScalProd(actions.values(), [needed_quality(action) for action in actions.keys()]) == 0) if data['buyer'] == Buyer.A_PALAEONTOLOGIST_WITH_HOARDING_PROPENSITIES: model.Add(skeleton_in_progress >= 100) # Revenue model.Add(primary_revenue == value + 5) model.Add(secondary_revenue == 500) # Difficulty Level model.Add(difficulty_level == 40*implausibility) # Exhaustion model.Add(exhaustion == cp_model.LinearExpr.ScalProd(actions.values(), [action.exhaustion for action in actions.keys()])) elif data['buyer'] == Buyer.A_NAIVE_COLLECTOR: model.Add(skeleton_in_progress >= 100) value_remainder = model.NewIntVar(0, 249, 'value remainder') model.AddModuloEquality(value_remainder, value, 250) # Revenue model.Add(primary_revenue == value - value_remainder) model.Add(secondary_revenue == 0) # Difficulty Level model.Add(difficulty_level == 25*implausibility) # Exhaustion model.Add(exhaustion == cp_model.LinearExpr.ScalProd(actions.values(), [action.exhaustion for action in actions.keys()])) elif data['buyer'] == Buyer.A_FAMILIAR_BOHEMIAN_SCULPTRESS: model.Add(skeleton_in_progress >= 100) model.Add(antiquity <= 0) value_remainder = model.NewIntVar(0, 249, 'value remainder') model.AddModuloEquality(value_remainder, value, 250) # Revenue model.Add(primary_revenue == value - value_remainder + 1000) model.Add(secondary_revenue == 250*counter_church) # Difficulty Level model.Add(difficulty_level == 50*implausibility) # Exhaustion model.Add(exhaustion == cp_model.LinearExpr.ScalProd(actions.values(), [action.exhaustion for action in actions.keys()])) elif data['buyer'] == Buyer.A_PEDAGOGICALLY_INCLINED_GRANDMOTHER: model.Add(skeleton_in_progress >= 100) model.Add(menace <= 0) value_remainder = model.NewIntVar(0, 49, 'value remainder') model.AddModuloEquality(value_remainder, value, 50) # Revenue model.Add(primary_revenue == value - value_remainder + 1000) model.Add(secondary_revenue == 0) # Difficulty Level model.Add(difficulty_level == 50*implausibility) # Exhaustion model.Add(exhaustion == cp_model.LinearExpr.ScalProd(actions.values(), [action.exhaustion for action in actions.keys()])) elif data['buyer'] == Buyer.A_THEOLOGIAN_OF_THE_OLD_SCHOOL: model.Add(skeleton_in_progress >= 100) model.Add(amalgamy <= 0) value_remainder = model.NewIntVar(0, 249, 'value remainder') model.AddModuloEquality(value_remainder, value, 250) # Revenue model.Add(primary_revenue == value - value_remainder + 1000) model.Add(secondary_revenue == 0) # Difficulty Level model.Add(difficulty_level == 50*implausibility) # Exhaustion model.Add(exhaustion == cp_model.LinearExpr.ScalProd(actions.values(), [action.exhaustion for action in actions.keys()])) elif data['buyer'] == Buyer.AN_ENTHUSIAST_OF_THE_ANCIENT_WORLD: model.Add(skeleton_in_progress >= 100) model.Add(antiquity > 0) value_remainder = model.NewIntVar(0, 49, 'value remainder') model.AddModuloEquality(value_remainder, value, 50) # Revenue model.Add(primary_revenue == value - value_remainder) model.Add(secondary_revenue == 250*antiquity + (250 if data['bone_market_fluctuations'] == Fluctuation.ANTIQUITY else 0)) # Difficulty Level model.Add(difficulty_level == 45*implausibility) # Exhaustion model.Add(exhaustion == cp_model.LinearExpr.ScalProd(actions.values(), [action.exhaustion for action in actions.keys()])) elif data['buyer'] == Buyer.MRS_PLENTY: model.Add(skeleton_in_progress >= 100) model.Add(menace > 0) value_remainder = model.NewIntVar(0, 49, 'value remainder') model.AddModuloEquality(value_remainder, value, 50) # Revenue model.Add(primary_revenue == value - value_remainder) model.Add(secondary_revenue == 250*menace) # Difficulty Level model.Add(difficulty_level == 45*implausibility) # Exhaustion model.Add(exhaustion == cp_model.LinearExpr.ScalProd(actions.values(), [action.exhaustion for action in actions.keys()])) elif data['buyer'] == Buyer.A_TENTACLED_SERVANT: model.Add(skeleton_in_progress >= 100) model.Add(amalgamy > 0) value_remainder = model.NewIntVar(0, 49, 'value remainder') model.AddModuloEquality(value_remainder, value, 50) # Revenue model.Add(primary_revenue == value - value_remainder + 250) model.Add(secondary_revenue == 250*amalgamy + (250 if data['bone_market_fluctuations'] == Fluctuation.AMALGAMY else 0)) # Difficulty Level model.Add(difficulty_level == 45*implausibility) # Exhaustion model.Add(exhaustion == cp_model.LinearExpr.ScalProd(actions.values(), [action.exhaustion for action in actions.keys()])) elif data['buyer'] == Buyer.AN_INVESTMENT_MINDED_AMBASSADOR: model.Add(skeleton_in_progress >= 100) model.Add(antiquity > 0) antiquity_squared = model.NewIntVar(0, cp_model.INT32_MAX, 'antiquity squared') model.AddMultiplicationEquality(antiquity_squared, [antiquity, antiquity]) tailfeathers = model.NewIntVar(0, cp_model.INT32_MAX, 'tailfeathers') if data['bone_market_fluctuations'] == Fluctuation.ANTIQUITY: model.AddApproximateExponentiationEquality(tailfeathers, antiquity, 2.2, MAXIMUM_ATTRIBUTE) else: model.Add(tailfeathers == antiquity_squared) value_remainder = model.NewIntVar(0, 49, 'value remainder') model.AddModuloEquality(value_remainder, value, 50) extra_value = model.NewIntermediateBoolVar('extra value', value_remainder, cp_model.Domain.FromFlatIntervals([0, cp_model.INT_MAX])) # Revenue model.Add(primary_revenue == value + 50*extra_value + 250) model.Add(secondary_revenue == 250*tailfeathers) # Difficulty Level model.Add(difficulty_level == 75*implausibility) # Exhaustion derived_exhaustion = model.NewIntVar(0, cp_model.INT32_MAX, 'derived exhaustion') model.AddDivisionEquality(derived_exhaustion, antiquity_squared, 20) model.Add(exhaustion == derived_exhaustion + cp_model.LinearExpr.ScalProd(actions.values(), [action.exhaustion for action in actions.keys()])) elif data['buyer'] == Buyer.A_TELLER_OF_TERRORS: model.Add(skeleton_in_progress >= 100) model.Add(menace > 0) menace_squared = model.NewIntVar(0, cp_model.INT32_MAX, 'menace squared') model.AddMultiplicationEquality(menace_squared, [menace, menace]) value_remainder = model.NewIntVar(0, 9, 'value remainder') model.AddModuloEquality(value_remainder, value, 10) # Revenue model.Add(primary_revenue == value - value_remainder + 50) model.Add(secondary_revenue == 50*menace_squared) # Difficulty Level model.Add(difficulty_level == 75*implausibility) # Exhaustion derived_exhaustion = model.NewIntVar(0, cp_model.INT32_MAX, 'derived exhaustion') model.AddDivisionEquality(derived_exhaustion, menace_squared, 100) model.Add(exhaustion == derived_exhaustion + cp_model.LinearExpr.ScalProd(actions.values(), [action.exhaustion for action in actions.keys()])) elif data['buyer'] == Buyer.A_TENTACLED_ENTREPRENEUR: model.Add(skeleton_in_progress >= 100) model.Add(amalgamy > 0) amalgamy_squared = model.NewIntVar(0, cp_model.INT32_MAX, 'amalgamy squared') model.AddMultiplicationEquality(amalgamy_squared, [amalgamy, amalgamy]) final_breaths = model.NewIntVar(0, cp_model.INT32_MAX, 'final breaths') if data['bone_market_fluctuations'] == Fluctuation.AMALGAMY: model.AddApproximateExponentiationEquality(final_breaths, amalgamy, 2.2, MAXIMUM_ATTRIBUTE) else: model.Add(final_breaths == amalgamy_squared) value_remainder = model.NewIntVar(0, 49, 'value remainder') model.AddModuloEquality(value_remainder, value, 50) # Revenue model.Add(primary_revenue == value - value_remainder + 250) model.Add(secondary_revenue == 50*final_breaths) # Difficulty Level model.Add(difficulty_level == 75*implausibility) # Exhaustion derived_exhaustion = model.NewIntVar(0, cp_model.INT32_MAX, 'derived exhaustion') model.AddDivisionEquality(derived_exhaustion, amalgamy_squared, 100) model.Add(exhaustion == derived_exhaustion + cp_model.LinearExpr.ScalProd(actions.values(), [action.exhaustion for action in actions.keys()])) elif data['buyer'] == Buyer.AN_AUTHOR_OF_GOTHIC_TALES: model.Add(skeleton_in_progress >= 100) model.Add(antiquity > 0) model.Add(menace > 0) antiquity_times_menace = model.NewIntVar(0, cp_model.INT32_MAX, 'antiquity times menace') model.AddMultiplicationEquality(antiquity_times_menace, [antiquity, menace]) value_remainder = model.NewIntVar(0, 49, 'value remainder') model.AddModuloEquality(value_remainder, value, 50) # Revenue model.Add(primary_revenue == value - value_remainder + 250) model.Add(secondary_revenue == 250*antiquity_times_menace + 250*(menace if data['bone_market_fluctuations'] == Fluctuation.ANTIQUITY else 0)) # Difficulty Level model.Add(difficulty_level == 75*implausibility) # Exhaustion derived_exhaustion = model.NewIntVar(0, cp_model.INT32_MAX, 'derived exhaustion') model.AddDivisionEquality(derived_exhaustion, antiquity_times_menace, 20) model.Add(exhaustion == cp_model.LinearExpr.ScalProd(actions.values(), [action.exhaustion for action in actions.keys()]) + derived_exhaustion) elif data['buyer'] == Buyer.A_ZAILOR_WITH_PARTICULAR_INTERESTS: model.Add(skeleton_in_progress >= 100) model.Add(antiquity > 0) model.Add(amalgamy > 0) amalgamy_times_antiquity = model.NewIntVar(0, cp_model.INT32_MAX, 'amalgamy times antiquity') model.AddMultiplicationEquality(amalgamy_times_antiquity, [amalgamy, antiquity]) value_remainder = model.NewIntVar(0, 9, 'value remainder') model.AddModuloEquality(value_remainder, value, 10) # Revenue model.Add(primary_revenue == value - value_remainder + 250) model.Add(secondary_revenue == 250*amalgamy_times_antiquity + 250*(amalgamy if data['bone_market_fluctuations'] == Fluctuation.ANTIQUITY else antiquity if data['bone_market_fluctuations'] == Fluctuation.AMALGAMY else 0)) # Difficulty Level model.Add(difficulty_level == 75*implausibility) # Exhaustion derived_exhaustion = model.NewIntVar(0, cp_model.INT32_MAX, 'derived exhaustion') model.AddDivisionEquality(derived_exhaustion, amalgamy_times_antiquity, 20) model.Add(exhaustion == cp_model.LinearExpr.ScalProd(actions.values(), [action.exhaustion for action in actions.keys()]) + derived_exhaustion) elif data['buyer'] == Buyer.A_RUBBERY_COLLECTOR: model.Add(skeleton_in_progress >= 100) model.Add(amalgamy > 0) model.Add(menace > 0) amalgamy_times_menace = model.NewIntVar(0, cp_model.INT32_MAX, 'amalgamy times menace') model.AddMultiplicationEquality(amalgamy_times_menace, [amalgamy, menace]) value_remainder = model.NewIntVar(0, 49, 'value remainder') model.AddModuloEquality(value_remainder, value, 50) # Revenue model.Add(primary_revenue == value - value_remainder + 250) model.Add(secondary_revenue == 250*amalgamy_times_menace + 250*(menace if data['bone_market_fluctuations'] == Fluctuation.AMALGAMY else 0)) # Difficulty Level model.Add(difficulty_level == 75*implausibility) # Exhaustion derived_exhaustion = model.NewIntVar(0, cp_model.INT32_MAX, 'derived exhaustion') model.AddDivisionEquality(derived_exhaustion, amalgamy_times_menace, 20) model.Add(exhaustion == cp_model.LinearExpr.ScalProd(actions.values(), [action.exhaustion for action in actions.keys()]) + derived_exhaustion) elif data['buyer'] == Buyer.A_CONSTABLE: model.AddLinearExpressionInDomain(skeleton_in_progress, cp_model.Domain.FromFlatIntervals([110, 119])) value_remainder = model.NewIntVar(0, 49, 'value remainder') model.AddModuloEquality(value_remainder, value, 50) # Revenue model.Add(primary_revenue == value - value_remainder + 1000) model.Add(secondary_revenue == 0) # Difficulty Level model.Add(difficulty_level == 50*implausibility) # Exhaustion model.Add(exhaustion == cp_model.LinearExpr.ScalProd(actions.values(), [action.exhaustion for action in actions.keys()])) elif data['buyer'] == Buyer.AN_ENTHUSIAST_IN_SKULLS: model.Add(skeleton_in_progress >= 100) model.Add(skulls >= 2) extra_skulls = model.NewIntVar(0, cp_model.INT32_MAX, 'extra skulls') model.Add(extra_skulls == skulls - 1) vital_intelligence = model.NewIntVar(0, cp_model.INT32_MAX, 'vital intelligence') model.AddApproximateExponentiationEquality(vital_intelligence, extra_skulls, 1.8, MAXIMUM_ATTRIBUTE) # Revenue model.Add(primary_revenue == value) model.Add(secondary_revenue == 1250*vital_intelligence) # Difficulty Level model.Add(difficulty_level == 60*implausibility) # Exhaustion derived_exhaustion = model.NewIntVar(0, cp_model.INT32_MAX, 'derived exhaustion') model.AddDivisionEquality(derived_exhaustion, vital_intelligence, 4) model.Add(exhaustion == cp_model.LinearExpr.ScalProd(actions.values(), [action.exhaustion for action in actions.keys()]) + derived_exhaustion) elif data['buyer'] == Buyer.A_DREARY_MIDNIGHTER: model.AddLinearExpressionInDomain(skeleton_in_progress, cp_model.Domain.FromFlatIntervals([110, 299])) model.Add(amalgamy <= 0) model.Add(counter_church <= 0) value_remainder = model.NewIntVar(0, 2, 'value remainder') model.AddModuloEquality(value_remainder, value, 3) # Revenue model.Add(primary_revenue == value - value_remainder + 300) model.Add(secondary_revenue == 250) # Difficulty Level model.Add(difficulty_level == 100*implausibility) # Exhaustion model.Add(exhaustion == cp_model.LinearExpr.ScalProd(actions.values(), [action.exhaustion for action in actions.keys()])) elif data['buyer'] == Buyer.THE_DUMBWAITER_OF_BALMORAL: model.AddLinearExpressionInDomain(skeleton_in_progress, cp_model.Domain.FromFlatIntervals([180, 189])) model.Add(value >= 250) value_remainder = model.NewIntVar(0, 249, 'value remainder') model.AddModuloEquality(value_remainder, value, 250) # Revenue model.Add(primary_revenue == value - value_remainder) model.Add(secondary_revenue == 0) # Difficulty Level model.Add(difficulty_level == 200) # Exhaustion model.Add(exhaustion == cp_model.LinearExpr.ScalProd(actions.values(), [action.exhaustion for action in actions.keys()])) # Maximize profit margin net_profit = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, 'net profit') 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') 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') 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') 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') 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) model.Add(profit_margin == absolute_profit_margin*-1).OnlyEnforceIf(positive_net_profit.Not()) del multiplied_net_profit, absolute_multiplied_net_profit, absolute_profit_margin, positive_net_profit model.Maximize(profit_margin) solver = cp_model.CpSolver() solver.parameters.num_search_workers = os.cpu_count() solver.parameters.log_search_progress = True status = solver.StatusName(solver.Solve(model)) if status == "INFEASIBLE": raise RuntimeError("There is no satisfactory skeleton.") elif status == "FEASIBLE": print("WARNING: skeleton may be suboptimal.") elif status != "OPTIMAL": raise RuntimeError("Unknown status returned: {}.".format(status)) for action in actions.keys(): for _ in range(int(solver.Value(actions[action]))): print(action) print("\nProfit: £{:,.2f}".format(solver.Value(net_profit)/100)) print("Profit Margin: {:+,.2%}".format(solver.Value(profit_margin)/PROFIT_MARGIN_MULTIPLIER)) print("\nTotal Revenue: £{:,.2f}".format(solver.Value(total_revenue)/100)) print("Primary Revenue: £{:,.2f}".format(solver.Value(primary_revenue)/100)) print("Secondary Revenue: £{:,.2f}".format(solver.Value(secondary_revenue)/100)) print("\nCost: £{:,.2f}".format(solver.Value(cost)/100)) print("\nValue: £{:,.2f}".format(solver.Value(value)/100)) print("Amalgamy: {:n}".format(solver.Value(amalgamy))) print("Antiquity: {:n}".format(solver.Value(antiquity))) print("Menace: {:n}".format(solver.Value(menace))) print("Counter-Church: {:n}".format(solver.Value(counter_church))) print("Implausibility: {:n}".format(solver.Value(implausibility))) print("\nExhaustion: {:n}".format(solver.Value(exhaustion))) if __name__ == '__main__': Solve()