Skip to content

Instantly share code, notes, and snippets.

@TheFeshy
Last active March 10, 2019 16:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save TheFeshy/230a9e62a234ffdb476514f01b847db3 to your computer and use it in GitHub Desktop.
Save TheFeshy/230a9e62a234ffdb476514f01b847db3 to your computer and use it in GitHub Desktop.
Fallen London Heist simulator
#!/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