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

gamescog.py 4.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import math
  2. import random
  3. from discord import Interaction, UnfurledMediaItem
  4. from discord.app_commands import Range, command, guild_only
  5. from discord.ui import Container, LayoutView, Section, TextDisplay, Thumbnail
  6. from rocketbot.bot import Rocketbot
  7. from rocketbot.cogs.basecog import BaseCog
  8. class GamesCog(BaseCog, name="Games"):
  9. """Some really basic games or approximations thereof."""
  10. def __init__(self, bot: Rocketbot):
  11. super().__init__(
  12. bot,
  13. config_prefix='games',
  14. short_description='Some stupid, low effort games.',
  15. )
  16. @command(
  17. description='Rolls a die and gives the result. Provides seconds of amusement.'
  18. )
  19. @guild_only
  20. async def roll(self, interaction: Interaction, sides: Range[int, 2, 100], share: bool = False):
  21. """
  22. Rolls a die.
  23. Parameters
  24. ----------
  25. sides : Range[int, 2, 100]
  26. how many sides on the die
  27. share : bool
  28. whether to show the result to everyone in chat, otherwise only shown to you
  29. """
  30. result = random.randint(1, sides)
  31. who = interaction.user.mention if share else 'You'
  32. text = f'## :game_die: D{sides} rolled\n'\
  33. '\n'\
  34. f' {who} rolled a **{result}**.'
  35. if result == 69:
  36. text += ' :smirk:'
  37. if sides == 20 and result == 1:
  38. text += ' :slight_frown:'
  39. if sides == 20 and result == 20:
  40. text += ' :tada:'
  41. # This is a lot of work for a dumb joke
  42. others = set()
  43. for _ in range(min(sides - 1, 3)):
  44. r = random.randint(1, sides)
  45. while r in others or r == result:
  46. r = random.randint(1, sides)
  47. others.add(r)
  48. text += f'\n\n-# Users who rolled {result} also liked {listed_items(list(others))}'
  49. await interaction.response.send_message(text, ephemeral=not share)
  50. self.log(interaction.guild, f'{interaction.user.name} used /roll {sides} {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. self.log(interaction.guild, f'{interaction.user.name} used /minesweeper')
  127. class _MinesweeperLayout(LayoutView):
  128. def __init__(self, text_content: str):
  129. super().__init__()
  130. text = TextDisplay(text_content)
  131. thumb = Thumbnail(UnfurledMediaItem('https://static.rksp.net/rocketbot/games/minesweeper/icon.png'), description='Minesweeper icon')
  132. section = Section(text, accessory=thumb)
  133. container = Container(section, accent_color=0xff0000)
  134. self.add_item(container)
  135. def listed_items(items: list) -> str:
  136. if len(items) == 0:
  137. return 'nothing'
  138. if len(items) == 1:
  139. return f'{items[0]}'
  140. if len(items) == 2:
  141. return f'{items[0]} and {items[1]}'
  142. return (', '.join([ str(i) for i in items[:-1]])) + f', and {items[-1]}'