import functools import enum import os import argparse import curses 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 # 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 number of pennies needed to produce a quality. class Cost(enum.Enum): # This is your baseline EPA: the pennies you could generate using an action for a generic grind. ACTION = 400 # Antique Mystery ANTIQUE_MYSTERY = 1250 # Favours: Bohemians # Various opportunity cards BOHEMIAN_FAVOURS = ACTION # Bone Fragment BONE_FRAGMENT = 1 # Cartographer's Hoard CARTOGRAPHERS_HOARD = 31250 # Favours: The Church # Various opportunity cards CHURCH_FAVOURS = ACTION # Collection Note: There's a 'Package' in London # Station VIII Lab COLLECTION_NOTE = ACTION # Volume of Collated Research COLLATED_RESEARCH = 250 # Deep-Zee Catch # Spear-fishing at the bottom of the Evenlode, 7 at a time DEEP_ZEE_CATCH = ACTION/7 # Crustacean Pincer # Ealing Gardens Butcher, 2 at a time CRUSTACEAN_PINCER = (ACTION + DEEP_ZEE_CATCH)/2 # Femur of a Surface Deer # Dumbwaiter of Balmoral, 25 at a time DEER_FEMUR = ACTION/25 # Favours: The Docks # Various opportunity cards DOCK_FAVOURS = ACTION # Extraordinary Implication EXTRAORDINARY_IMPLICATION = 250 # Eyeless Skull # No consistent source EYELESS_SKULL = cp_model.INT32_MAX/2 # Holy Relic of the Thigh of Saint Fiacre # Jericho Locks statue, 2 at a time FIACRE_THIGH = (ACTION + 4*CHURCH_FAVOURS)/2 # Fin Bones, Collected # Hunt and dissect Pinewood Shark, 40 at a time FIN_BONES = (11*ACTION)/40 # Amber-Crusted Fin # Helicon House AMBER_FIN = ACTION + 10*FIN_BONES # Five-Pointed Ribcage # Upwards FIVE_POINTED_RIBCAGE = 9*ACTION + CARTOGRAPHERS_HOARD # Esteem of the Guild # Jericho Parade, 2 at a time GUILD_ESTEEM = (ACTION + 5*DOCK_FAVOURS)/2 # Skull in Coral # Persephone, 1-2 at a time CORAL_SKULL = 1.5*(2*ACTION + 3*GUILD_ESTEEM) # Headless Skeleton # These are accumulated while acquiring other qualities. HEADLESS_SKELETON = 0 # Hinterland Scrip HINTERLAND_SCRIP = 50 # Fossilised Forelimb # Anning and Daughters FOSSILISED_FORELIMB = 55*HINTERLAND_SCRIP # Hedonist # Handsome Townhouse, 3cp at a time HEDONIST_CP = ACTION/3 # Human Arm # These are accumulated while acquiring other qualities. HUMAN_ARM = 0 # Incisive Observation INCISIVE_OBSERVATION = 50 # Crate of Incorruptible Biscuits INCORRUPTIBLE_BISCUITS = 250 # Inkling of Identity INKLING_OF_IDENTITY = 10 # A Custom-Engraved Skull # Feast of the Exceptional Rose, sent by one player and accepted by another ENGRAVED_SKULL = 2*ACTION + 200*INKLING_OF_IDENTITY # Ivory Humerus # Ealing Gardens statue, 2 at a time IVORY_HUMERUS = (ACTION + 4*BOHEMIAN_FAVOURS)/2 # Jade Fragment JADE_FRAGMENT = 1 # Femur of a Jurassic Beast # Brawling for yourself, large Bone Market crate, 12 at a time JURASSIC_FEMUR = (10*ACTION)/12 # Knotted Humerus # These are accumulated while acquiring other qualities. KNOTTED_HUMERUS = 0 # Nevercold Brass Sliver NEVERCOLD_BRASS = 1 # Obsidian Chitin Tail # No consistent source OBSIDIAN_TAIL = cp_model.INT32_MAX/2 # Parabolan Orange-apple # Parabolan Base-camp, electricity and hedonism, 2 at a time ORANGE_APPLE = (2*ACTION + 100*BONE_FRAGMENT + 21*HEDONIST_CP)/2 # Ivory Femur # Bohemian Sculptress IVORY_FEMUR = ACTION + 750*BONE_FRAGMENT + 3*ORANGE_APPLE # Penny PENNY = 1 # Bright Brass Skull # Merrigans Exchange BRASS_SKULL = 6250*PENNY # Pentagrammic Skull # Upwards PENTAGRAMMIC_SKULL = 9*ACTION # Hand-picked Peppercaps PEPPERCAPS = HINTERLAND_SCRIP # Revisionist Historical Narrative # Waswood REVISIONIST_NARRATIVE = ACTION + 4*EXTRAORDINARY_IMPLICATION + INCISIVE_OBSERVATION # Knob of Scintillack SCINTILLACK = 250 # Searing Enigma SEARING_ENIGMA = 6250 # Carved Ball of Stygian Ivory STYGIAN_IVORY = 250 # Preserved Surface Blooms SURFACE_BLOOMS = 250 # Consignment of Scintillack Snuff # Laboratory Manufacturing SCINTILLACK_SNUFF = (ACTION + 8*SCINTILLACK + SURFACE_BLOOMS)/2 # Elation at Feline Oration # Pinnock ELATION_AT_FELINE_ORATION = ACTION + 2*ANTIQUE_MYSTERY + COLLECTION_NOTE + 2*SCINTILLACK_SNUFF # Oil of Companionship # Station VIII Lab OIL_OF_COMPANIONSHIP = ACTION + ELATION_AT_FELINE_ORATION # Survey of the Neath's Bones # Laboratory Research SURVEY = 6*ACTION/25 # Plaster Tail Bones # Carpenter's Granddaughter, 2 at a time PLASTER_TAIL_BONES = (ACTION + 10*SURVEY)/2 # Human Ribcage # Ealing Gardens HUMAN_RIBCAGE = ACTION + 15*SURVEY # Palaeontological Discovery # Plain of Thirsty Grasses PALAEONTOLOGICAL_DISCOVERY = (ACTION + 140*SURVEY)/6 # Helical Thighbone # Results of Excavation, 6 at a time HELICAL_THIGH = (2*PALAEONTOLOGICAL_DISCOVERY)/6 # Leviathan Frame # Results of Excavation LEVIATHAN_FRAME = 25*PALAEONTOLOGICAL_DISCOVERY # Thorned Ribcage # Iron-Toothed Terror Bird THORNED_RIBCAGE = 6*ACTION # Flourishing Ribcage # Helicon House FLOURISHING_RIBCAGE = ACTION + HUMAN_RIBCAGE + THORNED_RIBCAGE # Time Remaining in the Woods # Compel Ghillie, 7 at a time TIME_REMAINING_IN_THE_WOODS = (ACTION + 4*COLLATED_RESEARCH)/7 # Observation: Red Deer # Balmoral Woods DEER_OBSERVATION = 13*ACTION + 12*TIME_REMAINING_IN_THE_WOODS # Mammoth Ribcage # Keeper of the Marigold Menagerie MAMMOTH_RIBCAGE = ACTION + DEER_OBSERVATION # Observation: Fox # Balmoral Woods FOX_OBSERVATION = 10*ACTION + 8*TIME_REMAINING_IN_THE_WOODS # Doubled Skull # Keeper of the Marigold Menagerie DOUBLED_SKULL = ACTION + FOX_OBSERVATION # Observation: Grouse # Balmoral Woods GROUSE_OBSERVATION = 9*ACTION + 8*TIME_REMAINING_IN_THE_WOODS # Skeleton with Seven Necks # Keeper of the Marigold Menagerie SKELETON_WITH_SEVEN_NECKS = ACTION + GROUSE_OBSERVATION # Nodule of Trembling Amber TREMBLING_AMBER = 1250 # Ribcage with a Bouquet of Eight Spines # Helicon House RIBCAGE_WITH_EIGHT_SPINES = ACTION + 3*SEARING_ENIGMA + SKELETON_WITH_SEVEN_NECKS +THORNED_RIBCAGE + 3*TREMBLING_AMBER # Rubbery Skull # Flute Street, including travel due to quality cap RUBBERY_SKULL = 25*ACTION # Rumour of the Upper River RUMOUR_OF_THE_UPPER_RIVER = 250 # Jet Black Stinger # Hunting with Sophia's, 5 at a time BLACK_STINGER = (ACTION + 5*RUMOUR_OF_THE_UPPER_RIVER)/5 # Prismatic Frame # Expedition at Station VIII PRISMATIC_FRAME = ACTION + OIL_OF_COMPANIONSHIP + 98*RUMOUR_OF_THE_UPPER_RIVER # Unidentified Thigh Bone # These are accumulated while acquiring other qualities. UNIDENTIFIED_THIGH = 0 # Nodule of Warm Amber WARM_AMBER = 10 # Albatross Wing # Ealing Gardens Butcher, 2 at a time ALBATROSS_WING = (ACTION + 2000*BONE_FRAGMENT + 25*WARM_AMBER)/2 # Bat Wing # Ealing Gardens Butcher, 2 at a time BAT_WING = (ACTION + 100*BONE_FRAGMENT + 2*WARM_AMBER)/2 # Horned Skull # Ealing Gardens Butcher HORNED_SKULL = ACTION + 1000*BONE_FRAGMENT + 5*WARM_AMBER # Plated Skull # Ealing Gardens Butcher PLATED_SKULL = ACTION + 1750*BONE_FRAGMENT + INCORRUPTIBLE_BISCUITS + 25*WARM_AMBER # Sabre-toothed Skull # Ealing Gardens Butcher SABRE_TOOTHED_SKULL = ACTION + 4900*BONE_FRAGMENT + 125*WARM_AMBER # Wing of a Young Terror Bird # Ealing Gardens Butcher, 2 at a time TERROR_BIRD_WING = (ACTION + 100*BONE_FRAGMENT + 25*WARM_AMBER)/2 # Tomb-Lion's Tail # Ealing Gardens Butcher TOMB_LION_TAIL = ACTION + 200*BONE_FRAGMENT + 2*WARM_AMBER # Warbler Skeleton # Ealing Gardens Butcher WARBLER_SKELETON = ACTION + 130*BONE_FRAGMENT + 2*WARM_AMBER # Withered Tentacle # Helicon House, 3 at a time WITHERED_TENTACLE = (ACTION + 5*WARM_AMBER)/3 # 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 # An action that affects a skeleton's qualities. class Action: def __init__(self, name, cost, torso_style = None, 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 # Skeleton: Torso Style self.torso_style = torso_style # 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) # Actions that initiate a skeleton. class Torso(enum.Enum): HEADLESS_HUMANOID = Action( "Reassemble your Headless Humanoid", cost = Cost.ACTION.value + Cost.HEADLESS_SKELETON.value, torso_style = 10, value = 250, skulls_needed = 1, arms = 2, legs = 2 ) # Licentiate # VICTIM_SKELETON = Action( # "Supply a skeleton of your own", # cost = Cost.ACTION.value, # torso_style = 10, # value = 250, # skulls_needed = 1, # arms = 2, # legs = 2 # ) HUMAN_RIBCAGE = Action( "Build on the Human Ribcage", cost = Cost.ACTION.value + Cost.HUMAN_RIBCAGE.value, torso_style = 15, value = 1250, skulls_needed = 1, limbs_needed = 4 ) THORNED_RIBCAGE = Action( "Make something of your Thorned Ribcage", cost = Cost.ACTION.value + Cost.THORNED_RIBCAGE.value, torso_style = 20, value = 1250, skulls_needed = 1, limbs_needed = 4, tails_needed = 1, amalgamy = 1, menace = 1 ) SKELETON_WITH_SEVEN_NECKS = Action( "Build on the Skeleton with Seven Necks", cost = Cost.ACTION.value + Cost.SKELETON_WITH_SEVEN_NECKS.value, torso_style = 30, value = 6250, skulls_needed = 7, limbs_needed = 2, legs = 2, amalgamy = 2, menace = 1 ) FLOURISHING_RIBCAGE = Action( "Build on the Flourishing Ribcage", cost = Cost.ACTION.value + Cost.FLOURISHING_RIBCAGE.value, torso_style = 40, value = 1250, skulls_needed = 2, limbs_needed = 6, tails_needed = 1, amalgamy = 2 ) MAMMOTH_RIBCAGE = Action( "Build on the Mammoth Ribcage", cost = Cost.ACTION.value + Cost.MAMMOTH_RIBCAGE.value, torso_style = 50, value = 6250, skulls_needed = 1, limbs_needed = 4, tails_needed = 1, antiquity = 2 ) RIBCAGE_WITH_A_BOUQUET_OF_EIGHT_SPINES = Action( "Build on the Ribcage with the Eight Spines", cost = Cost.ACTION.value + Cost.RIBCAGE_WITH_EIGHT_SPINES.value, torso_style = 60, value = 31250, skulls_needed = 8, limbs_needed = 4, tails_needed = 1, amalgamy = 1, menace = 2 ) LEVIATHAN_FRAME = Action("Build on the Leviathan Frame", cost = Cost.ACTION.value + Cost.LEVIATHAN_FRAME.value, torso_style = 70, value = 31250, skulls_needed = 1, limbs_needed = 2, tails = 1, antiquity = 1, menace = 1 ) PRISMATIC_FRAME = Action( "Build on the Prismatic Frame", cost = Cost.ACTION.value + Cost.PRISMATIC_FRAME.value, torso_style = 80, value = 31250, skulls_needed = 3, limbs_needed = 3, tails_needed = 3, amalgamy = 2, antiquity = 2 ) FIVE_POINTED_FRAME = Action( "Build on the Five-Pointed Frame", cost = Cost.ACTION.value + Cost.FIVE_POINTED_RIBCAGE.value, torso_style = 100, value = 31250, skulls_needed = 5, limbs_needed = 5, amalgamy = 2, menace = 1 ) def __str__(self): return str(self.value) # Actions that are taken immediately after starting a skeleton. class Skull(enum.Enum): BAPTIST_SKULL = Action( "Duplicate the skull of John the Baptist, if you can call that a skull", cost = Cost.ACTION.value + 500*Cost.BONE_FRAGMENT.value + 10*Cost.PEPPERCAPS.value, value = 1250, skulls_needed = -1, skulls = 1, counter_church = 1 ) BRASS_SKULL = Action( "Affix a Bright Brass Skull to your (Skeleton Type)", cost = Cost.ACTION.value + Cost.BRASS_SKULL.value + 200*Cost.NEVERCOLD_BRASS.value, value = 6500, skulls_needed = -1, skulls = 1, implausibility = 2 ) CORAL_SKULL = Action( "Affix a Skull in Coral to your (Skeleton Type)", cost = Cost.ACTION.value + Cost.CORAL_SKULL.value + Cost.SCINTILLACK.value, value = 1750, skulls_needed = -1, skulls = 1, amalgamy = 2 ) DOUBLED_SKULL = Action( "Affix a Doubled Skull to your (Skeleton Type)", cost = Cost.ACTION.value + Cost.DOUBLED_SKULL.value, value = 6250, skulls_needed = -1, skulls = 2, amalgamy = 1, antiquity = 2 ) # Adds Exhaustion # ENGRAVED_SKULL = Action( # "Affix a Custom-Engraved Skull to your (Skeleton Type)", # cost = Cost.ACTION.value + Cost.ENGRAVED_SKULL.value, # value = 10000, # skulls_needed = -1, # skulls = 1, # exhaustion = 2 # ) EYELESS_SKULL = Action( "Affix an Eyeless Skull to your (Skeleton Type)", cost = Cost.ACTION.value + Cost.EYELESS_SKULL.value, value = 3000, skulls_needed = -1, skulls = 1, menace = 2 ) HORNED_SKULL = Action( "Affix a Horned Skull to your (Skeleton Type)", cost = Cost.ACTION.value + Cost.HORNED_SKULL.value, value = 1250, skulls_needed = -1, skulls = 1, antiquity = 1, menace = 2 ) # Seeking the Name of Mr. Eaten # OWN_SKULL = Action( # "Duplicate your own skull and affix it here", # cost = Cost.ACTION.value + 1000*Cost.BONE_FRAGMENT.value, # value = -250, # skulls_needed = -1, # skulls = 1 # ) PENTAGRAMMIC_SKULL = Action( "Affix a Pentagrammic Skull to your (Skeleton Type)", cost = Cost.ACTION.value + Cost.PENTAGRAMMIC_SKULL.value, value = 1250, skulls_needed = -1, skulls = 1, amalgamy = 2, menace = 1 ) PLATED_SKULL = Action( "Affix a Plated Skull to your (Skeleton Type)", cost = Cost.ACTION.value + Cost.PLATED_SKULL.value, value = 2500, skulls_needed = -1, skulls = 1, menace = 2 ) RUBBERY_SKULL = Action( "Affix a Rubbery Skull to your (Skeleton Type)", cost = Cost.ACTION.value + Cost.RUBBERY_SKULL.value, value = 600, skulls_needed = -1, skulls = 1, amalgamy = 1 ) SABRE_TOOTHED_SKULL = Action( "Affix a Sabre-toothed Skull to your (Skeleton Type)", cost = Cost.ACTION.value + Cost.SABRE_TOOTHED_SKULL.value, value = 6250, skulls_needed = -1, skulls = 1, antiquity = 1, menace = 1 ) STYGIAN_IVORY = Action( "Use a Carved Ball of Stygian Ivory to cap off your (Skeleton Type)", cost = Cost.ACTION.value + Cost.STYGIAN_IVORY.value, value = 250, skulls_needed = -1 ) VAKE_SKULL = Action( "Duplicate the Vake's skull and use it to decorate your (Skeleton Type)", cost = Cost.ACTION.value + 6000*Cost.BONE_FRAGMENT.value, value = 6500, skulls_needed = -1, skulls = 1, menace = 3 ) # Licentiate # VICTIM_SKULL = Action( # "Cap this with a victim’s skull", # cost = Cost.ACTION.value, # value = 250, # skulls_needed = -1, # skulls = 1 # ) def __str__(self): return str(self.value) # Actions that are taken once all skulls are added to a skeleton. class Appendage(enum.Enum): # Cost from this scales with limbs and is partially implemented separately ADD_JOINTS = Action( "Add four more joints to your skeleton", cost = Cost.ACTION.value + Cost.TREMBLING_AMBER.value, limbs_needed = 4, amalgamy = 2 ) ALBATROSS_WING = Action( "Put an Albatross Wing on your (Skeleton Type)", cost = Cost.ACTION.value + Cost.ALBATROSS_WING.value, value = 1250, limbs_needed = -1, wings = 1, amalgamy = 1 ) AMBER_FIN = Action( "Attach the Amber-Crusted Fin to your (Skeleton Type)", cost = Cost.ACTION.value + Cost.AMBER_FIN.value, value = 1500, limbs_needed = -1, fins = 1, amalgamy = 1, menace = 1 ) BAT_WING = Action( "Add a Bat Wing to your (Skeleton Type)", cost = Cost.ACTION.value + Cost.BAT_WING.value, value = 1, limbs_needed = -1, wings = 1, menace = -1 ) BLACK_STINGER = Action("Apply a Jet Black Stinger to your (Skeleton Type)", cost = Cost.ACTION.value + Cost.BLACK_STINGER.value, value = 50, tails_needed = -1, tails = 1, menace = 2 ) CRUSTACEAN_PINCER = Action( "Apply a Crustacean Pincer to your (Skeleton Type)", cost = Cost.ACTION.value + Cost.CRUSTACEAN_PINCER.value, limbs_needed = -1, arms = 1, menace = 1 ) DEER_FEMUR = Action( "Apply the Femur of a Surface Deer to your (Skeleton Type)", cost = Cost.ACTION.value + Cost.DEER_FEMUR.value, value = 10, limbs_needed = -1, legs = 1, menace = -1 ) # Counter-Church theology from this scales with torso style and is implemented separately FIACRE_THIGH = Action( "Affix Saint Fiacre's Thigh Relic to your (Skeleton Type)", cost = Cost.ACTION.value + Cost.FIACRE_THIGH.value, value = 1250, limbs_needed = -1, legs = 1 ) FIN_BONES = Action( "Put Fins on your (Skeleton Type)", cost = Cost.ACTION.value + Cost.FIN_BONES.value, value = 50, limbs_needed = -1, fins = 1 ) FOSSILISED_FORELIMB = Action( "Apply a Fossilised Forelimb to your (Skeleton Type)", cost = Cost.ACTION.value + Cost.FOSSILISED_FORELIMB.value, value = 2750, limbs_needed = -1, arms = 1, antiquity = 2 ) HELICAL_THIGH = Action( "Affix the Helical Thighbone to your (Skeleton Type)", cost = Cost.ACTION.value + Cost.HELICAL_THIGH.value, value = 300, limbs_needed = -1, legs = 1, amalgamy = 2 ) HUMAN_ARM = Action( "Join a Human Arm to your (Skeleton Type)", cost = Cost.ACTION.value + Cost.HUMAN_ARM.value, value = 250, limbs_needed = -1, arms = 1, menace = -1 ) IVORY_FEMUR = Action( "Apply an Ivory Femur to your (Skeleton Type)", cost = Cost.ACTION.value + Cost.IVORY_FEMUR.value, value = 6500, limbs_needed = -1, legs = 1 ) IVORY_HUMERUS = Action( "Apply an Ivory Humerus to your (Skeleton Type)", cost = Cost.ACTION.value + Cost.IVORY_HUMERUS.value, value = 1500, limbs_needed = -1, arms = 1 ) JURASSIC_THIGH = Action( "Apply a Jurassic Thigh Bone to your (Skeleton Type)", cost = Cost.ACTION.value + Cost.JURASSIC_FEMUR.value, value = 300, limbs_needed = -1, legs = 1, antiquity = 1 ) KNOTTED_HUMERUS = Action( "Apply a Knotted Humerus to your (Skeleton Type)", cost = Cost.ACTION.value + Cost.KNOTTED_HUMERUS.value, value = 300, limbs_needed = -1, arms = 1, amalgamy = 1 ) OBSIDIAN_TAIL = Action( "Apply an Obsidian Chitin Tail to your (Skeleton Type)", cost = Cost.ACTION.value + Cost.OBSIDIAN_TAIL.value, value = 500, tails_needed = -1, tails = 1, amalgamy = 1 ) PLASTER_TAIL_BONES = Action( "Apply Plaster Tail Bones to your (Skeleton Type)", cost = Cost.ACTION.value + Cost.PLASTER_TAIL_BONES.value, value = 250, tails_needed = -1, tails = 1, implausibility = 1 ) TERROR_BIRD_WING = Action( "Add the Wing of a Young Terror Bird to your (Skeleton Type)", cost = Cost.ACTION.value + Cost.TERROR_BIRD_WING.value, value = 250, limbs_needed = -1, wings = 1, antiquity = 1, menace = 1 ) TOMB_LION_TAIL = Action( "Apply a Tomb-Lion's Tail to your (Skeleton Type)", cost = Cost.ACTION.value + Cost.TOMB_LION_TAIL.value, value = 250, tails_needed = -1, tails = 1, antiquity = 1 ) UNIDENTIFIED_THIGH = Action( "Apply an Unidentified Thigh Bone to your (Skeleton Type)", cost = Cost.ACTION.value + Cost.UNIDENTIFIED_THIGH.value, value = 100, limbs_needed = -1, legs = 1 ) WITHERED_TAIL = Action( "Apply a Withered Tentacle as a tail on your (Skeleton Type)", cost = Cost.ACTION.value + Cost.WITHERED_TENTACLE.value, value = 250, tails_needed = -1, tails = 1, antiquity = -1 ) WITHERED_TENTACLE = Action( "Put a Withered Tentacle on your (Skeleton Type)", cost = Cost.ACTION.value + Cost.WITHERED_TENTACLE.value, value = 250, limbs_needed = -1, tentacles = 1, antiquity = -1 ) REMOVE_TAIL = Action( "Remove the tail from your (Skeleton Type)", cost = Cost.ACTION.value, tails = -1 ) # This sets Skeleton: Tails Needed to 0 and is implemented separately SKIP_TAILS = Action( "Decide your Tailless Animal needs no tail", cost = Cost.ACTION.value ) def __str__(self): return str(self.value) # Actions that are taken after all parts have been added to a skeleton. class Adjustment(enum.Enum): CARVE_AWAY_AGE = Action( "Carve away some evidence of age", cost = Cost.ACTION.value, antiquity = -2 ) DISGUISE_AMALGAMY = Action( "Disguise the amalgamy of this piece", cost = Cost.ACTION.value + 25*Cost.JADE_FRAGMENT.value, amalgamy = -2 ) MAKE_LESS_DREADFUL = Action( "Make your skeleton less dreadful", cost = Cost.ACTION.value, menace = -2 ) def __str__(self): return str(self.value) # Which kind of skeleton is to be declared. class Declaration(enum.Enum): AMPHIBIAN = Action( "Declare your (Skeleton Type) a completed Amphibian", cost = Cost.ACTION.value ) APE = Action( "Declare your (Skeleton Type) a completed Ape", cost = Cost.ACTION.value ) BIRD = Action( "Declare your (Skeleton Type) a completed Bird", cost = Cost.ACTION.value ) CHIMERA = Action( "Declare your (Skeleton Type) a completed Chimera", cost = Cost.ACTION.value, implausibility = 3 ) CURATOR = Action( "Declare your (Skeleton Type) a completed Curator", cost = Cost.ACTION.value ) FISH = Action( "Declare your (Skeleton Type) a completed Fish", cost = Cost.ACTION.value ) HUMANOID = Action( "Declare your (Skeleton Type) a completed Humanoid", cost = Cost.ACTION.value ) INSECT = Action( "Declare your (Skeleton Type) a completed Insect", cost = Cost.ACTION.value ) MONKEY = Action( "Declare your (Skeleton Type) a completed Monkey", cost = Cost.ACTION.value ) REPTILE = Action( "Declare your (Skeleton Type) a completed Reptile", cost = Cost.ACTION.value ) SPIDER = Action( "Declare your (Skeleton Type) a completed Spider", cost = Cost.ACTION.value ) def __str__(self): return str(self.value) # Actions taken after a declaration is made. class Embellishment(enum.Enum): 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) # A way to convert a skeleton into revenue. class Buyer(enum.Enum): 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 ) 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 ) def __str__(self): return str(self.value) # Which skeleton attribute is currently boosted. class Fluctuation(enum.Enum): ANTIQUITY = 1 AMALGAMY = 2 # Which of several unusual buyers are available. class OccasionalBuyer(enum.Enum): 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, ] def Solve(bone_market_fluctuations, zoological_mania, occasional_buyer = None, desired_buyers = [], maximum_cost = cp_model.INT32_MAX, maximum_exhaustion = cp_model.INT32_MAX, time_limit = float('inf'), workers = os.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 ]) # 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()])) multiplied_value = model.NewIntVar(0, cp_model.INT32_MAX*11, "multiplied value") model.Add(multiplied_value == original_value*11).OnlyEnforceIf(actions[zoological_mania]) model.Add(multiplied_value == original_value*10).OnlyEnforceIf(actions[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.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.Add(torso_style >= 10).OnlyEnforceIf(actions[Declaration.HUMANOID]) model.Add(torso_style <= 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.Add(torso_style >= 10).OnlyEnforceIf(actions[Declaration.APE]) model.Add(torso_style <= 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.Add(torso_style >= 10).OnlyEnforceIf(actions[Declaration.MONKEY]) model.Add(torso_style <= 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.2, 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]) 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*menace_squared).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, 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.2, 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]) 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*(menace if bone_market_fluctuations == Fluctuation.ANTIQUITY 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, 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]) 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 if bone_market_fluctuations == Fluctuation.ANTIQUITY else antiquity if bone_market_fluctuations == Fluctuation.AMALGAMY 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, 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]) 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*(menace if bone_market_fluctuations == Fluctuation.AMALGAMY 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, 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 # 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]) # 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) # Prints the steps that comprise a skeleton, as well as relevant attributes. class SkeletonPrinter(cp_model.CpSolverSolutionCallback): def __init__(self): cp_model.CpSolverSolutionCallback.__init__(self) self.__solution_count = 0 # Prints the latest solution of a provided solver. def PrintableSolution(self, solver = None): 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.') parser.add_argument( '-f', '--bone-market-fluctuations', action=EnumAction, type=Fluctuation, required=True, help='current value of Bone Market Fluctuations, which grants various bonuses to certain buyers', dest='bone_market_fluctuations' ) parser.add_argument( '-m', '--zoological-mania', action=EnumAction, type=Declaration, required=True, help='current value of Zoological Mania, which grants a 10%% bonus to value for a certain declaration', dest='zoological_mania' ) buyer = parser.add_mutually_exclusive_group(required=True) buyer.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' ) buyer.add_argument( '-b','--buyer', '--desired-buyer', action=EnumAction, nargs='+', default=[], type=Buyer, help='specific buyer that skeleton should be designed for (if declared repeatedly, will choose from among those provided)', dest='desired_buyers' ) parser.add_argument( '-c', '--cost', '--maximum-cost', default=cp_model.INT32_MAX, type=int, help='maximum number of pennies that should be invested in skeleton', dest='maximum_cost' ) parser.add_argument( '-e', '--exhaustion', '--maximum_exhaustion', default=cp_model.INT32_MAX, type=int, help='maximum exhaustion that skeleton should generate', dest='maximum_exhaustion' ) parser.add_argument( '-v', '--verbose', nargs='?', const='True', default=False, type=bool, help='whether the solver should output search progress rather than showing intermediate solutions', dest='verbose' ) parser.add_argument( '-t', '--time-limit', default=float('inf'), type=float, help='maximum number of seconds that solver runs for', dest='time_limit' ) parser.add_argument( '-w', '--workers', default=os.cpu_count(), type=int, help='number of search worker threads to run in parallel', dest='workers' ) args = parser.parse_args() arguments = (args.bone_market_fluctuations, args.zoological_mania, args.occasional_buyer, args.desired_buyers, args.maximum_cost, args.maximum_exhaustion, args.time_limit, args.workers) if not args.verbose: def WrappedSolve(stdscr, arguments): # Move stdscr to last position return Solve(*arguments, stdscr) print(curses.wrapper(WrappedSolve, arguments)) else: print(Solve(*arguments)) main()