nonebot-plugin-slot-machine 0.0.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 万俊辉
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,30 @@
1
+ Metadata-Version: 2.4
2
+ Name: nonebot-plugin-slot-machine
3
+ Version: 0.0.4
4
+ Summary: NoneBot slot machine plugin
5
+ Requires-Python: <4.0,>=3.11
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Requires-Dist: aiosqlite>=0.22.1
9
+ Requires-Dist: nonebot-adapter-milky>=1.2.0
10
+ Requires-Dist: nonebot-plugin-alconna>=0.62.0
11
+ Requires-Dist: nonebot-plugin-localstore>=0.7.4
12
+ Requires-Dist: nonebot2[aiohttp,fastapi]>=2.5.0
13
+ Requires-Dist: pillow>=10.0.0
14
+ Provides-Extra: dev
15
+ Requires-Dist: pyright[nodejs]; extra == "dev"
16
+ Requires-Dist: ruff; extra == "dev"
17
+ Dynamic: license-file
18
+
19
+ # slot_machine
20
+
21
+ ## How to start
22
+
23
+ 1. generate project using `nb create` .
24
+ 2. create your plugin using `nb plugin create` .
25
+ 3. writing your plugins under `slot_machine/plugins` folder.
26
+ 4. run your bot using `nb run --reload` .
27
+
28
+ ## Documentation
29
+
30
+ See [Docs](https://nonebot.dev/)
@@ -0,0 +1,12 @@
1
+ # slot_machine
2
+
3
+ ## How to start
4
+
5
+ 1. generate project using `nb create` .
6
+ 2. create your plugin using `nb plugin create` .
7
+ 3. writing your plugins under `slot_machine/plugins` folder.
8
+ 4. run your bot using `nb run --reload` .
9
+
10
+ ## Documentation
11
+
12
+ See [Docs](https://nonebot.dev/)
@@ -0,0 +1,30 @@
1
+ Metadata-Version: 2.4
2
+ Name: nonebot-plugin-slot-machine
3
+ Version: 0.0.4
4
+ Summary: NoneBot slot machine plugin
5
+ Requires-Python: <4.0,>=3.11
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Requires-Dist: aiosqlite>=0.22.1
9
+ Requires-Dist: nonebot-adapter-milky>=1.2.0
10
+ Requires-Dist: nonebot-plugin-alconna>=0.62.0
11
+ Requires-Dist: nonebot-plugin-localstore>=0.7.4
12
+ Requires-Dist: nonebot2[aiohttp,fastapi]>=2.5.0
13
+ Requires-Dist: pillow>=10.0.0
14
+ Provides-Extra: dev
15
+ Requires-Dist: pyright[nodejs]; extra == "dev"
16
+ Requires-Dist: ruff; extra == "dev"
17
+ Dynamic: license-file
18
+
19
+ # slot_machine
20
+
21
+ ## How to start
22
+
23
+ 1. generate project using `nb create` .
24
+ 2. create your plugin using `nb plugin create` .
25
+ 3. writing your plugins under `slot_machine/plugins` folder.
26
+ 4. run your bot using `nb run --reload` .
27
+
28
+ ## Documentation
29
+
30
+ See [Docs](https://nonebot.dev/)
@@ -0,0 +1,15 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ nonebot_plugin_slot_machine.egg-info/PKG-INFO
5
+ nonebot_plugin_slot_machine.egg-info/SOURCES.txt
6
+ nonebot_plugin_slot_machine.egg-info/dependency_links.txt
7
+ nonebot_plugin_slot_machine.egg-info/requires.txt
8
+ nonebot_plugin_slot_machine.egg-info/top_level.txt
9
+ slot_machine/plugins/slot_machine/__init__.py
10
+ slot_machine/plugins/slot_machine/algorithm.py
11
+ slot_machine/plugins/slot_machine/constants.py
12
+ slot_machine/plugins/slot_machine/database.py
13
+ slot_machine/plugins/slot_machine/utils.py
14
+ slot_machine/plugins/slot_machine/image/image.png
15
+ slot_machine/plugins/slot_machine/plugins/screw_work/__init__.py
@@ -0,0 +1,10 @@
1
+ aiosqlite>=0.22.1
2
+ nonebot-adapter-milky>=1.2.0
3
+ nonebot-plugin-alconna>=0.62.0
4
+ nonebot-plugin-localstore>=0.7.4
5
+ nonebot2[aiohttp,fastapi]>=2.5.0
6
+ pillow>=10.0.0
7
+
8
+ [dev]
9
+ pyright[nodejs]
10
+ ruff
@@ -0,0 +1,117 @@
1
+ [project]
2
+ name = "nonebot-plugin-slot-machine"
3
+ version = "0.0.4"
4
+ description = "NoneBot slot machine plugin"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11, <4.0"
7
+ dependencies = [
8
+ "aiosqlite>=0.22.1",
9
+ "nonebot-adapter-milky>=1.2.0",
10
+ "nonebot-plugin-alconna>=0.62.0",
11
+ "nonebot-plugin-localstore>=0.7.4",
12
+ "nonebot2[aiohttp,fastapi]>=2.5.0",
13
+ "pillow>=10.0.0",
14
+ ]
15
+
16
+ [build-system]
17
+ requires = ["setuptools>=69"]
18
+ build-backend = "setuptools.build_meta"
19
+
20
+ [tool.setuptools.packages.find]
21
+ include = ["slot_machine*"]
22
+
23
+ [tool.setuptools.package-data]
24
+ "slot_machine.plugins.slot_machine" = ["image/*.png"]
25
+
26
+ [project.optional-dependencies]
27
+ dev = [
28
+ "pyright[nodejs]",
29
+ "ruff"
30
+ ]
31
+
32
+ [tool.nonebot]
33
+ plugin_dirs = ["slot_machine/plugins"]
34
+ builtin_plugins = []
35
+
36
+ [tool.nonebot.adapters]
37
+ nonebot-adapter-milky = [
38
+ { name = "nonebot-adapter-milky", module_name = "nonebot.adapters.milky" }
39
+ ]
40
+ "@local" = []
41
+
42
+ [tool.nonebot.plugins]
43
+ "@local" = []
44
+ nonebot-plugin-alconna = ["nonebot_plugin_alconna"]
45
+ [tool.ruff]
46
+ line-length = 88
47
+ target-version = "py311"
48
+
49
+ [tool.ruff.format]
50
+ line-ending = "lf"
51
+
52
+ [tool.ruff.lint]
53
+ # For more rules, see https://docs.astral.sh/ruff/rules/.
54
+ select = [
55
+ "F", # Pyflakes
56
+ "W", # pycodestyle warnings
57
+ "E", # pycodestyle errors
58
+ "I", # isort
59
+ "C90", # mccabe
60
+ "N", # pep8-naming
61
+ "PL", # pylint
62
+ "UP", # pyupgrade
63
+ "YTT", # flake8-2020
64
+ "ANN", # flake8-annotations
65
+ "ASYNC", # flake8-async
66
+ "BLE", # flake8-blind-except
67
+ "FBT", # flake8-boolean-trap
68
+ "B", # flake8-bugbear
69
+ "A", # flake8-builtins
70
+ "COM", # flake8-commas
71
+ "C4", # flake8-comprehensions
72
+ "DTZ", # flake8-datetimez
73
+ "T10", # flake8-debugger
74
+ "ICN", # flake8-import-conventions
75
+ "PIE", # flake8-pie
76
+ "T20", # flake8-print
77
+ "PYI", # flake8-pyi
78
+ "Q", # flake8-quotes
79
+ "RSE", # flake8-raise
80
+ "RET", # flake8-return
81
+ "SIM", # flake8-simplify
82
+ "SLOT", # flake8-slots
83
+ "TID", # flake8-tidy-imports
84
+ "TC", # flake8-type-checking
85
+ "ARG", # flake8-unused-arguments
86
+ "PTH", # flake8-use-pathlib
87
+ # "ERA", # eradicate
88
+ "FAST", # FastAPI
89
+ "PERF", # Perflint
90
+ "PGH", # pygrep-hooks
91
+ "FURB", # refurb
92
+ "TRY", # tryceratops
93
+ "RUF", # Ruff-specific rules
94
+ ]
95
+ ignore = [
96
+ "E402", # module-import-not-at-top-of-file # nonebot2 require() violates this
97
+ "B008", # function-call-in-default-argument # nonebot2 Depends() without Annotated violates this
98
+ "UP037", # quoted-annotation
99
+ # "RUF001", # ambiguous-unicode-character-string
100
+ # "RUF002", # ambiguous-unicode-character-docstring
101
+ # "RUF003", # ambiguous-unicode-character-comment
102
+ # "ANN201", # missing-return-type-undocumented-public-function
103
+ "ANN202", # missing-return-type-private-function
104
+ "ANN401", # any-type
105
+ "COM812", # missing-trailing-comma
106
+ "PLC0415", # import-outside-top-level
107
+ ]
108
+ allowed-confusables = [",", "。", "“", "”", ":", ";", "?", "!", "【", "】", "《", "》", "…", "—", "(", ")", "、"]
109
+
110
+ [tool.ruff.lint.pyupgrade]
111
+ keep-runtime-typing = true
112
+
113
+ [tool.pyright]
114
+ # For Pylance/Pyright configurations, see https://microsoft.github.io/pyright/#/configuration.
115
+ pythonVersion = "3.11"
116
+ pythonPlatform = "All"
117
+ typeCheckingMode = "standard"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,307 @@
1
+ import asyncio
2
+ from decimal import Decimal, InvalidOperation
3
+ from pathlib import Path
4
+
5
+ from nonebot import get_driver, load_plugins, logger, on_command
6
+ from nonebot.adapters.milky import MessageSegment
7
+ from nonebot.adapters.milky.event import MessageEvent
8
+ from nonebot.plugin import PluginMetadata
9
+ from nonebot_plugin_alconna import Alconna, Args, CommandMeta, on_alconna
10
+
11
+ from .algorithm import (
12
+ BetConfigError,
13
+ CascadeResult,
14
+ calculate_allowed_win_probability,
15
+ format_bet_summary,
16
+ parse_bet_config,
17
+ resolve_controlled_spin,
18
+ )
19
+ from .constants import REGISTRATION_REWARD
20
+ from .database import (
21
+ apply_spin_result,
22
+ get_bet_setting,
23
+ get_user,
24
+ initialize_database,
25
+ register_user,
26
+ transfer_user_coins,
27
+ upsert_bet_setting,
28
+ )
29
+ from .utils import (
30
+ GeneratedSpinImage,
31
+ SpinMessageContext,
32
+ build_forward_message,
33
+ draw_spin_result_image,
34
+ format_decimal,
35
+ )
36
+
37
+ __plugin_meta__ = PluginMetadata(
38
+ name="slot_machine",
39
+ description="一个老虎机游戏插件",
40
+ usage=(
41
+ "注册老虎机/注册:领取初始金币\n"
42
+ "设置投注 <投注大小> <投注倍数>:保存投注设置\n"
43
+ "查询老虎机/查询:查看账号和投注信息\n"
44
+ "开始旋转:开始抽奖"
45
+ ),
46
+ )
47
+
48
+ load_plugins(str(Path(__file__).parent / "plugins"))
49
+
50
+ slot_register = on_command(
51
+ "注册老虎机",
52
+ aliases={"注册"},
53
+ block=True,
54
+ )
55
+ slot_setting = on_alconna(
56
+ Alconna(
57
+ "设置投注",
58
+ Args["bet_size", str]["multiplier", int],
59
+ meta=CommandMeta(description="设置老虎机投注大小和投注倍数"),
60
+ ),
61
+ aliases={"setslot"},
62
+ block=True,
63
+ )
64
+ slot_transfer = on_alconna(
65
+ Alconna(
66
+ "转账",
67
+ Args["receiver_account", str]["amount", str],
68
+ meta=CommandMeta(description="向其他老虎机账号转账金币"),
69
+ ),
70
+ block=True,
71
+ )
72
+ slot_query = on_command(
73
+ "查询老虎机",
74
+ aliases={"查询"},
75
+ block=True,
76
+ )
77
+ slot_machine = on_command(
78
+ "开始旋转",
79
+ block=True,
80
+ )
81
+
82
+ @get_driver().on_startup
83
+ async def startup_slot_machine() -> None:
84
+ await initialize_database()
85
+
86
+
87
+ async def send_spin_result(event: MessageEvent, context: SpinMessageContext) -> None:
88
+ images: list[GeneratedSpinImage] = []
89
+ try:
90
+ images = list(
91
+ await asyncio.gather(
92
+ *(
93
+ draw_spin_result_image(
94
+ context=context,
95
+ cascade=cascade,
96
+ cascade_index=index,
97
+ )
98
+ for index, cascade in enumerate(context.all_cascades, start=1)
99
+ )
100
+ )
101
+ )
102
+
103
+ if len(images) <= 3: # noqa: PLR2004
104
+ for image in images[:-1]:
105
+ await slot_machine.send(MessageSegment.image(raw=image.data))
106
+ await slot_machine.finish(MessageSegment.image(raw=images[-1].data))
107
+
108
+ await slot_machine.finish(build_forward_message(event, context, images))
109
+ finally:
110
+ for image in images:
111
+ image.path.unlink(missing_ok=True)
112
+
113
+
114
+ async def send_miss_result(context: SpinMessageContext, board: list[list[str]]) -> None:
115
+ image = await draw_spin_result_image(
116
+ context,
117
+ CascadeResult(
118
+ board=board,
119
+ highlighted_positions=frozenset(),
120
+ bonus_multiplier=1,
121
+ payout=Decimal(0),
122
+ ),
123
+ )
124
+ try:
125
+ await slot_machine.finish(MessageSegment.image(raw=image.data))
126
+ finally:
127
+ image.path.unlink(missing_ok=True)
128
+
129
+
130
+ @slot_register.handle()
131
+ async def handle_slot_register(event: MessageEvent) -> None:
132
+ account = event.get_user_id()
133
+ registered = await register_user(account)
134
+ user = await get_user(account)
135
+
136
+ if user is None:
137
+ await slot_register.finish("注册失败,请稍后再试。")
138
+
139
+ if not registered:
140
+ await slot_register.finish(
141
+ "你已经注册过了。\n"
142
+ f"账号:{account}\n"
143
+ f"当前金币:{format_decimal(user.coins)}\n"
144
+ f"抽奖次数:{user.spin_count}"
145
+ )
146
+
147
+ await slot_register.finish(
148
+ "注册成功。\n"
149
+ f"账号:{account}\n"
150
+ f"赠送金币:{format_decimal(REGISTRATION_REWARD)}\n"
151
+ f"当前金币:{format_decimal(user.coins)}"
152
+ )
153
+
154
+
155
+ @slot_setting.handle()
156
+ async def handle_slot_setting(
157
+ event: MessageEvent,
158
+ bet_size: str,
159
+ multiplier: int,
160
+ ) -> None:
161
+ account = event.get_user_id()
162
+ user = await get_user(account)
163
+
164
+ if user is None:
165
+ await slot_setting.finish("你还没有注册。\n请先发送:注册老虎机")
166
+
167
+ try:
168
+ bet_config = parse_bet_config(f"{bet_size} {multiplier}")
169
+ except BetConfigError:
170
+ await slot_setting.finish(
171
+ "请输入:设置投注 <投注大小> <投注倍数>,例如:设置投注 0.2 5\n"
172
+ "投注大小只能是 0.02、0.2 或 1,投注倍数只能是 1 到 10 的整数。"
173
+ )
174
+
175
+ await upsert_bet_setting(account, bet_config)
176
+ await slot_setting.finish(f"已保存你的投注设置。\n{format_bet_summary(bet_config)}")
177
+
178
+
179
+ @slot_transfer.handle()
180
+ async def handle_slot_transfer(
181
+ event: MessageEvent,
182
+ receiver_account: str,
183
+ amount: str,
184
+ ) -> None:
185
+ sender_account = event.get_user_id()
186
+ sender = await get_user(sender_account)
187
+ if sender is None:
188
+ await slot_transfer.finish("你还没有注册。\n请先发送:注册老虎机")
189
+
190
+ try:
191
+ transfer_amount = Decimal(amount)
192
+ except InvalidOperation:
193
+ await slot_transfer.finish("请输入正确金额,例如:转账 123456 10")
194
+
195
+ if transfer_amount <= 0:
196
+ await slot_transfer.finish("转账金额必须大于 0。")
197
+
198
+ receiver = await get_user(receiver_account)
199
+ if receiver is None:
200
+ await slot_transfer.finish("对方还没有注册老虎机账号。")
201
+
202
+ if sender_account == receiver_account:
203
+ await slot_transfer.finish("不能给自己转账。")
204
+
205
+ if sender.coins < transfer_amount:
206
+ await slot_transfer.finish(
207
+ "金币不足,无法转账。\n"
208
+ f"当前金币:{format_decimal(sender.coins)}\n"
209
+ f"转账金额:{format_decimal(transfer_amount)}"
210
+ )
211
+
212
+ updated_sender, updated_receiver = await transfer_user_coins(
213
+ sender_account,
214
+ receiver_account,
215
+ transfer_amount,
216
+ )
217
+ await slot_transfer.finish(
218
+ "转账成功。\n"
219
+ f"收款账号:{receiver_account}\n"
220
+ f"转账金额:{format_decimal(transfer_amount)} 金币\n"
221
+ f"你的剩余金币:{format_decimal(updated_sender.coins)}\n"
222
+ f"对方当前金币:{format_decimal(updated_receiver.coins)}"
223
+ )
224
+
225
+
226
+ @slot_query.handle()
227
+ async def handle_slot_query(event: MessageEvent) -> None:
228
+ account = event.get_user_id()
229
+ user = await get_user(account)
230
+
231
+ if user is None:
232
+ await slot_query.finish("你还没有注册。\n请先发送:注册老虎机")
233
+
234
+ bet_config = await get_bet_setting(account)
235
+ bet_text = (
236
+ format_bet_summary(bet_config)
237
+ if bet_config is not None
238
+ else "尚未设置投注"
239
+ )
240
+ await slot_query.finish(
241
+ "老虎机账号信息\n"
242
+ f"账号:{account}\n"
243
+ f"当前金币:{format_decimal(user.coins)}\n"
244
+ f"抽奖次数:{user.spin_count}\n"
245
+ f"中奖次数:{user.win_count}\n"
246
+ f"累计获得:{format_decimal(user.total_payout)} 金币\n\n"
247
+ f"当前投注:\n{bet_text}"
248
+ )
249
+
250
+
251
+ @slot_machine.handle()
252
+ async def handle_slot_machine(event: MessageEvent) -> None:
253
+ account = event.get_user_id()
254
+ user = await get_user(account)
255
+
256
+ if user is None:
257
+ await slot_machine.finish("你还没有注册。\n请先发送:注册老虎机")
258
+
259
+ bet_config = await get_bet_setting(account)
260
+ if bet_config is None:
261
+ await slot_machine.finish(
262
+ "你还没有设置投注。\n"
263
+ "请先发送:设置投注 <投注大小> <投注倍数>\n"
264
+ "例如:设置投注 0.2 5"
265
+ )
266
+
267
+ if user.coins < bet_config.total_bet:
268
+ await slot_machine.finish(
269
+ "金币不足,无法抽奖。\n"
270
+ f"当前金币:{format_decimal(user.coins)}\n"
271
+ f"本次需要:{format_decimal(bet_config.total_bet)}"
272
+ )
273
+
274
+ allowed_probability = calculate_allowed_win_probability(
275
+ user.coins,
276
+ user.win_count,
277
+ user.total_payout,
278
+ )
279
+ logger.info(
280
+ f"老虎机抽奖概率 | 账号:{account} | "
281
+ f"金币:{format_decimal(user.coins)} | "
282
+ f"中奖次数:{user.win_count} | "
283
+ f"累计获得:{format_decimal(user.total_payout)} | "
284
+ f"本次概率:{allowed_probability:.2%}"
285
+ )
286
+
287
+ spin_result = resolve_controlled_spin(
288
+ bet_config,
289
+ user.coins,
290
+ user.win_count,
291
+ user.total_payout,
292
+ )
293
+ updated_user = await apply_spin_result(
294
+ account, bet_config.total_bet, spin_result.total_payout
295
+ )
296
+ context = SpinMessageContext(
297
+ account=account,
298
+ total_bet=format_decimal(bet_config.total_bet),
299
+ total_payout=format_decimal(spin_result.total_payout),
300
+ remaining_coins=format_decimal(updated_user.coins),
301
+ all_cascades=list(spin_result.cascades),
302
+ )
303
+
304
+ if not spin_result.cascades:
305
+ await send_miss_result(context, spin_result.final_board)
306
+
307
+ await send_spin_result(event, context)