Last active
May 3, 2018 05:52
-
-
Save rfong/a426c68e39fce82ba866274fc3036299 to your computer and use it in GitHub Desktop.
relationship rpg dice simulators
This file contains 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
""" | |
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") |
This file contains 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
""" | |
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") |
This file contains 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
""" | |
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") |
This file contains 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
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