"""Use constraint programming to devise the optimal skeleton at the Bone Market in Fallen London.""" __all__ = ['Declaration', 'DiplomatFascination', 'Fluctuation', 'OccasionalBuyer', 'Solve'] __author__ = "Jeremy Saklad" import argparse import curses from enum import Enum from functools import reduce from os import cpu_count from ortools.sat.python import cp_model from data.adjustments import Adjustment from data.appendages import Appendage from data.costs import Cost from data.declarations import Declaration from data.skulls import Skull from data.torsos import Torso from objects.action import Action # 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 # This is a constant used to calculate difficulty checks. You almost certainly do not need to change this. DIFFICULTY_SCALER = 0.6 def NewIntermediateBoolVar(self, name, expression, domain): """Add a fully-reified implication using an intermediate Boolean variable.""" intermediate = self.NewBoolVar(name) self.AddLinearExpressionInDomain(expression, domain).OnlyEnforceIf(intermediate) self.AddLinearExpressionInDomain(expression, domain.Complement()).OnlyEnforceIf(intermediate.Not()) return intermediate cp_model.CpModel.NewIntermediateBoolVar = NewIntermediateBoolVar del NewIntermediateBoolVar def AddApproximateExponentiationEquality(self, target, var, exp, upto): """Add an approximate exponentiation equality using a lookup table. Set `upto` to a value that is unlikely to come into play. """ return self.AddAllowedAssignments([target, var], [(int(base**exp), base) for base in range(upto + 1)]) cp_model.CpModel.AddApproximateExponentiationEquality = AddApproximateExponentiationEquality del AddApproximateExponentiationEquality def AddGeneralMultiplicationEquality(self, target, *variables): """Add a multiplication equality for any number of terms using intermediate 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 = reduce(function, variables) return self.Add(target == product) cp_model.CpModel.AddGeneralMultiplicationEquality = AddGeneralMultiplicationEquality del AddGeneralMultiplicationEquality class Embellishment(Enum): """An action is taken after a declaration has been made for a skeleton.""" MORE_PLAUSIBLE = Action( "Make it seem just a bit more plausible", cost = Cost.ACTION.value + Cost.REVISIONIST_NARRATIVE.value, implausibility = -1 ) CONVINCING_HISTORY = Action( "Invest great time and skill in coming up with a convincing history", cost = Cost.ACTION.value + 3*Cost.REVISIONIST_NARRATIVE.value, implausibility = -5 ) def __str__(self): return str(self.value) class Buyer(Enum): """An action that converts a skeleton into revenue.""" A_PALAEONTOLOGIST_WITH_HOARDING_PROPENSITIES = Action( "Sell a complete skeleton to the Bone Hoarder", cost = Cost.ACTION.value ) A_NAIVE_COLLECTOR = Action( "Sell your Skeleton to a Naive Collector", cost = Cost.ACTION.value ) A_FAMILIAR_BOHEMIAN_SCULPTRESS = Action( "Sell your Skeleton to the Sculptress", cost = Cost.ACTION.value ) A_PEDAGOGICALLY_INCLINED_GRANDMOTHER = Action( "Sell your skeleton to a Pedagogically Inclined Grandmother", cost = Cost.ACTION.value ) A_THEOLOGIAN_OF_THE_OLD_SCHOOL = Action( "Sell your Skeleton to the Theologian of the Old School", cost = Cost.ACTION.value ) AN_ENTHUSIAST_OF_THE_ANCIENT_WORLD = Action( "Sell your skeleton to an Enthusiast of the Ancient World", cost = Cost.ACTION.value ) MRS_PLENTY = Action( "Sell a complete skeleton to Mrs Plenty", cost = Cost.ACTION.value ) A_TENTACLED_SERVANT = Action( "Sell him your amalgamous skeleton", cost = Cost.ACTION.value ) AN_INVESTMENT_MINDED_AMBASSADOR = Action( "Sell your skeleton to the Ambassador", cost = Cost.ACTION.value ) A_TELLER_OF_TERRORS = Action( "Sell your skeleton to the Teller of Terrors", cost = Cost.ACTION.value ) A_TENTACLED_ENTREPRENEUR = Action( "Sell to the Tentacled Entrepreneur", cost = Cost.ACTION.value ) AN_AUTHOR_OF_GOTHIC_TALES = Action( "Sell to an Author of Gothic Tales", cost = Cost.ACTION.value ) A_ZAILOR_WITH_PARTICULAR_INTERESTS = Action( "Sell your skeleton to a Zailor", cost = Cost.ACTION.value ) A_RUBBERY_COLLECTOR = Action( "Sell to an Enthusiast of a Rubbery Menace", cost = Cost.ACTION.value ) A_CONSTABLE = Action( "Sell to a Constable", cost = Cost.ACTION.value ) AN_ENTHUSIAST_IN_SKULLS = Action( "Sell to the Cranial Enthusiast", cost = Cost.ACTION.value ) A_DREARY_MIDNIGHTER = Action( "Sell to the Dreary Midnighter", cost = Cost.ACTION.value ) A_COLOURFUL_PHANTASIST_BAZAARINE = Action( "Sell an amalgamous skeleton as a work of Bazaarine art", cost = Cost.ACTION.value ) A_COLOURFUL_PHANTASIST_NOCTURNAL = Action( "Sell a menacing skeleton as a work of Nocturnal art", cost = Cost.ACTION.value ) A_COLOURFUL_PHANTASIST_CELESTIAL = Action( "Sell an antique skeleton as a work of Celestial art", cost = Cost.ACTION.value ) AN_INGENUOUS_MALACOLOGIST = Action( "Sell him a tentacle-laden skeleton", cost = Cost.ACTION.value ) AN_ENTERPRISING_BOOT_SALESMAN = Action( "Sell to the Enterprising Boot Salesman", cost = Cost.ACTION.value ) THE_DUMBWAITER_OF_BALMORAL = Action( "Export the Skeleton of a Neathy Bird", cost = Cost.ACTION.value ) THE_CARPENTERS_GRANDDAUGHTER = Action( "Impress her with your own constructions", cost = Cost.ACTION.value ) THE_TRIFLING_DIPLOMAT_ANTIQUITY = Action( "Sell the Diplomat an antique skeleton", cost = Cost.ACTION.value ) THE_TRIFLING_DIPLOMAT_BIRD = Action( "Sell the Diplomat a fossil bird", cost = Cost.ACTION.value ) THE_TRIFLING_DIPLOMAT_FISH = Action( "Sell the Diplomat a fossil fish", cost = Cost.ACTION.value ) THE_TRIFLING_DIPLOMAT_INSECT = Action( "Sell the Diplomat a fossil insect", cost = Cost.ACTION.value ) def __str__(self): return str(self.value) class Fluctuation(Enum): """Which skeleton attribute is currently boosted.""" ANTIQUITY = 1 AMALGAMY = 2 MENACE = 3 class OccasionalBuyer(Enum): """Which of several unusual buyers are available.""" AN_ENTHUSIAST_IN_SKULLS = [Buyer.AN_ENTHUSIAST_IN_SKULLS] A_DREARY_MIDNIGHTER = [Buyer.A_DREARY_MIDNIGHTER] A_COLOURFUL_PHANTASIST = [ Buyer.A_COLOURFUL_PHANTASIST_BAZAARINE, Buyer.A_COLOURFUL_PHANTASIST_NOCTURNAL, Buyer.A_COLOURFUL_PHANTASIST_CELESTIAL, ] AN_INGENUOUS_MALACOLOGIST = [Buyer.AN_INGENUOUS_MALACOLOGIST] AN_ENTERPRISING_BOOT_SALESMAN = [Buyer.AN_ENTERPRISING_BOOT_SALESMAN] DiplomatFascination = Enum( 'DiplomatFascination', ( (diplomat.name[22:], diplomat) for diplomat in Buyer if diplomat.name.startswith('THE_TRIFLING_DIPLOMAT_') ), module = __name__ ) DiplomatFascination.__doc__ = "The current fascination of the Trifling Diplomat." def Solve(shadowy_level, bone_market_fluctuations = None, zoological_mania = None, occasional_buyer = None, diplomat_fascination = None, desired_buyers = [], maximum_cost = cp_model.INT32_MAX, maximum_exhaustion = cp_model.INT32_MAX, time_limit = float('inf'), workers = cpu_count(), stdscr = None): model = cp_model.CpModel() actions = {} # Torso for torso in Torso: actions[torso] = model.NewBoolVar(torso.value.name) # Skull for skull in Skull: actions[skull] = model.NewIntVar(0, cp_model.INT32_MAX, skull.value.name) # Appendage for appendage in Appendage: if appendage == Appendage.SKIP_TAILS: actions[appendage] = model.NewBoolVar(appendage.value.name) else: actions[appendage] = model.NewIntVar(0, cp_model.INT32_MAX, appendage.value.name) # Avoid adding joints at first model.AddHint(actions[Appendage.ADD_JOINTS], 0) # Adjustment for adjustment in Adjustment: actions[adjustment] = model.NewIntVar(0, cp_model.INT32_MAX, adjustment.value.name) # Declaration for declaration in Declaration: actions[declaration] = model.NewBoolVar(declaration.value.name) # Try non-Chimera declarations first model.AddHint(actions[Declaration.CHIMERA], 0) # Embellishment for embellishment in Embellishment: actions[embellishment] = model.NewIntVar(0, cp_model.INT32_MAX, embellishment.value.name) # Buyer for buyer in Buyer: actions[buyer] = model.NewBoolVar(buyer.value.name) # Mark unavailable buyers model.AddAssumptions([ actions[buyer].Not() for unavailable_buyer in OccasionalBuyer if unavailable_buyer != occasional_buyer for buyer in unavailable_buyer.value if buyer not in desired_buyers ]) model.AddAssumptions([ actions[outmoded_fascination.value].Not() for outmoded_fascination in DiplomatFascination if outmoded_fascination != diplomat_fascination and outmoded_fascination.value not in desired_buyers ]) # Restrict to desired buyers if desired_buyers: model.Add(cp_model.LinearExpr.Sum([actions[desired_buyer] for desired_buyer in desired_buyers]) == 1) # One torso model.Add(cp_model.LinearExpr.Sum([value for (key, value) in actions.items() if isinstance(key, Torso)]) == 1) # One declaration model.Add(cp_model.LinearExpr.Sum([value for (key, value) in actions.items() if isinstance(key, Declaration)]) == 1) # One buyer model.Add(cp_model.LinearExpr.Sum([value for (key, value) in actions.items() if isinstance(key, Buyer)]) == 1) # Value calculation original_value = model.NewIntVar(0, cp_model.INT32_MAX, 'original value') model.Add(original_value == cp_model.LinearExpr.ScalProd(actions.values(), [action.value.value for action in actions.keys()])) value = model.NewIntVar(0, cp_model.INT32_MAX, 'value') if zoological_mania: multiplier = 115 if zoological_mania in [Declaration.FISH, Declaration.INSECT, Declaration.SPIDER] else 110 multiplied_value = model.NewIntVar(0, cp_model.INT32_MAX, 'multiplied value') model.Add(multiplied_value == multiplier*original_value).OnlyEnforceIf(actions[zoological_mania]) model.Add(multiplied_value == 100*original_value).OnlyEnforceIf(actions[zoological_mania].Not()) model.AddDivisionEquality(value, multiplied_value, 100) else: model.Add(value == original_value) del original_value # Torso Style calculation torso_style = model.NewIntVarFromDomain(cp_model.Domain.FromValues([torso.value.torso_style for torso in Torso]), 'torso style') for torso, torso_variable in {key: value for (key, value) in actions.items() if isinstance(key, Torso)}.items(): model.Add(torso_style == torso.value.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.value.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.value.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.value.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.value.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.value.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.value.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.value.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.value.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.value.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.value.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.value.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 = actions[Appendage.FIACRE_THIGH] 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.value.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') # Exhaustion added by certain buyers added_exhaustion = model.NewIntVar(0, maximum_exhaustion, 'added exhaustion') model.Add(exhaustion == cp_model.LinearExpr.ScalProd(actions.values(), [action.value.exhaustion for action in actions.keys()]) + added_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(cp_model.INT32_MIN, 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*Cost.ACTION.value)), non_zero_difficulty_level) abstract_sale_cost = model.NewIntVar(0, cp_model.INT32_MAX, 'abstract sale cost') model.AddDivisionEquality(abstract_sale_cost, 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, 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 = actions[Appendage.ADD_JOINTS] base_joints = model.NewIntVar(0, cp_model.INT32_MAX, 'base joints') model.Add(base_joints == cp_model.LinearExpr.ScalProd([value for (key, value) in actions.items() if isinstance(key, Torso)], [torso.value.limbs_needed + torso.value.arms + torso.value.legs + torso.value.wings + torso.value.fins + torso.value.tentacles for torso in Torso])) 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, 25, 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, 100, 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, 100, 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, 400, 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 + 200*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, 200, 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.AddGeneralMultiplicationEquality(add_joints_amber_cost, add_joints, add_joints_amber_cost_multiple, Cost.WARM_AMBER.value) del add_joints, add_joints_amber_cost_multiple cost = model.NewIntVar(0, maximum_cost, 'cost') model.Add(cost == cp_model.LinearExpr.ScalProd(actions.values(), [int(action.value.cost) for action in actions.keys()]) + add_joints_amber_cost + sale_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(actions[Declaration.CHIMERA]) # Humanoid model.Add(skeleton_in_progress == 110) \ .OnlyEnforceIf(actions[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(actions[Declaration.HUMANOID]) \ .OnlyEnforceIf(model.NewIntermediateBoolVar('ancient humanoid antiquity', antiquity, cp_model.Domain.FromFlatIntervals([1, 5]))) # Neanderthal model.Add(skeleton_in_progress == 112) \ .OnlyEnforceIf(actions[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(actions[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(actions[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(actions[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(actions[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(actions[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(actions[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(actions[Declaration.REPTILE]) \ .OnlyEnforceIf(model.NewIntermediateBoolVar('dinosaur antiquity', antiquity, cp_model.Domain.FromFlatIntervals([2, 4]))) # Mesosaur (UNCERTAIN) model.Add(skeleton_in_progress == 162) \ .OnlyEnforceIf(actions[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(actions[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(actions[Declaration.AMPHIBIAN]) \ .OnlyEnforceIf(model.NewIntermediateBoolVar('primordial amphibian antiquity', antiquity, cp_model.Domain.FromFlatIntervals([2, 4]))) # Temnospondyl model.Add(skeleton_in_progress == 172) \ .OnlyEnforceIf(actions[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(actions[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(actions[Declaration.BIRD]) \ .OnlyEnforceIf(model.NewIntermediateBoolVar('archaeopteryx antiquity', antiquity, cp_model.Domain.FromFlatIntervals([2, 4]))) # Ornithomimosaur (UNCERTAIN) model.Add(skeleton_in_progress == 182) \ .OnlyEnforceIf(actions[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(actions[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(actions[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(actions[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(actions[Declaration.SPIDER]) \ .OnlyEnforceIf(model.NewIntermediateBoolVar('primordial orb-weaver antiquity', antiquity, cp_model.Domain.FromFlatIntervals([2, 7]))) # Trigonotarbid model.Add(skeleton_in_progress == 203) \ .OnlyEnforceIf(actions[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(actions[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(actions[Declaration.INSECT]) \ .OnlyEnforceIf(model.NewIntermediateBoolVar('primordial beetle antiquity', antiquity, cp_model.Domain.FromFlatIntervals([2, 6]))) # Rhyniognatha model.Add(skeleton_in_progress == 212) \ .OnlyEnforceIf(actions[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(actions[Declaration.CURATOR]) # Humanoid requirements model.Add(skulls == 1).OnlyEnforceIf(actions[Declaration.HUMANOID]) model.Add(legs == 2).OnlyEnforceIf(actions[Declaration.HUMANOID]) model.Add(arms == 2).OnlyEnforceIf(actions[Declaration.HUMANOID]) model.AddLinearExpressionInDomain(torso_style, cp_model.Domain.FromFlatIntervals([10, 20])).OnlyEnforceIf(actions[Declaration.HUMANOID]) for prohibited_quality in [tails, fins, wings]: model.Add(prohibited_quality == 0).OnlyEnforceIf(actions[Declaration.HUMANOID]) # Ape requirements model.Add(skulls == 1).OnlyEnforceIf(actions[Declaration.APE]) model.Add(arms == 4).OnlyEnforceIf(actions[Declaration.APE]) model.AddLinearExpressionInDomain(torso_style, cp_model.Domain.FromFlatIntervals([10, 20])).OnlyEnforceIf(actions[Declaration.APE]) for prohibited_quality in [legs, tails, fins, wings]: model.Add(prohibited_quality == 0).OnlyEnforceIf(actions[Declaration.APE]) # Monkey requirements model.Add(skulls == 1).OnlyEnforceIf(actions[Declaration.MONKEY]) model.Add(arms == 4).OnlyEnforceIf(actions[Declaration.MONKEY]) model.Add(tails == 1).OnlyEnforceIf(actions[Declaration.MONKEY]) model.AddLinearExpressionInDomain(torso_style, cp_model.Domain.FromFlatIntervals([10, 20])).OnlyEnforceIf(actions[Declaration.MONKEY]) for prohibited_quality in [legs, fins, wings]: model.Add(prohibited_quality == 0).OnlyEnforceIf(actions[Declaration.MONKEY]) # Bird requirements model.Add(legs == 2).OnlyEnforceIf(actions[Declaration.BIRD]) model.Add(wings == 2).OnlyEnforceIf(actions[Declaration.BIRD]) model.Add(torso_style >= 20).OnlyEnforceIf(actions[Declaration.BIRD]) for prohibited_quality in [arms, fins]: model.Add(prohibited_quality == 0).OnlyEnforceIf(actions[Declaration.BIRD]) model.Add(tails < 2).OnlyEnforceIf(actions[Declaration.BIRD]) # Curator requirements model.Add(skulls == 1).OnlyEnforceIf(actions[Declaration.CURATOR]) model.Add(arms == 2).OnlyEnforceIf(actions[Declaration.CURATOR]) model.Add(legs == 2).OnlyEnforceIf(actions[Declaration.CURATOR]) model.Add(wings == 2).OnlyEnforceIf(actions[Declaration.CURATOR]) for prohibited_quality in [fins, tails]: model.Add(prohibited_quality == 0).OnlyEnforceIf(actions[Declaration.CURATOR]) # Reptile requirements model.Add(torso_style >= 20).OnlyEnforceIf(actions[Declaration.REPTILE]) model.Add(tails == 1).OnlyEnforceIf(actions[Declaration.REPTILE]) model.Add(skulls == 1).OnlyEnforceIf(actions[Declaration.REPTILE]) for prohibited_quality in [fins, wings, arms]: model.Add(prohibited_quality == 0).OnlyEnforceIf(actions[Declaration.REPTILE]) model.Add(legs < 5).OnlyEnforceIf(actions[Declaration.REPTILE]) # Amphibian requirements model.Add(torso_style >= 20).OnlyEnforceIf(actions[Declaration.AMPHIBIAN]) model.Add(legs == 4).OnlyEnforceIf(actions[Declaration.AMPHIBIAN]) model.Add(skulls == 1).OnlyEnforceIf(actions[Declaration.AMPHIBIAN]) for prohibited_quality in [tails, fins, wings, arms]: model.Add(prohibited_quality == 0).OnlyEnforceIf(actions[Declaration.AMPHIBIAN]) # Fish requirements model.Add(skulls == 1).OnlyEnforceIf(actions[Declaration.FISH]) model.Add(fins >= 2).OnlyEnforceIf(actions[Declaration.FISH]) model.Add(tails <= 1).OnlyEnforceIf(actions[Declaration.FISH]) model.Add(torso_style >= 20).OnlyEnforceIf(actions[Declaration.FISH]) for prohibited_quality in [arms, legs, wings]: model.Add(prohibited_quality == 0).OnlyEnforceIf(actions[Declaration.FISH]) # Insect requirements model.Add(skulls == 1).OnlyEnforceIf(actions[Declaration.INSECT]) model.Add(legs == 6).OnlyEnforceIf(actions[Declaration.INSECT]) model.Add(torso_style >= 20).OnlyEnforceIf(actions[Declaration.INSECT]) for prohibited_quality in [arms, fins, tails]: model.Add(prohibited_quality == 0).OnlyEnforceIf(actions[Declaration.INSECT]) model.Add(wings < 5).OnlyEnforceIf(actions[Declaration.INSECT]) # Spider requirements model.Add(legs == 8).OnlyEnforceIf(actions[Declaration.SPIDER]) model.Add(tails <= 1).OnlyEnforceIf(actions[Declaration.SPIDER]) model.Add(torso_style >= 20).OnlyEnforceIf(actions[Declaration.SPIDER]) for prohibited_quality in [skulls, arms, wings, fins]: model.Add(prohibited_quality == 0).OnlyEnforceIf(actions[Declaration.SPIDER]) # Skeleton must have no unfilled skulls model.Add(cp_model.LinearExpr.ScalProd(actions.values(), [action.value.skulls_needed for action in actions.keys()]) == 0) # Skeleton must have no unfilled limbs model.Add(cp_model.LinearExpr.ScalProd(actions.values(), [action.value.limbs_needed for action in actions.keys()]) == 0) # Skeleton must have no unfilled tails, unless they were skipped model.Add(cp_model.LinearExpr.ScalProd(actions.values(), [action.value.tails_needed for action in actions.keys()]) == 0).OnlyEnforceIf(actions[Appendage.SKIP_TAILS].Not()) model.Add(cp_model.LinearExpr.ScalProd(actions.values(), [action.value.tails_needed for action in actions.keys()]) > 0).OnlyEnforceIf(actions[Appendage.SKIP_TAILS]) # A Palaeontologist with Hoarding Propensities model.Add(skeleton_in_progress >= 100).OnlyEnforceIf(actions[Buyer.A_PALAEONTOLOGIST_WITH_HOARDING_PROPENSITIES]) model.Add(primary_revenue == value + 5).OnlyEnforceIf(actions[Buyer.A_PALAEONTOLOGIST_WITH_HOARDING_PROPENSITIES]) model.Add(secondary_revenue == 500).OnlyEnforceIf(actions[Buyer.A_PALAEONTOLOGIST_WITH_HOARDING_PROPENSITIES]) model.Add(difficulty_level == 40*implausibility).OnlyEnforceIf(actions[Buyer.A_PALAEONTOLOGIST_WITH_HOARDING_PROPENSITIES]) model.Add(added_exhaustion == 0).OnlyEnforceIf(actions[Buyer.A_PALAEONTOLOGIST_WITH_HOARDING_PROPENSITIES]) # A Naive Collector model.Add(skeleton_in_progress >= 100).OnlyEnforceIf(actions[Buyer.A_NAIVE_COLLECTOR]) value_remainder = model.NewIntVar(0, 249, '{}: {}'.format(Buyer.A_NAIVE_COLLECTOR.name, 'value remainder')) model.AddModuloEquality(value_remainder, value, 250) model.Add(primary_revenue == value - value_remainder).OnlyEnforceIf(actions[Buyer.A_NAIVE_COLLECTOR]) model.Add(secondary_revenue == 0).OnlyEnforceIf(actions[Buyer.A_NAIVE_COLLECTOR]) model.Add(difficulty_level == 25*implausibility).OnlyEnforceIf(actions[Buyer.A_NAIVE_COLLECTOR]) model.Add(added_exhaustion == 0).OnlyEnforceIf(actions[Buyer.A_NAIVE_COLLECTOR]) del value_remainder # A Familiar Bohemian Sculptress model.Add(skeleton_in_progress >= 100).OnlyEnforceIf(actions[Buyer.A_FAMILIAR_BOHEMIAN_SCULPTRESS]) model.Add(antiquity <= 0).OnlyEnforceIf(actions[Buyer.A_FAMILIAR_BOHEMIAN_SCULPTRESS]) value_remainder = model.NewIntVar(0, 249, '{}: {}'.format(Buyer.A_FAMILIAR_BOHEMIAN_SCULPTRESS.name, 'value remainder')) model.AddModuloEquality(value_remainder, value, 250) model.Add(primary_revenue == value - value_remainder + 1000).OnlyEnforceIf(actions[Buyer.A_FAMILIAR_BOHEMIAN_SCULPTRESS]) model.Add(secondary_revenue == 250*counter_church).OnlyEnforceIf(actions[Buyer.A_FAMILIAR_BOHEMIAN_SCULPTRESS]) model.Add(difficulty_level == 50*implausibility).OnlyEnforceIf(actions[Buyer.A_FAMILIAR_BOHEMIAN_SCULPTRESS]) model.Add(added_exhaustion == 0).OnlyEnforceIf(actions[Buyer.A_FAMILIAR_BOHEMIAN_SCULPTRESS]) del value_remainder # A Pedagogically Inclined Grandmother model.Add(skeleton_in_progress >= 100).OnlyEnforceIf(actions[Buyer.A_PEDAGOGICALLY_INCLINED_GRANDMOTHER]) model.Add(menace <= 0).OnlyEnforceIf(actions[Buyer.A_PEDAGOGICALLY_INCLINED_GRANDMOTHER]) value_remainder = model.NewIntVar(0, 49, '{}: {}'.format(Buyer.A_PEDAGOGICALLY_INCLINED_GRANDMOTHER.name, 'value remainder')) model.AddModuloEquality(value_remainder, value, 50) model.Add(primary_revenue == value - value_remainder + 1000).OnlyEnforceIf(actions[Buyer.A_PEDAGOGICALLY_INCLINED_GRANDMOTHER]) model.Add(secondary_revenue == 0).OnlyEnforceIf(actions[Buyer.A_PEDAGOGICALLY_INCLINED_GRANDMOTHER]) model.Add(difficulty_level == 50*implausibility).OnlyEnforceIf(actions[Buyer.A_PEDAGOGICALLY_INCLINED_GRANDMOTHER]) model.Add(added_exhaustion == 0).OnlyEnforceIf(actions[Buyer.A_PEDAGOGICALLY_INCLINED_GRANDMOTHER]) del value_remainder # A Theologian of the Old School model.Add(skeleton_in_progress >= 100).OnlyEnforceIf(actions[Buyer.A_THEOLOGIAN_OF_THE_OLD_SCHOOL]) model.Add(amalgamy <= 0).OnlyEnforceIf(actions[Buyer.A_THEOLOGIAN_OF_THE_OLD_SCHOOL]) value_remainder = model.NewIntVar(0, 249, '{}: {}'.format(Buyer.A_THEOLOGIAN_OF_THE_OLD_SCHOOL.name, 'value remainder')) model.AddModuloEquality(value_remainder, value, 250) model.Add(primary_revenue == value - value_remainder + 1000).OnlyEnforceIf(actions[Buyer.A_THEOLOGIAN_OF_THE_OLD_SCHOOL]) model.Add(secondary_revenue == 0).OnlyEnforceIf(actions[Buyer.A_THEOLOGIAN_OF_THE_OLD_SCHOOL]) model.Add(difficulty_level == 50*implausibility).OnlyEnforceIf(actions[Buyer.A_THEOLOGIAN_OF_THE_OLD_SCHOOL]) model.Add(added_exhaustion == 0).OnlyEnforceIf(actions[Buyer.A_THEOLOGIAN_OF_THE_OLD_SCHOOL]) del value_remainder # An Enthusiast of the Ancient World model.Add(skeleton_in_progress >= 100).OnlyEnforceIf(actions[Buyer.AN_ENTHUSIAST_OF_THE_ANCIENT_WORLD]) model.Add(antiquity > 0).OnlyEnforceIf(actions[Buyer.AN_ENTHUSIAST_OF_THE_ANCIENT_WORLD]) value_remainder = model.NewIntVar(0, 49, '{}: {}'.format(Buyer.AN_ENTHUSIAST_OF_THE_ANCIENT_WORLD.name, 'value remainder')) model.AddModuloEquality(value_remainder, value, 50) model.Add(primary_revenue == value - value_remainder).OnlyEnforceIf(actions[Buyer.AN_ENTHUSIAST_OF_THE_ANCIENT_WORLD]) model.Add(secondary_revenue == 250*antiquity + (250 if bone_market_fluctuations == Fluctuation.ANTIQUITY else 0)).OnlyEnforceIf(actions[Buyer.AN_ENTHUSIAST_OF_THE_ANCIENT_WORLD]) model.Add(difficulty_level == 45*implausibility).OnlyEnforceIf(actions[Buyer.AN_ENTHUSIAST_OF_THE_ANCIENT_WORLD]) model.Add(added_exhaustion == 0).OnlyEnforceIf(actions[Buyer.AN_ENTHUSIAST_OF_THE_ANCIENT_WORLD]) del value_remainder # Mrs Plenty model.Add(skeleton_in_progress >= 100).OnlyEnforceIf(actions[Buyer.MRS_PLENTY]) model.Add(menace > 0).OnlyEnforceIf(actions[Buyer.MRS_PLENTY]) value_remainder = model.NewIntVar(0, 49, '{}: {}'.format(Buyer.MRS_PLENTY.name, 'value remainder')) model.AddModuloEquality(value_remainder, value, 50) model.Add(primary_revenue == value - value_remainder).OnlyEnforceIf(actions[Buyer.MRS_PLENTY]) model.Add(secondary_revenue == 250*menace).OnlyEnforceIf(actions[Buyer.MRS_PLENTY]) model.Add(difficulty_level == 45*implausibility).OnlyEnforceIf(actions[Buyer.MRS_PLENTY]) model.Add(added_exhaustion == 0).OnlyEnforceIf(actions[Buyer.MRS_PLENTY]) del value_remainder # A Tentacled Servant model.Add(skeleton_in_progress >= 100).OnlyEnforceIf(actions[Buyer.A_TENTACLED_SERVANT]) model.Add(amalgamy > 0).OnlyEnforceIf(actions[Buyer.A_TENTACLED_SERVANT]) value_remainder = model.NewIntVar(0, 49, '{}: {}'.format(Buyer.A_TENTACLED_SERVANT.name, 'value remainder')) model.AddModuloEquality(value_remainder, value, 50) model.Add(primary_revenue == value - value_remainder + 250).OnlyEnforceIf(actions[Buyer.A_TENTACLED_SERVANT]) model.Add(secondary_revenue == 250*amalgamy + (250 if bone_market_fluctuations == Fluctuation.AMALGAMY else 0)).OnlyEnforceIf(actions[Buyer.A_TENTACLED_SERVANT]) model.Add(difficulty_level == 45*implausibility).OnlyEnforceIf(actions[Buyer.A_TENTACLED_SERVANT]) model.Add(added_exhaustion == 0).OnlyEnforceIf(actions[Buyer.A_TENTACLED_SERVANT]) del value_remainder # An Investment-Minded Ambassador model.Add(skeleton_in_progress >= 100).OnlyEnforceIf(actions[Buyer.AN_INVESTMENT_MINDED_AMBASSADOR]) model.Add(antiquity > 0).OnlyEnforceIf(actions[Buyer.AN_INVESTMENT_MINDED_AMBASSADOR]) antiquity_squared = model.NewIntVar(0, cp_model.INT32_MAX, '{}: {}'.format(Buyer.AN_INVESTMENT_MINDED_AMBASSADOR.name, 'antiquity squared')) model.AddMultiplicationEquality(antiquity_squared, [antiquity, antiquity]) tailfeathers = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.AN_INVESTMENT_MINDED_AMBASSADOR.name, 'tailfeathers')) if bone_market_fluctuations == Fluctuation.ANTIQUITY: model.AddApproximateExponentiationEquality(tailfeathers, antiquity, 2.1, MAXIMUM_ATTRIBUTE) else: model.Add(tailfeathers == antiquity_squared).OnlyEnforceIf(actions[Buyer.AN_INVESTMENT_MINDED_AMBASSADOR]) value_remainder = model.NewIntVar(0, 49, '{}: {}'.format(Buyer.AN_INVESTMENT_MINDED_AMBASSADOR.name, 'value remainder')) model.AddModuloEquality(value_remainder, value, 50) extra_value = model.NewIntermediateBoolVar('{}: {}'.format(Buyer.AN_INVESTMENT_MINDED_AMBASSADOR.name, 'extra value'), value_remainder, cp_model.Domain.FromFlatIntervals([0, cp_model.INT_MAX])) model.Add(primary_revenue == value + 50*extra_value + 250).OnlyEnforceIf(actions[Buyer.AN_INVESTMENT_MINDED_AMBASSADOR]) model.Add(secondary_revenue == 250*tailfeathers).OnlyEnforceIf(actions[Buyer.AN_INVESTMENT_MINDED_AMBASSADOR]) model.Add(difficulty_level == 75*implausibility).OnlyEnforceIf(actions[Buyer.AN_INVESTMENT_MINDED_AMBASSADOR]) # The indirection is necessary for applying an enforcement literal derived_exhaustion = model.NewIntVar(0, cp_model.INT32_MAX, '{}: {}'.format(Buyer.AN_INVESTMENT_MINDED_AMBASSADOR.name, 'derived exhaustion')) model.AddDivisionEquality(derived_exhaustion, antiquity_squared, 20) model.Add(added_exhaustion == derived_exhaustion).OnlyEnforceIf(actions[Buyer.AN_INVESTMENT_MINDED_AMBASSADOR]) del antiquity_squared, tailfeathers, value_remainder, extra_value, derived_exhaustion # A Teller of Terrors model.Add(skeleton_in_progress >= 100).OnlyEnforceIf(actions[Buyer.A_TELLER_OF_TERRORS]) model.Add(menace > 0).OnlyEnforceIf(actions[Buyer.A_TELLER_OF_TERRORS]) menace_squared = model.NewIntVar(0, cp_model.INT32_MAX, '{}: {}'.format(Buyer.A_TELLER_OF_TERRORS.name, 'menace squared')) model.AddMultiplicationEquality(menace_squared, [menace, menace]) feathers = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.A_TELLER_OF_TERRORS.name, 'feathers')) if bone_market_fluctuations == Fluctuation.MENACE: model.AddApproximateExponentiationEquality(feathers, menace, 2.1, MAXIMUM_ATTRIBUTE) else: model.Add(feathers == menace_squared).OnlyEnforceIf(actions[Buyer.A_TELLER_OF_TERRORS]) value_remainder = model.NewIntVar(0, 9, '{}: {}'.format(Buyer.A_TELLER_OF_TERRORS.name, 'value remainder')) model.AddModuloEquality(value_remainder, value, 10) model.Add(primary_revenue == value - value_remainder + 50).OnlyEnforceIf(actions[Buyer.A_TELLER_OF_TERRORS]) model.Add(secondary_revenue == 50*feathers).OnlyEnforceIf(actions[Buyer.A_TELLER_OF_TERRORS]) model.Add(difficulty_level == 75*implausibility).OnlyEnforceIf(actions[Buyer.A_TELLER_OF_TERRORS]) # The indirection is necessary for applying an enforcement literal derived_exhaustion = model.NewIntVar(0, cp_model.INT32_MAX, '{}: {}'.format(Buyer.A_TELLER_OF_TERRORS.name, 'derived exhaustion')) model.AddDivisionEquality(derived_exhaustion, menace_squared, 100) model.Add(added_exhaustion == derived_exhaustion).OnlyEnforceIf(actions[Buyer.A_TELLER_OF_TERRORS]) del menace_squared, feathers, value_remainder, derived_exhaustion # A Tentacled Entrepreneur model.Add(skeleton_in_progress >= 100).OnlyEnforceIf(actions[Buyer.A_TENTACLED_ENTREPRENEUR]) model.Add(amalgamy > 0).OnlyEnforceIf(actions[Buyer.A_TENTACLED_ENTREPRENEUR]) amalgamy_squared = model.NewIntVar(0, cp_model.INT32_MAX, '{}: {}'.format(Buyer.A_TENTACLED_ENTREPRENEUR.name, 'amalgamy squared')) model.AddMultiplicationEquality(amalgamy_squared, [amalgamy, amalgamy]) final_breaths = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.A_TENTACLED_ENTREPRENEUR.name, 'final breaths')) if bone_market_fluctuations == Fluctuation.AMALGAMY: model.AddApproximateExponentiationEquality(final_breaths, amalgamy, 2.1, MAXIMUM_ATTRIBUTE) else: model.Add(final_breaths == amalgamy_squared).OnlyEnforceIf(actions[Buyer.A_TENTACLED_ENTREPRENEUR]) value_remainder = model.NewIntVar(0, 49, '{}: {}'.format(Buyer.A_TENTACLED_ENTREPRENEUR.name, 'value remainder')) model.AddModuloEquality(value_remainder, value, 50) model.Add(primary_revenue == value - value_remainder + 250).OnlyEnforceIf(actions[Buyer.A_TENTACLED_ENTREPRENEUR]) model.Add(secondary_revenue == 50*final_breaths).OnlyEnforceIf(actions[Buyer.A_TENTACLED_ENTREPRENEUR]) model.Add(difficulty_level == 75*implausibility).OnlyEnforceIf(actions[Buyer.A_TENTACLED_ENTREPRENEUR]) # The indirection is necessary for applying an enforcement literal derived_exhaustion = model.NewIntVar(0, cp_model.INT32_MAX, '{}: {}'.format(Buyer.A_TENTACLED_ENTREPRENEUR.name, 'derived exhaustion')) model.AddDivisionEquality(derived_exhaustion, amalgamy_squared, 100) model.Add(added_exhaustion == derived_exhaustion).OnlyEnforceIf(actions[Buyer.A_TENTACLED_ENTREPRENEUR]) del amalgamy_squared, final_breaths, value_remainder, derived_exhaustion # An Author of Gothic Tales model.Add(skeleton_in_progress >= 100).OnlyEnforceIf(actions[Buyer.AN_AUTHOR_OF_GOTHIC_TALES]) model.Add(antiquity > 0).OnlyEnforceIf(actions[Buyer.AN_AUTHOR_OF_GOTHIC_TALES]) model.Add(menace > 0).OnlyEnforceIf(actions[Buyer.AN_AUTHOR_OF_GOTHIC_TALES]) antiquity_times_menace = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.AN_AUTHOR_OF_GOTHIC_TALES.name, 'antiquity times menace')) model.AddMultiplicationEquality(antiquity_times_menace, [antiquity, menace]) antiquity_fluctuation_bonus = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.AN_AUTHOR_OF_GOTHIC_TALES.name, 'antiquity fluctuation bonus')) model.AddDivisionEquality(antiquity_fluctuation_bonus, antiquity, 2) menace_fluctuation_bonus = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.AN_AUTHOR_OF_GOTHIC_TALES.name, 'menace fluctuation bonus')) model.AddDivisionEquality(menace_fluctuation_bonus, menace, 2) value_remainder = model.NewIntVar(0, 49, '{}: {}'.format(Buyer.AN_AUTHOR_OF_GOTHIC_TALES.name, 'value remainder')) model.AddModuloEquality(value_remainder, value, 50) model.Add(primary_revenue == value - value_remainder + 250).OnlyEnforceIf(actions[Buyer.AN_AUTHOR_OF_GOTHIC_TALES]) model.Add(secondary_revenue == 250*antiquity_times_menace + 250*(antiquity_fluctuation_bonus if bone_market_fluctuations == Fluctuation.ANTIQUITY else menace_fluctuation_bonus if bone_market_fluctuations == Fluctuation.MENACE else 0)).OnlyEnforceIf(actions[Buyer.AN_AUTHOR_OF_GOTHIC_TALES]) model.Add(difficulty_level == 75*implausibility).OnlyEnforceIf(actions[Buyer.AN_AUTHOR_OF_GOTHIC_TALES]) # The indirection is necessary for applying an enforcement literal derived_exhaustion = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.AN_AUTHOR_OF_GOTHIC_TALES.name, 'derived exhaustion')) model.AddDivisionEquality(derived_exhaustion, antiquity_times_menace, 20) model.Add(added_exhaustion == derived_exhaustion).OnlyEnforceIf(actions[Buyer.AN_AUTHOR_OF_GOTHIC_TALES]) del antiquity_times_menace, antiquity_fluctuation_bonus, menace_fluctuation_bonus, value_remainder, derived_exhaustion # A Zailor with Particular Interests model.Add(skeleton_in_progress >= 100).OnlyEnforceIf(actions[Buyer.A_ZAILOR_WITH_PARTICULAR_INTERESTS]) model.Add(antiquity > 0).OnlyEnforceIf(actions[Buyer.A_ZAILOR_WITH_PARTICULAR_INTERESTS]) model.Add(amalgamy > 0).OnlyEnforceIf(actions[Buyer.A_ZAILOR_WITH_PARTICULAR_INTERESTS]) amalgamy_times_antiquity = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.A_ZAILOR_WITH_PARTICULAR_INTERESTS.name, 'amalgamy times antiquity')) model.AddMultiplicationEquality(amalgamy_times_antiquity, [amalgamy, antiquity]) amalgamy_fluctuation_bonus = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.A_ZAILOR_WITH_PARTICULAR_INTERESTS.name, 'amalgamy fluctuation bonus')) model.AddDivisionEquality(amalgamy_fluctuation_bonus, amalgamy, 2) antiquity_fluctuation_bonus = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.A_ZAILOR_WITH_PARTICULAR_INTERESTS.name, 'antiquity fluctuation bonus')) model.AddDivisionEquality(antiquity_fluctuation_bonus, antiquity, 2) value_remainder = model.NewIntVar(0, 9, '{}: {}'.format(Buyer.A_ZAILOR_WITH_PARTICULAR_INTERESTS.name, 'value remainder')) model.AddModuloEquality(value_remainder, value, 10) model.Add(primary_revenue == value - value_remainder + 250).OnlyEnforceIf(actions[Buyer.A_ZAILOR_WITH_PARTICULAR_INTERESTS]) model.Add(secondary_revenue == 250*amalgamy_times_antiquity + 250*(amalgamy_fluctuation_bonus if bone_market_fluctuations == Fluctuation.AMALGAMY else antiquity_fluctuation_bonus if bone_market_fluctuations == Fluctuation.ANTIQUITY else 0)).OnlyEnforceIf(actions[Buyer.A_ZAILOR_WITH_PARTICULAR_INTERESTS]) model.Add(difficulty_level == 75*implausibility).OnlyEnforceIf(actions[Buyer.A_ZAILOR_WITH_PARTICULAR_INTERESTS]) # The indirection is necessary for applying an enforcement literal derived_exhaustion = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.A_ZAILOR_WITH_PARTICULAR_INTERESTS.name, 'derived exhaustion')) model.AddDivisionEquality(derived_exhaustion, amalgamy_times_antiquity, 20) model.Add(added_exhaustion == derived_exhaustion).OnlyEnforceIf(actions[Buyer.A_ZAILOR_WITH_PARTICULAR_INTERESTS]) del amalgamy_times_antiquity, amalgamy_fluctuation_bonus, antiquity_fluctuation_bonus, value_remainder, derived_exhaustion # A Rubbery Collector model.Add(skeleton_in_progress >= 100).OnlyEnforceIf(actions[Buyer.A_RUBBERY_COLLECTOR]) model.Add(amalgamy > 0).OnlyEnforceIf(actions[Buyer.A_RUBBERY_COLLECTOR]) model.Add(menace > 0).OnlyEnforceIf(actions[Buyer.A_RUBBERY_COLLECTOR]) amalgamy_times_menace = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.A_RUBBERY_COLLECTOR.name, 'amalgamy times menace')) model.AddMultiplicationEquality(amalgamy_times_menace, [amalgamy, menace]) amalgamy_fluctuation_bonus = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.A_RUBBERY_COLLECTOR.name, 'amalgamy fluctuation bonus')) model.AddDivisionEquality(amalgamy_fluctuation_bonus, amalgamy, 2) menace_fluctuation_bonus = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.A_RUBBERY_COLLECTOR.name, 'menace fluctuation bonus')) model.AddDivisionEquality(menace_fluctuation_bonus, menace, 2) value_remainder = model.NewIntVar(0, 49, '{}: {}'.format(Buyer.A_RUBBERY_COLLECTOR.name, 'value remainder')) model.AddModuloEquality(value_remainder, value, 50) model.Add(primary_revenue == value - value_remainder + 250).OnlyEnforceIf(actions[Buyer.A_RUBBERY_COLLECTOR]) model.Add(secondary_revenue == 250*amalgamy_times_menace + 250*(amalgamy_fluctuation_bonus if bone_market_fluctuations == Fluctuation.AMALGAMY else menace_fluctuation_bonus if bone_market_fluctuations == Fluctuation.MENACE else 0)).OnlyEnforceIf(actions[Buyer.A_RUBBERY_COLLECTOR]) model.Add(difficulty_level == 75*implausibility).OnlyEnforceIf(actions[Buyer.A_RUBBERY_COLLECTOR]) # The indirection is necessary for applying an enforcement literal derived_exhaustion = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.A_RUBBERY_COLLECTOR.name, 'derived exhaustion')) model.AddDivisionEquality(derived_exhaustion, amalgamy_times_menace, 20) model.Add(added_exhaustion == derived_exhaustion).OnlyEnforceIf(actions[Buyer.A_RUBBERY_COLLECTOR]) del amalgamy_times_menace, amalgamy_fluctuation_bonus, menace_fluctuation_bonus, value_remainder, derived_exhaustion # A Constable model.AddLinearExpressionInDomain(skeleton_in_progress, cp_model.Domain.FromFlatIntervals([110, 119])).OnlyEnforceIf(actions[Buyer.A_CONSTABLE]) value_remainder = model.NewIntVar(0, 49, '{}: {}'.format(Buyer.A_CONSTABLE.name, 'value remainder')) model.AddModuloEquality(value_remainder, value, 50) model.Add(primary_revenue == value - value_remainder + 1000).OnlyEnforceIf(actions[Buyer.A_CONSTABLE]) model.Add(secondary_revenue == 0).OnlyEnforceIf(actions[Buyer.A_CONSTABLE]) model.Add(difficulty_level == 50*implausibility).OnlyEnforceIf(actions[Buyer.A_CONSTABLE]) model.Add(added_exhaustion == 0).OnlyEnforceIf(actions[Buyer.A_CONSTABLE]) del value_remainder # An Enthusiast in Skulls model.Add(skeleton_in_progress >= 100).OnlyEnforceIf(actions[Buyer.AN_ENTHUSIAST_IN_SKULLS]) model.Add(skulls >= 2).OnlyEnforceIf(actions[Buyer.AN_ENTHUSIAST_IN_SKULLS]) extra_skulls = model.NewIntVar(0, cp_model.INT32_MAX, '{}: {}'.format(Buyer.AN_ENTHUSIAST_IN_SKULLS.name, 'extra skulls')) model.Add(extra_skulls == skulls - 1).OnlyEnforceIf(actions[Buyer.AN_ENTHUSIAST_IN_SKULLS]) vital_intelligence = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.AN_ENTHUSIAST_IN_SKULLS.name, 'vital intelligence')) model.AddApproximateExponentiationEquality(vital_intelligence, extra_skulls, 1.8, MAXIMUM_ATTRIBUTE) model.Add(primary_revenue == value).OnlyEnforceIf(actions[Buyer.AN_ENTHUSIAST_IN_SKULLS]) model.Add(secondary_revenue == 1250*vital_intelligence).OnlyEnforceIf(actions[Buyer.AN_ENTHUSIAST_IN_SKULLS]) model.Add(difficulty_level == 60*implausibility).OnlyEnforceIf(actions[Buyer.AN_ENTHUSIAST_IN_SKULLS]) # The indirection is necessary for applying an enforcement literal derived_exhaustion = model.NewIntVar(0, cp_model.INT32_MAX, '{}: {}'.format(Buyer.AN_ENTHUSIAST_IN_SKULLS.name, 'derived exhaustion')) model.AddDivisionEquality(derived_exhaustion, vital_intelligence, 4) model.Add(added_exhaustion == derived_exhaustion).OnlyEnforceIf(actions[Buyer.AN_ENTHUSIAST_IN_SKULLS]) del extra_skulls, vital_intelligence, derived_exhaustion # A Dreary Midnighter model.AddLinearExpressionInDomain(skeleton_in_progress, cp_model.Domain.FromFlatIntervals([110, 299])).OnlyEnforceIf(actions[Buyer.A_DREARY_MIDNIGHTER]) model.Add(amalgamy <= 0).OnlyEnforceIf(actions[Buyer.A_DREARY_MIDNIGHTER]) model.Add(counter_church <= 0).OnlyEnforceIf(actions[Buyer.A_DREARY_MIDNIGHTER]) value_remainder = model.NewIntVar(0, 2, '{}: {}'.format(Buyer.A_DREARY_MIDNIGHTER.name, 'value remainder')) model.AddModuloEquality(value_remainder, value, 3) model.Add(primary_revenue == value - value_remainder + 300).OnlyEnforceIf(actions[Buyer.A_DREARY_MIDNIGHTER]) model.Add(secondary_revenue == 250).OnlyEnforceIf(actions[Buyer.A_DREARY_MIDNIGHTER]) model.Add(difficulty_level == 100*implausibility).OnlyEnforceIf(actions[Buyer.A_DREARY_MIDNIGHTER]) model.Add(added_exhaustion == 0).OnlyEnforceIf(actions[Buyer.A_DREARY_MIDNIGHTER]) del value_remainder # A Colourful Phantasist - Bazaarine model.Add(skeleton_in_progress >= 100).OnlyEnforceIf(actions[Buyer.A_COLOURFUL_PHANTASIST_BAZAARINE]) model.Add(implausibility >= 2).OnlyEnforceIf(actions[Buyer.A_COLOURFUL_PHANTASIST_BAZAARINE]) model.Add(amalgamy >= 4).OnlyEnforceIf(actions[Buyer.A_COLOURFUL_PHANTASIST_BAZAARINE]) amalgamy_times_implausibility = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.A_COLOURFUL_PHANTASIST_BAZAARINE.name, 'amalgamy times implausibility')) model.AddMultiplicationEquality(amalgamy_times_implausibility, [amalgamy, implausibility]) bazaarine_poetry = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.A_COLOURFUL_PHANTASIST_BAZAARINE.name, 'bazaarine poetry')) model.Add(bazaarine_poetry == amalgamy_times_implausibility + 1) value_remainder = model.NewIntVar(0, 49, '{}: {}'.format(Buyer.A_COLOURFUL_PHANTASIST_BAZAARINE.name, 'value remainder')) model.AddModuloEquality(value_remainder, value, 50) model.Add(primary_revenue == value - value_remainder + 100).OnlyEnforceIf(actions[Buyer.A_COLOURFUL_PHANTASIST_BAZAARINE]) model.Add(secondary_revenue == 250*bazaarine_poetry).OnlyEnforceIf(actions[Buyer.A_COLOURFUL_PHANTASIST_BAZAARINE]) model.Add(difficulty_level == 0).OnlyEnforceIf(actions[Buyer.A_COLOURFUL_PHANTASIST_BAZAARINE]) # The indirection is necessary for applying an enforcement literal derived_exhaustion = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.A_COLOURFUL_PHANTASIST_BAZAARINE.name, 'derived exhaustion')) model.AddDivisionEquality(derived_exhaustion, bazaarine_poetry, 20) model.Add(added_exhaustion == derived_exhaustion).OnlyEnforceIf(actions[Buyer.A_COLOURFUL_PHANTASIST_BAZAARINE]) del amalgamy_times_implausibility, bazaarine_poetry, value_remainder, derived_exhaustion # A Colourful Phantasist - Nocturnal model.Add(skeleton_in_progress >= 100).OnlyEnforceIf(actions[Buyer.A_COLOURFUL_PHANTASIST_NOCTURNAL]) model.Add(implausibility >= 2).OnlyEnforceIf(actions[Buyer.A_COLOURFUL_PHANTASIST_NOCTURNAL]) model.Add(menace >= 4).OnlyEnforceIf(actions[Buyer.A_COLOURFUL_PHANTASIST_NOCTURNAL]) menace_times_implausibility = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.A_COLOURFUL_PHANTASIST_NOCTURNAL.name, 'menace times implausibility')) model.AddMultiplicationEquality(menace_times_implausibility, [menace, implausibility]) stygian_ivory = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.A_COLOURFUL_PHANTASIST_NOCTURNAL.name, 'stygian ivory')) model.Add(stygian_ivory == menace_times_implausibility + 1) value_remainder = model.NewIntVar(0, 49, '{}: {}'.format(Buyer.A_COLOURFUL_PHANTASIST_NOCTURNAL.name, 'value remainder')) model.AddModuloEquality(value_remainder, value, 50) model.Add(primary_revenue == value - value_remainder + 100).OnlyEnforceIf(actions[Buyer.A_COLOURFUL_PHANTASIST_NOCTURNAL]) model.Add(secondary_revenue == 250*stygian_ivory).OnlyEnforceIf(actions[Buyer.A_COLOURFUL_PHANTASIST_NOCTURNAL]) model.Add(difficulty_level == 0).OnlyEnforceIf(actions[Buyer.A_COLOURFUL_PHANTASIST_NOCTURNAL]) # The indirection is necessary for applying an enforcement literal derived_exhaustion = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.A_COLOURFUL_PHANTASIST_NOCTURNAL.name, 'derived exhaustion')) model.AddDivisionEquality(derived_exhaustion, stygian_ivory, 20) model.Add(added_exhaustion == derived_exhaustion).OnlyEnforceIf(actions[Buyer.A_COLOURFUL_PHANTASIST_NOCTURNAL]) del menace_times_implausibility, stygian_ivory, value_remainder, derived_exhaustion # A Colourful Phantasist - Celestial model.Add(skeleton_in_progress >= 100).OnlyEnforceIf(actions[Buyer.A_COLOURFUL_PHANTASIST_CELESTIAL]) model.Add(implausibility >= 2).OnlyEnforceIf(actions[Buyer.A_COLOURFUL_PHANTASIST_CELESTIAL]) model.Add(antiquity >= 4).OnlyEnforceIf(actions[Buyer.A_COLOURFUL_PHANTASIST_CELESTIAL]) antiquity_times_implausibility = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.A_COLOURFUL_PHANTASIST_CELESTIAL.name, 'antiquity times implausibility')) model.AddMultiplicationEquality(antiquity_times_implausibility, [antiquity, implausibility]) knob_of_scintillack = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.A_COLOURFUL_PHANTASIST_CELESTIAL.name, 'knob of scintillack')) model.Add(knob_of_scintillack == antiquity_times_implausibility + 1) value_remainder = model.NewIntVar(0, 49, '{}: {}'.format(Buyer.A_COLOURFUL_PHANTASIST_CELESTIAL.name, 'value remainder')) model.AddModuloEquality(value_remainder, value, 50) model.Add(primary_revenue == value - value_remainder + 100).OnlyEnforceIf(actions[Buyer.A_COLOURFUL_PHANTASIST_CELESTIAL]) model.Add(secondary_revenue == 250*knob_of_scintillack).OnlyEnforceIf(actions[Buyer.A_COLOURFUL_PHANTASIST_CELESTIAL]) model.Add(difficulty_level == 0).OnlyEnforceIf(actions[Buyer.A_COLOURFUL_PHANTASIST_CELESTIAL]) # The indirection is necessary for applying an enforcement literal derived_exhaustion = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.A_COLOURFUL_PHANTASIST_CELESTIAL.name, 'derived exhaustion')) model.AddDivisionEquality(derived_exhaustion, knob_of_scintillack, 20) model.Add(added_exhaustion == derived_exhaustion).OnlyEnforceIf(actions[Buyer.A_COLOURFUL_PHANTASIST_CELESTIAL]) del antiquity_times_implausibility, knob_of_scintillack, value_remainder, derived_exhaustion # An Ingenuous Malacologist model.Add(tentacles >= 4).OnlyEnforceIf(actions[Buyer.AN_INGENUOUS_MALACOLOGIST]) model.Add(skeleton_in_progress >= 100).OnlyEnforceIf(actions[Buyer.AN_INGENUOUS_MALACOLOGIST]) exponentiated_tentacles = model.NewIntVar(0, cp_model.INT32_MAX, '{}: {}'.format(Buyer.AN_INGENUOUS_MALACOLOGIST.name, 'exponentiated tentacles')) model.AddApproximateExponentiationEquality(exponentiated_tentacles, tentacles, 2.2, MAXIMUM_ATTRIBUTE) collated_research = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.AN_INGENUOUS_MALACOLOGIST.name, 'collated research')) model.AddDivisionEquality(collated_research, exponentiated_tentacles, 5) value_remainder = model.NewIntVar(0, 249, '{}: {}'.format(Buyer.AN_INGENUOUS_MALACOLOGIST.name, 'value remainder')) model.AddModuloEquality(value_remainder, value, 250) model.Add(primary_revenue == value - value_remainder + 250).OnlyEnforceIf(actions[Buyer.AN_INGENUOUS_MALACOLOGIST]) model.Add(secondary_revenue == 250*collated_research).OnlyEnforceIf(actions[Buyer.AN_INGENUOUS_MALACOLOGIST]) model.Add(difficulty_level == 60*implausibility).OnlyEnforceIf(actions[Buyer.AN_INGENUOUS_MALACOLOGIST]) # The indirection is necessary for applying an enforcement literal derived_exhaustion = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.AN_INGENUOUS_MALACOLOGIST.name, 'derived exhaustion')) model.AddDivisionEquality(derived_exhaustion, exponentiated_tentacles, 100) model.Add(added_exhaustion == derived_exhaustion).OnlyEnforceIf(actions[Buyer.AN_INGENUOUS_MALACOLOGIST]) del exponentiated_tentacles, collated_research, value_remainder, derived_exhaustion # An Enterprising Boot Salesman model.Add(menace <= 0).OnlyEnforceIf(actions[Buyer.AN_ENTERPRISING_BOOT_SALESMAN]) model.Add(amalgamy <= 0).OnlyEnforceIf(actions[Buyer.AN_ENTERPRISING_BOOT_SALESMAN]) model.Add(skeleton_in_progress >= 100).OnlyEnforceIf(actions[Buyer.AN_ENTERPRISING_BOOT_SALESMAN]) model.Add(legs >= 4).OnlyEnforceIf(actions[Buyer.AN_ENTERPRISING_BOOT_SALESMAN]) diamonds = model.NewIntVar(0, cp_model.INT32_MAX, '{}: {}'.format(Buyer.AN_ENTERPRISING_BOOT_SALESMAN.name, 'diamonds')) model.AddApproximateExponentiationEquality(diamonds, legs, 2.2, MAXIMUM_ATTRIBUTE) value_remainder = model.NewIntVar(0, 49, '{}: {}'.format(Buyer.AN_ENTERPRISING_BOOT_SALESMAN.name, 'value remainder')) model.AddModuloEquality(value_remainder, value, 50) model.Add(primary_revenue == value - value_remainder).OnlyEnforceIf(actions[Buyer.AN_ENTERPRISING_BOOT_SALESMAN]) model.Add(secondary_revenue == 50*diamonds).OnlyEnforceIf(actions[Buyer.AN_ENTERPRISING_BOOT_SALESMAN]) model.Add(difficulty_level == 0).OnlyEnforceIf(actions[Buyer.AN_ENTERPRISING_BOOT_SALESMAN]) # The indirection is necessary for applying an enforcement literal derived_exhaustion = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.AN_ENTERPRISING_BOOT_SALESMAN.name, 'derived exhaustion')) model.AddDivisionEquality(derived_exhaustion, diamonds, 100) model.Add(added_exhaustion == derived_exhaustion).OnlyEnforceIf(actions[Buyer.AN_ENTERPRISING_BOOT_SALESMAN]) del diamonds, value_remainder, derived_exhaustion # The Dumbwaiter of Balmoral model.AddLinearExpressionInDomain(skeleton_in_progress, cp_model.Domain.FromFlatIntervals([180, 189])).OnlyEnforceIf(actions[Buyer.THE_DUMBWAITER_OF_BALMORAL]) model.Add(value >= 250).OnlyEnforceIf(actions[Buyer.THE_DUMBWAITER_OF_BALMORAL]) value_remainder = model.NewIntVar(0, 249, '{}: {}'.format(Buyer.THE_DUMBWAITER_OF_BALMORAL.name, 'value remainder')) model.AddModuloEquality(value_remainder, value, 250) model.Add(primary_revenue == value - value_remainder).OnlyEnforceIf(actions[Buyer.THE_DUMBWAITER_OF_BALMORAL]) model.Add(secondary_revenue == 0).OnlyEnforceIf(actions[Buyer.THE_DUMBWAITER_OF_BALMORAL]) model.Add(difficulty_level == 200).OnlyEnforceIf(actions[Buyer.THE_DUMBWAITER_OF_BALMORAL]) model.Add(added_exhaustion == 0).OnlyEnforceIf(actions[Buyer.THE_DUMBWAITER_OF_BALMORAL]) del value_remainder # The Carpenter's Granddaughter model.Add(skeleton_in_progress >= 100).OnlyEnforceIf(actions[Buyer.THE_CARPENTERS_GRANDDAUGHTER]) model.Add(value >= 30000).OnlyEnforceIf(actions[Buyer.THE_CARPENTERS_GRANDDAUGHTER]) model.Add(primary_revenue == 31250).OnlyEnforceIf(actions[Buyer.THE_CARPENTERS_GRANDDAUGHTER]) model.Add(secondary_revenue == 0).OnlyEnforceIf(actions[Buyer.THE_CARPENTERS_GRANDDAUGHTER]) model.Add(difficulty_level == 100*implausibility).OnlyEnforceIf(actions[Buyer.THE_CARPENTERS_GRANDDAUGHTER]) model.Add(added_exhaustion == 0).OnlyEnforceIf(actions[Buyer.THE_CARPENTERS_GRANDDAUGHTER]) # The Trifling Diplomat - Antiquity model.Add(skeleton_in_progress >= 100).OnlyEnforceIf(actions[Buyer.THE_TRIFLING_DIPLOMAT_ANTIQUITY]) model.Add(antiquity >= 5).OnlyEnforceIf(actions[Buyer.THE_TRIFLING_DIPLOMAT_ANTIQUITY]) antiquity_squared = model.NewIntVar(0, cp_model.INT32_MAX, '{}: {}'.format(Buyer.THE_TRIFLING_DIPLOMAT_ANTIQUITY.name, 'antiquity squared')) model.AddMultiplicationEquality(antiquity_squared, [antiquity, antiquity]) value_remainder = model.NewIntVar(0, 49, '{}: {}'.format(Buyer.THE_TRIFLING_DIPLOMAT_ANTIQUITY.name, 'value remainder')) model.AddModuloEquality(value_remainder, value, 50) model.Add(primary_revenue == value - value_remainder + 50).OnlyEnforceIf(actions[Buyer.THE_TRIFLING_DIPLOMAT_ANTIQUITY]) model.Add(secondary_revenue == 50*antiquity_squared).OnlyEnforceIf(actions[Buyer.THE_TRIFLING_DIPLOMAT_ANTIQUITY]) model.Add(difficulty_level == 0).OnlyEnforceIf(actions[Buyer.THE_TRIFLING_DIPLOMAT_ANTIQUITY]) # The indirection is necessary for applying an enforcement literal derived_exhaustion = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.THE_TRIFLING_DIPLOMAT_ANTIQUITY.name, 'derived exhaustion')) model.AddDivisionEquality(derived_exhaustion, antiquity_squared, 100) model.Add(added_exhaustion == derived_exhaustion).OnlyEnforceIf(actions[Buyer.THE_TRIFLING_DIPLOMAT_ANTIQUITY]) del antiquity_squared, value_remainder, derived_exhaustion # The Trifling Diplomat - Bird model.AddLinearExpressionInDomain(skeleton_in_progress, cp_model.Domain.FromFlatIntervals([180, 189])).OnlyEnforceIf(actions[Buyer.THE_TRIFLING_DIPLOMAT_BIRD]) non_negative_amalgamy = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.THE_TRIFLING_DIPLOMAT_BIRD.name, 'non-negative amalgamy')) model.AddMaxEquality(non_negative_amalgamy, [amalgamy, 0]) non_negative_menace = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.THE_TRIFLING_DIPLOMAT_BIRD.name, 'non-negative menace')) model.AddMaxEquality(non_negative_menace, [menace, 0]) non_negative_antiquity = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.THE_TRIFLING_DIPLOMAT_BIRD.name, 'non-negative antiquity')) model.AddMaxEquality(non_negative_antiquity, [antiquity, 0]) compromising_documents = model.NewIntVar(0, cp_model.INT32_MAX, '{}: {}'.format(Buyer.THE_TRIFLING_DIPLOMAT_BIRD.name, 'compromising documents')) model.AddGeneralMultiplicationEquality(compromising_documents, non_negative_amalgamy, non_negative_menace, non_negative_antiquity) value_remainder = model.NewIntVar(0, 49, '{}: {}'.format(Buyer.THE_TRIFLING_DIPLOMAT_BIRD.name, 'value remainder')) model.AddModuloEquality(value_remainder, value, 50) model.Add(primary_revenue == value - value_remainder + 50).OnlyEnforceIf(actions[Buyer.THE_TRIFLING_DIPLOMAT_BIRD]) model.Add(secondary_revenue == 50*compromising_documents).OnlyEnforceIf(actions[Buyer.THE_TRIFLING_DIPLOMAT_BIRD]) model.Add(difficulty_level == 0).OnlyEnforceIf(actions[Buyer.THE_TRIFLING_DIPLOMAT_BIRD]) # The indirection is necessary for applying an enforcement literal derived_exhaustion = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.THE_TRIFLING_DIPLOMAT_BIRD.name, 'derived exhaustion')) model.AddDivisionEquality(derived_exhaustion, compromising_documents, 100) model.Add(added_exhaustion == derived_exhaustion).OnlyEnforceIf(actions[Buyer.THE_TRIFLING_DIPLOMAT_BIRD]) del non_negative_amalgamy, non_negative_menace, non_negative_antiquity, compromising_documents, value_remainder, derived_exhaustion # The Trifling Diplomat - Fish model.AddLinearExpressionInDomain(skeleton_in_progress, cp_model.Domain.FromFlatIntervals([190, 199])).OnlyEnforceIf(actions[Buyer.THE_TRIFLING_DIPLOMAT_FISH]) non_negative_amalgamy = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.THE_TRIFLING_DIPLOMAT_FISH.name, 'non-negative amalgamy')) model.AddMaxEquality(non_negative_amalgamy, [amalgamy, 0]) non_negative_menace = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.THE_TRIFLING_DIPLOMAT_FISH.name, 'non-negative menace')) model.AddMaxEquality(non_negative_menace, [menace, 0]) non_negative_antiquity = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.THE_TRIFLING_DIPLOMAT_FISH.name, 'non-negative antiquity')) model.AddMaxEquality(non_negative_antiquity, [antiquity, 0]) compromising_documents = model.NewIntVar(0, cp_model.INT32_MAX, '{}: {}'.format(Buyer.THE_TRIFLING_DIPLOMAT_FISH.name, 'compromising documents')) model.AddGeneralMultiplicationEquality(compromising_documents, non_negative_amalgamy, non_negative_menace, non_negative_antiquity) value_remainder = model.NewIntVar(0, 49, '{}: {}'.format(Buyer.THE_TRIFLING_DIPLOMAT_FISH.name, 'value remainder')) model.AddModuloEquality(value_remainder, value, 50) model.Add(primary_revenue == value - value_remainder + 50).OnlyEnforceIf(actions[Buyer.THE_TRIFLING_DIPLOMAT_FISH]) model.Add(secondary_revenue == 50*compromising_documents).OnlyEnforceIf(actions[Buyer.THE_TRIFLING_DIPLOMAT_FISH]) model.Add(difficulty_level == 0).OnlyEnforceIf(actions[Buyer.THE_TRIFLING_DIPLOMAT_FISH]) # The indirection is necessary for applying an enforcement literal derived_exhaustion = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.THE_TRIFLING_DIPLOMAT_FISH.name, 'derived exhaustion')) model.AddDivisionEquality(derived_exhaustion, compromising_documents, 100) model.Add(added_exhaustion == derived_exhaustion).OnlyEnforceIf(actions[Buyer.THE_TRIFLING_DIPLOMAT_FISH]) del non_negative_amalgamy, non_negative_menace, non_negative_antiquity, compromising_documents, value_remainder, derived_exhaustion # The Trifling Diplomat - Insect model.AddLinearExpressionInDomain(skeleton_in_progress, cp_model.Domain.FromFlatIntervals([210, 219])).OnlyEnforceIf(actions[Buyer.THE_TRIFLING_DIPLOMAT_INSECT]) non_negative_amalgamy = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.THE_TRIFLING_DIPLOMAT_INSECT.name, 'non-negative amalgamy')) model.AddMaxEquality(non_negative_amalgamy, [amalgamy, 0]) non_negative_menace = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.THE_TRIFLING_DIPLOMAT_INSECT.name, 'non-negative menace')) model.AddMaxEquality(non_negative_menace, [menace, 0]) non_negative_antiquity = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.THE_TRIFLING_DIPLOMAT_INSECT.name, 'non-negative antiquity')) model.AddMaxEquality(non_negative_antiquity, [antiquity, 0]) compromising_documents = model.NewIntVar(0, cp_model.INT32_MAX, '{}: {}'.format(Buyer.THE_TRIFLING_DIPLOMAT_INSECT.name, 'compromising documents')) model.AddGeneralMultiplicationEquality(compromising_documents, non_negative_amalgamy, non_negative_menace, non_negative_antiquity) value_remainder = model.NewIntVar(0, 49, '{}: {}'.format(Buyer.THE_TRIFLING_DIPLOMAT_INSECT.name, 'value remainder')) model.AddModuloEquality(value_remainder, value, 50) model.Add(primary_revenue == value - value_remainder + 50).OnlyEnforceIf(actions[Buyer.THE_TRIFLING_DIPLOMAT_INSECT]) model.Add(secondary_revenue == 50*compromising_documents).OnlyEnforceIf(actions[Buyer.THE_TRIFLING_DIPLOMAT_INSECT]) model.Add(difficulty_level == 0).OnlyEnforceIf(actions[Buyer.THE_TRIFLING_DIPLOMAT_INSECT]) # The indirection is necessary for applying an enforcement literal derived_exhaustion = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, '{}: {}'.format(Buyer.THE_TRIFLING_DIPLOMAT_INSECT.name, 'derived exhaustion')) model.AddDivisionEquality(derived_exhaustion, compromising_documents, 100) model.Add(added_exhaustion == derived_exhaustion).OnlyEnforceIf(actions[Buyer.THE_TRIFLING_DIPLOMAT_INSECT]) del non_negative_amalgamy, non_negative_menace, non_negative_antiquity, compromising_documents, value_remainder, derived_exhaustion # 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) class SkeletonPrinter(cp_model.CpSolverSolutionCallback): """A class that prints the steps that comprise a skeleton as well as relevant attributes.""" def __init__(self): cp_model.CpSolverSolutionCallback.__init__(self) self.__solution_count = 0 def PrintableSolution(self, solver = None): """Print the latest solution of a provided solver.""" output = "" # Allows use as a callback if solver is None: solver = self for action in actions.keys(): for _ in range(int(solver.Value(actions[action]))): output += str(action) + "\n" output += "\nProfit: £{:,.2f}\n".format(solver.Value(net_profit)/100) output += "Profit Margin: {:+,.2%}\n".format(solver.Value(profit_margin)/PROFIT_MARGIN_MULTIPLIER) output += "\nTotal Revenue: £{:,.2f}\n".format(solver.Value(total_revenue)/100) output += "Primary Revenue: £{:,.2f}\n".format(solver.Value(primary_revenue)/100) output += "Secondary Revenue: £{:,.2f}\n".format(solver.Value(secondary_revenue)/100) output += "\nCost: £{:,.2f}\n".format(solver.Value(cost)/100) output += "\nValue: £{:,.2f}\n".format(solver.Value(value)/100) output += "Amalgamy: {:n}\n".format(solver.Value(amalgamy)) output += "Antiquity: {:n}\n".format(solver.Value(antiquity)) output += "Menace: {:n}\n".format(solver.Value(menace)) output += "Counter-Church: {:n}\n".format(solver.Value(counter_church)) output += "Implausibility: {:n}\n".format(solver.Value(implausibility)) output += "\nExhaustion: {:n}".format(solver.Value(exhaustion)) return output def OnSolutionCallback(self): self.__solution_count += 1 # Prints current solution to window stdscr.clear() stdscr.addstr(self.PrintableSolution()) stdscr.addstr(stdscr.getmaxyx()[0] - 1, 0, "Skeleton #{:n}".format(self.__solution_count)) stdscr.refresh() def SolutionCount(self): return self.__solution_count printer = SkeletonPrinter() solver = cp_model.CpSolver() solver.parameters.num_search_workers = workers solver.parameters.max_time_in_seconds = time_limit # There's no window in verbose mode if stdscr is None: solver.parameters.log_search_progress = True solver.Solve(model) else: solver.SolveWithSolutionCallback(model, printer) status = solver.StatusName() 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)) return printer.PrintableSolution(solver) class EnumAction(argparse.Action): def __init__(self, **kwargs): # Pop off the type value enum = kwargs.pop('type', None) nargs = kwargs.pop('nargs', None) # Generate choices from the Enum kwargs.setdefault('choices', tuple(member.name.lower() for member in enum)) super(EnumAction, self).__init__(**kwargs) self._enum = enum self._nargs = nargs def __call__(self, parser, namespace, values, option_string=None): # Convert value back into an Enum enum = self._enum[values.upper()] if self._nargs is None or self._nargs == '?': setattr(namespace, self.dest, enum) else: items = getattr(namespace, self.dest, list()) items.append(enum) setattr(namespace, self.dest, items) def main(): parser = argparse.ArgumentParser( prog='Bone Market Solver', description="Devise the optimal skeleton at the Bone Market in Fallen London.", argument_default=argparse.SUPPRESS, ) world_qualities = parser.add_argument_group( "world qualities", "Parameters shared across Fallen London, often changing on a routine basis" ) world_qualities.add_argument( "-f", "--bone-market-fluctuations", action=EnumAction, type=Fluctuation, help="current value of Bone Market Fluctuations, which grants various bonuses to certain buyers", dest='bone_market_fluctuations' ) world_qualities.add_argument( "-m", "--zoological-mania", action=EnumAction, type=Declaration, help="current value of Zoological Mania, which grants a percentage bonus to value for a certain declaration", dest='zoological_mania' ) world_qualities.add_argument( "-o", "--occasional-buyer", action=EnumAction, type=OccasionalBuyer, help="current value of Occasional Buyer, which allows access to a buyer that is not otherwise available", dest='occasional_buyer' ) world_qualities.add_argument( "-d", "--diplomat-fascination", action=EnumAction, type=DiplomatFascination, help="current value of The Diplomat's Current Fascination, which determines what the Trifling Diplomat is interested in", dest='diplomat_fascination' ) skeleton_parameters = parser.add_argument_group( "skeleton parameters", "Parameters that determine what you want the solver to produce" ) skeleton_parameters.add_argument( "-s", "--shadowy", type=int, required=True, help="the effective level of Shadowy used for selling to buyers", dest='shadowy_level' ) skeleton_parameters.add_argument( "-b", "--buyer", "--desired-buyer", action=EnumAction, nargs='+', type=Buyer, help="specific buyer that skeleton should be designed for (if declared repeatedly, will choose from among those provided)", dest='desired_buyers' ) skeleton_parameters.add_argument( "-c", "--cost", "--maximum-cost", type=int, help="maximum number of pennies that should be invested in skeleton", dest='maximum_cost' ) skeleton_parameters.add_argument( "-e", "--exhaustion", "--maximum_exhaustion", type=int, help="maximum exhaustion that skeleton should generate", dest='maximum_exhaustion' ) solver_options = parser.add_argument_group( "solver options", "Options that affect how the solver behaves" ) solver_options.add_argument( "-v", "--verbose", action='store_true', default=False, help="whether the solver should output search progress rather than showing intermediate solutions", dest='verbose' ) solver_options.add_argument( "-t", "--time-limit", type=float, help="maximum number of seconds that the solver runs for", dest='time_limit' ) solver_options.add_argument( "-w", "--workers", type=int, help="number of search worker threads to run in parallel (default: one worker per available CPU thread)", dest='workers' ) args = parser.parse_args() arguments = vars(args) if not arguments.pop('verbose'): def WrappedSolve(stdscr, arguments): # Prevents crash if window is too small to fit text stdscr.scrollok(True) # Move stdscr to last position arguments['stdscr'] = stdscr return Solve(**arguments) print(curses.wrapper(WrappedSolve, arguments)) else: print(Solve(**arguments)) if __name__ == '__main__': main()