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.
- nonebot_plugin_werewolf/__init__.py +3 -1
- nonebot_plugin_werewolf/config.py +18 -55
- nonebot_plugin_werewolf/constant.py +20 -58
- nonebot_plugin_werewolf/exception.py +1 -1
- nonebot_plugin_werewolf/game.py +286 -245
- nonebot_plugin_werewolf/matchers/__init__.py +2 -0
- nonebot_plugin_werewolf/matchers/depends.py +50 -0
- nonebot_plugin_werewolf/matchers/edit_preset.py +263 -0
- nonebot_plugin_werewolf/matchers/message_in_game.py +18 -3
- nonebot_plugin_werewolf/matchers/poke/__init__.py +8 -0
- nonebot_plugin_werewolf/matchers/poke/chronocat_poke.py +117 -0
- nonebot_plugin_werewolf/matchers/{ob11_ext.py → poke/ob11_poke.py} +21 -19
- nonebot_plugin_werewolf/matchers/start_game.py +266 -28
- nonebot_plugin_werewolf/matchers/superuser_ops.py +24 -0
- nonebot_plugin_werewolf/models.py +73 -0
- nonebot_plugin_werewolf/player_set.py +33 -34
- nonebot_plugin_werewolf/players/can_shoot.py +15 -20
- nonebot_plugin_werewolf/players/civilian.py +3 -3
- nonebot_plugin_werewolf/players/guard.py +16 -22
- nonebot_plugin_werewolf/players/hunter.py +3 -3
- nonebot_plugin_werewolf/players/idiot.py +4 -4
- nonebot_plugin_werewolf/players/joker.py +8 -4
- nonebot_plugin_werewolf/players/player.py +133 -70
- nonebot_plugin_werewolf/players/prophet.py +8 -15
- nonebot_plugin_werewolf/players/werewolf.py +54 -30
- nonebot_plugin_werewolf/players/witch.py +33 -38
- nonebot_plugin_werewolf/players/wolfking.py +3 -3
- nonebot_plugin_werewolf/utils.py +109 -179
- {nonebot_plugin_werewolf-1.1.3.dist-info → nonebot_plugin_werewolf-1.1.6.dist-info}/METADATA +78 -66
- nonebot_plugin_werewolf-1.1.6.dist-info/RECORD +34 -0
- {nonebot_plugin_werewolf-1.1.3.dist-info → nonebot_plugin_werewolf-1.1.6.dist-info}/WHEEL +1 -1
- nonebot_plugin_werewolf/_timeout.py +0 -110
- nonebot_plugin_werewolf-1.1.3.dist-info/RECORD +0 -29
- {nonebot_plugin_werewolf-1.1.3.dist-info → nonebot_plugin_werewolf-1.1.6.dist-info}/LICENSE +0 -0
- {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
|
-
|
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.
|
4
|
-
from
|
5
|
-
from
|
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 ..
|
27
|
+
from ..config import PresetData, config
|
28
|
+
from ..constant import STOP_COMMAND_PROMPT
|
8
29
|
from ..game import Game
|
9
|
-
from ..utils import
|
10
|
-
from .
|
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 =
|
13
|
-
|
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
|
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
|
-
|
34
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
11
|
-
|
3
|
+
import anyio
|
4
|
+
from nonebot_plugin_alconna.uniseg import UniMessage
|
12
5
|
|
13
|
-
|
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
|
59
|
-
async with
|
60
|
-
|
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
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
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
|
-
|
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
|
5
|
-
from ..
|
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(
|
17
|
+
UniMessage.text("🕵️玩家 ")
|
18
18
|
.at(self.user_id)
|
19
|
-
.text(
|
19
|
+
.text(" 死了\n请在私聊决定射杀目标...")
|
20
20
|
)
|
21
21
|
|
22
|
-
self.game.state.shoot =
|
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 =
|
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❌发送
|
45
|
+
+ f"\n❌发送 “{STOP_COMMAND_PROMPT}” 取消技能"
|
45
46
|
)
|
46
47
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
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 ..
|
2
|
-
from .player import Player
|
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
|
5
|
-
from ..
|
6
|
-
from .player import Player
|
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❌发送
|
25
|
+
.text(f"\n❌发送 “{STOP_COMMAND_PROMPT}” 结束回合")
|
19
26
|
)
|
20
27
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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 ..
|
1
|
+
from ..models import Role, RoleGroup
|
2
2
|
from .can_shoot import CanShoot
|
3
|
-
from .player import Player
|
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
|