Bone-Market-Solver/Bone Market Solver.py

1469 lines
65 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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