nonebot-plugin-werewolf 1.1.2__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 (35) hide show
  1. nonebot_plugin_werewolf/__init__.py +9 -4
  2. nonebot_plugin_werewolf/config.py +11 -15
  3. nonebot_plugin_werewolf/constant.py +40 -3
  4. nonebot_plugin_werewolf/game.py +219 -181
  5. nonebot_plugin_werewolf/matchers/__init__.py +2 -0
  6. nonebot_plugin_werewolf/matchers/depends.py +38 -0
  7. nonebot_plugin_werewolf/matchers/message_in_game.py +25 -0
  8. nonebot_plugin_werewolf/matchers/poke/__init__.py +8 -0
  9. nonebot_plugin_werewolf/matchers/poke/chronocat_poke.py +117 -0
  10. nonebot_plugin_werewolf/matchers/poke/ob11_poke.py +80 -0
  11. nonebot_plugin_werewolf/matchers/start_game.py +202 -0
  12. nonebot_plugin_werewolf/player_set.py +37 -36
  13. nonebot_plugin_werewolf/players/__init__.py +10 -0
  14. nonebot_plugin_werewolf/players/can_shoot.py +53 -0
  15. nonebot_plugin_werewolf/players/civilian.py +7 -0
  16. nonebot_plugin_werewolf/players/guard.py +30 -0
  17. nonebot_plugin_werewolf/players/hunter.py +8 -0
  18. nonebot_plugin_werewolf/players/idiot.py +44 -0
  19. nonebot_plugin_werewolf/players/joker.py +21 -0
  20. nonebot_plugin_werewolf/players/player.py +233 -0
  21. nonebot_plugin_werewolf/players/prophet.py +22 -0
  22. nonebot_plugin_werewolf/players/werewolf.py +89 -0
  23. nonebot_plugin_werewolf/players/witch.py +66 -0
  24. nonebot_plugin_werewolf/players/wolfking.py +14 -0
  25. nonebot_plugin_werewolf/utils.py +58 -173
  26. {nonebot_plugin_werewolf-1.1.2.dist-info → nonebot_plugin_werewolf-1.1.5.dist-info}/METADATA +24 -4
  27. nonebot_plugin_werewolf-1.1.5.dist-info/RECORD +31 -0
  28. {nonebot_plugin_werewolf-1.1.2.dist-info → nonebot_plugin_werewolf-1.1.5.dist-info}/WHEEL +1 -1
  29. nonebot_plugin_werewolf/_timeout.py +0 -110
  30. nonebot_plugin_werewolf/matchers.py +0 -62
  31. nonebot_plugin_werewolf/ob11_ext.py +0 -72
  32. nonebot_plugin_werewolf/player.py +0 -462
  33. nonebot_plugin_werewolf-1.1.2.dist-info/RECORD +0 -16
  34. {nonebot_plugin_werewolf-1.1.2.dist-info → nonebot_plugin_werewolf-1.1.5.dist-info}/LICENSE +0 -0
  35. {nonebot_plugin_werewolf-1.1.2.dist-info → nonebot_plugin_werewolf-1.1.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,38 @@
1
+ import itertools
2
+
3
+ from nonebot.adapters import Bot, Event
4
+ from nonebot_plugin_alconna import MsgTarget
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, target: MsgTarget) -> bool:
26
+ if not Game.running_games:
27
+ return False
28
+ if target.private:
29
+ return user_in_game(bot.self_id, target.id, None)
30
+ return user_in_game(bot.self_id, event.get_user_id(), target.id)
31
+
32
+
33
+ async def rule_not_in_game(bot: Bot, event: Event, target: MsgTarget) -> bool:
34
+ return not await rule_in_game(bot, event, target)
35
+
36
+
37
+ async def is_group(target: MsgTarget) -> bool:
38
+ return not target.private
@@ -0,0 +1,25 @@
1
+ from nonebot import on_command, on_message
2
+ from nonebot.adapters import Event
3
+ from nonebot_plugin_alconna import MsgTarget, UniMessage, UniMsg
4
+
5
+ from ..constant import STOP_COMMAND
6
+ from ..utils import InputStore
7
+ from .depends import rule_in_game
8
+
9
+ message_in_game = on_message(rule=rule_in_game, priority=10)
10
+
11
+
12
+ @message_in_game.handle()
13
+ async def handle_input(event: Event, target: MsgTarget, msg: UniMsg) -> None:
14
+ if target.private:
15
+ InputStore.put(msg, target.id)
16
+ else:
17
+ InputStore.put(msg, event.get_user_id(), target.id)
18
+
19
+
20
+ stopcmd = on_command("stop", rule=rule_in_game, block=True)
21
+
22
+
23
+ @stopcmd.handle()
24
+ async def handle_stopcmd(event: Event, target: MsgTarget) -> None:
25
+ 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"
@@ -0,0 +1,80 @@
1
+ import contextlib
2
+
3
+ from nonebot import on_notice
4
+ from nonebot.internal.matcher import current_bot
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 ob11_poke_enabled() -> bool:
15
+ return False
16
+
17
+
18
+ with contextlib.suppress(ImportError):
19
+ from nonebot.adapters.onebot.v11 import Bot
20
+ from nonebot.adapters.onebot.v11.event import PokeNotifyEvent
21
+
22
+ # 游戏内戳一戳等效 "stop" 命令
23
+ async def _rule_poke_stop(bot: Bot, event: PokeNotifyEvent) -> bool:
24
+ if not config.enable_poke:
25
+ return False
26
+
27
+ user_id = str(event.user_id)
28
+ group_id = str(event.group_id) if event.group_id is not None else None
29
+ return (event.target_id == event.self_id) and user_in_game(
30
+ bot.self_id, user_id, group_id
31
+ )
32
+
33
+ @on_notice(rule=_rule_poke_stop).handle()
34
+ async def handle_poke_stop(event: PokeNotifyEvent) -> None:
35
+ InputStore.put(
36
+ msg=UniMessage.text(STOP_COMMAND),
37
+ user_id=str(event.user_id),
38
+ group_id=str(event.group_id) if event.group_id is not None else None,
39
+ )
40
+
41
+ # 准备阶段戳一戳等效加入游戏
42
+ async def _rule_poke_join(
43
+ bot: Bot, event: PokeNotifyEvent, target: MsgTarget
44
+ ) -> bool:
45
+ if not config.enable_poke or event.group_id is None:
46
+ return False
47
+
48
+ user_id = str(event.user_id)
49
+ group_id = str(event.group_id)
50
+ return (
51
+ (event.target_id == event.self_id)
52
+ and not user_in_game(bot.self_id, user_id, group_id)
53
+ and any(target.verify(group) for group in Game.starting_games)
54
+ )
55
+
56
+ @on_notice(rule=_rule_poke_join).handle()
57
+ async def handle_poke_join(
58
+ bot: Bot,
59
+ event: PokeNotifyEvent,
60
+ target: MsgTarget,
61
+ ) -> None:
62
+ user_id = event.get_user_id()
63
+ players = next(p for g, p in Game.starting_games.items() if target.verify(g))
64
+
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)
75
+
76
+ def ob11_poke_enabled() -> bool:
77
+ if not config.enable_poke:
78
+ return False
79
+
80
+ return isinstance(current_bot.get(), Bot)
@@ -0,0 +1,202 @@
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
7
+ from nonebot import on_command
8
+ from nonebot.adapters import Bot, Event
9
+ from nonebot.rule import to_me
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
13
+
14
+ from ..config import config
15
+ from ..constant import STOP_COMMAND_PROMPT
16
+ from ..game import Game
17
+ from ..utils import extract_session_member_nick
18
+ from .depends import rule_not_in_game
19
+ from .poke import poke_enabled
20
+
21
+ start_game = on_command(
22
+ "werewolf",
23
+ rule=to_me() & rule_not_in_game,
24
+ aliases={"狼人杀"},
25
+ )
26
+
27
+
28
+ @start_game.handle()
29
+ async def handle_start_warning(target: MsgTarget) -> None:
30
+ if target.private:
31
+ await UniMessage("⚠️请在群组中创建新游戏").finish(reply_to=True)
32
+
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
+
172
+ @start_game.handle()
173
+ async def handle_start(
174
+ bot: Bot,
175
+ event: Event,
176
+ target: MsgTarget,
177
+ session: Uninfo,
178
+ interface: QryItrface,
179
+ ) -> None:
180
+ admin_id = event.get_user_id()
181
+ msg = (
182
+ UniMessage.text("🎉成功创建游戏\n\n")
183
+ .text(" 玩家请 @我 发送 “加入游戏”、“退出游戏”\n")
184
+ .text(" 玩家 @我 发送 “当前玩家” 可查看玩家列表\n")
185
+ .text(" 游戏发起者 @我 发送 “结束游戏” 可结束当前游戏\n")
186
+ .text(" 玩家均加入后,游戏发起者请 @我 发送 “开始游戏”\n")
187
+ )
188
+ if poke_enabled():
189
+ msg.text(f"\n可使用戳一戳代替游戏交互中的 “{STOP_COMMAND_PROMPT}” 命令\n")
190
+ await msg.text("\n游戏准备阶段限时5分钟,超时将自动结束").send(reply_to=True)
191
+
192
+ admin_name = extract_session_member_nick(session) or admin_id
193
+ players = {admin_id: admin_name}
194
+
195
+ try:
196
+ with anyio.fail_after(5 * 60):
197
+ await prepare_game(event, players)
198
+ except TimeoutError:
199
+ await UniMessage.text("⚠️游戏准备超时,已自动结束").finish()
200
+
201
+ game = Game(bot, target, set(players), interface)
202
+ await game.start()
@@ -1,15 +1,10 @@
1
- from __future__ import annotations
1
+ import functools
2
2
 
3
- import asyncio
4
- from typing import TYPE_CHECKING
3
+ import anyio
4
+ from nonebot_plugin_alconna.uniseg import UniMessage
5
5
 
6
- from ._timeout import timeout
7
- from .player import Player
8
-
9
- if TYPE_CHECKING:
10
- from nonebot_plugin_alconna.uniseg import UniMessage
11
-
12
- from .constant import Role, RoleGroup
6
+ from .constant import Role, RoleGroup
7
+ from .players import Player
13
8
 
14
9
 
15
10
  class PlayerSet(set[Player]):
@@ -17,26 +12,26 @@ class PlayerSet(set[Player]):
17
12
  def size(self) -> int:
18
13
  return len(self)
19
14
 
20
- def alive(self) -> PlayerSet:
15
+ def alive(self) -> "PlayerSet":
21
16
  return PlayerSet(p for p in self if p.alive)
22
17
 
23
- def dead(self) -> PlayerSet:
18
+ def dead(self) -> "PlayerSet":
24
19
  return PlayerSet(p for p in self if not p.alive)
25
20
 
26
- def killed(self) -> PlayerSet:
21
+ def killed(self) -> "PlayerSet":
27
22
  return PlayerSet(p for p in self if p.killed.is_set())
28
23
 
29
- def include(self, *types: Player | Role | RoleGroup) -> PlayerSet:
24
+ def include(self, *types: Player | Role | RoleGroup) -> "PlayerSet":
30
25
  return PlayerSet(
31
26
  player
32
27
  for player in self
33
28
  if (player in types or player.role in types or player.role_group in types)
34
29
  )
35
30
 
36
- def select(self, *types: Player | Role | RoleGroup) -> PlayerSet:
31
+ def select(self, *types: Player | Role | RoleGroup) -> "PlayerSet":
37
32
  return self.include(*types)
38
33
 
39
- def exclude(self, *types: Player | Role | RoleGroup) -> PlayerSet:
34
+ def exclude(self, *types: Player | Role | RoleGroup) -> "PlayerSet":
40
35
  return PlayerSet(
41
36
  player
42
37
  for player in self
@@ -47,37 +42,43 @@ class PlayerSet(set[Player]):
47
42
  )
48
43
  )
49
44
 
50
- def player_selected(self) -> PlayerSet:
45
+ def player_selected(self) -> "PlayerSet":
51
46
  return PlayerSet(p.selected for p in self.alive() if p.selected is not None)
52
47
 
48
+ @functools.cached_property
53
49
  def sorted(self) -> list[Player]:
54
50
  return sorted(self, key=lambda p: p.user_id)
55
51
 
56
- async def interact(self, timeout_secs: float = 60) -> None:
57
- async with timeout(timeout_secs):
58
- await asyncio.gather(*[p.interact() for p in self.alive()])
59
-
60
- async def vote(self, timeout_secs: float = 60) -> dict[Player, list[Player]]:
61
- async def vote(player: Player) -> tuple[Player, Player] | None:
62
- try:
63
- async with timeout(timeout_secs):
64
- return await player.vote(self)
65
- except TimeoutError:
66
- await player.send("投票超时,将视为弃票")
67
- 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)
68
56
 
57
+ async def vote(self) -> dict[Player, list[Player]]:
58
+ players = self.alive()
69
59
  result: dict[Player, list[Player]] = {}
70
- for item in await asyncio.gather(*[vote(p) for p in self.alive()]):
71
- if item is not None:
72
- player, voted = item
73
- 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
+
74
70
  return result
75
71
 
76
72
  async def broadcast(self, message: str | UniMessage) -> None:
77
- 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)
78
79
 
79
80
  def show(self) -> str:
80
- return "\n".join(f"{i}. {p.name}" for i, p in enumerate(self.sorted(), 1))
81
+ return "\n".join(f"{i}. {p.name}" for i, p in enumerate(self.sorted, 1))
81
82
 
82
83
  def __getitem__(self, __index: int) -> Player:
83
- return self.sorted()[__index]
84
+ return self.sorted[__index]
@@ -0,0 +1,10 @@
1
+ from .civilian import Civilian as Civilian
2
+ from .guard import Guard as Guard
3
+ from .hunter import Hunter as Hunter
4
+ from .idiot import Idiot as Idiot
5
+ from .joker import Joker as Joker
6
+ from .player import Player as Player
7
+ from .prophet import Prophet as Prophet
8
+ from .werewolf import Werewolf as Werewolf
9
+ from .witch import Witch as Witch
10
+ from .wolfking import WolfKing as WolfKing
@@ -0,0 +1,53 @@
1
+ from nonebot_plugin_alconna.uniseg import UniMessage
2
+ from typing_extensions import override
3
+
4
+ from ..constant import STOP_COMMAND_PROMPT, KillReason
5
+ from .player import Player
6
+
7
+
8
+ class CanShoot(Player):
9
+ @override
10
+ async def post_kill(self) -> None:
11
+ if self.kill_info and self.kill_info.reason == KillReason.Poison:
12
+ await self.send("⚠️你昨晚被女巫毒杀,无法使用技能")
13
+ return await super().post_kill()
14
+
15
+ await self.game.send(
16
+ UniMessage.text(f"🕵️{self.role_name} ")
17
+ .at(self.user_id)
18
+ .text(f" 死了\n请{self.role_name}决定击杀目标...")
19
+ )
20
+
21
+ self.game.state.shoot = None
22
+ shoot = await self.shoot()
23
+
24
+ if shoot is not None:
25
+ self.game.state.shoot = self
26
+ await self.send(
27
+ UniMessage.text(f"🔫{self.role_name} ")
28
+ .at(self.user_id)
29
+ .text(" 射杀了玩家 ")
30
+ .at(shoot.user_id)
31
+ )
32
+ await shoot.kill(KillReason.Shoot, self)
33
+ self.selected = shoot
34
+ else:
35
+ await self.send(f"ℹ️{self.role_name}选择了取消技能")
36
+ return await super().post_kill()
37
+
38
+ async def shoot(self) -> Player | None:
39
+ players = self.game.players.alive().exclude(self)
40
+ await self.send(
41
+ "💫请选择需要射杀的玩家:\n"
42
+ + players.show()
43
+ + "\n\n🔫发送编号选择玩家"
44
+ + f"\n❌发送 “{STOP_COMMAND_PROMPT}” 取消技能"
45
+ )
46
+
47
+ if selected := await self._select_player(
48
+ players,
49
+ on_stop="ℹ️已取消技能,回合结束",
50
+ ):
51
+ await self.send(f"🎯选择射杀的玩家: {selected.name}")
52
+
53
+ return selected
@@ -0,0 +1,7 @@
1
+ from ..constant import Role, RoleGroup
2
+ from .player import Player
3
+
4
+
5
+ @Player.register_role(Role.Civilian, RoleGroup.GoodGuy)
6
+ class Civilian(Player):
7
+ pass