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()