Last active
March 10, 2019 16:25
-
-
Save TheFeshy/230a9e62a234ffdb476514f01b847db3 to your computer and use it in GitHub Desktop.
Fallen London Heist simulator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/python3 | |
"""Heist Simulator.""" | |
import random | |
from enum import Enum | |
import argparse | |
import copy | |
""" | |
Heist Simulator for Fallen London. | |
CC NC-SA 2019 | |
""" | |
class Frequency(Enum): | |
"""Card frequencies.BaseException.""" | |
Rarererest = 1 | |
Rare = 10 | |
Unusual = 20 | |
VeryInfrequent = 50 | |
Infrequent = 80 | |
Standard = 100 | |
Frequent = 200 | |
Abundant = 500 | |
Ubiquitous = 1000 | |
class Unlock(Enum): | |
"""Card unlocks.""" | |
Always = 1 | |
EscapeRoute = 100 | |
FiveBurglar = 200 | |
Shuttered = 2 | |
TripleBolted = 3 | |
# WellGuarded=3 TODO: we don't actually have any of these in the game. | |
class Items(Enum): | |
"""Items a character can have.""" | |
Success = 0 | |
Cat = 1 | |
Burglar = 2 | |
Echo = 3 | |
Wound = 4 | |
Info = 5 | |
Key = 6 | |
Escape = 7 | |
Dreaded = 8 | |
Shadowy = 9 | |
Kifers = 10 | |
IntricateKifers = 11 | |
class Item: | |
"""A thing posessed by a character or given by a chosen option.""" | |
def __init__(self, type, value=0.0): | |
"""Init.""" | |
self.type = type | |
self.value = value | |
def cat(value=-1): | |
"""Generate an item.""" | |
return Item(Items.Cat, value) | |
def burglar(value=1): | |
"""Generate an item.""" | |
return Item(Items.Burglar, value) | |
def wound(value=1): | |
"""Generate an item.""" | |
return Item(Items.Wound, value) | |
def info(value=1): | |
"""Generate an item.""" | |
return Item(Items.Info, value) | |
def key(value=1): | |
"""Generate an item.""" | |
return Item(Items.Key, value) | |
def escape(value=1): | |
"""Generate an item.""" | |
return Item(Items.Escape, value) | |
def echo(value): | |
"""Generate an item.""" | |
return Item(Items.Echo, value) | |
def verb(level, string): | |
"""Print if verbosity is currently at or above level.""" | |
if SETTINGS.verbosity >= level: | |
print(string) | |
class Option: | |
"""A selectable option on a card.""" | |
def __init__(self, name, requirements=[], luck=1.0, onsuccess=[], onfail=[]): | |
"""init.""" | |
self.name = name | |
self.requirements = requirements | |
self.luck = luck | |
self.onsuccess = onsuccess | |
self.onfail = onfail | |
def __str__(self): | |
"""Pretty print.""" | |
str = " * {} (Luck {}".format(self.name, self.luck) | |
for r in self.requirements: | |
str += ", {}".format(r.type.name) | |
str += ")\n" | |
if len(self.onsuccess): | |
str += " + Success: " | |
for s in self.onsuccess: | |
str += " {}({})".format(s.type.name, s.value) | |
str += "\n" | |
if len(self.onfail): | |
str += " + Failure: " | |
for s in self.onfail: | |
str += " {}({})".format(s.type.name, s.value) | |
str += "\n" | |
avg = self.averageResult() | |
str += " + Average Result:" | |
for i in avg: | |
str += " {} ({:.1f})".format(i.type.name, i.value) | |
str += "\n" | |
return str | |
def averageResult(self): | |
"""Calculate the average result of choosing this option.""" | |
# handle the easy case first | |
if self.luck == 1.0: # this should be okay; every float type handles 1.0 | |
return self.onsuccess | |
else: | |
avg = [] | |
for i in self.onsuccess: | |
avg.append(Item(i.type, i.value * self.luck)) | |
failluck = 1.0 - self.luck | |
for i in self.onfail: | |
nomatch = True | |
for j in avg: | |
if i.type == j.type: | |
index = avg.index(j) | |
avg[index].value += (i.value * failluck) | |
nomatch = False | |
if nomatch: | |
avg.append(Item(i.type, i.value * failluck)) | |
return avg | |
class Card: | |
"""A card containing choosable options.""" | |
def __init__(self, name, odds=Frequency.Standard, options=[], unlock=Unlock.Always): | |
"""Init.""" | |
self.name = name | |
self.odds = odds.value | |
self.options = options | |
self.unlock = unlock | |
def __str__(self): | |
"""Pretty print.""" | |
str = "{} ({}, {})\n".format(self.name, self.unlock.name, self.odds) | |
for o in self.options: | |
str += o.__str__() | |
return str | |
def buildCardList(): | |
"""Build a list of all supported cards and their options.""" | |
cards = [] | |
# A Burly Night-Watchman ################################################# | |
o = [] | |
o.append(Option('Go Through', onsuccess=[Item.cat(-1), Item.burglar(1)])) | |
o.append(Option('Wait a few minutes', requirements=[Item.info()], luck=0.7, | |
onsuccess=[Item.burglar(1), Item.info(-1)], | |
onfail=[Item.burglar(-1), Item.info(-1)])) | |
o.append(Option('Get out of my way', | |
requirements=[Item(Items.Dreaded, 10)], | |
onsuccess=[Item.burglar(1)] | |
# Removed because we really don't support failure here. | |
# if we ever impliment Dreaded checks instead of pass/fail | |
# put this back. But snaffling logic for this card | |
# will also have to be adjusted! | |
# ,onfail=[Item.burglar(-2), Item.cat(-1)] | |
)) | |
o.append(Option('...go back', onsuccess=[Item.burglar(-1)])) | |
cards.append(Card('A Burly Night-Watchman', options=o)) | |
# A Clean Well-Lighted Place ############################################# | |
o = [] | |
o.append(Option('Snaffle Documents', luck=0.5, | |
onsuccess=[Item.burglar(), Item.echo(5.5)], | |
onfail=[Item.cat(-2)])) | |
o.append(Option('Pass through the study', luck=0.8, | |
onsuccess=[Item.burglar()], | |
onfail=[Item.cat(-1)])) | |
o.append(Option('Wait', requirements=[Item.info()], | |
onsuccess=[Item.burglar(), Item.info(-1)])) | |
cards.append(Card('A Clean Well-Lighted Place', options=o)) | |
# A clutter of bric-a-brac ############################################### | |
o = [] | |
# TODO: We don't support the rare success of a key here. | |
# TODO: we don't support the fail option of no change (if it exists) | |
o.append(Option('Poke through the possibilities', luck=0.7, | |
onsuccess=[Item.echo(0.8)], | |
onfail=[Item.cat(-1)])) | |
o.append(Option('Play it safe', onsuccess=[Item.burglar()])) | |
cards.append(Card('A clutter of bric-a-brac', options=o)) | |
# A Handy Window ######################################################### | |
o = [] | |
# TODO: We don't support escaping. | |
o.append(Option('Climb the wall', onsuccess=[Item.burglar()])) | |
cards.append(Card('A Handy Window', options=o, odds=Frequency.Unusual)) | |
# A Menacing Corridor #################################################### | |
o = [] | |
o.append(Option('Is it safe?', luck=0.3, onsuccess=[Item.burglar()], | |
onfail=[Item.cat(-1)])) | |
o.append(Option('Blindfold yourself', luck=0.5, | |
requirements=[Item(Items.Shadowy, 100)], | |
onsuccess=[Item.burglar(1)], onfail=[Item.cat(-1)])) | |
o.append(Option("It's safe tonight...", requirements=[Item.info(1)], | |
onsuccess=[Item.burglar(2), Item.info(-1)])) | |
cards.append(Card('A Menacing Corridor', options=o)) | |
# A Moment of Safety #################################################### | |
o = [] | |
o.append(Option('Hide for a little while', onsuccess=[Item.cat(1)])) | |
# TODO: We don't include the fate option. | |
cards.append(Card('A Moment of Safety', options=o)) | |
# A Nosy Servant ######################################################## | |
o = [] | |
o.append(Option('Deal with him', luck=0.6, onfail=[Item.cat(-2)])) | |
o.append(Option('Let it go', onsuccess=[Item.burglar(), Item.escape(-1)])) | |
cards.append(Card('A Nosy Servant', options=o, unlock=Unlock.EscapeRoute)) | |
# A Promising Door ###################################################### | |
o = [] | |
o.append(Option('Forward Planning', requirements=[Item.info(1)], | |
onsuccess=[Item.burglar(2), Item.info(-1)])) | |
o.append(Option('Chance it', luck=0.5, onsuccess=[Item.burglar(2)], | |
onfail=[Item.cat(-2)])) | |
cards.append(Card('A Promising Door', options=o)) | |
# A Sheer Climb ######################################################### | |
o = [] | |
# TODO: We don't support escaping. | |
o.append(Option('An Uncertain Path', luck=0.5, onsuccess=[Item.burglar()])) | |
o.append(Option('Foresight', requirements=[Item.info(1)], | |
onsuccess=[Item.burglar(1), Item.info(-1)])) | |
cards.append(Card('A Sheer Climb', unlock=Unlock.TripleBolted, | |
odds=Frequency.Unusual, options=o)) | |
# A Talkative Cat ####################################################### | |
o = [] | |
# TODO: We don't support using favor with the duchess or losing it, so this | |
# option is missing that negative - but we'll never pick it anyway. | |
o.append(Option('Grab the beast', luck=0.5, onsuccess=[Item.burglar()], | |
onfail=[Item.cat(-2)])) | |
o.append(Option('Bribe it with secrets', | |
onsuccess=[Item.burglar(), Item.echo(-1.5)])) | |
# TODO: We don't support using duchess connections | |
cards.append(Card('A Talkative Cat', options=o)) | |
# A Troublesome Lock #################################################### | |
o = [] | |
o.append(Option('There may be an easier way', requirements=[Item.info()], | |
onsuccess=[Item.burglar(2), Item.info(-1)])) | |
o.append(Option('What about that key?', requirements=[Item.key()], | |
onsuccess=[Item.burglar(2), Item.key(-1)])) | |
o.append(Option('Use your intricate kifers', luck=0.6, | |
requirements=[Item(Items.IntricateKifers, 1)], | |
onsuccess=[Item.burglar(2)])) | |
o.append(Option('Use your kifers', luck=0.4, | |
requirements=[Item(Items.Kifers, 1)], | |
onsuccess=[Item.burglar(2)])) | |
o.append(Option('Try your luck', luck=0.3, onsuccess=[Item.burglar(2)])) | |
cards.append(Card('A Troublesome Lock', options=o)) | |
# A Weeping Maid ######################################################### | |
o = [] | |
o.append(Option('Avoid her', onsuccess=[Item.burglar()])) | |
# TODO: This option is actually success/alternative succes, not luck. | |
# and the exact percentages aren't given, so 80/20 is assumed. | |
o.append(Option('Speak to her', luck=0.8, | |
onsuccess=[Item.burglar(-1)], | |
onfail=[Item.cat(-1)])) | |
# TODO: This card only appears in Cubit house, but that is also the only | |
# shuttered target. If that changes, we'll need to update the unlock. | |
cards.append(Card("A Weeping Maid", options=o, | |
odds=Frequency.Unusual, unlock=Unlock.Shuttered)) | |
# An Alarming Bust ###################################################### | |
o = [] | |
o.append(Option("Oh, it's just that bloody bust of the Consort", | |
requirements=[Item.info()], | |
onsuccess=[Item.burglar(2), Item.info(-1)])) | |
o.append(Option("Aaagh!", luck=0.5, onsuccess=[Item.burglar()], | |
onfail=[Item.cat(-1)])) | |
cards.append(Card('An Alarming Bust', options=o)) | |
# Look Up ############################################################### | |
o = [] | |
o.append(Option("Move slowly past", luck=0.3, | |
onfail=[Item.burglar(-1), Item.cat(-1), Item.wound(1)])) | |
o.append(Option("Dash Past", luck=0.6, | |
onsuccess=[Item.burglar(), Item.cat(-1)], | |
onfail=[Item.burglar(-1), Item.cat(-1)])) | |
cards.append(Card('Look up...', options=o, unlock=Unlock.TripleBolted)) | |
# Sleeping Dogs ######################################################### | |
o = [] | |
o.append(Option("Creep past", luck=0.4, onsuccess=[Item.burglar(1)], | |
onfail=[Item.burglar(-1), Item.cat(-1)])) | |
o.append(Option("Dash past", luck=0.7, | |
onsuccess=[Item.burglar(1), Item.cat(-1)], | |
onfail=[Item.burglar(-1), Item.cat(-1)])) | |
cards.append(Card('Sleeping Dogs', options=o)) | |
# Through Deeper Shadows ################################################ | |
o = [] | |
o.append(Option("Leaving the Light", luck=0.5, onsuccess=[Item.burglar()], | |
onfail=[Item.burglar(-1)])) | |
o.append(Option("These ways are strange", | |
requirements=[Item.info(), Item(Items.Shadowy, 100)], | |
onsuccess=[Item.burglar(2), Item.info(-1)])) | |
cards.append(Card('Through Deeper Shadows', options=o, | |
unlock=Unlock.TripleBolted)) | |
# Through the Shadows ################################################### | |
o = [] | |
# TODO: We don't handle the rare success that gives 2.5 echos, as no rare | |
# percentages are given. | |
o.append(Option("And here you are hard at work", luck=0.5, | |
onsuccess=[Item.burglar()])) | |
o.append(Option("Work wisely, not hard", requirements=[Item.info()], | |
onsuccess=[Item.burglar(2), Item.info(-1)])) | |
cards.append(Card('Through the Shadows', options=o, | |
unlock=Unlock.Shuttered)) | |
# Tiny Rivals ########################################################### | |
o = [] | |
o.append(Option("Well-met, theiflings!", luck=0.7, | |
onsuccess=[Item.info()], | |
onfail=[Item.cat(-1), Item.wound(2)])) | |
o.append(Option("Hang back", onsuccess=[Item.burglar(1)])) | |
o.append(Option("Sapphires?", onsuccess=[Item.cat(-1), Item.echo(1.8)])) | |
cards.append(Card("Tiny Rivals", options=o)) | |
# Winding Stairs ######################################################## | |
o = [] | |
o.append(Option("Upstairs. That's probably right", luck=0.7, | |
onsuccess=[Item.burglar(2)])) | |
o.append(Option("Play it safe", onsuccess=[Item.burglar(1)])) | |
cards.append(Card("Winding Stairs", options=o)) | |
# A Prize Achieved! ##################################################### | |
o = [] | |
o.append(Option("Done", luck=1.0, onsuccess=[Item(Items.Success, 1)])) | |
cards.append(Card("A Prize Achieved!", options=o, | |
unlock=Unlock.FiveBurglar, odds=Frequency.Ubiquitous)) | |
return cards | |
class Target: | |
"""A Hiest target.""" | |
def __init__(self, name, difficulty, worth, extraactions=0): | |
"""Init.""" | |
self.name = name | |
self.difficulty = difficulty | |
self.worth = worth | |
# 1 AP choose the heist, 1 AP start the heist | |
# Extra actions are choosing the reward, and cashing in the reward. | |
self.actions = 2 + extraactions | |
def isCardUnlocked(card, character): | |
"""Verify card is unlocked for the current character.""" | |
u = card.unlock | |
if u == Unlock.Always: | |
return True | |
elif u == Unlock.FiveBurglar: | |
return character["Burglar"] > 4 | |
elif u == Unlock.EscapeRoute: | |
return character["Escape"] > 0 | |
elif u == Unlock.Shuttered: | |
return character["Target"].difficulty == 2 | |
elif u == Unlock.TripleBolted: | |
return character["Target"].difficulty == 3 | |
else: | |
raise ValueError("Unsupported card unlock value") | |
def chooseRandomCard(all_cards): | |
"""Pick one random card from all available cards and returns it.""" | |
total = 0 | |
for c in all_cards: | |
total += c.odds | |
r = random.randint(1, total) | |
for c in all_cards: | |
if r <= c.odds: | |
return c | |
else: | |
r -= c.odds | |
raise ValueError("We shouldn't be able to get here.") | |
def fillHandX(hand, all_cards, character): | |
"""Refill the current hand using xnyhps suggested method.""" | |
# Found here: https://www.reddit.com/r/fallenlondon/comments/83aocr/opportunity_deck_research/ | |
deck = [] | |
for card in all_cards: | |
roll = random.random() | |
value = card.odds * roll | |
deck.append((value, card)) | |
deck.sort() | |
while len(hand) < SETTINGS.slots: | |
card = deck.pop()[1] | |
if isCardUnlocked(card, character) and card not in hand: | |
hand.append(card) | |
def fillHand(hand, all_cards, character): | |
"""Refill the current hand of cards.""" | |
# TODO: We might need to remove a card from hand if we no longer qualify | |
# for a card. Right now this can only happen if we use an escape, which | |
# we don't do anyway - so ignore this. | |
probabilities = [] | |
for c in all_cards: | |
probabilities.append(c.odds) | |
while len(hand) < SETTINGS.slots: | |
card = chooseRandomCard(all_cards) | |
if isCardUnlocked(card, character) and card not in hand: | |
hand.append(card) | |
# else keep trying | |
def freshCharacter(): | |
"""Generate a fresh character with values appropriate for the settings.""" | |
character = {x.name: 0 for x in Items} | |
# Set default values | |
character["Cat"] = 3 | |
character["Info"] = SETTINGS.infos | |
character["Key"] = SETTINGS.keys | |
character["Escape"] = SETTINGS.escapes | |
if SETTINGS.dreaded: | |
character["Dreaded"] = 10 | |
if SETTINGS.shadowy: | |
character["Shadowy"] = 100 | |
if SETTINGS.kifers: | |
character["Kifers"] = 1 | |
if SETTINGS.intricate_kifers: | |
character["IntricateKifers"] = 1 | |
if SETTINGS.secrets: | |
# TODO: we just abstract secrets as their echo value for now. | |
character["Echo"] = 100 | |
target = None | |
if SETTINGS.cubit: | |
# Cubit: the only shuttered heist, averaged the worth of the two 50/50 | |
# options, reward is picked straight from 'A prize achieved' | |
target = Target("The House on Cubit Square", | |
Unlock.Shuttered.value, | |
worth=17, | |
extraactions=0) | |
if SETTINGS.baseborn: | |
# Baseborn: worth averaged for 95%/5% chance of 20 echoes / 62.5 echoes | |
# reward is given straight from 'A prize achieved' | |
# TODO: Going for the papers instead of the sealed archives is not | |
# supported, since it would require us handling 7 burglar, and is only | |
# worth 27. Just choose a different heist | |
target = Target("The offices of Baseborn & Fowlingpiece", | |
Unlock.TripleBolted.value, | |
worth=22.125, | |
extraactions=0) | |
if SETTINGS.envoy or not target: # This is the default as well. | |
# The Circumspect Envoy's Townhouse | |
# two extra actions are required; one to choose a reward, and one to turn | |
# it in. | |
target = Target("The Circumspect Envoy's Townhouse", | |
Unlock.TripleBolted.value, | |
worth=30, | |
extraactions=2) | |
character["Target"] = target | |
return character | |
def getAllOptions(hand): | |
"""Get all options from all cards in the hand.""" | |
options = [] | |
for c in hand: | |
for o in c.options: | |
options.append(o) | |
return options | |
def filterUnable(options, character): | |
"""Filter out options our character can not perform.""" | |
goodops = [] | |
for o in options: | |
cando = True | |
for r in o.requirements: | |
req = r.type.name | |
val = r.value | |
if character[req] < val: | |
cando = False | |
if cando: | |
goodops.append(o) | |
return goodops | |
def filterNonFree(options, character): | |
"""Filter out all options that will cost us something.""" | |
c = copy.deepcopy(character) | |
c["Echo"] = 0 | |
c["Info"] = 0 | |
c["Key"] = 0 | |
c["Escape"] = 0 | |
return filterUnable(options, c) | |
def filterCat(options): | |
"""Filter out all options that give cat.""" | |
goodops = [] | |
for o in options: | |
avg = o.averageResult() | |
keep = True | |
for r in avg: | |
if r.type.name == "Cat" and r.value < 0: | |
keep = False | |
if keep: | |
goodops.append(o) | |
return goodops | |
def twoBurglar(options, character): | |
"""Return any options that give two burglar and no cat.""" | |
goodops = [] | |
for o in options: | |
avg = o.averageResult() | |
for r in avg: | |
if r.type.name == "Burglar" and r.value == 2: | |
goodops.append(o) | |
return filterCat(goodops) | |
def greater(a, b): | |
"""Functor for use in getting costs.""" | |
return a > b | |
def lesser(a, b): | |
"""Functor for use in getting costs.""" | |
return a < b | |
def getCostOption(options, type, func=greater): | |
"""Return the options with the lowest or greatest cost in Item type.""" | |
cost = 1000 if func(0, 1) else -1000 | |
goodops = [] | |
for o in options: | |
avg = o.averageResult() | |
for c in avg: | |
if c.type == type: | |
if func(c.value, cost): | |
cost = c.value | |
goodops = [o] | |
elif c.value == cost: | |
goodops.append(o) | |
return goodops | |
def selectBestTwoBurglar(options, character): | |
"""If there are any two-burglar options we can use, select the best.""" | |
twoops = twoBurglar(options, character) | |
if len(twoops) > 1: | |
# First, select the key option, as it isn't good for anything else | |
# we should use it first. | |
for o in options: | |
for r in o.requirements: | |
if "Key" == r.type.name: | |
return o | |
# Otherwise, just pick the first one as they are all the same. | |
return twoops | |
elif len(twoops) == 1: | |
return twoops | |
else: | |
return [] | |
def findOptionByName(name, options): | |
"""Find an option in a list by its name.""" | |
for o in options: | |
if o.name == name: | |
return o | |
return None | |
def getBurglarFromItems(items): | |
"""Get the amount of burglar found in a list of items.""" | |
if len(items): | |
for i in items: | |
if i.type.value == "Burglar": | |
return i.value | |
return 0 | |
def certainBurglar(options): | |
"""Find the number of burglar points we can be certain of getting.""" | |
certain = 0 | |
for o in options: | |
avg = o.averageResult() | |
safe = True | |
# determine if this option is safe (gives no cat) | |
# this will falsly flag "a moment of safety" but that card doesn't | |
# help us here anyway. | |
for e in avg: | |
if e.type.value == "Cat": | |
safe = False | |
break | |
if not safe: | |
break | |
fail = o.onfail | |
# If we can fail, use failure as the minimum Burglar | |
if len(fail): | |
c = getBurglarFromItems(fail) | |
else: | |
c = getBurglarFromItems(o.onsuccess) | |
if c > certain: | |
certain = c | |
return certain | |
def snaffleLogic(options, character): | |
"""Snaffle documents if appropriate.""" | |
snaffle = findOptionByName("Snaffle Documents", options) | |
if not snaffle: | |
return None # it's not an option anyway. | |
method = SETTINGS.snaffle | |
if "none" == method: | |
return None # the user doesn't want us to. | |
burglar = character["Burglar"] | |
cat = character["Cat"] | |
if "at-four" == method: | |
# Use the old method for snaffling. | |
if findOptionByName("Done", options): | |
return None # don't snaffle if we're ready to leave | |
if burglar >= 4 and cat >= 3: | |
verb(3, "Choosing to snaffle!") | |
return snaffle | |
# Discover if we have a safe option to get one or more burglar | |
ops = getCostOption(filterCat(options), Items.Burglar, greater) | |
certain = certainBurglar(ops) | |
if (burglar + certain) >= 5 and cat >= 3: | |
verb(3, "Choosing to snaffle because it should be safe to do so!") | |
return snaffle | |
return None | |
def selectOption(hand, character): | |
"""Choos the best option from a hand of cards.""" | |
options = filterUnable(getAllOptions(hand), character) | |
# Snaffle documents, if it is wise. | |
snaffle = snaffleLogic(options, character) | |
if snaffle: | |
return snaffle | |
# Choose the victory card if it's available | |
done = findOptionByName("Done", options) | |
if done: | |
return done | |
# First, always choose two-burglar paid options, if available. | |
best = selectBestTwoBurglar(options, character) | |
if len(best): | |
verb(3, "Selecting 2-Burglar option") | |
return best[0] | |
verb(3, "No two-burglar options available with current supplies") | |
# Our next best bet is the best free-cost, no cat, highest burglar. | |
ops = filterNonFree(filterCat(options), character) | |
ops = getCostOption(ops, Items.Burglar, greater) | |
if len(ops): | |
verb(3, "Selecting free, no cat loss option") | |
return ops[0] | |
verb(3, "No free, no-cat options available with current supplies") | |
# if that fails, try again but include options that cost: | |
ops = getCostOption(filterCat(options), Items.Burglar, greater) | |
if len(ops): | |
verb(3, "Selecting best non-free, no cat loss option") | |
return ops[0] | |
verb(3, "No paid no-cat options available with current supplies") | |
# If that fails, and we're stuck with only bad options, choose the one | |
# with the least cat: | |
ops = getCostOption(options, Items.Cat, greater) | |
if len(ops): | |
verb(3, "Selecting best option despite potential cat loss") | |
return ops[0] | |
raise ValueError("We had no cards in our hand and we should have!") | |
def findCardWithOption(option, hand): | |
"""Find the card that contains the option.""" | |
for c in hand: | |
for o in c.options: | |
if o == option: | |
return c | |
raise ValueError("The hand did not contain a card with the chosen option!") | |
def applyOption(option, character): | |
"""Apply an option to a character (rolling luck if necessary).""" | |
luck = random.random() | |
if luck <= option.luck: | |
apply = option.onsuccess | |
else: | |
apply = option.onfail | |
verb(3, "Failed luck roll!") | |
for item in apply: | |
verb(3, " Got: {} ({})".format(item.type.name, item.value)) | |
character[item.type.name] += item.value | |
# Clamp burglar to >= 0 | |
character["Burglar"] = max(character["Burglar"], 0) | |
def chooseOption(option, hand, character): | |
"""Apply the selected option, update the character and hand.""" | |
applyOption(option, character) | |
card = findCardWithOption(option, hand) | |
hand.remove(card) | |
def getCasing(character): | |
"""Spend actions and echoes to gather the necessary amount of casing.""" | |
needed = 15 # Necessary to get the five casing necessary to start | |
needed += SETTINGS.keys * 5 | |
needed += SETTINGS.infos * 5 | |
needed += SETTINGS.escapes * 10 | |
# It takes one action to pick up each item too | |
actions = SETTINGS.keys + SETTINGS.infos + SETTINGS.escapes | |
totalneeded = needed | |
if SETTINGS.bigrat: | |
while needed > 0: | |
# Using the big rat takes 1 action, gives 9 casing, and uses 1.5 echoes | |
verb(3, "Getting casing with the Big Rat") | |
actions += 1 | |
needed -= 9 | |
character["Echo"] -= 2.4 | |
while needed > 9 and SETTINGS.hoodlum and SETTINGS.posi: | |
# Use your Gang of Hoodlums for big amounts of needed | |
verb(3, "Getting casing with a gang of hoodlums") | |
actions += 5 | |
needed -= 18 | |
while needed > 9 and SETTINGS.posi: | |
# Unless you don't have one; then use the POSI option | |
verb(3, "Getting casing with POSI") | |
actions += 5 | |
needed -= 15 | |
# TODO: we only support The Decoy, not lower casing options. | |
while needed > 3: | |
# If we need between 4 and 9, use the 3-action non-POSI options | |
verb(3, "Getting casing with The Decoy") | |
actions += 3 | |
needed -= 9 | |
character["Echo"] += .30 # whispered hints on best 9-option. | |
while needed > 0: | |
# Steal paintings for the Topsy King if you need 3 or less | |
verb(3, "Getting casing by stealing for the Topsy King") | |
actions += 1 | |
needed -= 3 | |
verb(2, "Gathered {} casing with {} actions".format(totalneeded, actions)) | |
return actions | |
def runOneHeist(all_cards): | |
"""Run one heist.""" | |
verb(3, "================================================") | |
c = freshCharacter() | |
hand = [] | |
drawcount = 0 | |
drawcount += getCasing(c) # Gather all our casing with actions first | |
while c["Cat"] > 0 and c["Success"] == 0: | |
drawcount += 1 | |
verb(2, "Choosing a card on round {}, Burglar is at {}, Cat-like Tread is {}".format(drawcount, c["Burglar"], c["Cat"])) | |
if SETTINGS.xdraw: | |
fillHandX(hand, all_cards, c) | |
else: | |
fillHand(hand, all_cards, c) | |
verb(4, "+++ Current Hand: ++++++++++++++++++++++++++++") | |
for card in hand: | |
verb(4, card) | |
verb(4, "++++++++++++++++++++++++++++++++++++++++++++++") | |
op = selectOption(hand, c) | |
verb(2, "Selecting option {}".format(op.name)) | |
chooseOption(op, hand, c) | |
jail = False if c["Cat"] > 0 else True | |
c2 = freshCharacter() | |
echoes = c["Echo"] - c2["Echo"] | |
target = c["Target"] | |
verb(3, "Adding {} actions for selecting target and selling loot".format(target.actions)) | |
drawcount += target.actions | |
if not jail: | |
echoes += target.worth | |
else: | |
drawcount += SETTINGS.jail_cost | |
return jail, echoes, drawcount, c["Wound"] | |
print(c) | |
def runSeveralHeists(count, all_cards): | |
"""Run multiple heists and gather statistics.""" | |
jails = 0 | |
echoes = 0 | |
draws = 0 | |
wounds = 0 | |
for i in range(count): | |
j, e, d, w = runOneHeist(all_cards) | |
if j: | |
jails += 1 | |
htext = "caught!" | |
else: | |
htext = "successful." | |
echoes += e | |
draws += d | |
verb(1, "Ran one heist, it took {} turns, got {} echoes ({:4.3} per AP), {} wounds, and we were {}.".format(d, e, e/d, w, htext)) | |
verb(1, "================================================") | |
avgjail = (jails / count) if jails else 0 | |
avgdraw = draws / count | |
avgecho = echoes / count | |
avgwound = wounds / count | |
perap = avgecho / avgdraw | |
print("Completed run of {} Heists.".format(count)) | |
print("Caught: {} ({:.2%}). Average actions: {:4.3}. Wounds per heist: {:4.3}. Echoes per heist: {:4.3}. Echoes per action: {:4.3}.".format(jails, avgjail, avgdraw, avgwound, avgecho, perap)) | |
def help(): | |
"""Set up command-line argument parsing.""" | |
parser = argparse.ArgumentParser(description='Simulate Heists') | |
parser.add_argument('--show-cards', dest='show_cards', action='store_true', | |
help="Show all possible cards and exit.") | |
parser.add_argument('--slots', dest='slots', action='store', type=int, | |
help='number of cards a character can hold', | |
default=5) | |
parser.add_argument('--no-shadowy', dest='shadowy', action='store_false', | |
help='character does not have shadowy < 100') | |
parser.add_argument('--no-dreaded', dest='dreaded', action='store_false', | |
help='character does not have dreaded < 10') | |
parser.add_argument('--no-kiffers', dest='kifers', action='store_false', | |
help='character has no standard kifers') | |
parser.add_argument('--no-intricate', dest='intricate_kifers', | |
action='store_false', | |
help='character has no intricate kifers') | |
parser.add_argument('--no-secrets', dest='secrets', action='store_false', | |
help='character has no appalling secrets') | |
parser.add_argument('--no-posi', dest='posi', action='store_false', | |
help='character is not yet a Person of Some Importance') | |
parser.add_argument('--no-goh', dest='hoodlum', action='store_false', | |
help='character does not have a Gang of Hoodlums') | |
parser.add_argument('--bigrat', dest='bigrat', action='store_true', | |
help='use the big rat to gather casing. Assumes buying talkative rats for 0.8 Echoes') | |
parser.add_argument('--snaffle', dest='snaffle', action='store', | |
choices=["none", "at-four", "safe"], default="safe", | |
help="Choose snaffle document logic.\nnone: Don't snaffle documents.\nat-four: attempt to snaffle documents at burglar four.\nsafe: only attempt to snaffle documents at a guaranteed 5 burglar") | |
parser.add_argument('--infos', dest='infos', action='store', type=int, | |
help='number of inside info the character wants to use', | |
default=0) | |
parser.add_argument('--keys', dest='keys', action='store', type=int, | |
help='number of keys the character wants to use', | |
default=0) | |
parser.add_argument('--escapes', dest='escapes', action='store', type=int, | |
help='number of escape routes the character wants to use', | |
default=0) | |
parser.add_argument('--runs', dest='runs', action='store', type=int, | |
help='number of heists to simulate', default=100) | |
parser.add_argument('--jail-cost', dest='jail_cost', action='store', | |
type=int, default=0, | |
help="action cost of being sent to New Newgate") | |
parser.add_argument('--xdraw', dest='xdraw', action='store_true', | |
help="Use xnyhps's draw method instead of a probability distribution.") | |
parser.add_argument('--verbose', '-v', dest='verbosity', action='count', | |
help='Program verbosity (up to vvvv)', default=0) | |
targetgroup = parser.add_mutually_exclusive_group(required=False) | |
targetgroup.add_argument("--cubit", dest='cubit', action='store_true', | |
help="Target The House on Cubit Square") | |
targetgroup.add_argument("--baseborn", dest='baseborn', action='store_true', | |
help="Target The offices of Baseborn & Fowlingpiece") | |
targetgroup.add_argument("--envoy", dest='envoy', action='store_true', | |
help="Target The Circumspect Envoy's Townhouse. This is the default.") | |
global SETTINGS | |
SETTINGS = parser.parse_args() | |
SETTINGS = None | |
def printSettings(): | |
"""Show the user the settings for this current run.""" | |
print("Running {} heists with the following settings:".format(SETTINGS.runs)) | |
print("Character can hold {} cards".format(SETTINGS.slots)) | |
if SETTINGS.cubit: | |
print("Target: The House on Cubit Square") | |
if SETTINGS.baseborn: | |
print("Target: The offices of Baseborn & Fowlingpiece") | |
if SETTINGS.envoy: | |
print("Target: The Circumspect Envoy's Townhouse") | |
print("Character has Shadowy 100: {}".format(SETTINGS.shadowy)) | |
print("Character has Dreaded 10: {}".format(SETTINGS.dreaded)) | |
print("Character has kifers: {}".format(SETTINGS.kifers)) | |
print("Character has intricate kifers: {}".format(SETTINGS.intricate_kifers)) | |
print("Character can use appalling secrets: {}".format(SETTINGS.secrets)) | |
print("Character snaffling method is: {}".format(SETTINGS.snaffle)) | |
print("Action cost of jail is assumed to be: {}".format(SETTINGS.jail_cost)) | |
if SETTINGS.bigrat: | |
print("Using the Big Rat to gather casing") | |
elif SETTINGS.hoodlum and SETTINGS.posi: | |
print("Using a Gang of Hoodlums to gather casing") | |
elif SETTINGS.posi: | |
print("Using Well-planned villainy to gather casing") | |
else: | |
print("Using The Decoy / Stealing paintings to gather casing") | |
print("{} Infos, {} Keys, {} Escapes per heist".format(SETTINGS.infos, SETTINGS.keys, SETTINGS.escapes)) | |
print("================================================") | |
def main(): | |
"""Do the thing.""" | |
help() | |
cards = buildCardList() | |
# Handle the option where we just show available cards | |
if SETTINGS.show_cards: | |
for card in cards: | |
print(card) | |
exit(0) | |
printSettings() | |
runSeveralHeists(SETTINGS.runs, cards) | |
main() | |
# Look ma, no tests! |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment