Skip to content

Instantly share code, notes, and snippets.

@rfong
Last active May 3, 2018 05:52
Show Gist options
  • Save rfong/a426c68e39fce82ba866274fc3036299 to your computer and use it in GitHub Desktop.
Save rfong/a426c68e39fce82ba866274fc3036299 to your computer and use it in GitHub Desktop.
relationship rpg dice simulators
"""
Simulate 'Cumulative' type relationshipRPG trials.
"""
import csv
import math
import random
import time
import unittest
from util import *
def generate(filename):
"""Play a lot of Cumulative Trials, write the outcomes to CSV"""
with open(filename, 'w') as csvfile:
fieldnames = ['dice', 'threshold', 'pass_probability']
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
for dice in xrange(1, MAX_STAT + MAX_ENERGY_USE + 1):
for threshold in xrange(10, 31): # [10..30] inclusive
passes = [
play(threshold, dice) # lol this is so lazy/redundant
for _ in xrange(TRIALS_PER_CELL)
]
prob = float(len(filter(lambda x: x, passes))) / TRIALS_PER_CELL
if prob == 0.0 or prob == 1.0:
continue
writer.writerow({
'dice': dice,
'threshold': threshold,
'pass_probability': prob
})
def play(threshold, num_dice):
"""
Randomly play one "Cumulative" type trial, and return whether it passed
"""
return sum(roll_dice(num_dice)) >= threshold
if __name__ == "__main__":
random.seed(a=time.time())
generate("cumulative_results.csv")
"""
Simulate 'Match' type relationshipRPG trials so that we can get a probability
distribution to use to balance Trial difficulties.
It was too inelegant to find the pure closed form solution.
We would like to solve for match value given a desired energy input and
expected stat value, so we are generating a probability distribution of
passes.
"""
from collections import Counter, defaultdict
import csv
import math
from operator import itemgetter
import random
import time
import unittest
from util import *
def generate(filename):
"""Play a lot of Match Trials, write the outcomes to CSV"""
with open(filename, 'w') as csvfile:
fieldnames = ['match', 'stat', 'energy_allocated', 'pass_probability']
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
for stat in xrange(1, 8): # [1..7] inclusive
for match_val in xrange(2, 6): # [2..5] inclusive
energies = [
play_match(match_val, stat)
for _ in xrange(TRIALS_PER_CELL)]
energy_counts = Counter(energies)
#print "<%d,%d>" % (stat, match_val), energy_counts
# Individual probabilities are for the amount of energy used on success.
# Get cumulative probabilities so we can write the probability of
# success with X energy allocated prior to roll.
cum = 0
for nrg, c in energy_counts.iteritems():
if nrg == -1:
continue
cum += c
#print nrg, float(cum)/TRIALS_PER_CELL
writer.writerow({
'match': match_val,
'stat': stat,
'energy_allocated': nrg,
'pass_probability': float(cum)/TRIALS_PER_CELL,
})
def play_match(match_value, stat):
"""
Randomly play one "Match" type trial, and return the amount of Energy used
to pass it.
:param match_value <int>: the number of dice you must match to pass this trial
:param stat <int>: the value that came out of your stat formula
:returns <int>: the number of Energy it took to reach the desired match value.
Returns -1 if we exceeded max energy usage.
The structure of a Match trial:
1. Start by rolling `stat` dice.
2. Reroll any subset for free.
2b. For each successive Energy, may add one additional die, and reroll any
subset of available dice. You have `stat`+k dice on the `k`th Energy use.
"""
# 1. Start by rolling `stat` dice.
dice = roll_dice(stat)
if is_success(match_value, dice):
return 0
# 2. Rerolls. First one costs 0 energy.
energy_used = 0
while energy_used < MAX_ENERGY_USE:
dice = match_reroll(dice, stat+energy_used)
if is_success(match_value, dice):
return energy_used
energy_used += 1
return -1
def match_reroll(current_dice, reroll_dice_allowed):
"""Rerolls and returns new dice values."""
kept_value, kept_count = winnow_dice(current_dice)
return [kept_value]*kept_count + roll_dice(reroll_dice_allowed-kept_count)
def winnow_dice(dice):
"""
Select dice to keep from a Match roll.
If multiple options are equally good, randomly selects one of them.
:returns: a tuple of ints - (value of kept dice, number of dice kept)
"""
assert len(dice) > 0
counter = Counter(dice)
# `most_common` deterministically orders by value, so let's randomly select
# one of the top most common values.
most_matched = counter.most_common(1)[0][1]
return random.choice(
filter(lambda tup: tup[1] == most_matched, counter.items())
)
def is_success(match_value, dice):
"""
Evaluate whether this set of dice values passed a Match trial.
:returns <bool>: true if at least `match_value` dice are a match.
"""
assert len(dice) > 0
return Counter(dice).most_common(1)[0][1] >= match_value
class TestMatchMethods(unittest.TestCase):
"""unit tests"""
def test_match_reroll(self):
"""Just a sanity check; not actually inspecting dice values."""
stat = 5
dice = roll_dice(stat)
dice = match_reroll(dice, stat)
self.assertEqual(len(dice), stat)
dice = match_reroll(dice, stat+1)
self.assertEqual(len(dice), stat+1)
def test_winnow_dice(self):
cases = [ # <dice, expected number kept>
([1,2,3,4], 1),
([1,1,3,4], 2),
([3,3,4,4], 2),
([3,3,3,4], 3),
([3,3,3,3], 4),
]
for (dice, kept_cnt) in cases:
val,cnt = winnow_dice(dice)
self.assertEqual(cnt, kept_cnt)
def test_is_success(self):
self.assertEqual(is_success(1, [1, 2, 3]), True)
self.assertEqual(is_success(2, [1, 2, 3]), False)
self.assertEqual(is_success(2, [1, 3, 1]), True)
self.assertEqual(is_success(2, [4, 4, 4]), True)
self.assertEqual(is_success(3, [1, 2, 2]), False)
self.assertEqual(is_success(3, [2, 2, 2]), True)
if __name__ == "__main__":
random.seed(a=time.time())
#unittest.main()
generate("match_results.csv")
"""
Simulate 'Pass/Fail' type relationshipRPG trials.
"""
import csv
import math
from util import *
def generate(filename):
"""Write pass/fail probabilities to CSV"""
with open(filename, 'w') as csvfile:
fieldnames = ['stat', 'energy', 'pass_probability']
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
for stat in xrange(1, MAX_STAT+1):
for energy in xrange(1, MAX_ENERGY_USE+1):
prob = 1.0 - math.pow(((6-stat)/6.0), energy)
writer.writerow({
'stat': stat,
'energy': energy,
'pass_probability': prob if prob <= 1.0 else 1.0,
})
if __name__ == "__main__":
generate("passfail.csv")
import random
MAX_ENERGY_USE = 10 # Much more Energy than players will ever spend
TRIALS_PER_CELL = 1000
MAX_STAT = 7 # Output from formula
def roll_die():
"""One random die roll."""
return random.randint(1, 6)
def roll_dice(n):
"""Randomly roll `n` dice."""
return [roll_die() for _ in xrange(n)]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment