nonebot-plugin-werewolf 1.0.7__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 +75 -7
- nonebot_plugin_werewolf/constant.py +13 -40
- nonebot_plugin_werewolf/exception.py +19 -0
- nonebot_plugin_werewolf/game.py +102 -69
- nonebot_plugin_werewolf/matchers.py +3 -8
- nonebot_plugin_werewolf/ob11_ext.py +1 -1
- nonebot_plugin_werewolf/player.py +72 -35
- nonebot_plugin_werewolf/player_set.py +9 -3
- nonebot_plugin_werewolf/utils.py +20 -16
- {nonebot_plugin_werewolf-1.0.7.dist-info → nonebot_plugin_werewolf-1.1.0.dist-info}/METADATA +22 -10
- nonebot_plugin_werewolf-1.1.0.dist-info/RECORD +15 -0
- {nonebot_plugin_werewolf-1.0.7.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.7.dist-info/RECORD +0 -13
- {nonebot_plugin_werewolf-1.0.7.dist-info/licenses → nonebot_plugin_werewolf-1.1.0.dist-info}/LICENSE +0 -0
@@ -1,14 +1,81 @@
|
|
1
|
-
from
|
2
|
-
from pydantic import BaseModel
|
1
|
+
from typing import Literal, overload
|
3
2
|
|
4
|
-
from
|
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
|
+
)
|
5
31
|
|
6
32
|
|
7
33
|
class PluginConfig(BaseModel):
|
8
|
-
enable_poke: bool = True
|
9
|
-
role_preset: list[tuple[int, int, int, int]] |
|
10
|
-
|
11
|
-
|
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
|
12
79
|
|
13
80
|
|
14
81
|
class Config(BaseModel):
|
@@ -16,3 +83,4 @@ class Config(BaseModel):
|
|
16
83
|
|
17
84
|
|
18
85
|
config = get_plugin_config(Config).werewolf
|
86
|
+
logger.debug(f"加载插件配置: {config}")
|
@@ -20,6 +20,9 @@ class Role(Enum):
|
|
20
20
|
Guard = 14
|
21
21
|
Idiot = 15
|
22
22
|
|
23
|
+
# 其他
|
24
|
+
Joker = 51
|
25
|
+
|
23
26
|
# 平民
|
24
27
|
Civilian = 0
|
25
28
|
|
@@ -27,19 +30,21 @@ class Role(Enum):
|
|
27
30
|
class RoleGroup(Enum):
|
28
31
|
Werewolf = auto()
|
29
32
|
GoodGuy = auto()
|
33
|
+
Others = auto()
|
30
34
|
|
31
35
|
|
32
36
|
class KillReason(Enum):
|
33
|
-
|
37
|
+
Werewolf = auto()
|
34
38
|
Poison = auto()
|
35
39
|
Shoot = auto()
|
36
40
|
Vote = auto()
|
37
41
|
|
38
42
|
|
39
43
|
class GameStatus(Enum):
|
40
|
-
|
41
|
-
|
44
|
+
GoodGuy = auto()
|
45
|
+
Werewolf = auto()
|
42
46
|
Unset = auto()
|
47
|
+
Joker = auto()
|
43
48
|
|
44
49
|
|
45
50
|
@dataclass
|
@@ -59,12 +64,14 @@ role_name_conv: dict[Role | RoleGroup, str] = {
|
|
59
64
|
Role.Hunter: "猎人",
|
60
65
|
Role.Guard: "守卫",
|
61
66
|
Role.Idiot: "白痴",
|
67
|
+
Role.Joker: "小丑",
|
62
68
|
Role.Civilian: "平民",
|
63
69
|
RoleGroup.Werewolf: "狼人",
|
64
70
|
RoleGroup.GoodGuy: "好人",
|
71
|
+
RoleGroup.Others: "其他",
|
65
72
|
}
|
66
73
|
|
67
|
-
|
74
|
+
default_role_preset: dict[int, tuple[int, int, int]] = {
|
68
75
|
# 总人数: (狼, 神, 民)
|
69
76
|
6: (1, 2, 3),
|
70
77
|
7: (2, 2, 3),
|
@@ -75,50 +82,16 @@ role_preset: dict[int, tuple[int, int, int]] = {
|
|
75
82
|
12: (4, 5, 3),
|
76
83
|
}
|
77
84
|
|
78
|
-
|
85
|
+
default_werewolf_priority: list[Role] = [
|
79
86
|
Role.Werewolf,
|
80
87
|
Role.Werewolf,
|
81
88
|
Role.WolfKing,
|
82
89
|
Role.Werewolf,
|
83
90
|
]
|
84
|
-
|
91
|
+
default_priesthood_proirity: list[Role] = [
|
85
92
|
Role.Witch,
|
86
93
|
Role.Prophet,
|
87
94
|
Role.Hunter,
|
88
95
|
Role.Guard,
|
89
96
|
Role.Idiot,
|
90
97
|
]
|
91
|
-
|
92
|
-
|
93
|
-
def __apply_config():
|
94
|
-
from .config import config
|
95
|
-
|
96
|
-
global role_preset, werewolf_priority, priesthood_proirity
|
97
|
-
|
98
|
-
if config.role_preset is not None:
|
99
|
-
for preset in config.role_preset:
|
100
|
-
if preset[0] != preset[1:]:
|
101
|
-
raise RuntimeError(
|
102
|
-
"配置项 `role_preset` 错误: "
|
103
|
-
f"预设总人数为 {preset[0]}, 实际总人数为 {sum(preset[1:])}"
|
104
|
-
)
|
105
|
-
role_preset |= {i[0]: i[1:] for i in config.role_preset}
|
106
|
-
|
107
|
-
if (priority := config.werewolf_priority) is not None:
|
108
|
-
min_length = max(i[0] for i in role_preset.values())
|
109
|
-
if len(priority) < min_length:
|
110
|
-
raise RuntimeError(
|
111
|
-
f"配置项 `werewolf_priority` 错误: 应至少为 {min_length} 项"
|
112
|
-
)
|
113
|
-
werewolf_priority = priority
|
114
|
-
|
115
|
-
if (priority := config.priesthood_proirity) is not None:
|
116
|
-
min_length = max(i[1] for i in role_preset.values())
|
117
|
-
if len(priority) < min_length:
|
118
|
-
raise RuntimeError(
|
119
|
-
f"配置项 `priesthood_proirity` 错误: 应至少为 {min_length} 项"
|
120
|
-
)
|
121
|
-
priesthood_proirity = priority
|
122
|
-
|
123
|
-
|
124
|
-
__apply_config()
|
@@ -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
@@ -5,30 +5,29 @@ import asyncio.timeouts
|
|
5
5
|
import contextlib
|
6
6
|
import random
|
7
7
|
import time
|
8
|
+
from typing import TYPE_CHECKING, NoReturn
|
8
9
|
|
9
|
-
from nonebot.adapters import Bot
|
10
10
|
from nonebot.log import logger
|
11
|
-
from nonebot_plugin_alconna import Target, UniMessage
|
12
|
-
|
13
|
-
from .
|
14
|
-
|
15
|
-
|
16
|
-
KillReason,
|
17
|
-
Role,
|
18
|
-
RoleGroup,
|
19
|
-
priesthood_proirity,
|
20
|
-
role_preset,
|
21
|
-
werewolf_priority,
|
22
|
-
)
|
11
|
+
from nonebot_plugin_alconna import At, Target, UniMessage
|
12
|
+
|
13
|
+
from .config import config
|
14
|
+
from .constant import GameState, GameStatus, KillReason, Role, RoleGroup, role_name_conv
|
15
|
+
from .exception import GameFinishedError
|
23
16
|
from .player import Player
|
24
17
|
from .player_set import PlayerSet
|
25
18
|
from .utils import InputStore
|
26
19
|
|
20
|
+
if TYPE_CHECKING:
|
21
|
+
from nonebot.adapters import Bot
|
22
|
+
from nonebot_plugin_alconna.uniseg.message import Receipt
|
23
|
+
|
27
24
|
starting_games: dict[str, dict[str, str]] = {}
|
28
25
|
running_games: dict[str, Game] = {}
|
29
26
|
|
30
27
|
|
31
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()
|
32
31
|
preset = role_preset.get(len(players))
|
33
32
|
if preset is None:
|
34
33
|
raise ValueError(
|
@@ -37,17 +36,24 @@ def init_players(bot: Bot, game: Game, players: dict[str, str]) -> PlayerSet:
|
|
37
36
|
)
|
38
37
|
|
39
38
|
roles: list[Role] = []
|
40
|
-
roles.extend(werewolf_priority[: preset[0]])
|
41
|
-
roles.extend(priesthood_proirity[: preset[1]])
|
39
|
+
roles.extend(config.werewolf_priority[: preset[0]])
|
40
|
+
roles.extend(config.priesthood_proirity[: preset[1]])
|
42
41
|
roles.extend([Role.Civilian] * preset[2])
|
43
42
|
|
44
|
-
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
|
+
|
45
49
|
shuffled: list[Role] = []
|
46
50
|
for _ in range(len(players)):
|
47
51
|
idx = r.randint(0, len(roles) - 1)
|
48
52
|
shuffled.append(roles.pop(idx))
|
49
53
|
|
50
|
-
|
54
|
+
logger.debug(f"职业分配: {shuffled}")
|
55
|
+
|
56
|
+
async def selector(target_: Target, b: Bot) -> bool:
|
51
57
|
return target_.self_id == bot.self_id and b is bot
|
52
58
|
|
53
59
|
return PlayerSet(
|
@@ -63,7 +69,7 @@ def init_players(bot: Bot, game: Game, players: dict[str, str]) -> PlayerSet:
|
|
63
69
|
),
|
64
70
|
players[user_id],
|
65
71
|
)
|
66
|
-
for user_id, role in zip(players, shuffled)
|
72
|
+
for user_id, role in zip(players, shuffled, strict=True)
|
67
73
|
)
|
68
74
|
|
69
75
|
|
@@ -71,24 +77,28 @@ class Game:
|
|
71
77
|
bot: Bot
|
72
78
|
group: Target
|
73
79
|
players: PlayerSet
|
80
|
+
_player_map: dict[str, Player]
|
74
81
|
state: GameState
|
75
82
|
killed_players: list[Player]
|
76
83
|
|
77
|
-
def __init__(
|
78
|
-
self,
|
79
|
-
bot: Bot,
|
80
|
-
group: Target,
|
81
|
-
players: dict[str, str],
|
82
|
-
) -> None:
|
84
|
+
def __init__(self, bot: Bot, group: Target, players: dict[str, str]) -> None:
|
83
85
|
self.bot = bot
|
84
86
|
self.group = group
|
85
87
|
self.players = init_players(bot, self, players)
|
88
|
+
self._player_map = {p.user_id: p for p in self.players}
|
86
89
|
self.state = GameState(0)
|
87
90
|
self.killed_players = []
|
88
91
|
|
89
|
-
async def send(self, message: str | UniMessage):
|
92
|
+
async def send(self, message: str | UniMessage) -> Receipt:
|
90
93
|
if isinstance(message, str):
|
91
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"))
|
92
102
|
return await message.send(self.group, self.bot)
|
93
103
|
|
94
104
|
def at_all(self) -> UniMessage:
|
@@ -102,14 +112,18 @@ class Game:
|
|
102
112
|
w = players.select(RoleGroup.Werewolf)
|
103
113
|
p = players.exclude(RoleGroup.Werewolf)
|
104
114
|
|
115
|
+
# 狼人数量大于其他职业数量
|
105
116
|
if w.size >= p.size:
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
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
|
+
# 狼人全灭
|
111
125
|
if not w.size:
|
112
|
-
|
126
|
+
raise GameFinishedError(GameStatus.GoodGuy)
|
113
127
|
|
114
128
|
return GameStatus.Unset
|
115
129
|
|
@@ -124,7 +138,7 @@ class Game:
|
|
124
138
|
p.name for p in player.kill_info.killers
|
125
139
|
)
|
126
140
|
match player.kill_info.reason:
|
127
|
-
case KillReason.
|
141
|
+
case KillReason.Werewolf:
|
128
142
|
msg += " 刀了"
|
129
143
|
case KillReason.Poison:
|
130
144
|
msg += " 毒死"
|
@@ -137,7 +151,7 @@ class Game:
|
|
137
151
|
return msg.strip()
|
138
152
|
|
139
153
|
async def notify_player_role(self) -> None:
|
140
|
-
preset =
|
154
|
+
preset = config.get_role_preset()[len(self.players)]
|
141
155
|
await asyncio.gather(
|
142
156
|
self.send(
|
143
157
|
self.at_all()
|
@@ -156,7 +170,7 @@ class Game:
|
|
156
170
|
if isinstance(players, Player):
|
157
171
|
players = PlayerSet([players])
|
158
172
|
|
159
|
-
async def wait(p: Player):
|
173
|
+
async def wait(p: Player) -> None:
|
160
174
|
while True:
|
161
175
|
msg = await InputStore.fetch(p.user_id, self.group.id)
|
162
176
|
if msg.extract_plain_text() == "/stop":
|
@@ -176,9 +190,9 @@ class Game:
|
|
176
190
|
type_.role_name # Player
|
177
191
|
if isinstance(type_, Player)
|
178
192
|
else (
|
179
|
-
type_
|
193
|
+
role_name_conv[type_] # Role
|
180
194
|
if isinstance(type_, Role)
|
181
|
-
else f"{type_
|
195
|
+
else f"{role_name_conv[type_]}阵营" # RoleGroup
|
182
196
|
)
|
183
197
|
)
|
184
198
|
|
@@ -186,6 +200,7 @@ class Game:
|
|
186
200
|
try:
|
187
201
|
await players.interact(timeout_secs)
|
188
202
|
except TimeoutError:
|
203
|
+
logger.opt(colors=True).debug(f"{text}交互超时 (<y>{timeout_secs}</y>s)")
|
189
204
|
await players.broadcast(f"{text}交互时间结束")
|
190
205
|
|
191
206
|
async def select_killed(self) -> None:
|
@@ -205,7 +220,7 @@ class Game:
|
|
205
220
|
await self.interact(Role.Witch, 60)
|
206
221
|
# 否则等待 5-20s
|
207
222
|
else:
|
208
|
-
await asyncio.sleep(random.uniform(5, 20))
|
223
|
+
await asyncio.sleep(random.uniform(5, 20)) # noqa: S311
|
209
224
|
|
210
225
|
async def handle_new_dead(self, players: Player | PlayerSet) -> None:
|
211
226
|
if isinstance(players, Player):
|
@@ -257,6 +272,8 @@ class Game:
|
|
257
272
|
# 收集到的总票数
|
258
273
|
total_votes = sum(map(len, vote_result.values()))
|
259
274
|
|
275
|
+
logger.debug(f"投票结果: {vote_result}")
|
276
|
+
|
260
277
|
# 投票结果公示
|
261
278
|
msg = UniMessage.text("投票结果:\n")
|
262
279
|
for p, v in sorted(vote_result.items(), key=lambda x: len(x[1]), reverse=True):
|
@@ -306,23 +323,23 @@ class Game:
|
|
306
323
|
loop = asyncio.get_event_loop()
|
307
324
|
queue: asyncio.Queue[tuple[Player, UniMessage]] = asyncio.Queue()
|
308
325
|
|
309
|
-
async def send():
|
326
|
+
async def send() -> NoReturn:
|
310
327
|
while True:
|
311
328
|
player, msg = await queue.get()
|
312
329
|
msg = f"玩家 {player.name}:\n" + msg
|
313
|
-
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()
|
314
335
|
|
315
|
-
async def recv(player: Player):
|
316
336
|
counter = 0
|
317
337
|
|
318
|
-
def decrease():
|
338
|
+
def decrease() -> None:
|
319
339
|
nonlocal counter
|
320
340
|
counter -= 1
|
321
341
|
|
322
342
|
while True:
|
323
|
-
if not player.killed:
|
324
|
-
await asyncio.sleep(1)
|
325
|
-
continue
|
326
343
|
msg = await player.receive()
|
327
344
|
counter += 1
|
328
345
|
if counter <= 10:
|
@@ -333,13 +350,14 @@ class Game:
|
|
333
350
|
|
334
351
|
await asyncio.gather(send(), *[recv(p) for p in self.players])
|
335
352
|
|
336
|
-
async def run(self) ->
|
353
|
+
async def run(self) -> NoReturn:
|
337
354
|
# 告知玩家角色信息
|
338
355
|
await self.notify_player_role()
|
339
356
|
# 天数记录 主要用于第一晚狼人击杀的遗言
|
340
357
|
day_count = 0
|
341
358
|
|
342
|
-
|
359
|
+
# 游戏主循环
|
360
|
+
while True:
|
343
361
|
# 重置游戏状态,进入下一夜
|
344
362
|
self.state = GameState(day_count)
|
345
363
|
players = self.players.alive()
|
@@ -348,10 +366,12 @@ class Game:
|
|
348
366
|
# 狼人、预言家、守卫 同时交互,女巫在狼人后交互
|
349
367
|
await asyncio.gather(
|
350
368
|
self.select_killed(),
|
351
|
-
players.select(Role.Witch).broadcast("请等待狼人决定目标..."),
|
352
|
-
players.select(Role.Civilian).broadcast("请等待其他玩家结束交互..."),
|
353
369
|
self.interact(Role.Prophet, 60),
|
354
370
|
self.interact(Role.Guard, 60),
|
371
|
+
players.select(Role.Witch).broadcast("请等待狼人决定目标..."),
|
372
|
+
players.select(Role.Civilian, RoleGroup.Others).broadcast(
|
373
|
+
"请等待其他玩家结束交互..."
|
374
|
+
),
|
355
375
|
)
|
356
376
|
|
357
377
|
# 狼人击杀目标
|
@@ -361,13 +381,13 @@ class Game:
|
|
361
381
|
# 女巫的操作目标和内容
|
362
382
|
potioned, (antidote, poison) = self.state.potion
|
363
383
|
|
364
|
-
#
|
365
|
-
if killed is not None
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
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
|
+
)
|
371
391
|
# 如果女巫使用毒药且守卫未保护,杀死该玩家
|
372
392
|
if poison and (potioned is not None) and (potioned is not protected):
|
373
393
|
await potioned.kill(KillReason.Poison, *players.select(Role.Witch))
|
@@ -396,8 +416,7 @@ class Game:
|
|
396
416
|
await self.post_kill(dead)
|
397
417
|
|
398
418
|
# 判断游戏状态
|
399
|
-
|
400
|
-
break
|
419
|
+
self.check_game_status()
|
401
420
|
|
402
421
|
# 公示存活玩家
|
403
422
|
await self.send(f"当前存活玩家: \n\n{self.players.alive().show()}")
|
@@ -410,37 +429,51 @@ class Game:
|
|
410
429
|
await self.send("讨论结束, 进入投票环节,限时1分钟\n请在私聊中进行投票交互")
|
411
430
|
await self.run_vote()
|
412
431
|
|
413
|
-
|
414
|
-
|
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
|
+
|
415
446
|
msg = UniMessage.text(f"🎉游戏结束,{winner}获胜\n\n")
|
416
447
|
for p in sorted(self.players, key=lambda p: (p.role.value, p.user_id)):
|
417
448
|
msg.at(p.user_id).text(f": {p.role_name}\n")
|
418
449
|
await self.send(msg)
|
419
|
-
await self.send(f"
|
450
|
+
await self.send(f"📌玩家死亡报告:\n\n{self.show_killed_players()}")
|
420
451
|
|
421
|
-
def start(self):
|
422
|
-
|
452
|
+
def start(self) -> None:
|
453
|
+
finished = asyncio.Event()
|
423
454
|
game_task = asyncio.create_task(self.run())
|
424
|
-
game_task.add_done_callback(lambda _:
|
455
|
+
game_task.add_done_callback(lambda _: finished.set())
|
425
456
|
dead_channel = asyncio.create_task(self.run_dead_channel())
|
426
457
|
|
427
|
-
async def daemon():
|
428
|
-
await
|
458
|
+
async def daemon() -> None:
|
459
|
+
await finished.wait()
|
429
460
|
|
430
461
|
try:
|
431
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)
|
432
467
|
logger.info(f"{self.group.id} 的狼人杀游戏进程正常退出")
|
433
|
-
except asyncio.CancelledError as err:
|
434
|
-
logger.warning(f"{self.group.id} 的狼人杀游戏进程被取消: {err}")
|
435
468
|
except Exception as err:
|
436
|
-
msg = f"{self.group.id}
|
469
|
+
msg = f"{self.group.id} 的狼人杀游戏进程出现未知错误: {err!r}"
|
437
470
|
logger.opt(exception=err).error(msg)
|
438
471
|
await self.send(msg)
|
439
472
|
finally:
|
440
473
|
dead_channel.cancel()
|
441
474
|
running_games.pop(self.group.id, None)
|
442
475
|
|
443
|
-
def daemon_callback(task: asyncio.Task[None]):
|
476
|
+
def daemon_callback(task: asyncio.Task[None]) -> None:
|
444
477
|
if err := task.exception():
|
445
478
|
logger.opt(exception=err).error(
|
446
479
|
f"{self.group.id} 的狼人杀守护进程出现错误: {err!r}"
|
@@ -9,7 +9,7 @@ from nonebot.rule import to_me
|
|
9
9
|
from nonebot_plugin_alconna import MsgTarget, UniMessage, UniMsg
|
10
10
|
from nonebot_plugin_userinfo import EventUserInfo, UserInfo
|
11
11
|
|
12
|
-
from .game import Game
|
12
|
+
from .game import Game
|
13
13
|
from .ob11_ext import ob11_ext_enabled
|
14
14
|
from .utils import InputStore, is_group, prepare_game, rule_in_game, rule_not_in_game
|
15
15
|
|
@@ -36,9 +36,6 @@ async def handle_start(
|
|
36
36
|
target: MsgTarget,
|
37
37
|
admin_info: Annotated[UserInfo, EventUserInfo()],
|
38
38
|
) -> None:
|
39
|
-
if target.id in running_games:
|
40
|
-
await UniMessage.text("当前群聊内有正在进行的游戏,无法创建游戏").finish()
|
41
|
-
|
42
39
|
admin_id = event.get_user_id()
|
43
40
|
msg = (
|
44
41
|
UniMessage.at(admin_id)
|
@@ -52,7 +49,7 @@ async def handle_start(
|
|
52
49
|
msg.text("\n可使用戳一戳代替游戏交互中的 “/stop” 命令")
|
53
50
|
await msg.text("\n\n游戏准备阶段限时5分钟,超时将自动结束").send()
|
54
51
|
|
55
|
-
players =
|
52
|
+
players = {admin_id: admin_info.user_name}
|
56
53
|
|
57
54
|
try:
|
58
55
|
async with asyncio.timeouts.timeout(5 * 60):
|
@@ -61,8 +58,6 @@ async def handle_start(
|
|
61
58
|
raise
|
62
59
|
except TimeoutError:
|
63
60
|
await UniMessage.text("游戏准备超时,已自动结束").finish()
|
64
|
-
finally:
|
65
|
-
del starting_games[target.id]
|
66
61
|
|
67
|
-
game = Game(bot
|
62
|
+
game = Game(bot, target, players)
|
68
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:
|
@@ -4,15 +4,20 @@ import asyncio
|
|
4
4
|
import asyncio.timeouts
|
5
5
|
from dataclasses import dataclass
|
6
6
|
from typing import TYPE_CHECKING, ClassVar, TypeVar, final
|
7
|
-
from typing_extensions import override
|
8
7
|
|
9
|
-
from nonebot.
|
8
|
+
from nonebot.log import logger
|
10
9
|
from nonebot_plugin_alconna.uniseg import Receipt, Target, UniMessage
|
10
|
+
from typing_extensions import override
|
11
11
|
|
12
|
-
from .constant import KillReason, Role, RoleGroup, role_name_conv
|
12
|
+
from .constant import GameStatus, KillReason, Role, RoleGroup, role_name_conv
|
13
|
+
from .exception import GameFinishedError
|
13
14
|
from .utils import InputStore, check_index
|
14
15
|
|
15
16
|
if TYPE_CHECKING:
|
17
|
+
from collections.abc import Callable
|
18
|
+
|
19
|
+
from nonebot.adapters import Bot
|
20
|
+
|
16
21
|
from .game import Game
|
17
22
|
from .player_set import PlayerSet
|
18
23
|
|
@@ -21,7 +26,7 @@ P = TypeVar("P", bound=type["Player"])
|
|
21
26
|
PLAYER_CLASS: dict[Role, type[Player]] = {}
|
22
27
|
|
23
28
|
|
24
|
-
def register_role(role: Role, role_group: RoleGroup, /):
|
29
|
+
def register_role(role: Role, role_group: RoleGroup, /) -> Callable[[P], P]:
|
25
30
|
def decorator(cls: P, /) -> P:
|
26
31
|
cls.role = role
|
27
32
|
cls.role_group = role_group
|
@@ -46,7 +51,7 @@ class Player:
|
|
46
51
|
user: Target
|
47
52
|
name: str
|
48
53
|
alive: bool = True
|
49
|
-
killed:
|
54
|
+
killed: asyncio.Event
|
50
55
|
kill_info: KillInfo | None = None
|
51
56
|
selected: Player | None = None
|
52
57
|
|
@@ -56,6 +61,7 @@ class Player:
|
|
56
61
|
self.game = game
|
57
62
|
self.user = user
|
58
63
|
self.name = name
|
64
|
+
self.killed = asyncio.Event()
|
59
65
|
|
60
66
|
@final
|
61
67
|
@classmethod
|
@@ -73,7 +79,7 @@ class Player:
|
|
73
79
|
return PLAYER_CLASS[role](bot, game, user, name)
|
74
80
|
|
75
81
|
def __repr__(self) -> str:
|
76
|
-
return f"<{self.role_name}: user={self.
|
82
|
+
return f"<{self.role_name}: user={self.name!r} alive={self.alive}>"
|
77
83
|
|
78
84
|
@property
|
79
85
|
def user_id(self) -> str:
|
@@ -83,18 +89,32 @@ class Player:
|
|
83
89
|
def role_name(self) -> str:
|
84
90
|
return role_name_conv[self.role]
|
85
91
|
|
92
|
+
@final
|
93
|
+
def _log(self, text: str) -> None:
|
94
|
+
text = text.replace("\n", "\\n")
|
95
|
+
logger.opt(colors=True).info(
|
96
|
+
f"<b><e>{self.game.group.id}</e></b> | "
|
97
|
+
f"[<b><m>{self.role_name}</m></b>] "
|
98
|
+
f"<y>{self.name}</y>(<e>{self.user_id}</e>) | "
|
99
|
+
f"{text}",
|
100
|
+
)
|
101
|
+
|
86
102
|
@final
|
87
103
|
async def send(self, message: str | UniMessage) -> Receipt:
|
88
104
|
if isinstance(message, str):
|
89
105
|
message = UniMessage.text(message)
|
90
106
|
|
107
|
+
self._log(f"<g>Send</g> | {message}")
|
91
108
|
return await message.send(target=self.user, bot=self.bot)
|
92
109
|
|
93
110
|
@final
|
94
111
|
async def receive(self, prompt: str | UniMessage | None = None) -> UniMessage:
|
95
112
|
if prompt:
|
96
113
|
await self.send(prompt)
|
97
|
-
|
114
|
+
|
115
|
+
result = await InputStore.fetch(self.user.id)
|
116
|
+
self._log(f"<y>Recv</y> | {result}")
|
117
|
+
return result
|
98
118
|
|
99
119
|
@final
|
100
120
|
async def receive_text(self) -> str:
|
@@ -106,11 +126,7 @@ class Player:
|
|
106
126
|
async def notify_role(self) -> None:
|
107
127
|
await self.send(f"你的身份: {self.role_name}")
|
108
128
|
|
109
|
-
async def kill(
|
110
|
-
self,
|
111
|
-
reason: KillReason,
|
112
|
-
*killers: Player,
|
113
|
-
) -> bool:
|
129
|
+
async def kill(self, reason: KillReason, *killers: Player) -> bool:
|
114
130
|
from .player_set import PlayerSet
|
115
131
|
|
116
132
|
self.alive = False
|
@@ -118,7 +134,7 @@ class Player:
|
|
118
134
|
return True
|
119
135
|
|
120
136
|
async def post_kill(self) -> None:
|
121
|
-
self.killed
|
137
|
+
self.killed.set()
|
122
138
|
|
123
139
|
async def vote(self, players: PlayerSet) -> tuple[Player, Player] | None:
|
124
140
|
await self.send(
|
@@ -184,7 +200,7 @@ class CanShoot(Player):
|
|
184
200
|
text = await self.receive_text()
|
185
201
|
if text == "/stop":
|
186
202
|
await self.send("已取消技能")
|
187
|
-
return
|
203
|
+
return None
|
188
204
|
index = check_index(text, len(players))
|
189
205
|
if index is not None:
|
190
206
|
selected = index - 1
|
@@ -213,7 +229,7 @@ class Werewolf(Player):
|
|
213
229
|
partners = players.select(RoleGroup.Werewolf).exclude(self)
|
214
230
|
|
215
231
|
# 避免阻塞
|
216
|
-
def broadcast(msg: str | UniMessage):
|
232
|
+
def broadcast(msg: str | UniMessage) -> asyncio.Task[None]:
|
217
233
|
return asyncio.create_task(partners.broadcast(msg))
|
218
234
|
|
219
235
|
msg = UniMessage()
|
@@ -234,8 +250,8 @@ class Werewolf(Player):
|
|
234
250
|
selected = None
|
235
251
|
finished = False
|
236
252
|
while selected is None or not finished:
|
237
|
-
|
238
|
-
text =
|
253
|
+
input_msg = await self.receive()
|
254
|
+
text = input_msg.extract_plain_text()
|
239
255
|
index = check_index(text, len(players))
|
240
256
|
if index is not None:
|
241
257
|
selected = index - 1
|
@@ -249,14 +265,17 @@ class Werewolf(Player):
|
|
249
265
|
broadcast(f"队友 {self.name} 结束当前回合")
|
250
266
|
else:
|
251
267
|
await self.send("当前未选择玩家,无法结束回合")
|
252
|
-
broadcast(UniMessage.text(f"队友 {self.name}:\n") +
|
268
|
+
broadcast(UniMessage.text(f"队友 {self.name}:\n") + input_msg)
|
253
269
|
|
254
270
|
self.selected = players[selected]
|
255
271
|
|
256
272
|
|
257
273
|
@register_role(Role.WolfKing, RoleGroup.Werewolf)
|
258
274
|
class WolfKing(CanShoot, Werewolf):
|
259
|
-
|
275
|
+
@override
|
276
|
+
async def notify_role(self) -> None:
|
277
|
+
await super().notify_role()
|
278
|
+
await self.send("作为狼王,你可以在死后射杀一名玩家")
|
260
279
|
|
261
280
|
|
262
281
|
@register_role(Role.Prophet, RoleGroup.GoodGuy)
|
@@ -279,7 +298,7 @@ class Prophet(Player):
|
|
279
298
|
await self.send("输入错误,请发送编号选择玩家")
|
280
299
|
|
281
300
|
player = players[selected]
|
282
|
-
result =
|
301
|
+
result = "狼人" if player.role_group == RoleGroup.Werewolf else "好人"
|
283
302
|
await self.send(f"玩家 {player.name} 的阵营是『{result}』")
|
284
303
|
|
285
304
|
|
@@ -293,7 +312,7 @@ class Witch(Player):
|
|
293
312
|
*,
|
294
313
|
antidote: Player | None = None,
|
295
314
|
posion: Player | None = None,
|
296
|
-
):
|
315
|
+
) -> None:
|
297
316
|
if antidote is not None:
|
298
317
|
self.antidote = 0
|
299
318
|
self.selected = antidote
|
@@ -327,10 +346,9 @@ class Witch(Player):
|
|
327
346
|
self.set_state(antidote=killed)
|
328
347
|
await self.send(f"你对 {killed.name} 使用了解药,回合结束")
|
329
348
|
return True
|
330
|
-
|
349
|
+
if text == "/stop":
|
331
350
|
return False
|
332
|
-
|
333
|
-
await self.send("输入错误: 请输入 “1” 或 “/stop”")
|
351
|
+
await self.send("输入错误: 请输入 “1” 或 “/stop”")
|
334
352
|
|
335
353
|
@override
|
336
354
|
async def interact(self) -> None:
|
@@ -357,12 +375,11 @@ class Witch(Player):
|
|
357
375
|
if index is not None:
|
358
376
|
selected = index - 1
|
359
377
|
break
|
360
|
-
|
378
|
+
if text == "/stop":
|
361
379
|
await self.send("你选择不使用毒药,回合结束")
|
362
380
|
self.set_state()
|
363
381
|
return
|
364
|
-
|
365
|
-
await self.send("输入错误: 请发送玩家编号或 “/stop”")
|
382
|
+
await self.send("输入错误: 请发送玩家编号或 “/stop”")
|
366
383
|
|
367
384
|
self.poison = 0
|
368
385
|
self.selected = player = players[selected]
|
@@ -379,9 +396,10 @@ class Hunter(CanShoot, Player):
|
|
379
396
|
class Guard(Player):
|
380
397
|
@override
|
381
398
|
async def interact(self) -> None:
|
382
|
-
players = self.game.players.alive()
|
399
|
+
players = self.game.players.alive()
|
383
400
|
await self.send(
|
384
|
-
UniMessage.text(
|
401
|
+
UniMessage.text("请选择需要保护的玩家:\n")
|
402
|
+
.text(players.show())
|
385
403
|
.text("\n\n发送编号选择玩家")
|
386
404
|
.text("\n发送 “/stop” 结束回合")
|
387
405
|
)
|
@@ -409,17 +427,20 @@ class Idiot(Player):
|
|
409
427
|
voted: bool = False
|
410
428
|
|
411
429
|
@override
|
412
|
-
async def
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
430
|
+
async def notify_role(self) -> None:
|
431
|
+
await super().notify_role()
|
432
|
+
await self.send(
|
433
|
+
"作为白痴,你可以在首次被投票放逐时免疫放逐,但在之后的投票中无法继续投票"
|
434
|
+
)
|
435
|
+
|
436
|
+
@override
|
437
|
+
async def kill(self, reason: KillReason, *killers: Player) -> bool:
|
417
438
|
if reason == KillReason.Vote and not self.voted:
|
418
439
|
self.voted = True
|
419
440
|
await self.game.send(
|
420
441
|
UniMessage.at(self.user_id)
|
421
442
|
.text(" 的身份是白痴\n")
|
422
|
-
.text("免疫本次投票放逐,且接下来无法参与投票")
|
443
|
+
.text("免疫本次投票放逐,且接下来无法参与投票"),
|
423
444
|
)
|
424
445
|
return False
|
425
446
|
return await super().kill(reason, *killers)
|
@@ -432,6 +453,22 @@ class Idiot(Player):
|
|
432
453
|
return await super().vote(players)
|
433
454
|
|
434
455
|
|
456
|
+
@register_role(Role.Joker, RoleGroup.Others)
|
457
|
+
class Joker(Player):
|
458
|
+
@override
|
459
|
+
async def notify_role(self) -> None:
|
460
|
+
await super().notify_role()
|
461
|
+
await self.send("你的胜利条件: 被投票放逐")
|
462
|
+
|
463
|
+
@override
|
464
|
+
async def kill(self, reason: KillReason, *killers: Player) -> bool:
|
465
|
+
result = await super().kill(reason, *killers)
|
466
|
+
if reason == KillReason.Vote:
|
467
|
+
self.game.killed_players.append(self)
|
468
|
+
raise GameFinishedError(GameStatus.Joker)
|
469
|
+
return result
|
470
|
+
|
471
|
+
|
435
472
|
@register_role(Role.Civilian, RoleGroup.GoodGuy)
|
436
473
|
class Civilian(Player):
|
437
474
|
pass
|
@@ -2,12 +2,15 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import asyncio
|
4
4
|
import asyncio.timeouts
|
5
|
+
from typing import TYPE_CHECKING
|
5
6
|
|
6
|
-
from nonebot_plugin_alconna.uniseg import UniMessage
|
7
|
-
|
8
|
-
from .constant import Role, RoleGroup
|
9
7
|
from .player import Player
|
10
8
|
|
9
|
+
if TYPE_CHECKING:
|
10
|
+
from nonebot_plugin_alconna.uniseg import UniMessage
|
11
|
+
|
12
|
+
from .constant import Role, RoleGroup
|
13
|
+
|
11
14
|
|
12
15
|
class PlayerSet(set[Player]):
|
13
16
|
@property
|
@@ -20,6 +23,9 @@ class PlayerSet(set[Player]):
|
|
20
23
|
def dead(self) -> PlayerSet:
|
21
24
|
return PlayerSet(p for p in self if not p.alive)
|
22
25
|
|
26
|
+
def killed(self) -> PlayerSet:
|
27
|
+
return PlayerSet(p for p in self if p.killed.is_set())
|
28
|
+
|
23
29
|
def include(self, *types: Player | Role | RoleGroup) -> PlayerSet:
|
24
30
|
return PlayerSet(
|
25
31
|
player
|
nonebot_plugin_werewolf/utils.py
CHANGED
@@ -6,11 +6,12 @@ from typing import Annotated, Any, ClassVar
|
|
6
6
|
|
7
7
|
import nonebot_plugin_waiter as waiter
|
8
8
|
from nonebot.adapters import Event
|
9
|
+
from nonebot.log import logger
|
9
10
|
from nonebot.rule import to_me
|
10
11
|
from nonebot_plugin_alconna import MsgTarget, UniMessage, UniMsg
|
11
12
|
from nonebot_plugin_userinfo import EventUserInfo, UserInfo
|
12
13
|
|
13
|
-
from .
|
14
|
+
from .config import config
|
14
15
|
|
15
16
|
|
16
17
|
def check_index(text: str, arrlen: int) -> int | None:
|
@@ -60,7 +61,7 @@ async def rule_in_game(event: Event, target: MsgTarget) -> bool:
|
|
60
61
|
return False
|
61
62
|
if target.private:
|
62
63
|
return user_in_game(target.id, None)
|
63
|
-
|
64
|
+
if target.id in running_games:
|
64
65
|
return user_in_game(event.get_user_id(), target.id)
|
65
66
|
return False
|
66
67
|
|
@@ -104,8 +105,7 @@ async def _prepare_game_receive(
|
|
104
105
|
async for user, name, text in wait(default=(None, "", "")):
|
105
106
|
if user is None:
|
106
107
|
continue
|
107
|
-
|
108
|
-
await queue.put((user, name, text))
|
108
|
+
await queue.put((user, re.sub(r"[\u2066-\u2069]", "", name), text))
|
109
109
|
|
110
110
|
|
111
111
|
async def _prepare_game_handle(
|
@@ -113,13 +113,17 @@ async def _prepare_game_handle(
|
|
113
113
|
players: dict[str, str],
|
114
114
|
admin_id: str,
|
115
115
|
) -> None:
|
116
|
+
log = logger.opt(colors=True)
|
117
|
+
|
116
118
|
while True:
|
117
119
|
user, name, text = await queue.get()
|
118
120
|
msg = UniMessage.at(user)
|
121
|
+
colored = f"<y>{name}</y>(<c>{user}</c>)"
|
119
122
|
|
120
123
|
match (text, user == admin_id):
|
121
124
|
case ("开始游戏", True):
|
122
125
|
player_num = len(players)
|
126
|
+
role_preset = config.get_role_preset()
|
123
127
|
if player_num < min(role_preset):
|
124
128
|
await (
|
125
129
|
msg.text(f"游戏至少需要 {min(role_preset)} 人, ")
|
@@ -140,12 +144,14 @@ async def _prepare_game_handle(
|
|
140
144
|
)
|
141
145
|
else:
|
142
146
|
await msg.text("游戏即将开始...").send()
|
147
|
+
log.info(f"游戏发起者 {colored} 开始游戏")
|
143
148
|
return
|
144
149
|
|
145
150
|
case ("开始游戏", False):
|
146
151
|
await msg.text("只有游戏发起者可以开始游戏").send()
|
147
152
|
|
148
153
|
case ("结束游戏", True):
|
154
|
+
log.info(f"游戏发起者 {colored} 结束游戏")
|
149
155
|
await msg.text("已结束当前游戏").finish()
|
150
156
|
|
151
157
|
case ("结束游戏", False):
|
@@ -157,6 +163,7 @@ async def _prepare_game_handle(
|
|
157
163
|
case ("加入游戏", False):
|
158
164
|
if user not in players:
|
159
165
|
players[user] = name
|
166
|
+
log.info(f"玩家 {colored} 加入游戏")
|
160
167
|
await msg.text("成功加入游戏").send()
|
161
168
|
else:
|
162
169
|
await msg.text("你已经加入游戏了").send()
|
@@ -167,6 +174,7 @@ async def _prepare_game_handle(
|
|
167
174
|
case ("退出游戏", False):
|
168
175
|
if user in players:
|
169
176
|
del players[user]
|
177
|
+
log.info(f"玩家 {colored} 退出游戏")
|
170
178
|
await msg.text("成功退出游戏").send()
|
171
179
|
else:
|
172
180
|
await msg.text("你还没有加入游戏").send()
|
@@ -179,20 +187,16 @@ async def _prepare_game_handle(
|
|
179
187
|
|
180
188
|
|
181
189
|
async def prepare_game(event: Event, players: dict[str, str]) -> None:
|
190
|
+
from .game import starting_games
|
191
|
+
|
192
|
+
group_id = UniMessage.get_target(event).id
|
193
|
+
starting_games[group_id] = players
|
194
|
+
|
182
195
|
queue: asyncio.Queue[tuple[str, str, str]] = asyncio.Queue()
|
183
|
-
task_receive = asyncio.create_task(
|
184
|
-
_prepare_game_receive(
|
185
|
-
queue,
|
186
|
-
event,
|
187
|
-
UniMessage.get_target(event).id,
|
188
|
-
)
|
189
|
-
)
|
196
|
+
task_receive = asyncio.create_task(_prepare_game_receive(queue, event, group_id))
|
190
197
|
|
191
198
|
try:
|
192
|
-
await _prepare_game_handle(
|
193
|
-
queue,
|
194
|
-
players,
|
195
|
-
event.get_user_id(),
|
196
|
-
)
|
199
|
+
await _prepare_game_handle(queue, players, event.get_user_id())
|
197
200
|
finally:
|
198
201
|
task_receive.cancel()
|
202
|
+
del starting_games[group_id]
|
{nonebot_plugin_werewolf-1.0.7.dist-info → nonebot_plugin_werewolf-1.1.0.dist-info}/METADATA
RENAMED
@@ -1,15 +1,16 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: nonebot-plugin-werewolf
|
3
|
-
Version: 1.0
|
4
|
-
Summary:
|
5
|
-
Author-
|
3
|
+
Version: 1.1.0
|
4
|
+
Summary: 适用于 Nonebot2 的狼人杀插件
|
5
|
+
Author-email: wyf7685 <wyf7685@163.com>
|
6
6
|
License: MIT
|
7
7
|
Requires-Python: >=3.10
|
8
|
-
Requires-Dist: nonebot2>=2.3.3
|
9
|
-
Requires-Dist: nonebot-plugin-alconna>=0.52.1
|
10
|
-
Requires-Dist: nonebot-plugin-userinfo>=0.2.6
|
11
|
-
Requires-Dist: nonebot-plugin-waiter>=0.7.1
|
12
8
|
Description-Content-Type: text/markdown
|
9
|
+
License-File: LICENSE
|
10
|
+
Requires-Dist: nonebot2 >=2.3.3
|
11
|
+
Requires-Dist: nonebot-plugin-alconna >=0.52.1
|
12
|
+
Requires-Dist: nonebot-plugin-userinfo >=0.2.6
|
13
|
+
Requires-Dist: nonebot-plugin-waiter >=0.7.1
|
13
14
|
|
14
15
|
<div align="center">
|
15
16
|
<a href="https://v2.nonebot.dev/store">
|
@@ -27,12 +28,15 @@ _✨ 简单的狼人杀插件 ✨_
|
|
27
28
|
[](https://pypi.python.org/pypi/nonebot-plugin-werewolf)
|
28
29
|
[](https://www.python.org/)
|
29
30
|
|
30
|
-
[](https://github.com/astral-sh/uv)
|
31
32
|
[](https://pycqa.github.io/isort/)
|
32
33
|
[](https://github.com/psf/black)
|
33
34
|
[](https://github.com/Microsoft/pyright)
|
34
35
|
[](https://github.com/astral-sh/ruff)
|
35
36
|
|
37
|
+
[](https://registry.nonebot.dev/plugin/nonebot-plugin-werewolf:nonebot_plugin_werewolf)
|
38
|
+
[](https://registry.nonebot.dev/plugin/nonebot-plugin-werewolf:nonebot_plugin_werewolf)
|
39
|
+
|
36
40
|
</div>
|
37
41
|
|
38
42
|
## 📖 介绍
|
@@ -86,7 +90,7 @@ _✨ 简单的狼人杀插件 ✨_
|
|
86
90
|
|
87
91
|
## ⚙️ 配置
|
88
92
|
|
89
|
-
在 nonebot2
|
93
|
+
在 nonebot2 项目的 `.env` 文件中添加如下配置
|
90
94
|
|
91
95
|
| 配置项 | 必填 | 默认值 | 说明 |
|
92
96
|
| :-----------------------------: | :--: | :----: | :-----------------------------------------------------------: |
|
@@ -94,6 +98,7 @@ _✨ 简单的狼人杀插件 ✨_
|
|
94
98
|
| `werewolf__role_preset` | 否 | - | 覆写插件内置的职业预设 |
|
95
99
|
| `werewolf__werewolf_priority` | 否 | - | 自定义狼人职业优先级 |
|
96
100
|
| `werewolf__priesthood_proirity` | 否 | - | 自定义神职职业优先级 |
|
101
|
+
| `werewolf__joker_probability` | 否 | `0.0` | 小丑职业替换平民的概率, 范围`[0,1]` |
|
97
102
|
|
98
103
|
`werewolf__role_preset`, `werewolf__werewolf_priority`, `werewolf__priesthood_proirity` 的配置格式请参考 [`游戏内容`](#游戏内容) 部分
|
99
104
|
|
@@ -192,7 +197,7 @@ werewolf__werewolf_priority=[1, 2, 1, 1]
|
|
192
197
|
|
193
198
|
上述配置中,`[1, 2, 1, 1]` 表示狼人的职业优先级为 `狼人`, `狼王`, `狼人`, `狼人`
|
194
199
|
|
195
|
-
#### 配置项 `
|
200
|
+
#### 配置项 `werewolf__priesthood_proirity`
|
196
201
|
|
197
202
|
```env
|
198
203
|
werewolf__priesthood_proirity=[11, 12, 13, 14, 15]
|
@@ -226,6 +231,13 @@ werewolf__priesthood_proirity=[11, 12, 13, 14, 15]
|
|
226
231
|
|
227
232
|
<!-- CHANGELOG -->
|
228
233
|
|
234
|
+
- 2024.09.09 v1.1.0
|
235
|
+
|
236
|
+
- 新增职业 `小丑`
|
237
|
+
- 修复守卫无法保护自己的 bug
|
238
|
+
- 添加部分特殊职业的说明
|
239
|
+
- 添加游戏过程中的日志输出
|
240
|
+
|
229
241
|
- 2024.09.04 v1.0.7
|
230
242
|
|
231
243
|
- 优先使用群名片作为玩家名
|
@@ -0,0 +1,15 @@
|
|
1
|
+
nonebot_plugin_werewolf/__init__.py,sha256=A2138SSGhd2FdZ8V9Op50k5-pdfIVSfT0xBNG6qWacM,734
|
2
|
+
nonebot_plugin_werewolf/config.py,sha256=8HnSt7sk8WIwnQlHm0tLdiSk6OTwbysDwoQf8cvFgoE,2929
|
3
|
+
nonebot_plugin_werewolf/constant.py,sha256=yrhyw67KTaZ7DoGWZl3USWheaHSsUI94kl6ChDs0phI,1829
|
4
|
+
nonebot_plugin_werewolf/exception.py,sha256=Ui8rB1sBg6_p8JHSvrjCpUaXJlgrM_9XsLJ09k8F1bg,391
|
5
|
+
nonebot_plugin_werewolf/game.py,sha256=t1Gq-adeD5xprDH4uxfGF70wV_N-YZKBXn2CrMEH5Sc,17761
|
6
|
+
nonebot_plugin_werewolf/matchers.py,sha256=5FioDfARlxTEibGL1JMKUMzmTzOwljjWKAJxBnyzaYs,2106
|
7
|
+
nonebot_plugin_werewolf/ob11_ext.py,sha256=P8uc3AdN5K5MzJaK80WDK85VKFg_CK5avDHu7ueMkho,2418
|
8
|
+
nonebot_plugin_werewolf/player.py,sha256=IdkVKt31DMw6W_0idW04c-ix6JVtg7VPl7ZaegxUN0Y,15360
|
9
|
+
nonebot_plugin_werewolf/player_set.py,sha256=AI8v6KjH9ACtP4lvrd5IJfayan0qALmpTRLC3s_3DFw,2754
|
10
|
+
nonebot_plugin_werewolf/utils.py,sha256=l7oNSK471SV5CaB1eEVyZm10XSBFf_TwNotvb30U6vE,6835
|
11
|
+
nonebot_plugin_werewolf-1.1.0.dist-info/LICENSE,sha256=B_WbEqjGr6GYVNfEJPY31T1Opik7OtgOkhRs4Ig3e2M,1064
|
12
|
+
nonebot_plugin_werewolf-1.1.0.dist-info/METADATA,sha256=6hjSQYd7yYi9UgZ4OLqDUDwHhL6imH21MMDEu9hS6AA,9234
|
13
|
+
nonebot_plugin_werewolf-1.1.0.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
|
14
|
+
nonebot_plugin_werewolf-1.1.0.dist-info/top_level.txt,sha256=wLTfg8sTKbH9lLT9LtU118C9cTspEBJareLsrYM52YA,24
|
15
|
+
nonebot_plugin_werewolf-1.1.0.dist-info/RECORD,,
|
@@ -0,0 +1 @@
|
|
1
|
+
nonebot_plugin_werewolf
|
@@ -1,13 +0,0 @@
|
|
1
|
-
nonebot_plugin_werewolf-1.0.7.dist-info/METADATA,sha256=6Zjo-smpLFf6SX6TLAefU4OFCamMLTZmIKsI5QgzXOs,8479
|
2
|
-
nonebot_plugin_werewolf-1.0.7.dist-info/WHEEL,sha256=rSwsxJWe3vzyR5HCwjWXQruDgschpei4h_giTm0dJVE,90
|
3
|
-
nonebot_plugin_werewolf-1.0.7.dist-info/licenses/LICENSE,sha256=B_WbEqjGr6GYVNfEJPY31T1Opik7OtgOkhRs4Ig3e2M,1064
|
4
|
-
nonebot_plugin_werewolf/__init__.py,sha256=sO1RtjlQkMIdgMEhS8bVwhJpCo8dd36Z_Fc5y-YpEoY,734
|
5
|
-
nonebot_plugin_werewolf/config.py,sha256=VSXAWZa3u6_ehXzS0q_qYpPYr5GRqHzs-WR0zHEJiRI,437
|
6
|
-
nonebot_plugin_werewolf/constant.py,sha256=w-KWhLTq1_uAVy0ALER-yCn0J1r5Wr07xYvSYyHNHsw,2859
|
7
|
-
nonebot_plugin_werewolf/game.py,sha256=ZBOM_yK8Dux0nu6om7_PruH4Kr82-JVOPfpD5JcOFdA,15909
|
8
|
-
nonebot_plugin_werewolf/matchers.py,sha256=nRnXYNjwM_Pp-Z1tBoMwd2TrxCPCB5du_qMHbXlZw3w,2373
|
9
|
-
nonebot_plugin_werewolf/ob11_ext.py,sha256=I6bPCv5SgAStTJuvBl5F7wqgiksWeFkb4R7n06jXprA,2399
|
10
|
-
nonebot_plugin_werewolf/player.py,sha256=sAT8qI-zGLgjQJqWwwq-ow0vaWFdFlpMP0sWEOj-ujg,13817
|
11
|
-
nonebot_plugin_werewolf/player_set.py,sha256=WXmJtL3jbfkFCcKbzLCLu70elhBJE5DJwPA3ENNoKJM,2595
|
12
|
-
nonebot_plugin_werewolf/utils.py,sha256=EhrlYgDLyNZbpRCGDgpcUnpp8RWJBiZ6l8rDdC5hHvM,6396
|
13
|
-
nonebot_plugin_werewolf-1.0.7.dist-info/RECORD,,
|
{nonebot_plugin_werewolf-1.0.7.dist-info/licenses → nonebot_plugin_werewolf-1.1.0.dist-info}/LICENSE
RENAMED
File without changes
|