|
|
@@ -0,0 +1,218 @@ |
|
|
|
import argparse, random |
|
|
|
from math import floor |
|
|
|
from statistics import mean, median |
|
|
|
|
|
|
|
class PotatoGame: |
|
|
|
def __init__(self, min_potatoes_per_orc=None, alternate_po_mechanic=False, verbose=False): |
|
|
|
self.potatoes = 0 |
|
|
|
self.destiny = 0 |
|
|
|
self.orcs = 0 |
|
|
|
self.potatoes_per_orc = 1 |
|
|
|
self.min_potatoes = min_potatoes_per_orc |
|
|
|
self.default_game_state = [0, 0, 0] |
|
|
|
self.alternate_po_mechanic = alternate_po_mechanic |
|
|
|
self.verbose = verbose |
|
|
|
self.rounds = 0 |
|
|
|
|
|
|
|
def vprint(self, message): |
|
|
|
if self.verbose: |
|
|
|
print(message) |
|
|
|
|
|
|
|
def event(self): |
|
|
|
self.rounds += 1 |
|
|
|
if self.alternate_po_mechanic and self.potatoes >= self.potatoes_per_orc: |
|
|
|
if (self.orcs >= 8 \ |
|
|
|
and self.potatoes < 8) \ |
|
|
|
or (self.orcs > 0 \ |
|
|
|
and random.randint(1, 10) < (self.orcs + 1)): |
|
|
|
self.vprint("Removing an orc.") |
|
|
|
self.remove_orc() |
|
|
|
if not self.alternate_po_mechanic \ |
|
|
|
and (self.min_potatoes and self.orcs > 0) \ |
|
|
|
and ((self.potatoes / self.potatoes_per_orc)/self.orcs >= self.min_potatoes): |
|
|
|
self.vprint(f"Removing an orc. Potatoes, PPO, orcs, ratio: {self.potatoes, self.potatoes_per_orc, self.orcs, self.min_potatoes}") |
|
|
|
self.remove_orc() |
|
|
|
roll = random.randint(1,3) |
|
|
|
if roll == 1: |
|
|
|
self.vprint("In the garden...") |
|
|
|
self.garden() |
|
|
|
elif roll == 2: |
|
|
|
self.vprint("A knock at the door...") |
|
|
|
self.door() |
|
|
|
else: |
|
|
|
self.vprint("The world becomes a darker, more dangerous place...") |
|
|
|
self.darken() |
|
|
|
self.potatoes = self.potatoes if self.potatoes >= 0 else 0 |
|
|
|
self.destiny = self.destiny if self.destiny >= 0 else 0 |
|
|
|
self.orcs = self.orcs if self.orcs >=0 else 0 |
|
|
|
game_state = [[0, 0, 0], self.rounds] |
|
|
|
if self.destiny >= 10: |
|
|
|
game_state[0][0] = 1 |
|
|
|
if self.potatoes >= 10: |
|
|
|
game_state[0][1] = 1 |
|
|
|
if self.orcs >= 10: |
|
|
|
game_state[0][2] = 1 |
|
|
|
if game_state[0] == [1, 0, 1] or game_state[0] == [0, 1, 1]: |
|
|
|
game_state[0] = [0, 0, 1] |
|
|
|
if game_state[0] != self.default_game_state: |
|
|
|
self.vprint(f"Game over! Game state: {game_state}") |
|
|
|
return game_state |
|
|
|
|
|
|
|
def remove_orc(self): |
|
|
|
self.potatoes -= self.potatoes_per_orc |
|
|
|
self.orcs -= 1 |
|
|
|
|
|
|
|
|
|
|
|
def garden(self): |
|
|
|
roll = random.randint(1,6) |
|
|
|
if roll == 1: |
|
|
|
self.potatoes += 1 |
|
|
|
elif roll == 2: |
|
|
|
self.potatoes += 1 |
|
|
|
self.destiny += 1 |
|
|
|
elif roll == 3: |
|
|
|
self.destiny += 1 |
|
|
|
self.orcs += 1 |
|
|
|
elif roll == 4: |
|
|
|
self.orcs += 1 |
|
|
|
self.potatoes -= 1 |
|
|
|
elif roll == 5: |
|
|
|
self.potatoes -= 1 |
|
|
|
else: |
|
|
|
self.potatoes += 2 |
|
|
|
|
|
|
|
def door(self): |
|
|
|
roll = random.randint(1,6) |
|
|
|
if roll == 1: |
|
|
|
self.orcs += 1 |
|
|
|
elif roll == 2: |
|
|
|
self.destiny += 1 |
|
|
|
elif roll == 3: |
|
|
|
self.destiny += 1 |
|
|
|
self.orcs += 1 |
|
|
|
elif roll == 4: |
|
|
|
self.orcs += 1 |
|
|
|
self.potatoes -= 1 |
|
|
|
elif roll == 5: |
|
|
|
self.destiny += 1 |
|
|
|
else: |
|
|
|
self.potatoes += 2 |
|
|
|
|
|
|
|
def darken(self): |
|
|
|
self.potatoes_per_orc += 1 |
|
|
|
|
|
|
|
def instructions(): |
|
|
|
print(""" |
|
|
|
Simulator for Potato, a one-page RPG |
|
|
|
See https://twitter.com/deathbybadger/status/1567425842526945280 |
|
|
|
for details. |
|
|
|
|
|
|
|
Potato is a single-player, one-page RPG where you play as a halfling, |
|
|
|
farming and harvesting potatoes while the Dark Lord amasses his forces |
|
|
|
elsewhere in the world. |
|
|
|
|
|
|
|
You have three scores: DESTINY, POTATOES, and ORCS. Each round, you |
|
|
|
roll d6 to determine which type of event you experience: a day in the |
|
|
|
garden, a knock at the door, or a darkening of the world. |
|
|
|
|
|
|
|
For the first two, you then roll a second d6 to determine how your |
|
|
|
scores change; the third event makes it cost more potatoes to get rid |
|
|
|
of orcs. (At the beginning of the game, you can spend 1 potato to |
|
|
|
reduce your ORCS score by 1.) If any score reaches 10, the game ends |
|
|
|
with that score's ending; if DESTINY and ORCS reach 10 in the same |
|
|
|
round, ORCS wins, and if DESTINY and POTATOES reach 10 in the same |
|
|
|
round, you get a multi-win. (ORCS and POTATOES can never reach 10 in |
|
|
|
the same round.) |
|
|
|
|
|
|
|
When the simulator has simulated the requested number of games, it will |
|
|
|
report its results: the number and percentage of wins for each score |
|
|
|
type, and the mean and median number of rounds it took to win a game. |
|
|
|
""") |
|
|
|
input("Press any key to continue. ") |
|
|
|
print(""" |
|
|
|
Game Options |
|
|
|
|
|
|
|
-v, --verbose Print detailed information about each game. |
|
|
|
This gets very long; probably don't use it if |
|
|
|
you're asking for a large number of games! |
|
|
|
|
|
|
|
-n, --numruns [N] The number of games to simulate. Default: 10000 |
|
|
|
|
|
|
|
-m, --multiwin Announce on the console when a game ends in a |
|
|
|
multi-win. This can lead to a lot of output. |
|
|
|
|
|
|
|
-i, --instructions Print these instructions. |
|
|
|
|
|
|
|
Mutually Exclusive Options: |
|
|
|
|
|
|
|
-a, --alt Use Wanderer's algorithm for determining when |
|
|
|
to spend potatoes to reduce orcs. This spends |
|
|
|
a potato if ORCS could win this round and |
|
|
|
POTATOES could not, OR if ORCS is greater than |
|
|
|
one and a 1d10 comes up less than the ORCS |
|
|
|
score. It will only spend a potato if there |
|
|
|
are enough potatoes to spend. |
|
|
|
|
|
|
|
-p, --potatoper [N] Use the Potato-Per algorithm for determining |
|
|
|
when to spend potatoes to reduce orcs. This |
|
|
|
spends a potato when the ratio of POTATOES to |
|
|
|
ORCS rises above a certain amount, modified by |
|
|
|
the POTATOES cost to reduce ORCS by 1. It will |
|
|
|
only spend a potato if there are enough |
|
|
|
potatoes to spend. |
|
|
|
""") |
|
|
|
return None |
|
|
|
|
|
|
|
|
|
|
|
parser = argparse.ArgumentParser(description="Simulator for Potato, a one-page RPG.") |
|
|
|
parser.add_argument("-v", "--verbose", help="Prints information about each run", action="store_true") |
|
|
|
parser.add_argument("-n", "--numruns", help="Number of games to simulate", nargs="?", type=int, const=10000, default=10000) |
|
|
|
parser.add_argument("-m", "--multiwin", help="Announce multi-wins", action="store_true") |
|
|
|
parser.add_argument("-i", "--instructions", help="Print full instructions and exit", action="store_true") |
|
|
|
potato_spending = parser.add_mutually_exclusive_group() |
|
|
|
potato_spending.add_argument("-a", "--alt", help="Use Wanderer's potato-spending algorithm", action="store_true") |
|
|
|
potato_spending.add_argument("-p", "--potatoper", help="Spend when I have this many potatoes per orc", type=int) |
|
|
|
|
|
|
|
def main(): |
|
|
|
global parser |
|
|
|
args = parser.parse_args() |
|
|
|
|
|
|
|
if args.instructions: |
|
|
|
return instructions() |
|
|
|
|
|
|
|
vic_types = ["destiny", "potatoes", "orcs"] |
|
|
|
game_wins = {k: {"wins": 0, "percent": 0} for k in vic_types} |
|
|
|
rounds_counter = [] |
|
|
|
args.numruns = 1 if args.numruns < 1 else args.numruns |
|
|
|
plural = "s" if args.numruns != 1 else "" |
|
|
|
print(f"Running {args.numruns} game simulation{plural}", end="") |
|
|
|
if args.potatoper: |
|
|
|
print(f", using potatoes-per algorithm at {args.potatoper} potatoes per orc", end="") |
|
|
|
if args.alt: |
|
|
|
print(f", using Wanderer's potato-spending algorithm", end="") |
|
|
|
if args.verbose: |
|
|
|
print(", printing detailed information about each simulated game", end="") |
|
|
|
print(".\n") |
|
|
|
|
|
|
|
for _ in range(args.numruns): |
|
|
|
pg = PotatoGame(min_potatoes_per_orc=args.potatoper, alternate_po_mechanic=args.alt, verbose=args.verbose) |
|
|
|
game_end = None |
|
|
|
while not game_end: |
|
|
|
game_end = pg.event() |
|
|
|
rich_game_end = [v for i, v in enumerate(vic_types) if game_end[0][i] == 1] |
|
|
|
rounds_counter.append(game_end[1]) |
|
|
|
if args.multiwin and sum(game_end[0]) > 1: |
|
|
|
print(f"Multi-win! {rich_game_end}") |
|
|
|
for v in rich_game_end: |
|
|
|
game_wins[v]["wins"] = game_wins[v]["wins"] + 1 |
|
|
|
|
|
|
|
tallies = [] |
|
|
|
for k, v in game_wins.items(): |
|
|
|
game_wins[k]["percent"] = f"{floor((v['wins'] / args.numruns) * 10000)/100}%" |
|
|
|
tallies.append(f"{k.capitalize()}: {v['wins']} wins ({v['percent']} percent)") |
|
|
|
rounds_tally = f"Rounds per game: average {floor(mean(rounds_counter)*100)/100}, median {median(rounds_counter)}" |
|
|
|
tally_print = "Final tally:\n\t" + '\n\t'.join(tallies) + "\n\t" + rounds_tally |
|
|
|
print(tally_print) |
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
|
main() |