nonebot-plugin-werewolf 1.1.3__py3-none-any.whl → 1.1.6__py3-none-any.whl

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.
Files changed (35) hide show
  1. nonebot_plugin_werewolf/__init__.py +3 -1
  2. nonebot_plugin_werewolf/config.py +18 -55
  3. nonebot_plugin_werewolf/constant.py +20 -58
  4. nonebot_plugin_werewolf/exception.py +1 -1
  5. nonebot_plugin_werewolf/game.py +286 -245
  6. nonebot_plugin_werewolf/matchers/__init__.py +2 -0
  7. nonebot_plugin_werewolf/matchers/depends.py +50 -0
  8. nonebot_plugin_werewolf/matchers/edit_preset.py +263 -0
  9. nonebot_plugin_werewolf/matchers/message_in_game.py +18 -3
  10. nonebot_plugin_werewolf/matchers/poke/__init__.py +8 -0
  11. nonebot_plugin_werewolf/matchers/poke/chronocat_poke.py +117 -0
  12. nonebot_plugin_werewolf/matchers/{ob11_ext.py → poke/ob11_poke.py} +21 -19
  13. nonebot_plugin_werewolf/matchers/start_game.py +266 -28
  14. nonebot_plugin_werewolf/matchers/superuser_ops.py +24 -0
  15. nonebot_plugin_werewolf/models.py +73 -0
  16. nonebot_plugin_werewolf/player_set.py +33 -34
  17. nonebot_plugin_werewolf/players/can_shoot.py +15 -20
  18. nonebot_plugin_werewolf/players/civilian.py +3 -3
  19. nonebot_plugin_werewolf/players/guard.py +16 -22
  20. nonebot_plugin_werewolf/players/hunter.py +3 -3
  21. nonebot_plugin_werewolf/players/idiot.py +4 -4
  22. nonebot_plugin_werewolf/players/joker.py +8 -4
  23. nonebot_plugin_werewolf/players/player.py +133 -70
  24. nonebot_plugin_werewolf/players/prophet.py +8 -15
  25. nonebot_plugin_werewolf/players/werewolf.py +54 -30
  26. nonebot_plugin_werewolf/players/witch.py +33 -38
  27. nonebot_plugin_werewolf/players/wolfking.py +3 -3
  28. nonebot_plugin_werewolf/utils.py +109 -179
  29. {nonebot_plugin_werewolf-1.1.3.dist-info → nonebot_plugin_werewolf-1.1.6.dist-info}/METADATA +78 -66
  30. nonebot_plugin_werewolf-1.1.6.dist-info/RECORD +34 -0
  31. {nonebot_plugin_werewolf-1.1.3.dist-info → nonebot_plugin_werewolf-1.1.6.dist-info}/WHEEL +1 -1
  32. nonebot_plugin_werewolf/_timeout.py +0 -110
  33. nonebot_plugin_werewolf-1.1.3.dist-info/RECORD +0 -29
  34. {nonebot_plugin_werewolf-1.1.3.dist-info → nonebot_plugin_werewolf-1.1.6.dist-info}/LICENSE +0 -0
  35. {nonebot_plugin_werewolf-1.1.3.dist-info → nonebot_plugin_werewolf-1.1.6.dist-info}/top_level.txt +0 -0
@@ -1,2 +1,4 @@
1
+ from . import edit_preset as edit_preset
1
2
  from . import message_in_game as message_in_game
2
3
  from . import start_game as start_game
4
+ from . import superuser_ops as superuser_ops
@@ -0,0 +1,50 @@
1
+ import itertools
2
+
3
+ from nonebot.adapters import Bot, Event
4
+ from nonebot_plugin_alconna import MsgTarget, UniMessage
5
+
6
+ from ..game import Game
7
+
8
+
9
+ def user_in_game(self_id: str, user_id: str, group_id: str | None) -> bool:
10
+ if group_id is None:
11
+ return any(
12
+ self_id == p.bot.self_id and user_id == p.user_id
13
+ for p in itertools.chain(*[g.players for g in Game.running_games])
14
+ )
15
+
16
+ def check(game: Game) -> bool:
17
+ return self_id == game.group.self_id and group_id == game.group.id
18
+
19
+ if game := next(filter(check, Game.running_games), None):
20
+ return any(user_id == player.user_id for player in game.players)
21
+
22
+ return False
23
+
24
+
25
+ async def rule_in_game(bot: Bot, event: Event) -> bool:
26
+ if not Game.running_games:
27
+ return False
28
+
29
+ try:
30
+ target = UniMessage.get_target(event, bot)
31
+ except NotImplementedError:
32
+ return False
33
+
34
+ if target.private:
35
+ return user_in_game(bot.self_id, target.id, None)
36
+
37
+ try:
38
+ user_id = event.get_user_id()
39
+ except Exception:
40
+ return False
41
+
42
+ return user_in_game(bot.self_id, user_id, target.id)
43
+
44
+
45
+ async def rule_not_in_game(bot: Bot, event: Event) -> bool:
46
+ return not await rule_in_game(bot, event)
47
+
48
+
49
+ async def is_group(target: MsgTarget) -> bool:
50
+ return not target.private
@@ -0,0 +1,263 @@
1
+ from typing import Any, NoReturn
2
+
3
+ import nonebot_plugin_waiter as waiter
4
+ from arclet.alconna import AllParam
5
+ from nonebot.permission import SUPERUSER
6
+ from nonebot.typing import T_State
7
+ from nonebot_plugin_alconna import (
8
+ Alconna,
9
+ Args,
10
+ CommandMeta,
11
+ Match,
12
+ Subcommand,
13
+ UniMessage,
14
+ on_alconna,
15
+ )
16
+
17
+ from ..config import PresetData
18
+ from ..constant import role_name_conv
19
+ from ..models import Role
20
+
21
+ alc = Alconna(
22
+ "狼人杀预设",
23
+ Subcommand(
24
+ "role",
25
+ Args["total#总人数", int],
26
+ Args["werewolf#狼人数量", int],
27
+ Args["priesthood#神职数量", int],
28
+ Args["civilian#平民数量", int],
29
+ alias={"职业"},
30
+ help_text="设置总人数为 <total> 的职业分配预设",
31
+ ),
32
+ Subcommand(
33
+ "del",
34
+ Args["total#总人数", int],
35
+ alias={"删除"},
36
+ help_text="删除总人数为 <total> 的职业分配预设",
37
+ ),
38
+ Subcommand(
39
+ "werewolf",
40
+ Args["roles?#职业", AllParam],
41
+ alias={"狼人"},
42
+ help_text="设置狼人优先级",
43
+ ),
44
+ Subcommand(
45
+ "priesthood",
46
+ Args["roles?#职业", AllParam],
47
+ alias={"神职"},
48
+ help_text="设置神职优先级",
49
+ ),
50
+ Subcommand(
51
+ "joker",
52
+ Args["probability?#概率", float],
53
+ alias={"小丑"},
54
+ help_text="设置小丑概率",
55
+ ),
56
+ Subcommand("reset", alias={"重置"}, help_text="重置为默认预设"),
57
+ meta=CommandMeta(
58
+ description="编辑狼人杀游戏预设",
59
+ usage="狼人杀预设 --help",
60
+ example=(
61
+ "狼人杀预设\n"
62
+ "狼人杀预设 职业 6 1 2 3\n"
63
+ "狼人杀预设 删除 6\n"
64
+ "狼人杀预设 狼人 狼 狼 狼王 狼 狼\n"
65
+ "狼人杀预设 神职 巫 预 猎 守卫 白痴\n"
66
+ "狼人杀预设 小丑 15\n"
67
+ "狼人杀预设 重置"
68
+ ),
69
+ author="wyf7685",
70
+ ),
71
+ )
72
+
73
+ edit_preset = on_alconna(
74
+ alc,
75
+ permission=SUPERUSER,
76
+ use_cmd_start=True,
77
+ )
78
+
79
+
80
+ async def finish(text: str) -> NoReturn:
81
+ await UniMessage.text(text).finish(reply_to=True)
82
+
83
+
84
+ def display_roles(roles: list[Role]) -> str:
85
+ role_name = role_name_conv.__getitem__
86
+ return ", ".join(map(role_name, roles))
87
+
88
+
89
+ @edit_preset.assign("role")
90
+ async def assign_role(
91
+ total: Match[int],
92
+ werewolf: Match[int],
93
+ priesthood: Match[int],
94
+ civilian: Match[int],
95
+ ) -> None:
96
+ preset = (
97
+ werewolf.result,
98
+ priesthood.result,
99
+ civilian.result,
100
+ )
101
+ if sum(preset) != total.result:
102
+ await finish("总人数与职业数量不匹配")
103
+
104
+ data = PresetData.load()
105
+ if werewolf.result > len(data.werewolf_priority):
106
+ await finish("狼人数量超出优先级列表长度,请先设置足够多的狼人预设")
107
+ if priesthood.result > len(data.priesthood_proirity):
108
+ await finish("神职数量超出优先级列表长度,请先设置足够多的神职预设")
109
+
110
+ data.role_preset[total.result] = preset
111
+ data.save()
112
+ await finish(
113
+ f"设置成功\n{total.result} 人: "
114
+ f"狼人x{werewolf.result}, 神职x{priesthood.result}, 平民x{civilian.result}"
115
+ )
116
+
117
+
118
+ @edit_preset.assign("del")
119
+ async def delete_role(total: Match[int]) -> None:
120
+ data = PresetData.load()
121
+ if total.result not in data.role_preset:
122
+ await finish("未找到对应预设")
123
+ del data.role_preset[total.result]
124
+ data.save()
125
+ await finish("删除成功")
126
+
127
+
128
+ @edit_preset.assign("werewolf")
129
+ async def handle_werewolf_input_roles(roles: Match[Any], state: T_State) -> None:
130
+ if roles.available:
131
+ state["roles"] = UniMessage(roles.result).extract_plain_text().split(" ")
132
+ return
133
+
134
+ result = await waiter.prompt(
135
+ "请发送狼人优先级列表,以空格隔开\n发送 “取消” 取消操作"
136
+ )
137
+ if result is None:
138
+ await finish("发送超时,已自动取消")
139
+
140
+ text = result.extract_plain_text()
141
+ if text == "取消":
142
+ await finish("已取消操作")
143
+
144
+ state["roles"] = text.split(" ")
145
+
146
+
147
+ @edit_preset.assign("werewolf")
148
+ async def assign_werewolf(state: T_State) -> None:
149
+ roles: list[str] = state["roles"]
150
+ result: list[Role] = []
151
+
152
+ for role in roles:
153
+ match role:
154
+ case "狼人" | "狼":
155
+ result.append(Role.Werewolf)
156
+ case "狼王":
157
+ result.append(Role.WolfKing)
158
+ case x:
159
+ await finish(f"未知职业: {x}")
160
+
161
+ data = PresetData.load()
162
+ min_length = max(w for w, _, _ in data.role_preset.values())
163
+ if len(result) < min_length:
164
+ await finish(f"狼人数量不足,至少需要 {min_length} 个狼人")
165
+
166
+ data.werewolf_priority = result
167
+ data.save()
168
+ await finish("设置成功: " + display_roles(result))
169
+
170
+
171
+ @edit_preset.assign("priesthood")
172
+ async def handle_priesthood_input_roles(roles: Match[Any], state: T_State) -> None:
173
+ if roles.available:
174
+ state["roles"] = UniMessage(roles.result).extract_plain_text().split(" ")
175
+ return
176
+
177
+ result = await waiter.prompt(
178
+ "请发送神职优先级列表,以空格隔开\n发送 “取消” 取消操作"
179
+ )
180
+ if result is None:
181
+ await finish("发送超时,已自动取消")
182
+
183
+ text = result.extract_plain_text()
184
+ if text == "取消":
185
+ await finish("已取消操作")
186
+
187
+ state["roles"] = text.split(" ")
188
+
189
+
190
+ @edit_preset.assign("priesthood")
191
+ async def assign_priesthood(state: T_State) -> None:
192
+ roles: list[str] = state["roles"]
193
+ result: list[Role] = []
194
+
195
+ for role in roles:
196
+ match role:
197
+ case "预言家" | "预言" | "预":
198
+ result.append(Role.Prophet)
199
+ case "女巫" | "巫":
200
+ result.append(Role.Witch)
201
+ case "猎人" | "猎":
202
+ result.append(Role.Hunter)
203
+ case "守卫":
204
+ result.append(Role.Guard)
205
+ case "白痴":
206
+ result.append(Role.Idiot)
207
+ case x:
208
+ await finish(f"未知职业: {x}")
209
+
210
+ data = PresetData.load()
211
+ min_length = max(p for _, p, _ in data.role_preset.values())
212
+ if len(result) < min_length:
213
+ await finish(f"神职数量不足,至少需要 {min_length} 个神职")
214
+
215
+ data.priesthood_proirity = result
216
+ data.save()
217
+ await finish("设置成功: " + display_roles(result))
218
+
219
+
220
+ @edit_preset.assign("joker")
221
+ async def assign_joker(probability: Match[float]) -> None:
222
+ if not probability.available:
223
+ result = await waiter.prompt_until(
224
+ message="请发送小丑概率,范围 0-100\n发送 “取消” 取消操作",
225
+ checker=lambda m: (s := m.extract_plain_text()).isdigit() or s == "取消",
226
+ retry_prompt="输入错误,请重新输入一个正确的数字。\n剩余次数:{count}",
227
+ )
228
+ if result is None:
229
+ await finish("发送超时,已自动取消")
230
+ text = result.extract_plain_text()
231
+ if text == "取消":
232
+ await finish("已取消操作")
233
+ probability.result = float(text)
234
+
235
+ if not 0 <= probability.result <= 100:
236
+ await finish("输入错误,概率应在 0 到 100 之间")
237
+
238
+ data = PresetData.load()
239
+ data.joker_probability = probability.result / 100
240
+ data.save()
241
+ await finish(f"设置成功: 小丑概率 {probability.result:.1f}%")
242
+
243
+
244
+ @edit_preset.assign("reset")
245
+ async def reset_preset() -> None:
246
+ PresetData().save()
247
+ await finish("已重置为默认预设")
248
+
249
+
250
+ @edit_preset.handle()
251
+ async def handle_default() -> None:
252
+ lines = ["当前游戏预设:", ""]
253
+ data = PresetData.load()
254
+
255
+ for total, (w, p, c) in data.role_preset.items():
256
+ lines.append(f"{total} 人: 狼人x{w}, 神职x{p}, 平民x{c}")
257
+ lines.append("")
258
+
259
+ lines.append("狼人优先级: " + display_roles(data.werewolf_priority))
260
+ lines.append("神职优先级: " + display_roles(data.priesthood_proirity))
261
+ lines.append(f"小丑概率: {data.joker_probability:.0%}")
262
+
263
+ await finish("\n".join(lines))
@@ -1,10 +1,12 @@
1
1
  from nonebot import on_message
2
2
  from nonebot.adapters import Event
3
- from nonebot_plugin_alconna import MsgTarget, UniMsg
3
+ from nonebot_plugin_alconna import Alconna, MsgTarget, UniMessage, UniMsg, on_alconna
4
4
 
5
- from ..utils import InputStore, rule_in_game
5
+ from ..constant import STOP_COMMAND
6
+ from ..utils import InputStore
7
+ from .depends import rule_in_game
6
8
 
7
- message_in_game = on_message(rule=rule_in_game)
9
+ message_in_game = on_message(rule=rule_in_game, priority=10)
8
10
 
9
11
 
10
12
  @message_in_game.handle()
@@ -13,3 +15,16 @@ async def handle_input(event: Event, target: MsgTarget, msg: UniMsg) -> None:
13
15
  InputStore.put(msg, target.id)
14
16
  else:
15
17
  InputStore.put(msg, event.get_user_id(), target.id)
18
+
19
+
20
+ stopcmd = on_alconna(
21
+ Alconna("stop"),
22
+ rule=rule_in_game,
23
+ block=True,
24
+ use_cmd_start=True,
25
+ )
26
+
27
+
28
+ @stopcmd.handle()
29
+ async def handle_stopcmd(event: Event, target: MsgTarget) -> None:
30
+ await handle_input(event=event, target=target, msg=UniMessage.text(STOP_COMMAND))
@@ -0,0 +1,8 @@
1
+ from .chronocat_poke import chronocat_poke_enabled
2
+ from .ob11_poke import ob11_poke_enabled
3
+
4
+ checks = [chronocat_poke_enabled, ob11_poke_enabled]
5
+
6
+
7
+ def poke_enabled() -> bool:
8
+ return any(check() for check in checks)
@@ -0,0 +1,117 @@
1
+ import contextlib
2
+
3
+ from nonebot import on_message
4
+ from nonebot.internal.matcher import current_event
5
+ from nonebot_plugin_alconna import MsgTarget, UniMessage
6
+
7
+ from ...config import config
8
+ from ...constant import STOP_COMMAND
9
+ from ...game import Game
10
+ from ...utils import InputStore
11
+ from ..depends import user_in_game
12
+
13
+
14
+ def chronocat_poke_enabled() -> bool:
15
+ return False
16
+
17
+
18
+ with contextlib.suppress(ImportError):
19
+ from nonebot.adapters.satori import Bot
20
+ from nonebot.adapters.satori.event import (
21
+ MessageCreatedEvent,
22
+ PublicMessageCreatedEvent,
23
+ )
24
+
25
+ def extract_poke_tome(event: MessageCreatedEvent) -> str | None:
26
+ if event.login and event.login.platform and event.login.platform != "chronocat":
27
+ return None
28
+
29
+ poke = event.get_message().include("chronocat:poke")
30
+ if not poke:
31
+ return None
32
+
33
+ gen = (
34
+ seg.data["operatorId"]
35
+ for seg in poke
36
+ if seg.data["userId"] == event.self_id
37
+ )
38
+ return next(gen, None)
39
+
40
+ def extract_user_group(event: MessageCreatedEvent) -> tuple[str, str | None]:
41
+ user_id = event.get_user_id()
42
+ group_id = None
43
+ if isinstance(event, PublicMessageCreatedEvent):
44
+ group_id = (event.guild and event.guild.id) or event.channel.id
45
+ return user_id, group_id
46
+
47
+ # 游戏内戳一戳等效 "stop" 命令
48
+ async def _rule_poke_stop(bot: Bot, event: MessageCreatedEvent) -> bool:
49
+ if not config.enable_poke:
50
+ return False
51
+ return extract_poke_tome(event) is not None and (
52
+ user_in_game(bot.self_id, *extract_user_group(event))
53
+ )
54
+
55
+ @on_message(rule=_rule_poke_stop).handle()
56
+ async def handle_poke_stop(event: MessageCreatedEvent) -> None:
57
+ InputStore.put(
58
+ UniMessage.text(STOP_COMMAND),
59
+ extract_poke_tome(event) or event.get_user_id(),
60
+ extract_user_group(event)[1],
61
+ )
62
+
63
+ # 准备阶段戳一戳等效加入游戏
64
+ async def _rule_poke_join(
65
+ bot: Bot,
66
+ event: PublicMessageCreatedEvent,
67
+ target: MsgTarget,
68
+ ) -> bool:
69
+ if not config.enable_poke:
70
+ return False
71
+
72
+ return (
73
+ (user_id := extract_poke_tome(event)) is not None
74
+ and not user_in_game(
75
+ self_id=bot.self_id,
76
+ user_id=user_id,
77
+ group_id=(event.guild and event.guild.id) or event.channel.id,
78
+ )
79
+ and any(target.verify(group) for group in Game.starting_games)
80
+ )
81
+
82
+ @on_message(rule=_rule_poke_join).handle()
83
+ async def handle_poke_join(
84
+ bot: Bot,
85
+ event: PublicMessageCreatedEvent,
86
+ target: MsgTarget,
87
+ ) -> None:
88
+ user_id = extract_poke_tome(event) or event.get_user_id()
89
+ players = next(p for g, p in Game.starting_games.items() if target.verify(g))
90
+
91
+ if user_id not in players:
92
+ # XXX:
93
+ # 截止 chronocat v0.2.19
94
+ # 通过 guild.member.get / user.get 获取的用户信息均不包含用户名
95
+ # 跳过用户名获取, 使用用户 ID 代替
96
+ #
97
+ # member = await bot.guild_member_get(
98
+ # guild_id=(event.guild and event.guild.id) or event.channel.id,
99
+ # user_id=user_id,
100
+ # )
101
+ # name = member.nick or (
102
+ # member.user and (member.user.nick or member.user.name)
103
+ # )
104
+ # if name is None:
105
+ # user = await bot.user_get(user_id=user_id)
106
+ # name = user.nick or user.name
107
+ # players[user_id] = name or user_id
108
+
109
+ players[user_id] = user_id
110
+ await UniMessage.at(user_id).text("\n✅成功加入游戏").send(target, bot)
111
+
112
+ def chronocat_poke_enabled() -> bool:
113
+ if not config.enable_poke:
114
+ return False
115
+
116
+ event = current_event.get()
117
+ return isinstance(event, MessageCreatedEvent) and event.platform == "chronocat"
@@ -4,12 +4,14 @@ from nonebot import on_notice
4
4
  from nonebot.internal.matcher import current_bot
5
5
  from nonebot_plugin_alconna import MsgTarget, UniMessage
6
6
 
7
- from ..config import config
8
- from ..game import Game
9
- from ..utils import InputStore, user_in_game
7
+ from ...config import config
8
+ from ...constant import STOP_COMMAND
9
+ from ...game import Game
10
+ from ...utils import InputStore
11
+ from ..depends import user_in_game
10
12
 
11
13
 
12
- def ob11_ext_enabled() -> bool:
14
+ def ob11_poke_enabled() -> bool:
13
15
  return False
14
16
 
15
17
 
@@ -17,23 +19,21 @@ with contextlib.suppress(ImportError):
17
19
  from nonebot.adapters.onebot.v11 import Bot
18
20
  from nonebot.adapters.onebot.v11.event import PokeNotifyEvent
19
21
 
20
- # 游戏内戳一戳等效 "/stop"
22
+ # 游戏内戳一戳等效 "stop" 命令
21
23
  async def _rule_poke_stop(bot: Bot, event: PokeNotifyEvent) -> bool:
22
24
  if not config.enable_poke:
23
25
  return False
24
26
 
25
27
  user_id = str(event.user_id)
26
28
  group_id = str(event.group_id) if event.group_id is not None else None
27
- return (
28
- config.enable_poke
29
- and (event.target_id == event.self_id)
30
- and user_in_game(bot.self_id, user_id, group_id)
29
+ return (event.target_id == event.self_id) and user_in_game(
30
+ bot.self_id, user_id, group_id
31
31
  )
32
32
 
33
33
  @on_notice(rule=_rule_poke_stop).handle()
34
34
  async def handle_poke_stop(event: PokeNotifyEvent) -> None:
35
35
  InputStore.put(
36
- msg=UniMessage.text("/stop"),
36
+ msg=UniMessage.text(STOP_COMMAND),
37
37
  user_id=str(event.user_id),
38
38
  group_id=str(event.group_id) if event.group_id is not None else None,
39
39
  )
@@ -60,18 +60,20 @@ with contextlib.suppress(ImportError):
60
60
  target: MsgTarget,
61
61
  ) -> None:
62
62
  user_id = event.get_user_id()
63
- group_id = target.id
64
63
  players = next(p for g, p in Game.starting_games.items() if target.verify(g))
65
64
 
66
- if user_id not in players:
67
- res: dict[str, str] = await bot.get_group_member_info(
68
- group_id=int(group_id),
69
- user_id=int(user_id),
70
- )
71
- players[user_id] = res.get("card") or res.get("nickname") or user_id
72
- await UniMessage.at(user_id).text("\n✅成功加入游戏").send(target, bot)
65
+ if event.group_id is None or user_id in players:
66
+ return
67
+
68
+ member_info = await bot.get_group_member_info(
69
+ group_id=event.group_id,
70
+ user_id=event.user_id,
71
+ no_cache=True,
72
+ )
73
+ players[user_id] = member_info["card"] or member_info["nickname"] or user_id
74
+ await UniMessage.at(user_id).text("\n✅成功加入游戏").send(target, bot)
73
75
 
74
- def ob11_ext_enabled() -> bool:
76
+ def ob11_poke_enabled() -> bool:
75
77
  if not config.enable_poke:
76
78
  return False
77
79