nonebot-plugin-werewolf 1.1.3__py3-none-any.whl → 1.1.5__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 (29) hide show
  1. nonebot_plugin_werewolf/__init__.py +2 -1
  2. nonebot_plugin_werewolf/constant.py +23 -1
  3. nonebot_plugin_werewolf/game.py +172 -113
  4. nonebot_plugin_werewolf/matchers/depends.py +38 -0
  5. nonebot_plugin_werewolf/matchers/message_in_game.py +14 -4
  6. nonebot_plugin_werewolf/matchers/poke/__init__.py +8 -0
  7. nonebot_plugin_werewolf/matchers/poke/chronocat_poke.py +117 -0
  8. nonebot_plugin_werewolf/matchers/{ob11_ext.py → poke/ob11_poke.py} +21 -19
  9. nonebot_plugin_werewolf/matchers/start_game.py +161 -15
  10. nonebot_plugin_werewolf/player_set.py +33 -34
  11. nonebot_plugin_werewolf/players/can_shoot.py +12 -18
  12. nonebot_plugin_werewolf/players/civilian.py +2 -2
  13. nonebot_plugin_werewolf/players/guard.py +15 -22
  14. nonebot_plugin_werewolf/players/hunter.py +2 -2
  15. nonebot_plugin_werewolf/players/idiot.py +3 -3
  16. nonebot_plugin_werewolf/players/joker.py +2 -2
  17. nonebot_plugin_werewolf/players/player.py +137 -65
  18. nonebot_plugin_werewolf/players/prophet.py +7 -15
  19. nonebot_plugin_werewolf/players/werewolf.py +51 -29
  20. nonebot_plugin_werewolf/players/witch.py +32 -38
  21. nonebot_plugin_werewolf/players/wolfking.py +2 -2
  22. nonebot_plugin_werewolf/utils.py +56 -190
  23. {nonebot_plugin_werewolf-1.1.3.dist-info → nonebot_plugin_werewolf-1.1.5.dist-info}/METADATA +14 -2
  24. nonebot_plugin_werewolf-1.1.5.dist-info/RECORD +31 -0
  25. {nonebot_plugin_werewolf-1.1.3.dist-info → nonebot_plugin_werewolf-1.1.5.dist-info}/WHEEL +1 -1
  26. nonebot_plugin_werewolf/_timeout.py +0 -110
  27. nonebot_plugin_werewolf-1.1.3.dist-info/RECORD +0 -29
  28. {nonebot_plugin_werewolf-1.1.3.dist-info → nonebot_plugin_werewolf-1.1.5.dist-info}/LICENSE +0 -0
  29. {nonebot_plugin_werewolf-1.1.3.dist-info → nonebot_plugin_werewolf-1.1.5.dist-info}/top_level.txt +0 -0
@@ -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
 
@@ -1,13 +1,22 @@
1
+ import re
2
+
3
+ import anyio
4
+ import nonebot
5
+ import nonebot_plugin_waiter as waiter
6
+ from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
1
7
  from nonebot import on_command
2
8
  from nonebot.adapters import Bot, Event
3
9
  from nonebot.rule import to_me
4
- from nonebot_plugin_alconna import MsgTarget, UniMessage
5
- from nonebot_plugin_uninfo import Uninfo
10
+ from nonebot.utils import escape_tag
11
+ from nonebot_plugin_alconna import MsgTarget, Target, UniMessage, UniMsg
12
+ from nonebot_plugin_uninfo import QryItrface, Uninfo
6
13
 
7
- from .._timeout import timeout
14
+ from ..config import config
15
+ from ..constant import STOP_COMMAND_PROMPT
8
16
  from ..game import Game
9
- from ..utils import prepare_game, rule_not_in_game
10
- from .ob11_ext import ob11_ext_enabled
17
+ from ..utils import extract_session_member_nick
18
+ from .depends import rule_not_in_game
19
+ from .poke import poke_enabled
11
20
 
12
21
  start_game = on_command(
13
22
  "werewolf",
@@ -22,35 +31,172 @@ async def handle_start_warning(target: MsgTarget) -> None:
22
31
  await UniMessage("⚠️请在群组中创建新游戏").finish(reply_to=True)
23
32
 
24
33
 
34
+ async def _prepare_receive(
35
+ stream: MemoryObjectSendStream[tuple[Event, str, str]],
36
+ event: Event,
37
+ group: Target,
38
+ ) -> None:
39
+ async def same_group(target: MsgTarget) -> bool:
40
+ return group.verify(target)
41
+
42
+ @waiter.waiter(
43
+ waits=[event.get_type()],
44
+ keep_session=False,
45
+ rule=to_me() & same_group & rule_not_in_game,
46
+ )
47
+ def wait(event: Event, msg: UniMsg, session: Uninfo) -> tuple[Event, str, str]:
48
+ text = msg.extract_plain_text().strip()
49
+ name = extract_session_member_nick(session) or event.get_user_id()
50
+ return (event, text, name)
51
+
52
+ async for evt, text, name in wait(default=(None, "", "")):
53
+ if evt is None:
54
+ continue
55
+ await stream.send((evt, text, re.sub(r"[\u2066-\u2069]", "", name)))
56
+
57
+
58
+ async def _prepare_handle(
59
+ stream: MemoryObjectReceiveStream[tuple[Event, str, str]],
60
+ players: dict[str, str],
61
+ admin_id: str,
62
+ finished: anyio.Event,
63
+ ) -> None:
64
+ logger = nonebot.logger.opt(colors=True)
65
+
66
+ async def send(msg: str, /) -> None:
67
+ await UniMessage.text(msg).send(target=event, reply_to=True)
68
+
69
+ while True:
70
+ event, text, name = await stream.receive()
71
+ user_id = event.get_user_id()
72
+ colored = f"<y>{escape_tag(name)}</y>(<c>{escape_tag(user_id)}</c>)"
73
+
74
+ # 更新用户名
75
+ # 当用户通过 chronoca:poke 加入游戏时, 插件无法获取用户名, 原字典值为用户ID
76
+ if user_id in players and players.get(user_id) != name:
77
+ logger.debug(f"更新玩家显示名称: {colored}")
78
+ players[user_id] = name
79
+
80
+ match (text, user_id == admin_id):
81
+ case ("开始游戏", True):
82
+ player_num = len(players)
83
+ role_preset = config.get_role_preset()
84
+ if player_num < min(role_preset):
85
+ await send(
86
+ f"⚠️游戏至少需要 {min(role_preset)} 人, "
87
+ f"当前已有 {player_num} 人"
88
+ )
89
+ elif player_num > max(role_preset):
90
+ await send(
91
+ f"⚠️游戏最多需要 {max(role_preset)} 人, "
92
+ f"当前已有 {player_num} 人"
93
+ )
94
+ elif player_num not in role_preset:
95
+ await send(f"⚠️不存在总人数为 {player_num} 的预设, 无法开始游戏")
96
+ else:
97
+ await send("✏️游戏即将开始...")
98
+ logger.info(f"游戏发起者 {colored} 开始游戏")
99
+ finished.set()
100
+ players["#$start_game$#"] = user_id
101
+ return
102
+
103
+ case ("开始游戏", False):
104
+ await send("⚠️只有游戏发起者可以开始游戏")
105
+
106
+ case ("结束游戏", True):
107
+ logger.info(f"游戏发起者 {colored} 结束游戏")
108
+ await send("ℹ️已结束当前游戏")
109
+ finished.set()
110
+ return
111
+
112
+ case ("结束游戏", False):
113
+ await send("⚠️只有游戏发起者可以结束游戏")
114
+
115
+ case ("加入游戏", True):
116
+ await send("ℹ️游戏发起者已经加入游戏了")
117
+
118
+ case ("加入游戏", False):
119
+ if user_id not in players:
120
+ players[user_id] = name
121
+ logger.info(f"玩家 {colored} 加入游戏")
122
+ await send("✅成功加入游戏")
123
+ else:
124
+ await send("ℹ️你已经加入游戏了")
125
+
126
+ case ("退出游戏", True):
127
+ await send("ℹ️游戏发起者无法退出游戏")
128
+
129
+ case ("退出游戏", False):
130
+ if user_id in players:
131
+ del players[user_id]
132
+ logger.info(f"玩家 {colored} 退出游戏")
133
+ await send("✅成功退出游戏")
134
+ else:
135
+ await send("ℹ️你还没有加入游戏")
136
+
137
+ case ("当前玩家", _):
138
+ await send(
139
+ "✨当前玩家:\n"
140
+ + "\n".join(
141
+ f"{idx}. {players[user_id]}"
142
+ for idx, user_id in enumerate(players, 1)
143
+ )
144
+ )
145
+
146
+
147
+ async def prepare_game(event: Event, players: dict[str, str]) -> None:
148
+ admin_id = event.get_user_id()
149
+ group = UniMessage.get_target(event)
150
+ Game.starting_games[group] = players
151
+
152
+ finished = anyio.Event()
153
+ send, recv = anyio.create_memory_object_stream[tuple[Event, str, str]]()
154
+
155
+ async def _handle_cancel() -> None:
156
+ await finished.wait()
157
+ tg.cancel_scope.cancel()
158
+
159
+ try:
160
+ async with anyio.create_task_group() as tg:
161
+ tg.start_soon(_handle_cancel)
162
+ tg.start_soon(_prepare_receive, send, event, group)
163
+ tg.start_soon(_prepare_handle, recv, players, admin_id, finished)
164
+ except Exception as err:
165
+ await UniMessage(f"狼人杀准备阶段出现未知错误: {err!r}").send()
166
+
167
+ del Game.starting_games[group]
168
+ if players.pop("#$start_game$#", None) != admin_id:
169
+ await start_game.finish()
170
+
171
+
25
172
  @start_game.handle()
26
173
  async def handle_start(
27
174
  bot: Bot,
28
175
  event: Event,
29
176
  target: MsgTarget,
30
177
  session: Uninfo,
178
+ interface: QryItrface,
31
179
  ) -> None:
32
180
  admin_id = event.get_user_id()
33
181
  msg = (
34
- UniMessage.at(admin_id)
35
- .text("\n🎉成功创建游戏\n\n")
182
+ UniMessage.text("🎉成功创建游戏\n\n")
36
183
  .text(" 玩家请 @我 发送 “加入游戏”、“退出游戏”\n")
37
184
  .text(" 玩家 @我 发送 “当前玩家” 可查看玩家列表\n")
38
185
  .text(" 游戏发起者 @我 发送 “结束游戏” 可结束当前游戏\n")
39
186
  .text(" 玩家均加入后,游戏发起者请 @我 发送 “开始游戏”\n")
40
187
  )
41
- if ob11_ext_enabled():
42
- msg.text("\n可使用戳一戳代替游戏交互中的 “/stop” 命令\n")
43
- await msg.text("\n游戏准备阶段限时5分钟,超时将自动结束").send()
188
+ if poke_enabled():
189
+ msg.text(f"\n可使用戳一戳代替游戏交互中的 “{STOP_COMMAND_PROMPT}” 命令\n")
190
+ await msg.text("\n游戏准备阶段限时5分钟,超时将自动结束").send(reply_to=True)
44
191
 
45
- admin_name = session.user.nick or session.user.name or admin_id
46
- if session.member:
47
- admin_name = session.member.nick or admin_name
192
+ admin_name = extract_session_member_nick(session) or admin_id
48
193
  players = {admin_id: admin_name}
49
194
 
50
195
  try:
51
- async with timeout(5 * 60):
196
+ with anyio.fail_after(5 * 60):
52
197
  await prepare_game(event, players)
53
198
  except TimeoutError:
54
199
  await UniMessage.text("⚠️游戏准备超时,已自动结束").finish()
55
200
 
56
- Game(bot, target, players).start()
201
+ game = Game(bot, target, set(players), interface)
202
+ await game.start()
@@ -1,16 +1,10 @@
1
- from __future__ import annotations
2
-
3
- import asyncio
4
1
  import functools
5
- from typing import TYPE_CHECKING
6
-
7
- from ._timeout import timeout
8
- from .players import Player
9
2
 
10
- if TYPE_CHECKING:
11
- from nonebot_plugin_alconna.uniseg import UniMessage
3
+ import anyio
4
+ from nonebot_plugin_alconna.uniseg import UniMessage
12
5
 
13
- from .constant import Role, RoleGroup
6
+ from .constant import Role, RoleGroup
7
+ from .players import Player
14
8
 
15
9
 
16
10
  class PlayerSet(set[Player]):
@@ -18,26 +12,26 @@ class PlayerSet(set[Player]):
18
12
  def size(self) -> int:
19
13
  return len(self)
20
14
 
21
- def alive(self) -> PlayerSet:
15
+ def alive(self) -> "PlayerSet":
22
16
  return PlayerSet(p for p in self if p.alive)
23
17
 
24
- def dead(self) -> PlayerSet:
18
+ def dead(self) -> "PlayerSet":
25
19
  return PlayerSet(p for p in self if not p.alive)
26
20
 
27
- def killed(self) -> PlayerSet:
21
+ def killed(self) -> "PlayerSet":
28
22
  return PlayerSet(p for p in self if p.killed.is_set())
29
23
 
30
- def include(self, *types: Player | Role | RoleGroup) -> PlayerSet:
24
+ def include(self, *types: Player | Role | RoleGroup) -> "PlayerSet":
31
25
  return PlayerSet(
32
26
  player
33
27
  for player in self
34
28
  if (player in types or player.role in types or player.role_group in types)
35
29
  )
36
30
 
37
- def select(self, *types: Player | Role | RoleGroup) -> PlayerSet:
31
+ def select(self, *types: Player | Role | RoleGroup) -> "PlayerSet":
38
32
  return self.include(*types)
39
33
 
40
- def exclude(self, *types: Player | Role | RoleGroup) -> PlayerSet:
34
+ def exclude(self, *types: Player | Role | RoleGroup) -> "PlayerSet":
41
35
  return PlayerSet(
42
36
  player
43
37
  for player in self
@@ -48,35 +42,40 @@ class PlayerSet(set[Player]):
48
42
  )
49
43
  )
50
44
 
51
- def player_selected(self) -> PlayerSet:
45
+ def player_selected(self) -> "PlayerSet":
52
46
  return PlayerSet(p.selected for p in self.alive() if p.selected is not None)
53
47
 
54
48
  @functools.cached_property
55
49
  def sorted(self) -> list[Player]:
56
50
  return sorted(self, key=lambda p: p.user_id)
57
51
 
58
- async def interact(self, timeout_secs: float = 60) -> None:
59
- async with timeout(timeout_secs):
60
- await asyncio.gather(*[p.interact() for p in self.alive()])
61
-
62
- async def vote(self, timeout_secs: float = 60) -> dict[Player, list[Player]]:
63
- async def vote(player: Player) -> tuple[Player, Player] | None:
64
- try:
65
- async with timeout(timeout_secs):
66
- return await player.vote(self)
67
- except TimeoutError:
68
- await player.send("⚠️投票超时,将视为弃票")
69
- return None
52
+ async def interact(self) -> None:
53
+ async with anyio.create_task_group() as tg:
54
+ for p in self.alive():
55
+ tg.start_soon(p.interact)
70
56
 
57
+ async def vote(self) -> dict[Player, list[Player]]:
58
+ players = self.alive()
71
59
  result: dict[Player, list[Player]] = {}
72
- for item in await asyncio.gather(*[vote(p) for p in self.alive()]):
73
- if item is not None:
74
- player, voted = item
75
- result[voted] = [*result.get(voted, []), player]
60
+
61
+ async def _vote(player: Player) -> None:
62
+ vote = await player.vote(players)
63
+ if vote is not None:
64
+ result[vote] = [*result.get(vote, []), player]
65
+
66
+ async with anyio.create_task_group() as tg:
67
+ for p in players:
68
+ tg.start_soon(_vote, p)
69
+
76
70
  return result
77
71
 
78
72
  async def broadcast(self, message: str | UniMessage) -> None:
79
- await asyncio.gather(*[p.send(message) for p in self])
73
+ if not self:
74
+ return
75
+
76
+ async with anyio.create_task_group() as tg:
77
+ for p in self:
78
+ tg.start_soon(p.send, message)
80
79
 
81
80
  def show(self) -> str:
82
81
  return "\n".join(f"{i}. {p.name}" for i, p in enumerate(self.sorted, 1))
@@ -1,8 +1,7 @@
1
1
  from nonebot_plugin_alconna.uniseg import UniMessage
2
2
  from typing_extensions import override
3
3
 
4
- from ..constant import KillReason
5
- from ..utils import check_index
4
+ from ..constant import STOP_COMMAND_PROMPT, KillReason
6
5
  from .player import Player
7
6
 
8
7
 
@@ -19,11 +18,11 @@ class CanShoot(Player):
19
18
  .text(f" 死了\n请{self.role_name}决定击杀目标...")
20
19
  )
21
20
 
22
- self.game.state.shoot = (None, None)
21
+ self.game.state.shoot = None
23
22
  shoot = await self.shoot()
24
23
 
25
24
  if shoot is not None:
26
- self.game.state.shoot = (self, shoot)
25
+ self.game.state.shoot = self
27
26
  await self.send(
28
27
  UniMessage.text(f"🔫{self.role_name} ")
29
28
  .at(self.user_id)
@@ -31,6 +30,7 @@ class CanShoot(Player):
31
30
  .at(shoot.user_id)
32
31
  )
33
32
  await shoot.kill(KillReason.Shoot, self)
33
+ self.selected = shoot
34
34
  else:
35
35
  await self.send(f"ℹ️{self.role_name}选择了取消技能")
36
36
  return await super().post_kill()
@@ -41,19 +41,13 @@ class CanShoot(Player):
41
41
  "💫请选择需要射杀的玩家:\n"
42
42
  + players.show()
43
43
  + "\n\n🔫发送编号选择玩家"
44
- + "\n❌发送 “/stop” 取消技能"
44
+ + f"\n❌发送 “{STOP_COMMAND_PROMPT}” 取消技能"
45
45
  )
46
46
 
47
- while True:
48
- text = await self.receive_text()
49
- if text == "/stop":
50
- await self.send("ℹ️已取消技能")
51
- return None
52
- index = check_index(text, len(players))
53
- if index is not None:
54
- selected = index - 1
55
- break
56
- await self.send("⚠️输入错误: 请发送编号选择玩家")
57
-
58
- await self.send(f"🎯选择射杀的玩家: {players[selected].name}")
59
- return players[selected]
47
+ if selected := await self._select_player(
48
+ players,
49
+ on_stop="ℹ️已取消技能,回合结束",
50
+ ):
51
+ await self.send(f"🎯选择射杀的玩家: {selected.name}")
52
+
53
+ return selected
@@ -1,7 +1,7 @@
1
1
  from ..constant import Role, RoleGroup
2
- from .player import Player, register_role
2
+ from .player import Player
3
3
 
4
4
 
5
- @register_role(Role.Civilian, RoleGroup.GoodGuy)
5
+ @Player.register_role(Role.Civilian, RoleGroup.GoodGuy)
6
6
  class Civilian(Player):
7
7
  pass
@@ -1,13 +1,19 @@
1
1
  from nonebot_plugin_alconna.uniseg import UniMessage
2
2
  from typing_extensions import override
3
3
 
4
- from ..constant import Role, RoleGroup
5
- from ..utils import check_index
6
- from .player import Player, register_role
4
+ from ..constant import STOP_COMMAND_PROMPT, Role, RoleGroup
5
+ from .player import Player
7
6
 
8
7
 
9
- @register_role(Role.Guard, RoleGroup.GoodGuy)
8
+ @Player.register_role(Role.Guard, RoleGroup.GoodGuy)
10
9
  class Guard(Player):
10
+ @override
11
+ async def _check_selected(self, player: Player) -> Player | None:
12
+ if player is not self.selected:
13
+ return player
14
+ await self.send("⚠️守卫不能连续两晚保护同一目标,请重新选择")
15
+ return None
16
+
11
17
  @override
12
18
  async def interact(self) -> None:
13
19
  players = self.game.players.alive()
@@ -15,23 +21,10 @@ class Guard(Player):
15
21
  UniMessage.text("💫请选择需要保护的玩家:\n")
16
22
  .text(players.show())
17
23
  .text("\n\n🛡️发送编号选择玩家")
18
- .text("\n❌发送 “/stop” 结束回合")
24
+ .text(f"\n❌发送 “{STOP_COMMAND_PROMPT}” 结束回合")
19
25
  )
20
26
 
21
- while True:
22
- text = await self.receive_text()
23
- if text == "/stop":
24
- await self.send("ℹ️你选择了取消,回合结束")
25
- return
26
- index = check_index(text, len(players))
27
- if index is not None:
28
- selected = index - 1
29
- if players[selected] is self.selected:
30
- await self.send("⚠️守卫不能连续两晚保护同一目标,请重新选择")
31
- continue
32
- break
33
- await self.send("⚠️输入错误: 请发送编号选择玩家")
34
-
35
- self.selected = players[selected]
36
- self.game.state.protected.add(self.selected)
37
- await self.send(f"✅本回合保护的玩家: {self.selected.name}")
27
+ self.selected = await self._select_player(players)
28
+ if self.selected:
29
+ self.game.state.protected.add(self.selected)
30
+ await self.send(f"✅本回合保护的玩家: {self.selected.name}")
@@ -1,8 +1,8 @@
1
1
  from ..constant import Role, RoleGroup
2
2
  from .can_shoot import CanShoot
3
- from .player import Player, register_role
3
+ from .player import Player
4
4
 
5
5
 
6
- @register_role(Role.Hunter, RoleGroup.GoodGuy)
6
+ @Player.register_role(Role.Hunter, RoleGroup.GoodGuy)
7
7
  class Hunter(CanShoot, Player):
8
8
  pass
@@ -6,13 +6,13 @@ from nonebot_plugin_alconna.uniseg import UniMessage
6
6
  from typing_extensions import override
7
7
 
8
8
  from ..constant import KillReason, Role, RoleGroup
9
- from .player import Player, register_role
9
+ from .player import Player
10
10
 
11
11
  if TYPE_CHECKING:
12
12
  from ..player_set import PlayerSet
13
13
 
14
14
 
15
- @register_role(Role.Idiot, RoleGroup.GoodGuy)
15
+ @Player.register_role(Role.Idiot, RoleGroup.GoodGuy)
16
16
  class Idiot(Player):
17
17
  voted: bool = False
18
18
 
@@ -37,7 +37,7 @@ class Idiot(Player):
37
37
  return await super().kill(reason, *killers)
38
38
 
39
39
  @override
40
- async def vote(self, players: PlayerSet) -> tuple[Player, Player] | None:
40
+ async def vote(self, players: PlayerSet) -> Player | None:
41
41
  if self.voted:
42
42
  await self.send("ℹ️你已经发动过白痴身份的技能,无法参与本次投票")
43
43
  return None