Experimental Discord bot written in Python
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

gamescog.py 4.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. import math
  2. import random
  3. from time import perf_counter
  4. from discord import Interaction, UnfurledMediaItem
  5. from discord.app_commands import command, Range, guild_only
  6. from discord.ui import LayoutView, Section, TextDisplay, Thumbnail, Container
  7. from rocketbot.bot import Rocketbot
  8. from rocketbot.cogs.basecog import BaseCog
  9. class GamesCog(BaseCog, name="Games"):
  10. """Some really basic games or approximations thereof."""
  11. def __init__(self, bot: Rocketbot):
  12. super().__init__(
  13. bot,
  14. config_prefix='games',
  15. short_description='Some stupid, low effort games.',
  16. )
  17. @command(
  18. description='Rolls a die and gives the result. Provides seconds of amusement.'
  19. )
  20. @guild_only
  21. async def roll(self, interaction: Interaction, sides: Range[int, 2, 100], share: bool = False):
  22. """
  23. Rolls a die.
  24. Parameters
  25. ----------
  26. sides : Range[int, 2, 100]
  27. how many sides on the die
  28. share : bool
  29. whether to show the result to everyone in chat, otherwise only shown to you
  30. """
  31. result = random.randint(1, sides)
  32. who = interaction.user.mention if share else 'You'
  33. text = f'## :game_die: D{sides} rolled\n'\
  34. '\n'\
  35. f' {who} rolled a **{result}**.'
  36. if result == 69:
  37. text += ' :smirk:'
  38. if sides == 20 and result == 1:
  39. text += ' :slight_frown:'
  40. if sides == 20 and result == 20:
  41. text += ' :tada:'
  42. # This is a lot of work for a dumb joke
  43. others = set()
  44. for _ in range(min(sides - 1, 3)):
  45. r = random.randint(1, sides)
  46. while r in others or r == result:
  47. r = random.randint(1, sides)
  48. others.add(r)
  49. text += f'\n\n-# Users who rolled {result} also liked {listed_items(list(others))}'
  50. await interaction.response.send_message(text, ephemeral=not share)
  51. @command(
  52. description='Creates a really terrible Minesweeper puzzle.'
  53. )
  54. @guild_only
  55. async def minesweeper(self, interaction: Interaction):
  56. MINE = -1
  57. BLANK = 0
  58. # Generate grid
  59. width, height = 8, 8 # about as big as you can get. 10x10 has too many spoiler blocks and Discord doesn't parse them.
  60. mine_count = 10
  61. grid = [[BLANK for _ in range(width)] for _ in range(height)]
  62. all_positions = [(x, y) for y in range(height) for x in range(width)]
  63. # Place mines randomly
  64. random.shuffle(all_positions)
  65. for x, y in all_positions[:mine_count]:
  66. grid[x][y] = MINE
  67. # Update free spaces with mine counts
  68. for y in range(height):
  69. for x in range(width):
  70. if grid[x][y] == MINE:
  71. continue
  72. nearby_mine_count = 0
  73. for y0 in range(y - 1, y + 2):
  74. for x0 in range(x - 1, x + 2):
  75. if 0 <= x0 < width and 0 <= y0 < height and grid[x0][y0] < 0:
  76. nearby_mine_count += 1
  77. grid[x][y] = nearby_mine_count
  78. # Pick a non-mine starting tile to reveal (favoring 0s)
  79. start_x, start_y = 0, 0
  80. for n in range(8):
  81. start_positions = [ pt for pt in all_positions if BLANK <= grid[pt[0]][pt[1]] <= n ]
  82. if len(start_positions) > 0:
  83. # sort by closeness to center
  84. start_positions.sort(key=lambda pt: math.fabs(pt[0] - width / 2) + math.fabs(pt[1] - height / 2))
  85. # pick a random one from the top 10
  86. start_x, start_y = random.choice(start_positions[:10])
  87. break
  88. # Render
  89. puzzle = ''
  90. symbols = [
  91. '\u274C', # cross mark (red X)
  92. '0\uFE0F\u20E3', # 0 + variation selector 16 + combining enclosing keycap
  93. '1\uFE0F\u20E3',
  94. '2\uFE0F\u20E3',
  95. '3\uFE0F\u20E3',
  96. '4\uFE0F\u20E3',
  97. '5\uFE0F\u20E3',
  98. '6\uFE0F\u20E3',
  99. '7\uFE0F\u20E3',
  100. '8\uFE0F\u20E3',
  101. ]
  102. for y in range(height):
  103. puzzle += ' '
  104. for x in range(width):
  105. is_revealed = x == start_x and y == start_y
  106. if not is_revealed:
  107. puzzle += '||'
  108. val = grid[x][y]
  109. puzzle += symbols[val + 1]
  110. if not is_revealed:
  111. puzzle += '||'
  112. puzzle += ' '
  113. puzzle += '\n'
  114. text = "## Minesweeper (kinda)\n"\
  115. f"Here's a really terrible randomized Minesweeper puzzle. Sorry. I did my best.¹\n"\
  116. "\n"\
  117. f"Uncover everything except the {mine_count} :x: mines. There's no game logic or anything. Just a markdown parlor trick. Police yourselves.\n"\
  118. "\n"\
  119. f"{puzzle}"\
  120. "\n"\
  121. "-# ¹ I didn't really do my best."
  122. await interaction.response.send_message(
  123. view=_MinesweeperLayout(text),
  124. ephemeral=True,
  125. )
  126. class _MinesweeperLayout(LayoutView):
  127. def __init__(self, text_content: str):
  128. super().__init__()
  129. text = TextDisplay(text_content)
  130. thumb = Thumbnail(UnfurledMediaItem('https://static.rksp.net/rocketbot/games/minesweeper/icon.png'), description='Minesweeper icon')
  131. section = Section(text, accessory=thumb)
  132. container = Container(section, accent_color=0xff0000)
  133. self.add_item(container)
  134. def listed_items(items: list) -> str:
  135. if len(items) == 0:
  136. return 'nothing'
  137. if len(items) == 1:
  138. return f'{items[0]}'
  139. if len(items) == 2:
  140. return f'{items[0]} and {items[1]}'
  141. return (', '.join([ str(i) for i in items[:-1]])) + f', and {items[-1]}'