nonebot-plugin-werewolf 1.0.6__py3-none-any.whl → 1.1.0__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 +1 -1
- nonebot_plugin_werewolf/config.py +76 -4
- nonebot_plugin_werewolf/constant.py +35 -26
- nonebot_plugin_werewolf/exception.py +19 -0
- nonebot_plugin_werewolf/game.py +117 -68
- nonebot_plugin_werewolf/matchers.py +6 -8
- nonebot_plugin_werewolf/ob11_ext.py +1 -1
- nonebot_plugin_werewolf/player.py +97 -71
- nonebot_plugin_werewolf/player_set.py +17 -9
- nonebot_plugin_werewolf/utils.py +34 -24
- {nonebot_plugin_werewolf-1.0.6.dist-info → nonebot_plugin_werewolf-1.1.0.dist-info}/METADATA +83 -20
- nonebot_plugin_werewolf-1.1.0.dist-info/RECORD +15 -0
- {nonebot_plugin_werewolf-1.0.6.dist-info → nonebot_plugin_werewolf-1.1.0.dist-info}/WHEEL +2 -1
- nonebot_plugin_werewolf-1.1.0.dist-info/top_level.txt +1 -0
- nonebot_plugin_werewolf-1.0.6.dist-info/RECORD +0 -13
- {nonebot_plugin_werewolf-1.0.6.dist-info/licenses → nonebot_plugin_werewolf-1.1.0.dist-info}/LICENSE +0 -0
@@ -1,10 +1,81 @@
|
|
1
|
-
from
|
2
|
-
|
1
|
+
from typing import Literal, overload
|
2
|
+
|
3
|
+
from nonebot import get_plugin_config, logger
|
4
|
+
from nonebot.compat import PYDANTIC_V2
|
5
|
+
from pydantic import BaseModel, Field
|
6
|
+
from typing_extensions import Self
|
7
|
+
|
8
|
+
from .constant import (
|
9
|
+
Role,
|
10
|
+
default_priesthood_proirity,
|
11
|
+
default_role_preset,
|
12
|
+
default_werewolf_priority,
|
13
|
+
)
|
14
|
+
|
15
|
+
if PYDANTIC_V2:
|
16
|
+
from pydantic import model_validator as model_validator
|
17
|
+
else:
|
18
|
+
from pydantic import root_validator
|
19
|
+
|
20
|
+
@overload
|
21
|
+
def model_validator(*, mode: Literal["before"]): ... # noqa: ANN201
|
22
|
+
|
23
|
+
@overload
|
24
|
+
def model_validator(*, mode: Literal["after"]): ... # noqa: ANN201
|
25
|
+
|
26
|
+
def model_validator(*, mode: Literal["before", "after"]):
|
27
|
+
return root_validator(
|
28
|
+
pre=mode == "before", # pyright: ignore[reportArgumentType]
|
29
|
+
allow_reuse=True,
|
30
|
+
)
|
3
31
|
|
4
32
|
|
5
33
|
class PluginConfig(BaseModel):
|
6
|
-
enable_poke: bool = True
|
7
|
-
|
34
|
+
enable_poke: bool = Field(default=True)
|
35
|
+
role_preset: list[tuple[int, int, int, int]] | dict[int, tuple[int, int, int]] = (
|
36
|
+
Field(default_factory=default_role_preset.copy)
|
37
|
+
)
|
38
|
+
werewolf_priority: list[Role] = Field(
|
39
|
+
default_factory=default_werewolf_priority.copy
|
40
|
+
)
|
41
|
+
priesthood_proirity: list[Role] = Field(
|
42
|
+
default_factory=default_priesthood_proirity.copy
|
43
|
+
)
|
44
|
+
joker_probability: float = Field(default=0.0, ge=0.0, le=1.0)
|
45
|
+
|
46
|
+
@model_validator(mode="after")
|
47
|
+
def _validate(self) -> Self:
|
48
|
+
if isinstance(self.role_preset, list):
|
49
|
+
for preset in self.role_preset:
|
50
|
+
if preset[0] != sum(preset[1:]):
|
51
|
+
raise RuntimeError(
|
52
|
+
"配置项 `role_preset` 错误: "
|
53
|
+
f"预设总人数为 {preset[0]}, 实际总人数为 {sum(preset[1:])} "
|
54
|
+
f"({', '.join(map(str, preset[1:]))})"
|
55
|
+
)
|
56
|
+
self.role_preset = default_role_preset | {
|
57
|
+
i[0]: i[1:] for i in self.role_preset
|
58
|
+
}
|
59
|
+
logger.debug(f"覆写配置 role_preset: {self.role_preset}")
|
60
|
+
|
61
|
+
min_length = max(i[0] for i in self.role_preset.values())
|
62
|
+
if len(self.werewolf_priority) < min_length:
|
63
|
+
raise RuntimeError(
|
64
|
+
f"配置项 `werewolf_priority` 错误: 应至少为 {min_length} 项"
|
65
|
+
)
|
66
|
+
|
67
|
+
min_length = max(i[1] for i in self.role_preset.values())
|
68
|
+
if len(self.priesthood_proirity) < min_length:
|
69
|
+
raise RuntimeError(
|
70
|
+
f"配置项 `priesthood_proirity` 错误: 应至少为 {min_length} 项"
|
71
|
+
)
|
72
|
+
|
73
|
+
return self
|
74
|
+
|
75
|
+
def get_role_preset(self) -> dict[int, tuple[int, int, int]]:
|
76
|
+
if isinstance(self.role_preset, list):
|
77
|
+
self.role_preset = {i[0]: i[1:] for i in self.role_preset}
|
78
|
+
return self.role_preset
|
8
79
|
|
9
80
|
|
10
81
|
class Config(BaseModel):
|
@@ -12,3 +83,4 @@ class Config(BaseModel):
|
|
12
83
|
|
13
84
|
|
14
85
|
config = get_plugin_config(Config).werewolf
|
86
|
+
logger.debug(f"加载插件配置: {config}")
|
@@ -4,46 +4,47 @@ from dataclasses import dataclass
|
|
4
4
|
from enum import Enum, auto
|
5
5
|
from typing import TYPE_CHECKING
|
6
6
|
|
7
|
-
from strenum import StrEnum
|
8
|
-
|
9
|
-
from .config import config
|
10
|
-
|
11
7
|
if TYPE_CHECKING:
|
12
8
|
from .player import Player
|
13
9
|
|
14
10
|
|
15
|
-
class Role(
|
11
|
+
class Role(Enum):
|
16
12
|
# 狼人
|
17
|
-
Werewolf =
|
18
|
-
WolfKing =
|
13
|
+
Werewolf = 1
|
14
|
+
WolfKing = 2
|
19
15
|
|
20
16
|
# 神职
|
21
|
-
Prophet =
|
22
|
-
Witch =
|
23
|
-
Hunter =
|
24
|
-
Guard =
|
25
|
-
Idiot =
|
17
|
+
Prophet = 11
|
18
|
+
Witch = 12
|
19
|
+
Hunter = 13
|
20
|
+
Guard = 14
|
21
|
+
Idiot = 15
|
22
|
+
|
23
|
+
# 其他
|
24
|
+
Joker = 51
|
26
25
|
|
27
26
|
# 平民
|
28
|
-
Civilian =
|
27
|
+
Civilian = 0
|
29
28
|
|
30
29
|
|
31
|
-
class RoleGroup(
|
30
|
+
class RoleGroup(Enum):
|
32
31
|
Werewolf = auto()
|
33
32
|
GoodGuy = auto()
|
33
|
+
Others = auto()
|
34
34
|
|
35
35
|
|
36
36
|
class KillReason(Enum):
|
37
|
-
|
37
|
+
Werewolf = auto()
|
38
38
|
Poison = auto()
|
39
39
|
Shoot = auto()
|
40
40
|
Vote = auto()
|
41
41
|
|
42
42
|
|
43
43
|
class GameStatus(Enum):
|
44
|
-
|
45
|
-
|
44
|
+
GoodGuy = auto()
|
45
|
+
Werewolf = auto()
|
46
46
|
Unset = auto()
|
47
|
+
Joker = auto()
|
47
48
|
|
48
49
|
|
49
50
|
@dataclass
|
@@ -55,7 +56,7 @@ class GameState:
|
|
55
56
|
potion: tuple[Player | None, tuple[bool, bool]] = (None, (False, False))
|
56
57
|
|
57
58
|
|
58
|
-
role_name_conv: dict[Role, str] = {
|
59
|
+
role_name_conv: dict[Role | RoleGroup, str] = {
|
59
60
|
Role.Werewolf: "狼人",
|
60
61
|
Role.WolfKing: "狼王",
|
61
62
|
Role.Prophet: "预言家",
|
@@ -63,16 +64,14 @@ role_name_conv: dict[Role, str] = {
|
|
63
64
|
Role.Hunter: "猎人",
|
64
65
|
Role.Guard: "守卫",
|
65
66
|
Role.Idiot: "白痴",
|
67
|
+
Role.Joker: "小丑",
|
66
68
|
Role.Civilian: "平民",
|
67
|
-
}
|
68
|
-
|
69
|
-
|
70
|
-
role_group_name_conv: dict[RoleGroup, str] = {
|
71
69
|
RoleGroup.Werewolf: "狼人",
|
72
70
|
RoleGroup.GoodGuy: "好人",
|
71
|
+
RoleGroup.Others: "其他",
|
73
72
|
}
|
74
73
|
|
75
|
-
|
74
|
+
default_role_preset: dict[int, tuple[int, int, int]] = {
|
76
75
|
# 总人数: (狼, 神, 民)
|
77
76
|
6: (1, 2, 3),
|
78
77
|
7: (2, 2, 3),
|
@@ -83,6 +82,16 @@ player_preset: dict[int, tuple[int, int, int]] = {
|
|
83
82
|
12: (4, 5, 3),
|
84
83
|
}
|
85
84
|
|
86
|
-
|
87
|
-
|
88
|
-
|
85
|
+
default_werewolf_priority: list[Role] = [
|
86
|
+
Role.Werewolf,
|
87
|
+
Role.Werewolf,
|
88
|
+
Role.WolfKing,
|
89
|
+
Role.Werewolf,
|
90
|
+
]
|
91
|
+
default_priesthood_proirity: list[Role] = [
|
92
|
+
Role.Witch,
|
93
|
+
Role.Prophet,
|
94
|
+
Role.Hunter,
|
95
|
+
Role.Guard,
|
96
|
+
Role.Idiot,
|
97
|
+
]
|
@@ -0,0 +1,19 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import TYPE_CHECKING
|
4
|
+
|
5
|
+
if TYPE_CHECKING:
|
6
|
+
from .constant import GameStatus
|
7
|
+
|
8
|
+
|
9
|
+
class Error(Exception):
|
10
|
+
"""插件错误类型基类"""
|
11
|
+
|
12
|
+
|
13
|
+
class GameFinishedError(Error):
|
14
|
+
"""游戏结束时抛出,无视游戏进程进入结算"""
|
15
|
+
|
16
|
+
status: GameStatus
|
17
|
+
|
18
|
+
def __init__(self, status: GameStatus) -> None:
|
19
|
+
self.status = status
|
nonebot_plugin_werewolf/game.py
CHANGED
@@ -1,46 +1,59 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import asyncio
|
2
4
|
import asyncio.timeouts
|
3
5
|
import contextlib
|
4
6
|
import random
|
5
7
|
import time
|
8
|
+
from typing import TYPE_CHECKING, NoReturn
|
6
9
|
|
7
|
-
from nonebot.adapters import Bot
|
8
10
|
from nonebot.log import logger
|
9
|
-
from nonebot_plugin_alconna import Target, UniMessage
|
11
|
+
from nonebot_plugin_alconna import At, Target, UniMessage
|
10
12
|
|
11
|
-
from .
|
13
|
+
from .config import config
|
14
|
+
from .constant import GameState, GameStatus, KillReason, Role, RoleGroup, role_name_conv
|
15
|
+
from .exception import GameFinishedError
|
12
16
|
from .player import Player
|
13
17
|
from .player_set import PlayerSet
|
14
18
|
from .utils import InputStore
|
15
19
|
|
20
|
+
if TYPE_CHECKING:
|
21
|
+
from nonebot.adapters import Bot
|
22
|
+
from nonebot_plugin_alconna.uniseg.message import Receipt
|
23
|
+
|
16
24
|
starting_games: dict[str, dict[str, str]] = {}
|
17
|
-
running_games: dict[str,
|
25
|
+
running_games: dict[str, Game] = {}
|
18
26
|
|
19
27
|
|
20
|
-
def init_players(bot: Bot, game:
|
21
|
-
|
28
|
+
def init_players(bot: Bot, game: Game, players: dict[str, str]) -> PlayerSet:
|
29
|
+
logger.opt(colors=True).debug(f"初始化 <c>{game.group.id}</c> 的玩家职业")
|
30
|
+
role_preset = config.get_role_preset()
|
31
|
+
preset = role_preset.get(len(players))
|
22
32
|
if preset is None:
|
23
33
|
raise ValueError(
|
24
34
|
f"玩家人数不符: "
|
25
|
-
f"应为 {', '.join(map(str,
|
35
|
+
f"应为 {', '.join(map(str, role_preset))} 人, 传入{len(players)}人"
|
26
36
|
)
|
27
37
|
|
28
38
|
roles: list[Role] = []
|
29
|
-
roles.extend(
|
30
|
-
|
31
|
-
)
|
32
|
-
roles.extend(
|
33
|
-
[Role.Prophet, Role.Witch, Role.Hunter, Role.Guard, Role.Idiot][: preset[1]]
|
34
|
-
)
|
39
|
+
roles.extend(config.werewolf_priority[: preset[0]])
|
40
|
+
roles.extend(config.priesthood_proirity[: preset[1]])
|
35
41
|
roles.extend([Role.Civilian] * preset[2])
|
36
42
|
|
37
|
-
r = random.Random(time.time())
|
43
|
+
r = random.Random(time.time()) # noqa: S311
|
44
|
+
|
45
|
+
if roles.count(Role.Civilian) >= 2 and r.random() <= config.joker_probability:
|
46
|
+
roles.remove(Role.Civilian)
|
47
|
+
roles.append(Role.Joker)
|
48
|
+
|
38
49
|
shuffled: list[Role] = []
|
39
50
|
for _ in range(len(players)):
|
40
51
|
idx = r.randint(0, len(roles) - 1)
|
41
52
|
shuffled.append(roles.pop(idx))
|
42
53
|
|
43
|
-
|
54
|
+
logger.debug(f"职业分配: {shuffled}")
|
55
|
+
|
56
|
+
async def selector(target_: Target, b: Bot) -> bool:
|
44
57
|
return target_.self_id == bot.self_id and b is bot
|
45
58
|
|
46
59
|
return PlayerSet(
|
@@ -56,7 +69,7 @@ def init_players(bot: Bot, game: "Game", players: dict[str, str]) -> PlayerSet:
|
|
56
69
|
),
|
57
70
|
players[user_id],
|
58
71
|
)
|
59
|
-
for user_id, role in zip(players, shuffled)
|
72
|
+
for user_id, role in zip(players, shuffled, strict=True)
|
60
73
|
)
|
61
74
|
|
62
75
|
|
@@ -64,24 +77,28 @@ class Game:
|
|
64
77
|
bot: Bot
|
65
78
|
group: Target
|
66
79
|
players: PlayerSet
|
80
|
+
_player_map: dict[str, Player]
|
67
81
|
state: GameState
|
68
82
|
killed_players: list[Player]
|
69
83
|
|
70
|
-
def __init__(
|
71
|
-
self,
|
72
|
-
bot: Bot,
|
73
|
-
group: Target,
|
74
|
-
players: dict[str, str],
|
75
|
-
) -> None:
|
84
|
+
def __init__(self, bot: Bot, group: Target, players: dict[str, str]) -> None:
|
76
85
|
self.bot = bot
|
77
86
|
self.group = group
|
78
87
|
self.players = init_players(bot, self, players)
|
88
|
+
self._player_map = {p.user_id: p for p in self.players}
|
79
89
|
self.state = GameState(0)
|
80
90
|
self.killed_players = []
|
81
91
|
|
82
|
-
async def send(self, message: str | UniMessage):
|
92
|
+
async def send(self, message: str | UniMessage) -> Receipt:
|
83
93
|
if isinstance(message, str):
|
84
94
|
message = UniMessage.text(message)
|
95
|
+
text = f"<b><e>{self.group.id}</e></b> | <g>Send</g> | "
|
96
|
+
for seg in message:
|
97
|
+
if isinstance(seg, At):
|
98
|
+
text += f"<y>@{self._player_map[seg.target].name}</y>"
|
99
|
+
else:
|
100
|
+
text += str(seg)
|
101
|
+
logger.opt(colors=True).info(text.replace("\n", "\\n"))
|
85
102
|
return await message.send(self.group, self.bot)
|
86
103
|
|
87
104
|
def at_all(self) -> UniMessage:
|
@@ -95,14 +112,18 @@ class Game:
|
|
95
112
|
w = players.select(RoleGroup.Werewolf)
|
96
113
|
p = players.exclude(RoleGroup.Werewolf)
|
97
114
|
|
115
|
+
# 狼人数量大于其他职业数量
|
98
116
|
if w.size >= p.size:
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
117
|
+
raise GameFinishedError(GameStatus.Werewolf)
|
118
|
+
# 屠边-村民/中立全灭
|
119
|
+
if not p.select(Role.Civilian, RoleGroup.Others).size:
|
120
|
+
raise GameFinishedError(GameStatus.Werewolf)
|
121
|
+
# 屠边-神职全灭
|
122
|
+
if not p.exclude(Role.Civilian).size:
|
123
|
+
raise GameFinishedError(GameStatus.Werewolf)
|
124
|
+
# 狼人全灭
|
104
125
|
if not w.size:
|
105
|
-
|
126
|
+
raise GameFinishedError(GameStatus.GoodGuy)
|
106
127
|
|
107
128
|
return GameStatus.Unset
|
108
129
|
|
@@ -117,7 +138,7 @@ class Game:
|
|
117
138
|
p.name for p in player.kill_info.killers
|
118
139
|
)
|
119
140
|
match player.kill_info.reason:
|
120
|
-
case KillReason.
|
141
|
+
case KillReason.Werewolf:
|
121
142
|
msg += " 刀了"
|
122
143
|
case KillReason.Poison:
|
123
144
|
msg += " 毒死"
|
@@ -130,7 +151,7 @@ class Game:
|
|
130
151
|
return msg.strip()
|
131
152
|
|
132
153
|
async def notify_player_role(self) -> None:
|
133
|
-
preset =
|
154
|
+
preset = config.get_role_preset()[len(self.players)]
|
134
155
|
await asyncio.gather(
|
135
156
|
self.send(
|
136
157
|
self.at_all()
|
@@ -149,7 +170,7 @@ class Game:
|
|
149
170
|
if isinstance(players, Player):
|
150
171
|
players = PlayerSet([players])
|
151
172
|
|
152
|
-
async def wait(p: Player):
|
173
|
+
async def wait(p: Player) -> None:
|
153
174
|
while True:
|
154
175
|
msg = await InputStore.fetch(p.user_id, self.group.id)
|
155
176
|
if msg.extract_plain_text() == "/stop":
|
@@ -169,9 +190,9 @@ class Game:
|
|
169
190
|
type_.role_name # Player
|
170
191
|
if isinstance(type_, Player)
|
171
192
|
else (
|
172
|
-
type_
|
193
|
+
role_name_conv[type_] # Role
|
173
194
|
if isinstance(type_, Role)
|
174
|
-
else f"{type_
|
195
|
+
else f"{role_name_conv[type_]}阵营" # RoleGroup
|
175
196
|
)
|
176
197
|
)
|
177
198
|
|
@@ -179,6 +200,7 @@ class Game:
|
|
179
200
|
try:
|
180
201
|
await players.interact(timeout_secs)
|
181
202
|
except TimeoutError:
|
203
|
+
logger.opt(colors=True).debug(f"{text}交互超时 (<y>{timeout_secs}</y>s)")
|
182
204
|
await players.broadcast(f"{text}交互时间结束")
|
183
205
|
|
184
206
|
async def select_killed(self) -> None:
|
@@ -198,7 +220,7 @@ class Game:
|
|
198
220
|
await self.interact(Role.Witch, 60)
|
199
221
|
# 否则等待 5-20s
|
200
222
|
else:
|
201
|
-
await asyncio.sleep(random.uniform(5, 20))
|
223
|
+
await asyncio.sleep(random.uniform(5, 20)) # noqa: S311
|
202
224
|
|
203
225
|
async def handle_new_dead(self, players: Player | PlayerSet) -> None:
|
204
226
|
if isinstance(players, Player):
|
@@ -250,6 +272,8 @@ class Game:
|
|
250
272
|
# 收集到的总票数
|
251
273
|
total_votes = sum(map(len, vote_result.values()))
|
252
274
|
|
275
|
+
logger.debug(f"投票结果: {vote_result}")
|
276
|
+
|
253
277
|
# 投票结果公示
|
254
278
|
msg = UniMessage.text("投票结果:\n")
|
255
279
|
for p, v in sorted(vote_result.items(), key=lambda x: len(x[1]), reverse=True):
|
@@ -299,23 +323,23 @@ class Game:
|
|
299
323
|
loop = asyncio.get_event_loop()
|
300
324
|
queue: asyncio.Queue[tuple[Player, UniMessage]] = asyncio.Queue()
|
301
325
|
|
302
|
-
async def send():
|
326
|
+
async def send() -> NoReturn:
|
303
327
|
while True:
|
304
328
|
player, msg = await queue.get()
|
305
329
|
msg = f"玩家 {player.name}:\n" + msg
|
306
|
-
await self.players.
|
330
|
+
await self.players.killed().exclude(player).broadcast(msg)
|
331
|
+
queue.task_done()
|
332
|
+
|
333
|
+
async def recv(player: Player) -> NoReturn:
|
334
|
+
await player.killed.wait()
|
307
335
|
|
308
|
-
async def recv(player: Player):
|
309
336
|
counter = 0
|
310
337
|
|
311
|
-
def decrease():
|
338
|
+
def decrease() -> None:
|
312
339
|
nonlocal counter
|
313
340
|
counter -= 1
|
314
341
|
|
315
342
|
while True:
|
316
|
-
if not player.killed:
|
317
|
-
await asyncio.sleep(1)
|
318
|
-
continue
|
319
343
|
msg = await player.receive()
|
320
344
|
counter += 1
|
321
345
|
if counter <= 10:
|
@@ -326,13 +350,14 @@ class Game:
|
|
326
350
|
|
327
351
|
await asyncio.gather(send(), *[recv(p) for p in self.players])
|
328
352
|
|
329
|
-
async def run(self) ->
|
353
|
+
async def run(self) -> NoReturn:
|
330
354
|
# 告知玩家角色信息
|
331
355
|
await self.notify_player_role()
|
332
356
|
# 天数记录 主要用于第一晚狼人击杀的遗言
|
333
357
|
day_count = 0
|
334
358
|
|
335
|
-
|
359
|
+
# 游戏主循环
|
360
|
+
while True:
|
336
361
|
# 重置游戏状态,进入下一夜
|
337
362
|
self.state = GameState(day_count)
|
338
363
|
players = self.players.alive()
|
@@ -341,10 +366,12 @@ class Game:
|
|
341
366
|
# 狼人、预言家、守卫 同时交互,女巫在狼人后交互
|
342
367
|
await asyncio.gather(
|
343
368
|
self.select_killed(),
|
344
|
-
players.select(Role.Witch).broadcast("请等待狼人决定目标..."),
|
345
|
-
players.select(Role.Civilian).broadcast("请等待其他玩家结束交互..."),
|
346
369
|
self.interact(Role.Prophet, 60),
|
347
370
|
self.interact(Role.Guard, 60),
|
371
|
+
players.select(Role.Witch).broadcast("请等待狼人决定目标..."),
|
372
|
+
players.select(Role.Civilian, RoleGroup.Others).broadcast(
|
373
|
+
"请等待其他玩家结束交互..."
|
374
|
+
),
|
348
375
|
)
|
349
376
|
|
350
377
|
# 狼人击杀目标
|
@@ -354,13 +381,13 @@ class Game:
|
|
354
381
|
# 女巫的操作目标和内容
|
355
382
|
potioned, (antidote, poison) = self.state.potion
|
356
383
|
|
357
|
-
#
|
358
|
-
if killed is not None
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
384
|
+
# 狼人未空刀,除非守卫保护或女巫使用解药,否则狼人正常击杀玩家
|
385
|
+
if killed is not None and (
|
386
|
+
not ((killed is protected) or (antidote and potioned is killed))
|
387
|
+
):
|
388
|
+
await killed.kill(
|
389
|
+
KillReason.Werewolf, *players.select(RoleGroup.Werewolf)
|
390
|
+
)
|
364
391
|
# 如果女巫使用毒药且守卫未保护,杀死该玩家
|
365
392
|
if poison and (potioned is not None) and (potioned is not protected):
|
366
393
|
await potioned.kill(KillReason.Poison, *players.select(Role.Witch))
|
@@ -389,8 +416,7 @@ class Game:
|
|
389
416
|
await self.post_kill(dead)
|
390
417
|
|
391
418
|
# 判断游戏状态
|
392
|
-
|
393
|
-
break
|
419
|
+
self.check_game_status()
|
394
420
|
|
395
421
|
# 公示存活玩家
|
396
422
|
await self.send(f"当前存活玩家: \n\n{self.players.alive().show()}")
|
@@ -403,32 +429,55 @@ class Game:
|
|
403
429
|
await self.send("讨论结束, 进入投票环节,限时1分钟\n请在私聊中进行投票交互")
|
404
430
|
await self.run_vote()
|
405
431
|
|
406
|
-
|
407
|
-
|
432
|
+
# 判断游戏状态
|
433
|
+
self.check_game_status()
|
434
|
+
|
435
|
+
async def handle_game_finish(self, status: GameStatus) -> None:
|
436
|
+
match status:
|
437
|
+
case GameStatus.GoodGuy:
|
438
|
+
winner = "好人"
|
439
|
+
case GameStatus.Werewolf:
|
440
|
+
winner = "狼人"
|
441
|
+
case GameStatus.Joker:
|
442
|
+
winner = "小丑"
|
443
|
+
case GameStatus.Unset:
|
444
|
+
raise RuntimeError(f"错误的游戏状态: {status!r}")
|
445
|
+
|
408
446
|
msg = UniMessage.text(f"🎉游戏结束,{winner}获胜\n\n")
|
409
447
|
for p in sorted(self.players, key=lambda p: (p.role.value, p.user_id)):
|
410
448
|
msg.at(p.user_id).text(f": {p.role_name}\n")
|
411
449
|
await self.send(msg)
|
412
|
-
await self.send(f"
|
450
|
+
await self.send(f"📌玩家死亡报告:\n\n{self.show_killed_players()}")
|
413
451
|
|
414
|
-
def start(self):
|
415
|
-
|
452
|
+
def start(self) -> None:
|
453
|
+
finished = asyncio.Event()
|
454
|
+
game_task = asyncio.create_task(self.run())
|
455
|
+
game_task.add_done_callback(lambda _: finished.set())
|
416
456
|
dead_channel = asyncio.create_task(self.run_dead_channel())
|
417
457
|
|
418
|
-
async def daemon():
|
419
|
-
|
420
|
-
await asyncio.sleep(1)
|
458
|
+
async def daemon() -> None:
|
459
|
+
await finished.wait()
|
421
460
|
|
422
461
|
try:
|
423
|
-
|
424
|
-
except asyncio.CancelledError
|
425
|
-
logger.warning(f"
|
462
|
+
game_task.result()
|
463
|
+
except asyncio.CancelledError:
|
464
|
+
logger.warning(f"{self.group.id} 的狼人杀游戏进程被取消")
|
465
|
+
except GameFinishedError as result:
|
466
|
+
await self.handle_game_finish(result.status)
|
467
|
+
logger.info(f"{self.group.id} 的狼人杀游戏进程正常退出")
|
426
468
|
except Exception as err:
|
427
|
-
msg = f"
|
469
|
+
msg = f"{self.group.id} 的狼人杀游戏进程出现未知错误: {err!r}"
|
428
470
|
logger.opt(exception=err).error(msg)
|
429
471
|
await self.send(msg)
|
430
472
|
finally:
|
431
473
|
dead_channel.cancel()
|
432
474
|
running_games.pop(self.group.id, None)
|
433
475
|
|
434
|
-
|
476
|
+
def daemon_callback(task: asyncio.Task[None]) -> None:
|
477
|
+
if err := task.exception():
|
478
|
+
logger.opt(exception=err).error(
|
479
|
+
f"{self.group.id} 的狼人杀守护进程出现错误: {err!r}"
|
480
|
+
)
|
481
|
+
|
482
|
+
running_games[self.group.id] = self
|
483
|
+
asyncio.create_task(daemon()).add_done_callback(daemon_callback)
|
@@ -4,11 +4,12 @@ from typing import Annotated
|
|
4
4
|
|
5
5
|
from nonebot import on_command, on_message
|
6
6
|
from nonebot.adapters import Bot, Event
|
7
|
+
from nonebot.exception import FinishedException
|
7
8
|
from nonebot.rule import to_me
|
8
9
|
from nonebot_plugin_alconna import MsgTarget, UniMessage, UniMsg
|
9
10
|
from nonebot_plugin_userinfo import EventUserInfo, UserInfo
|
10
11
|
|
11
|
-
from .game import Game
|
12
|
+
from .game import Game
|
12
13
|
from .ob11_ext import ob11_ext_enabled
|
13
14
|
from .utils import InputStore, is_group, prepare_game, rule_in_game, rule_not_in_game
|
14
15
|
|
@@ -35,9 +36,6 @@ async def handle_start(
|
|
35
36
|
target: MsgTarget,
|
36
37
|
admin_info: Annotated[UserInfo, EventUserInfo()],
|
37
38
|
) -> None:
|
38
|
-
if target.id in running_games:
|
39
|
-
await UniMessage.text("当前群聊内有正在进行的游戏,无法创建游戏").finish()
|
40
|
-
|
41
39
|
admin_id = event.get_user_id()
|
42
40
|
msg = (
|
43
41
|
UniMessage.at(admin_id)
|
@@ -51,15 +49,15 @@ async def handle_start(
|
|
51
49
|
msg.text("\n可使用戳一戳代替游戏交互中的 “/stop” 命令")
|
52
50
|
await msg.text("\n\n游戏准备阶段限时5分钟,超时将自动结束").send()
|
53
51
|
|
54
|
-
players =
|
52
|
+
players = {admin_id: admin_info.user_name}
|
55
53
|
|
56
54
|
try:
|
57
55
|
async with asyncio.timeouts.timeout(5 * 60):
|
58
56
|
await prepare_game(event, players)
|
57
|
+
except FinishedException:
|
58
|
+
raise
|
59
59
|
except TimeoutError:
|
60
60
|
await UniMessage.text("游戏准备超时,已自动结束").finish()
|
61
|
-
finally:
|
62
|
-
del starting_games[target.id]
|
63
61
|
|
64
|
-
game = Game(bot
|
62
|
+
game = Game(bot, target, players)
|
65
63
|
game.start()
|
@@ -62,7 +62,7 @@ with contextlib.suppress(ImportError):
|
|
62
62
|
group_id=int(group_id),
|
63
63
|
user_id=int(user_id),
|
64
64
|
)
|
65
|
-
players[user_id] = res.get("nickname") or user_id
|
65
|
+
players[user_id] = res.get("card") or res.get("nickname") or user_id
|
66
66
|
await bot.send(event, MessageSegment.at(user_id) + "成功加入游戏")
|
67
67
|
|
68
68
|
def ob11_ext_enabled() -> bool:
|