소스 검색

Minesweeper UI improvements(?)

Adding roll die command
Making everything but /hello a guild only command
pull/13/head
Rocketsoup 2 달 전
부모
커밋
17397b0eaa
2개의 변경된 파일105개의 추가작업 그리고 34개의 파일을 삭제
  1. 100
    33
      rocketbot/cogs/gamescog.py
  2. 5
    1
      rocketbot/cogsetting.py

+ 100
- 33
rocketbot/cogs/gamescog.py 파일 보기

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

+ 5
- 1
rocketbot/cogsetting.py 파일 보기

@@ -340,6 +340,7 @@ class CogSetting:
340 340
 		cls.__set_group = Group(
341 341
 			name='set',
342 342
 			description='Sets a configuration value for this guild.',
343
+			guild_only=True,
343 344
 			default_permissions=MOD_PERMISSIONS,
344 345
 			extras={
345 346
 				'long_description': 'Settings are guild-specific. If no value is set, a default is used. Use `/get` to '
@@ -349,6 +350,7 @@ class CogSetting:
349 350
 		cls.__get_group = Group(
350 351
 			name='get',
351 352
 			description='Shows a configuration value for this guild.',
353
+			guild_only=True,
352 354
 			default_permissions=MOD_PERMISSIONS,
353 355
 			extras={
354 356
 				'long_description': 'Settings are guild-specific. If no value is set, a default is used. Use `/set` to '
@@ -358,6 +360,7 @@ class CogSetting:
358 360
 		cls.__enable_group = Group(
359 361
 			name='enable',
360 362
 			description='Enables a module for this guild.',
363
+			guild_only=True,
361 364
 			default_permissions=MOD_PERMISSIONS,
362 365
 			extras={
363 366
 				'long_description': 'Modules are enabled on a per-guild basis and are off by default. Use `/disable` '
@@ -367,6 +370,7 @@ class CogSetting:
367 370
 		cls.__disable_group = Group(
368 371
 			name='disable',
369 372
 			description='Disables a module for this guild.',
373
+			guild_only=True,
370 374
 			default_permissions=MOD_PERMISSIONS,
371 375
 			extras={
372 376
 				'long_description': 'Modules are enabled on a per-guild basis and are off by default. Use `/enable` '
@@ -384,7 +388,7 @@ class CogSetting:
384 388
 				guild = interaction.guild
385 389
 				if guild is None:
386 390
 					await interaction.response.send_message(
387
-						f'{CONFIG["error_emoji"]} No guild.',
391
+						f'{CONFIG["failure_emoji"]} No guild.',
388 392
 						ephemeral=True,
389 393
 						delete_after=10,
390 394
 					)

Loading…
취소
저장