You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

potato_game.py 8.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. import argparse, random
  2. from math import floor
  3. from statistics import mean, median
  4. class PotatoGame:
  5. def __init__(self, min_potatoes_per_orc=None, alternate_po_mechanic=False, verbose=False):
  6. self.potatoes = 0
  7. self.destiny = 0
  8. self.orcs = 0
  9. self.potatoes_per_orc = 1
  10. self.min_potatoes = min_potatoes_per_orc
  11. self.default_game_state = [0, 0, 0]
  12. self.alternate_po_mechanic = alternate_po_mechanic
  13. self.verbose = verbose
  14. self.rounds = 0
  15. def vprint(self, message):
  16. if self.verbose:
  17. print(message)
  18. def event(self):
  19. self.rounds += 1
  20. if self.alternate_po_mechanic and self.potatoes >= self.potatoes_per_orc:
  21. if (self.orcs >= 8 \
  22. and self.potatoes < 8) \
  23. or (self.orcs > 0 \
  24. and random.randint(1, 10) < (self.orcs + 1)):
  25. self.vprint("Removing an orc.")
  26. self.remove_orc()
  27. if not self.alternate_po_mechanic \
  28. and (self.min_potatoes and self.orcs > 0) \
  29. and ((self.potatoes / self.potatoes_per_orc)/self.orcs >= self.min_potatoes):
  30. self.vprint(f"Removing an orc. Potatoes, PPO, orcs, ratio: {self.potatoes, self.potatoes_per_orc, self.orcs, self.min_potatoes}")
  31. self.remove_orc()
  32. roll = random.randint(1,3)
  33. if roll == 1:
  34. self.vprint("In the garden...")
  35. self.garden()
  36. elif roll == 2:
  37. self.vprint("A knock at the door...")
  38. self.door()
  39. else:
  40. self.vprint("The world becomes a darker, more dangerous place...")
  41. self.darken()
  42. self.potatoes = self.potatoes if self.potatoes >= 0 else 0
  43. self.destiny = self.destiny if self.destiny >= 0 else 0
  44. self.orcs = self.orcs if self.orcs >=0 else 0
  45. game_state = [[0, 0, 0], self.rounds]
  46. if self.destiny >= 10:
  47. game_state[0][0] = 1
  48. if self.potatoes >= 10:
  49. game_state[0][1] = 1
  50. if self.orcs >= 10:
  51. game_state[0][2] = 1
  52. if game_state[0] == [1, 0, 1] or game_state[0] == [0, 1, 1]:
  53. game_state[0] = [0, 0, 1]
  54. if game_state[0] != self.default_game_state:
  55. self.vprint(f"Game over! Game state: {game_state}")
  56. return game_state
  57. def remove_orc(self):
  58. self.potatoes -= self.potatoes_per_orc
  59. self.orcs -= 1
  60. def garden(self):
  61. roll = random.randint(1,6)
  62. if roll == 1:
  63. self.potatoes += 1
  64. elif roll == 2:
  65. self.potatoes += 1
  66. self.destiny += 1
  67. elif roll == 3:
  68. self.destiny += 1
  69. self.orcs += 1
  70. elif roll == 4:
  71. self.orcs += 1
  72. self.potatoes -= 1
  73. elif roll == 5:
  74. self.potatoes -= 1
  75. else:
  76. self.potatoes += 2
  77. def door(self):
  78. roll = random.randint(1,6)
  79. if roll == 1:
  80. self.orcs += 1
  81. elif roll == 2:
  82. self.destiny += 1
  83. elif roll == 3:
  84. self.destiny += 1
  85. self.orcs += 1
  86. elif roll == 4:
  87. self.orcs += 1
  88. self.potatoes -= 1
  89. elif roll == 5:
  90. self.destiny += 1
  91. else:
  92. self.potatoes += 2
  93. def darken(self):
  94. self.potatoes_per_orc += 1
  95. def instructions():
  96. print("""
  97. Simulator for Potato, a one-page RPG
  98. See https://twitter.com/deathbybadger/status/1567425842526945280
  99. for details.
  100. Potato is a single-player, one-page RPG where you play as a halfling,
  101. farming and harvesting potatoes while the Dark Lord amasses his forces
  102. elsewhere in the world.
  103. You have three scores: DESTINY, POTATOES, and ORCS. Each round, you
  104. roll d6 to determine which type of event you experience: a day in the
  105. garden, a knock at the door, or a darkening of the world.
  106. For the first two, you then roll a second d6 to determine how your
  107. scores change; the third event makes it cost more potatoes to get rid
  108. of orcs. (At the beginning of the game, you can spend 1 potato to
  109. reduce your ORCS score by 1.) If any score reaches 10, the game ends
  110. with that score's ending; if DESTINY and ORCS reach 10 in the same
  111. round, ORCS wins, and if DESTINY and POTATOES reach 10 in the same
  112. round, you get a multi-win. (ORCS and POTATOES can never reach 10 in
  113. the same round.)
  114. When the simulator has simulated the requested number of games, it will
  115. report its results: the number and percentage of wins for each score
  116. type, and the mean and median number of rounds it took to win a game.
  117. """)
  118. input("Press any key to continue. ")
  119. print("""
  120. Game Options
  121. -v, --verbose Print detailed information about each game.
  122. This gets very long; probably don't use it if
  123. you're asking for a large number of games!
  124. -n, --numruns [N] The number of games to simulate. Default: 10000
  125. -m, --multiwin Announce on the console when a game ends in a
  126. multi-win. This can lead to a lot of output.
  127. -i, --instructions Print these instructions.
  128. Mutually Exclusive Options:
  129. -a, --alt Use Wanderer's algorithm for determining when
  130. to spend potatoes to reduce orcs. This spends
  131. a potato if ORCS could win this round and
  132. POTATOES could not, OR if ORCS is greater than
  133. one and a 1d10 comes up less than the ORCS
  134. score. It will only spend a potato if there
  135. are enough potatoes to spend.
  136. -p, --potatoper [N] Use the Potato-Per algorithm for determining
  137. when to spend potatoes to reduce orcs. This
  138. spends a potato when the ratio of POTATOES to
  139. ORCS rises above a certain amount, modified by
  140. the POTATOES cost to reduce ORCS by 1. It will
  141. only spend a potato if there are enough
  142. potatoes to spend.
  143. """)
  144. return None
  145. parser = argparse.ArgumentParser(description="Simulator for Potato, a one-page RPG.")
  146. parser.add_argument("-v", "--verbose", help="Prints information about each run", action="store_true")
  147. parser.add_argument("-n", "--numruns", help="Number of games to simulate", nargs="?", type=int, const=10000, default=10000)
  148. parser.add_argument("-m", "--multiwin", help="Announce multi-wins", action="store_true")
  149. parser.add_argument("-i", "--instructions", help="Print full instructions and exit", action="store_true")
  150. potato_spending = parser.add_mutually_exclusive_group()
  151. potato_spending.add_argument("-a", "--alt", help="Use Wanderer's potato-spending algorithm", action="store_true")
  152. potato_spending.add_argument("-p", "--potatoper", help="Spend when I have this many potatoes per orc", type=int)
  153. def main():
  154. global parser
  155. args = parser.parse_args()
  156. if args.instructions:
  157. return instructions()
  158. vic_types = ["destiny", "potatoes", "orcs"]
  159. game_wins = {k: {"wins": 0, "percent": 0} for k in vic_types}
  160. rounds_counter = []
  161. args.numruns = 1 if args.numruns < 1 else args.numruns
  162. plural = "s" if args.numruns != 1 else ""
  163. print(f"Running {args.numruns} game simulation{plural}", end="")
  164. if args.potatoper:
  165. print(f", using potatoes-per algorithm at {args.potatoper} potatoes per orc", end="")
  166. if args.alt:
  167. print(f", using Wanderer's potato-spending algorithm", end="")
  168. if args.verbose:
  169. print(", printing detailed information about each simulated game", end="")
  170. print(".\n")
  171. for _ in range(args.numruns):
  172. pg = PotatoGame(min_potatoes_per_orc=args.potatoper, alternate_po_mechanic=args.alt, verbose=args.verbose)
  173. game_end = None
  174. while not game_end:
  175. game_end = pg.event()
  176. rich_game_end = [v for i, v in enumerate(vic_types) if game_end[0][i] == 1]
  177. rounds_counter.append(game_end[1])
  178. if args.multiwin and sum(game_end[0]) > 1:
  179. print(f"Multi-win! {rich_game_end}")
  180. for v in rich_game_end:
  181. game_wins[v]["wins"] = game_wins[v]["wins"] + 1
  182. tallies = []
  183. for k, v in game_wins.items():
  184. game_wins[k]["percent"] = f"{floor((v['wins'] / args.numruns) * 10000)/100}%"
  185. tallies.append(f"{k.capitalize()}: {v['wins']} wins ({v['percent']} percent)")
  186. rounds_tally = f"Rounds per game: average {floor(mean(rounds_counter)*100)/100}, median {median(rounds_counter)}"
  187. tally_print = "Final tally:\n\t" + '\n\t'.join(tallies) + "\n\t" + rounds_tally
  188. print(tally_print)
  189. if __name__ == "__main__":
  190. main()