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,25 +1,272 @@
1
- from nonebot import on_command
1
+ import json
2
+ import re
3
+
4
+ import anyio
5
+ import nonebot
6
+ import nonebot_plugin_waiter as waiter
2
7
  from nonebot.adapters import Bot, Event
3
- from nonebot.rule import to_me
4
- from nonebot_plugin_alconna import MsgTarget, UniMessage
5
- from nonebot_plugin_uninfo import Uninfo
8
+ from nonebot.internal.matcher import current_bot
9
+ from nonebot.permission import SUPERUSER
10
+ from nonebot.rule import Rule, to_me
11
+ from nonebot.typing import T_State
12
+ from nonebot.utils import escape_tag
13
+ from nonebot_plugin_alconna import (
14
+ Alconna,
15
+ Button,
16
+ FallbackStrategy,
17
+ MsgTarget,
18
+ Option,
19
+ Target,
20
+ UniMessage,
21
+ UniMsg,
22
+ on_alconna,
23
+ )
24
+ from nonebot_plugin_localstore import get_plugin_data_file
25
+ from nonebot_plugin_uninfo import QryItrface, Uninfo
6
26
 
7
- from .._timeout import timeout
27
+ from ..config import PresetData, config
28
+ from ..constant import STOP_COMMAND_PROMPT
8
29
  from ..game import Game
9
- from ..utils import prepare_game, rule_not_in_game
10
- from .ob11_ext import ob11_ext_enabled
30
+ from ..utils import ObjectStream, extract_session_member_nick
31
+ from .depends import rule_not_in_game
32
+ from .poke import poke_enabled
11
33
 
12
- start_game = on_command(
13
- "werewolf",
34
+ start_game = on_alconna(
35
+ Alconna(
36
+ "werewolf",
37
+ Option("restart|--restart|重开", dest="restart"),
38
+ ),
14
39
  rule=to_me() & rule_not_in_game,
15
40
  aliases={"狼人杀"},
41
+ use_cmd_start=True,
16
42
  )
43
+ player_data_file = get_plugin_data_file("players.json")
44
+ if not player_data_file.exists():
45
+ player_data_file.write_text("[]")
46
+
47
+
48
+ def dump_players(target: Target, players: dict[str, str]) -> None:
49
+ data: list[dict] = json.loads(player_data_file.read_text(encoding="utf-8"))
50
+
51
+ for item in data:
52
+ if Target.load(item["target"]).verify(target):
53
+ item["players"] = players
54
+ break
55
+ else:
56
+ data.append({"target": target.dump(), "players": players})
57
+
58
+ player_data_file.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
59
+
60
+
61
+ def load_players(target: Target) -> dict[str, str] | None:
62
+ data: list[dict] = json.loads(player_data_file.read_text(encoding="utf-8"))
63
+
64
+ for item in data:
65
+ if Target.load(item["target"]).verify(target):
66
+ return item["players"]
67
+ return None
68
+
69
+
70
+ def solve_button(msg: UniMessage) -> UniMessage:
71
+ if config.enable_button:
72
+ msg.keyboard(
73
+ *[
74
+ Button("input", i, text=i)
75
+ for i in ["加入游戏", "退出游戏", "当前玩家", "开始游戏", "结束游戏"]
76
+ ]
77
+ )
78
+ return msg
79
+
80
+
81
+ async def _prepare_receive(
82
+ stream: ObjectStream[tuple[Event, str, str]],
83
+ event_type: str,
84
+ group: Target,
85
+ ) -> None:
86
+ @Rule
87
+ async def same_group(target: MsgTarget) -> bool:
88
+ return group.verify(target)
89
+
90
+ @waiter.waiter(
91
+ waits=[event_type],
92
+ keep_session=False,
93
+ rule=same_group & rule_not_in_game,
94
+ )
95
+ def wait(event: Event, msg: UniMsg, session: Uninfo) -> tuple[Event, str, str]:
96
+ text = msg.extract_plain_text().strip()
97
+ name = extract_session_member_nick(session) or event.get_user_id()
98
+ return (event, text, re.sub(r"[\u2066-\u2069]", "", name))
99
+
100
+ async for event, text, name in wait(default=(None, "", "")):
101
+ if event is None:
102
+ continue
103
+ await stream.send((event, text, name))
104
+
105
+
106
+ async def _prepare_handle(
107
+ stream: ObjectStream[tuple[Event, str, str]],
108
+ players: dict[str, str],
109
+ admin_id: str,
110
+ ) -> None:
111
+ logger = nonebot.logger.opt(colors=True)
112
+
113
+ async def send(msg: str, /, *, button: bool = True) -> None:
114
+ message = UniMessage.text(msg)
115
+ if button:
116
+ message = solve_button(message)
117
+
118
+ await message.send(
119
+ target=event,
120
+ reply_to=True,
121
+ fallback=FallbackStrategy.ignore,
122
+ )
123
+
124
+ while not stream.closed:
125
+ event, text, name = await stream.recv()
126
+ user_id = event.get_user_id()
127
+ colored = f"<y>{escape_tag(name)}</y>(<c>{escape_tag(user_id)}</c>)"
128
+
129
+ # 更新用户名
130
+ # 当用户通过 chronoca:poke 加入游戏时, 插件无法获取用户名, 原字典值为用户ID
131
+ if user_id in players and players.get(user_id) != name:
132
+ logger.debug(f"更新玩家显示名称: {colored}")
133
+ players[user_id] = name
134
+
135
+ match (text, user_id == admin_id):
136
+ case ("开始游戏", True):
137
+ player_num = len(players)
138
+ role_preset = PresetData.load().role_preset
139
+ if player_num < min(role_preset):
140
+ await send(
141
+ f"⚠️游戏至少需要 {min(role_preset)} 人, "
142
+ f"当前已有 {player_num} 人"
143
+ )
144
+ elif player_num > max(role_preset):
145
+ await send(
146
+ f"⚠️游戏最多需要 {max(role_preset)} 人, "
147
+ f"当前已有 {player_num} 人"
148
+ )
149
+ elif player_num not in role_preset:
150
+ await send(f"⚠️不存在总人数为 {player_num} 的预设, 无法开始游戏")
151
+ else:
152
+ await send("✏️游戏即将开始...")
153
+ logger.info(f"游戏发起者 {colored} 开始游戏")
154
+ stream.close()
155
+ players["#$start_game$#"] = user_id
156
+ return
157
+
158
+ case ("开始游戏", False):
159
+ await send("⚠️只有游戏发起者可以开始游戏")
160
+
161
+ case ("结束游戏", True):
162
+ logger.info(f"游戏发起者 {colored} 结束游戏")
163
+ await send("ℹ️已结束当前游戏", button=False)
164
+ stream.close()
165
+ return
166
+
167
+ case ("结束游戏", False):
168
+ if await SUPERUSER(current_bot.get(), event):
169
+ logger.info(f"超级用户 {colored} 结束游戏")
170
+ await send("ℹ️已结束当前游戏", button=False)
171
+ stream.close()
172
+ return
173
+ await send("⚠️只有游戏发起者或超级用户可以结束游戏")
174
+
175
+ case ("加入游戏", True):
176
+ await send("ℹ️游戏发起者已经加入游戏了")
177
+
178
+ case ("加入游戏", False):
179
+ if user_id not in players:
180
+ players[user_id] = name
181
+ logger.info(f"玩家 {colored} 加入游戏")
182
+ await send("✅成功加入游戏")
183
+ else:
184
+ await send("ℹ️你已经加入游戏了")
185
+
186
+ case ("退出游戏", True):
187
+ await send("ℹ️游戏发起者无法退出游戏")
188
+
189
+ case ("退出游戏", False):
190
+ if user_id in players:
191
+ del players[user_id]
192
+ logger.info(f"玩家 {colored} 退出游戏")
193
+ await send("✅成功退出游戏")
194
+ else:
195
+ await send("ℹ️你还没有加入游戏")
196
+
197
+ case ("当前玩家", _):
198
+ await send(
199
+ "✨当前玩家:\n"
200
+ + "\n".join(
201
+ f"{idx}. {players[user_id]}"
202
+ for idx, user_id in enumerate(players, 1)
203
+ )
204
+ )
205
+
206
+
207
+ async def prepare_game(event: Event, players: dict[str, str]) -> None:
208
+ admin_id = event.get_user_id()
209
+ group = UniMessage.get_target(event)
210
+ Game.starting_games[group] = players
211
+
212
+ stream = ObjectStream[tuple[Event, str, str]](16)
213
+
214
+ async def _handle_cancel() -> None:
215
+ await stream.wait_closed()
216
+ tg.cancel_scope.cancel()
217
+
218
+ try:
219
+ async with anyio.create_task_group() as tg:
220
+ tg.start_soon(_handle_cancel)
221
+ tg.start_soon(_prepare_receive, stream, event.get_type(), group)
222
+ tg.start_soon(_prepare_handle, stream, players, admin_id)
223
+ except Exception as err:
224
+ await UniMessage(f"狼人杀准备阶段出现未知错误: {err!r}").send()
225
+
226
+ del Game.starting_games[group]
227
+ if players.pop("#$start_game$#", None) != admin_id:
228
+ await start_game.finish()
17
229
 
18
230
 
19
231
  @start_game.handle()
20
- async def handle_start_warning(target: MsgTarget) -> None:
232
+ async def handle_notice(target: MsgTarget, state: T_State) -> None:
21
233
  if target.private:
22
234
  await UniMessage("⚠️请在群组中创建新游戏").finish(reply_to=True)
235
+ if any(target.verify(g.group) for g in Game.running_games):
236
+ await (
237
+ UniMessage.text("⚠️当前群组内有正在进行的游戏\n")
238
+ .text("无法开始新游戏")
239
+ .finish(reply_to=True)
240
+ )
241
+
242
+ msg = (
243
+ UniMessage.text("🎉成功创建游戏\n\n")
244
+ .text(" 玩家请发送 “加入游戏”、“退出游戏”\n")
245
+ .text(" 玩家发送 “当前玩家” 可查看玩家列表\n")
246
+ .text(" 游戏发起者发送 “结束游戏” 可结束当前游戏\n")
247
+ .text(" 玩家均加入后,游戏发起者请发送 “开始游戏”\n")
248
+ )
249
+ if poke_enabled():
250
+ msg.text(f"\n💫可使用戳一戳代替游戏交互中的 “{STOP_COMMAND_PROMPT}” 命令\n")
251
+ msg.text("\nℹ️游戏准备阶段限时5分钟,超时将自动结束")
252
+ await solve_button(msg).send(reply_to=True, fallback=FallbackStrategy.ignore)
253
+
254
+ state["players"] = {}
255
+
256
+
257
+ @start_game.assign("restart")
258
+ async def handle_restart(target: MsgTarget, state: T_State) -> None:
259
+ players = load_players(target)
260
+ if players is None:
261
+ await UniMessage.text("ℹ️未找到历史游戏记录,将创建新游戏").send()
262
+ return
263
+
264
+ msg = UniMessage.text("🎉成功加载上次游戏:\n")
265
+ for user in players:
266
+ msg.text("\n- ").at(user)
267
+ await msg.send()
268
+
269
+ state["players"] = players
23
270
 
24
271
 
25
272
  @start_game.handle()
@@ -28,29 +275,20 @@ async def handle_start(
28
275
  event: Event,
29
276
  target: MsgTarget,
30
277
  session: Uninfo,
278
+ interface: QryItrface,
279
+ state: T_State,
31
280
  ) -> None:
281
+ players: dict[str, str] = state["players"]
32
282
  admin_id = event.get_user_id()
33
- msg = (
34
- UniMessage.at(admin_id)
35
- .text("\n🎉成功创建游戏\n\n")
36
- .text(" 玩家请 @我 发送 “加入游戏”、“退出游戏”\n")
37
- .text(" 玩家 @我 发送 “当前玩家” 可查看玩家列表\n")
38
- .text(" 游戏发起者 @我 发送 “结束游戏” 可结束当前游戏\n")
39
- .text(" 玩家均加入后,游戏发起者请 @我 发送 “开始游戏”\n")
40
- )
41
- if ob11_ext_enabled():
42
- msg.text("\n可使用戳一戳代替游戏交互中的 “/stop” 命令\n")
43
- await msg.text("\n游戏准备阶段限时5分钟,超时将自动结束").send()
44
-
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
48
- players = {admin_id: admin_name}
283
+ admin_name = extract_session_member_nick(session) or admin_id
284
+ players[admin_id] = admin_name
49
285
 
50
286
  try:
51
- async with timeout(5 * 60):
287
+ with anyio.fail_after(5 * 60):
52
288
  await prepare_game(event, players)
53
289
  except TimeoutError:
54
290
  await UniMessage.text("⚠️游戏准备超时,已自动结束").finish()
55
291
 
56
- Game(bot, target, players).start()
292
+ dump_players(target, players)
293
+ game = Game(bot, target, set(players), interface)
294
+ await game.start()
@@ -0,0 +1,24 @@
1
+ from nonebot.permission import SUPERUSER
2
+ from nonebot.rule import to_me
3
+ from nonebot_plugin_alconna import Alconna, MsgTarget, UniMessage, on_alconna
4
+
5
+ from ..game import Game
6
+
7
+
8
+ def rule_game_running(target: MsgTarget) -> bool:
9
+ return any(target.verify(g.group) for g in Game.running_games)
10
+
11
+
12
+ force_stop = on_alconna(
13
+ Alconna("中止游戏"),
14
+ rule=to_me() & rule_game_running,
15
+ permission=SUPERUSER,
16
+ use_cmd_start=True,
17
+ )
18
+
19
+
20
+ @force_stop.handle()
21
+ async def _(target: MsgTarget) -> None:
22
+ game = next(g for g in Game.running_games if target.verify(g.group))
23
+ game.terminate()
24
+ await UniMessage.text("已中止当前群组的游戏进程").finish(reply_to=True)
@@ -0,0 +1,73 @@
1
+ import dataclasses
2
+ from enum import Enum, auto
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from .players import Player
7
+
8
+
9
+ class Role(int, Enum):
10
+ # 狼人
11
+ Werewolf = 1
12
+ WolfKing = 2
13
+
14
+ # 神职
15
+ Prophet = 11
16
+ Witch = 12
17
+ Hunter = 13
18
+ Guard = 14
19
+ Idiot = 15
20
+
21
+ # 其他
22
+ Joker = 51
23
+
24
+ # 平民
25
+ Civilian = 0
26
+
27
+
28
+ class RoleGroup(Enum):
29
+ Werewolf = auto()
30
+ GoodGuy = auto()
31
+ Others = auto()
32
+
33
+
34
+ class KillReason(Enum):
35
+ Werewolf = auto()
36
+ Poison = auto()
37
+ Shoot = auto()
38
+ Vote = auto()
39
+
40
+
41
+ class GameStatus(Enum):
42
+ GoodGuy = auto()
43
+ Werewolf = auto()
44
+ Joker = auto()
45
+
46
+
47
+ @dataclasses.dataclass
48
+ class GameState:
49
+ day: int
50
+ """当前天数记录, 不会被 `reset()` 重置"""
51
+ killed: "Player | None" = None
52
+ """当晚狼人击杀目标, `None` 则为空刀"""
53
+ shoot: "Player | None" = None
54
+ """当前执行射杀操作的玩家"""
55
+ antidote: set["Player"] = dataclasses.field(default_factory=set)
56
+ """当晚女巫使用解药的目标"""
57
+ poison: set["Player"] = dataclasses.field(default_factory=set)
58
+ """当晚使用了毒药的女巫"""
59
+ protected: set["Player"] = dataclasses.field(default_factory=set)
60
+ """当晚守卫保护的目标"""
61
+
62
+ def reset(self) -> None:
63
+ self.killed = None
64
+ self.shoot = None
65
+ self.antidote = set()
66
+ self.poison = set()
67
+ self.protected = set()
68
+
69
+
70
+ @dataclasses.dataclass
71
+ class KillInfo:
72
+ reason: KillReason
73
+ killers: list[str]
@@ -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 .models 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,8 @@
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
5
+ from ..models import KillReason
6
6
  from .player import Player
7
7
 
8
8
 
@@ -14,16 +14,16 @@ class CanShoot(Player):
14
14
  return await super().post_kill()
15
15
 
16
16
  await self.game.send(
17
- UniMessage.text(f"🕵️{self.role_name} ")
17
+ UniMessage.text("🕵️玩家 ")
18
18
  .at(self.user_id)
19
- .text(f" 死了\n请{self.role_name}决定击杀目标...")
19
+ .text(" 死了\n请在私聊决定射杀目标...")
20
20
  )
21
21
 
22
- self.game.state.shoot = (None, None)
22
+ self.game.state.shoot = None
23
23
  shoot = await self.shoot()
24
24
 
25
25
  if shoot is not None:
26
- self.game.state.shoot = (self, shoot)
26
+ self.game.state.shoot = self
27
27
  await self.send(
28
28
  UniMessage.text(f"🔫{self.role_name} ")
29
29
  .at(self.user_id)
@@ -31,6 +31,7 @@ class CanShoot(Player):
31
31
  .at(shoot.user_id)
32
32
  )
33
33
  await shoot.kill(KillReason.Shoot, self)
34
+ self.selected = shoot
34
35
  else:
35
36
  await self.send(f"ℹ️{self.role_name}选择了取消技能")
36
37
  return await super().post_kill()
@@ -41,19 +42,13 @@ class CanShoot(Player):
41
42
  "💫请选择需要射杀的玩家:\n"
42
43
  + players.show()
43
44
  + "\n\n🔫发送编号选择玩家"
44
- + "\n❌发送 “/stop” 取消技能"
45
+ + f"\n❌发送 “{STOP_COMMAND_PROMPT}” 取消技能"
45
46
  )
46
47
 
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]
48
+ if selected := await self._select_player(
49
+ players,
50
+ on_stop="ℹ️已取消技能,回合结束",
51
+ ):
52
+ await self.send(f"🎯选择射杀的玩家: {selected.name}")
53
+
54
+ return selected
@@ -1,7 +1,7 @@
1
- from ..constant import Role, RoleGroup
2
- from .player import Player, register_role
1
+ from ..models import Role, RoleGroup
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,20 @@
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
5
+ from ..models import Role, RoleGroup
6
+ from .player import Player
7
7
 
8
8
 
9
- @register_role(Role.Guard, RoleGroup.GoodGuy)
9
+ @Player.register_role(Role.Guard, RoleGroup.GoodGuy)
10
10
  class Guard(Player):
11
+ @override
12
+ async def _check_selected(self, player: Player) -> Player | None:
13
+ if player is not self.selected:
14
+ return player
15
+ await self.send("⚠️守卫不能连续两晚保护同一目标,请重新选择")
16
+ return None
17
+
11
18
  @override
12
19
  async def interact(self) -> None:
13
20
  players = self.game.players.alive()
@@ -15,23 +22,10 @@ class Guard(Player):
15
22
  UniMessage.text("💫请选择需要保护的玩家:\n")
16
23
  .text(players.show())
17
24
  .text("\n\n🛡️发送编号选择玩家")
18
- .text("\n❌发送 “/stop” 结束回合")
25
+ .text(f"\n❌发送 “{STOP_COMMAND_PROMPT}” 结束回合")
19
26
  )
20
27
 
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}")
28
+ self.selected = await self._select_player(players)
29
+ if self.selected:
30
+ self.game.state.protected.add(self.selected)
31
+ await self.send(f"✅本回合保护的玩家: {self.selected.name}")
@@ -1,8 +1,8 @@
1
- from ..constant import Role, RoleGroup
1
+ from ..models 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