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

gamescog.py 4.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  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. self.log(interaction.guild, f'{interaction.user.name} used /roll {sides} {share}')
  52. @command(
  53. description='Creates a really terrible Minesweeper puzzle.'
  54. )
  55. @guild_only
  56. async def minesweeper(self, interaction: Interaction):
  57. MINE = -1
  58. BLANK = 0
  59. # Generate grid
  60. width, height = 8, 8 # about as big as you can get. 10x10 has too many spoiler blocks and Discord doesn't parse them.
  61. mine_count = 10
  62. grid = [[BLANK for _ in range(width)] for _ in range(height)]
  63. all_positions = [(x, y) for y in range(height) for x in range(width)]
  64. # Place mines randomly
  65. random.shuffle(all_positions)
  66. for x, y in all_positions[:mine_count]:
  67. grid[x][y] = MINE
  68. # Update free spaces with mine counts
  69. for y in range(height):
  70. for x in range(width):
  71. if grid[x][y] == MINE:
  72. continue
  73. nearby_mine_count = 0
  74. for y0 in range(y - 1, y + 2):
  75. for x0 in range(x - 1, x + 2):
  76. if 0 <= x0 < width and 0 <= y0 < height and grid[x0][y0] < 0:
  77. nearby_mine_count += 1
  78. grid[x][y] = nearby_mine_count
  79. # Pick a non-mine starting tile to reveal (favoring 0s)
  80. start_x, start_y = 0, 0
  81. for n in range(8):
  82. start_positions = [ pt for pt in all_positions if BLANK <= grid[pt[0]][pt[1]] <= n ]
  83. if len(start_positions) > 0:
  84. # sort by closeness to center
  85. start_positions.sort(key=lambda pt: math.fabs(pt[0] - width / 2) + math.fabs(pt[1] - height / 2))
  86. # pick a random one from the top 10
  87. start_x, start_y = random.choice(start_positions[:10])
  88. break
  89. # Render
  90. puzzle = ''
  91. symbols = [
  92. '\u274C', # cross mark (red X)
  93. '0\uFE0F\u20E3', # 0 + variation selector 16 + combining enclosing keycap
  94. '1\uFE0F\u20E3',
  95. '2\uFE0F\u20E3',
  96. '3\uFE0F\u20E3',
  97. '4\uFE0F\u20E3',
  98. '5\uFE0F\u20E3',
  99. '6\uFE0F\u20E3',
  100. '7\uFE0F\u20E3',
  101. '8\uFE0F\u20E3',
  102. ]
  103. for y in range(height):
  104. puzzle += ' '
  105. for x in range(width):
  106. is_revealed = x == start_x and y == start_y
  107. if not is_revealed:
  108. puzzle += '||'
  109. val = grid[x][y]
  110. puzzle += symbols[val + 1]
  111. if not is_revealed:
  112. puzzle += '||'
  113. puzzle += ' '
  114. puzzle += '\n'
  115. text = "## Minesweeper (kinda)\n"\
  116. f"Here's a really terrible randomized Minesweeper puzzle. Sorry. I did my best.¹\n"\
  117. "\n"\
  118. f"Uncover everything except the {mine_count} :x: mines. There's no game logic or anything. Just a markdown parlor trick. Police yourselves.\n"\
  119. "\n"\
  120. f"{puzzle}"\
  121. "\n"\
  122. "-# ¹ I didn't really do my best."
  123. await interaction.response.send_message(
  124. view=_MinesweeperLayout(text),
  125. ephemeral=True,
  126. )
  127. self.log(interaction.guild, f'{interaction.user.name} used /minesweeper')
  128. class _MinesweeperLayout(LayoutView):
  129. def __init__(self, text_content: str):
  130. super().__init__()
  131. text = TextDisplay(text_content)
  132. thumb = Thumbnail(UnfurledMediaItem('https://static.rksp.net/rocketbot/games/minesweeper/icon.png'), description='Minesweeper icon')
  133. section = Section(text, accessory=thumb)
  134. container = Container(section, accent_color=0xff0000)
  135. self.add_item(container)
  136. def listed_items(items: list) -> str:
  137. if len(items) == 0:
  138. return 'nothing'
  139. if len(items) == 1:
  140. return f'{items[0]}'
  141. if len(items) == 2:
  142. return f'{items[0]} and {items[1]}'
  143. return (', '.join([ str(i) for i in items[:-1]])) + f', and {items[-1]}'