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