nonebot-plugin-werewolf 1.0.7__py3-none-any.whl → 1.1.1__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 +74 -7
- nonebot_plugin_werewolf/constant.py +18 -45
- nonebot_plugin_werewolf/exception.py +19 -0
- nonebot_plugin_werewolf/game.py +113 -86
- nonebot_plugin_werewolf/matchers.py +3 -8
- nonebot_plugin_werewolf/ob11_ext.py +1 -1
- nonebot_plugin_werewolf/player.py +76 -58
- 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.1.dist-info}/METADATA +40 -14
- nonebot_plugin_werewolf-1.1.1.dist-info/RECORD +15 -0
- {nonebot_plugin_werewolf-1.0.7.dist-info → nonebot_plugin_werewolf-1.1.1.dist-info}/WHEEL +2 -1
- nonebot_plugin_werewolf-1.1.1.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.1.dist-info}/LICENSE +0 -0
@@ -1,14 +1,80 @@
|
|
1
|
-
from
|
2
|
-
from pydantic import BaseModel
|
1
|
+
from typing import Literal, Self, 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
|
+
|
7
|
+
from .constant import (
|
8
|
+
Role,
|
9
|
+
default_priesthood_proirity,
|
10
|
+
default_role_preset,
|
11
|
+
default_werewolf_priority,
|
12
|
+
)
|
13
|
+
|
14
|
+
if PYDANTIC_V2:
|
15
|
+
from pydantic import model_validator as model_validator
|
16
|
+
else:
|
17
|
+
from pydantic import root_validator
|
18
|
+
|
19
|
+
@overload
|
20
|
+
def model_validator(*, mode: Literal["before"]): ... # noqa: ANN201
|
21
|
+
|
22
|
+
@overload
|
23
|
+
def model_validator(*, mode: Literal["after"]): ... # noqa: ANN201
|
24
|
+
|
25
|
+
def model_validator(*, mode: Literal["before", "after"]):
|
26
|
+
return root_validator(
|
27
|
+
pre=mode == "before", # pyright: ignore[reportArgumentType]
|
28
|
+
allow_reuse=True,
|
29
|
+
)
|
5
30
|
|
6
31
|
|
7
32
|
class PluginConfig(BaseModel):
|
8
|
-
enable_poke: bool = True
|
9
|
-
role_preset: list[tuple[int, int, int, int]] |
|
10
|
-
|
11
|
-
|
33
|
+
enable_poke: bool = Field(default=True)
|
34
|
+
role_preset: list[tuple[int, int, int, int]] | dict[int, tuple[int, int, int]] = (
|
35
|
+
Field(default_factory=default_role_preset.copy)
|
36
|
+
)
|
37
|
+
werewolf_priority: list[Role] = Field(
|
38
|
+
default_factory=default_werewolf_priority.copy
|
39
|
+
)
|
40
|
+
priesthood_proirity: list[Role] = Field(
|
41
|
+
default_factory=default_priesthood_proirity.copy
|
42
|
+
)
|
43
|
+
joker_probability: float = Field(default=0.0, ge=0.0, le=1.0)
|
44
|
+
|
45
|
+
@model_validator(mode="after")
|
46
|
+
def _validate(self) -> Self:
|
47
|
+
if isinstance(self.role_preset, list):
|
48
|
+
for preset in self.role_preset:
|
49
|
+
if preset[0] != sum(preset[1:]):
|
50
|
+
raise RuntimeError(
|
51
|
+
"配置项 `role_preset` 错误: "
|
52
|
+
f"预设总人数为 {preset[0]}, 实际总人数为 {sum(preset[1:])} "
|
53
|
+
f"({', '.join(map(str, preset[1:]))})"
|
54
|
+
)
|
55
|
+
self.role_preset = default_role_preset | {
|
56
|
+
i[0]: i[1:] for i in self.role_preset
|
57
|
+
}
|
58
|
+
logger.debug(f"覆写配置 role_preset: {self.role_preset}")
|
59
|
+
|
60
|
+
min_length = max(i[0] for i in self.role_preset.values())
|
61
|
+
if len(self.werewolf_priority) < min_length:
|
62
|
+
raise RuntimeError(
|
63
|
+
f"配置项 `werewolf_priority` 错误: 应至少为 {min_length} 项"
|
64
|
+
)
|
65
|
+
|
66
|
+
min_length = max(i[1] for i in self.role_preset.values())
|
67
|
+
if len(self.priesthood_proirity) < min_length:
|
68
|
+
raise RuntimeError(
|
69
|
+
f"配置项 `priesthood_proirity` 错误: 应至少为 {min_length} 项"
|
70
|
+
)
|
71
|
+
|
72
|
+
return self
|
73
|
+
|
74
|
+
def get_role_preset(self) -> dict[int, tuple[int, int, int]]:
|
75
|
+
if isinstance(self.role_preset, list):
|
76
|
+
self.role_preset = {i[0]: i[1:] for i in self.role_preset}
|
77
|
+
return self.role_preset
|
12
78
|
|
13
79
|
|
14
80
|
class Config(BaseModel):
|
@@ -16,3 +82,4 @@ class Config(BaseModel):
|
|
16
82
|
|
17
83
|
|
18
84
|
config = get_plugin_config(Config).werewolf
|
85
|
+
logger.debug(f"加载插件配置: {config}")
|
@@ -1,6 +1,6 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
|
3
|
+
import dataclasses
|
4
4
|
from enum import Enum, auto
|
5
5
|
from typing import TYPE_CHECKING
|
6
6
|
|
@@ -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,28 +30,30 @@ 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
|
-
|
42
|
-
|
44
|
+
GoodGuy = auto()
|
45
|
+
Werewolf = auto()
|
46
|
+
Joker = auto()
|
43
47
|
|
44
48
|
|
45
|
-
@dataclass
|
49
|
+
@dataclasses.dataclass
|
46
50
|
class GameState:
|
47
51
|
day: int
|
48
52
|
killed: Player | None = None
|
49
53
|
shoot: tuple[Player, Player] | tuple[None, None] = (None, None)
|
50
|
-
|
51
|
-
|
54
|
+
antidote: set[Player] = dataclasses.field(default_factory=set)
|
55
|
+
poison: set[tuple[Player, Player]] = dataclasses.field(default_factory=set)
|
56
|
+
protected: set[Player] = dataclasses.field(default_factory=set)
|
52
57
|
|
53
58
|
|
54
59
|
role_name_conv: dict[Role | RoleGroup, str] = {
|
@@ -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
@@ -3,32 +3,30 @@ from __future__ import annotations
|
|
3
3
|
import asyncio
|
4
4
|
import asyncio.timeouts
|
5
5
|
import contextlib
|
6
|
-
import
|
7
|
-
import
|
6
|
+
import secrets
|
7
|
+
from typing import TYPE_CHECKING, NoReturn
|
8
8
|
|
9
|
-
from nonebot.adapters import Bot
|
10
9
|
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
|
-
)
|
10
|
+
from nonebot_plugin_alconna import At, Target, UniMessage
|
11
|
+
|
12
|
+
from .config import config
|
13
|
+
from .constant import GameState, GameStatus, KillReason, Role, RoleGroup, role_name_conv
|
14
|
+
from .exception import GameFinishedError
|
23
15
|
from .player import Player
|
24
16
|
from .player_set import PlayerSet
|
25
17
|
from .utils import InputStore
|
26
18
|
|
19
|
+
if TYPE_CHECKING:
|
20
|
+
from nonebot.adapters import Bot
|
21
|
+
from nonebot_plugin_alconna.uniseg.message import Receipt
|
22
|
+
|
27
23
|
starting_games: dict[str, dict[str, str]] = {}
|
28
24
|
running_games: dict[str, Game] = {}
|
29
25
|
|
30
26
|
|
31
27
|
def init_players(bot: Bot, game: Game, players: dict[str, str]) -> PlayerSet:
|
28
|
+
logger.opt(colors=True).debug(f"初始化 <c>{game.group.id}</c> 的玩家职业")
|
29
|
+
role_preset = config.get_role_preset()
|
32
30
|
preset = role_preset.get(len(players))
|
33
31
|
if preset is None:
|
34
32
|
raise ValueError(
|
@@ -36,18 +34,24 @@ def init_players(bot: Bot, game: Game, players: dict[str, str]) -> PlayerSet:
|
|
36
34
|
f"应为 {', '.join(map(str, role_preset))} 人, 传入{len(players)}人"
|
37
35
|
)
|
38
36
|
|
37
|
+
w, p, c = preset
|
39
38
|
roles: list[Role] = []
|
40
|
-
roles.extend(werewolf_priority[:
|
41
|
-
roles.extend(priesthood_proirity[:
|
42
|
-
roles.extend([Role.Civilian] *
|
39
|
+
roles.extend(config.werewolf_priority[:w])
|
40
|
+
roles.extend(config.priesthood_proirity[:p])
|
41
|
+
roles.extend([Role.Civilian] * c)
|
42
|
+
|
43
|
+
if c >= 2 and secrets.randbelow(100) <= config.joker_probability * 100:
|
44
|
+
roles.remove(Role.Civilian)
|
45
|
+
roles.append(Role.Joker)
|
43
46
|
|
44
|
-
r = random.Random(time.time())
|
45
47
|
shuffled: list[Role] = []
|
46
|
-
|
47
|
-
idx =
|
48
|
+
while roles:
|
49
|
+
idx = secrets.randbelow(len(roles))
|
48
50
|
shuffled.append(roles.pop(idx))
|
49
51
|
|
50
|
-
|
52
|
+
logger.debug(f"职业分配: {shuffled}")
|
53
|
+
|
54
|
+
async def selector(target_: Target, b: Bot) -> bool:
|
51
55
|
return target_.self_id == bot.self_id and b is bot
|
52
56
|
|
53
57
|
return PlayerSet(
|
@@ -63,7 +67,7 @@ def init_players(bot: Bot, game: Game, players: dict[str, str]) -> PlayerSet:
|
|
63
67
|
),
|
64
68
|
players[user_id],
|
65
69
|
)
|
66
|
-
for user_id, role in zip(players, shuffled)
|
70
|
+
for user_id, role in zip(players, shuffled, strict=True)
|
67
71
|
)
|
68
72
|
|
69
73
|
|
@@ -71,24 +75,28 @@ class Game:
|
|
71
75
|
bot: Bot
|
72
76
|
group: Target
|
73
77
|
players: PlayerSet
|
78
|
+
_player_map: dict[str, Player]
|
74
79
|
state: GameState
|
75
80
|
killed_players: list[Player]
|
76
81
|
|
77
|
-
def __init__(
|
78
|
-
self,
|
79
|
-
bot: Bot,
|
80
|
-
group: Target,
|
81
|
-
players: dict[str, str],
|
82
|
-
) -> None:
|
82
|
+
def __init__(self, bot: Bot, group: Target, players: dict[str, str]) -> None:
|
83
83
|
self.bot = bot
|
84
84
|
self.group = group
|
85
85
|
self.players = init_players(bot, self, players)
|
86
|
+
self._player_map = {p.user_id: p for p in self.players}
|
86
87
|
self.state = GameState(0)
|
87
88
|
self.killed_players = []
|
88
89
|
|
89
|
-
async def send(self, message: str | UniMessage):
|
90
|
+
async def send(self, message: str | UniMessage) -> Receipt:
|
90
91
|
if isinstance(message, str):
|
91
92
|
message = UniMessage.text(message)
|
93
|
+
text = f"<b><e>{self.group.id}</e></b> | <g>Send</g> | "
|
94
|
+
for seg in message:
|
95
|
+
if isinstance(seg, At):
|
96
|
+
text += f"<y>@{self._player_map[seg.target].name}</y>"
|
97
|
+
else:
|
98
|
+
text += str(seg)
|
99
|
+
logger.opt(colors=True).info(text.replace("\n", "\\n"))
|
92
100
|
return await message.send(self.group, self.bot)
|
93
101
|
|
94
102
|
def at_all(self) -> UniMessage:
|
@@ -97,21 +105,23 @@ class Game:
|
|
97
105
|
msg.at(p.user_id)
|
98
106
|
return msg
|
99
107
|
|
100
|
-
def check_game_status(self) ->
|
108
|
+
def check_game_status(self) -> None:
|
101
109
|
players = self.players.alive()
|
102
110
|
w = players.select(RoleGroup.Werewolf)
|
103
111
|
p = players.exclude(RoleGroup.Werewolf)
|
104
112
|
|
113
|
+
# 狼人数量大于其他职业数量
|
105
114
|
if w.size >= p.size:
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
115
|
+
raise GameFinishedError(GameStatus.Werewolf)
|
116
|
+
# 屠边-村民/中立全灭
|
117
|
+
if not p.select(Role.Civilian, RoleGroup.Others).size:
|
118
|
+
raise GameFinishedError(GameStatus.Werewolf)
|
119
|
+
# 屠边-神职全灭
|
120
|
+
if not p.exclude(Role.Civilian).size:
|
121
|
+
raise GameFinishedError(GameStatus.Werewolf)
|
122
|
+
# 狼人全灭
|
111
123
|
if not w.size:
|
112
|
-
|
113
|
-
|
114
|
-
return GameStatus.Unset
|
124
|
+
raise GameFinishedError(GameStatus.GoodGuy)
|
115
125
|
|
116
126
|
def show_killed_players(self) -> str:
|
117
127
|
msg = ""
|
@@ -124,7 +134,7 @@ class Game:
|
|
124
134
|
p.name for p in player.kill_info.killers
|
125
135
|
)
|
126
136
|
match player.kill_info.reason:
|
127
|
-
case KillReason.
|
137
|
+
case KillReason.Werewolf:
|
128
138
|
msg += " 刀了"
|
129
139
|
case KillReason.Poison:
|
130
140
|
msg += " 毒死"
|
@@ -137,7 +147,7 @@ class Game:
|
|
137
147
|
return msg.strip()
|
138
148
|
|
139
149
|
async def notify_player_role(self) -> None:
|
140
|
-
preset =
|
150
|
+
preset = config.get_role_preset()[len(self.players)]
|
141
151
|
await asyncio.gather(
|
142
152
|
self.send(
|
143
153
|
self.at_all()
|
@@ -156,7 +166,7 @@ class Game:
|
|
156
166
|
if isinstance(players, Player):
|
157
167
|
players = PlayerSet([players])
|
158
168
|
|
159
|
-
async def wait(p: Player):
|
169
|
+
async def wait(p: Player) -> None:
|
160
170
|
while True:
|
161
171
|
msg = await InputStore.fetch(p.user_id, self.group.id)
|
162
172
|
if msg.extract_plain_text() == "/stop":
|
@@ -176,9 +186,9 @@ class Game:
|
|
176
186
|
type_.role_name # Player
|
177
187
|
if isinstance(type_, Player)
|
178
188
|
else (
|
179
|
-
type_
|
189
|
+
role_name_conv[type_] # Role
|
180
190
|
if isinstance(type_, Role)
|
181
|
-
else f"{type_
|
191
|
+
else f"{role_name_conv[type_]}阵营" # RoleGroup
|
182
192
|
)
|
183
193
|
)
|
184
194
|
|
@@ -186,6 +196,7 @@ class Game:
|
|
186
196
|
try:
|
187
197
|
await players.interact(timeout_secs)
|
188
198
|
except TimeoutError:
|
199
|
+
logger.opt(colors=True).debug(f"{text}交互超时 (<y>{timeout_secs}</y>s)")
|
189
200
|
await players.broadcast(f"{text}交互时间结束")
|
190
201
|
|
191
202
|
async def select_killed(self) -> None:
|
@@ -205,7 +216,7 @@ class Game:
|
|
205
216
|
await self.interact(Role.Witch, 60)
|
206
217
|
# 否则等待 5-20s
|
207
218
|
else:
|
208
|
-
await asyncio.sleep(
|
219
|
+
await asyncio.sleep(5 + secrets.randbelow(15))
|
209
220
|
|
210
221
|
async def handle_new_dead(self, players: Player | PlayerSet) -> None:
|
211
222
|
if isinstance(players, Player):
|
@@ -257,6 +268,8 @@ class Game:
|
|
257
268
|
# 收集到的总票数
|
258
269
|
total_votes = sum(map(len, vote_result.values()))
|
259
270
|
|
271
|
+
logger.debug(f"投票结果: {vote_result}")
|
272
|
+
|
260
273
|
# 投票结果公示
|
261
274
|
msg = UniMessage.text("投票结果:\n")
|
262
275
|
for p, v in sorted(vote_result.items(), key=lambda x: len(x[1]), reverse=True):
|
@@ -306,23 +319,23 @@ class Game:
|
|
306
319
|
loop = asyncio.get_event_loop()
|
307
320
|
queue: asyncio.Queue[tuple[Player, UniMessage]] = asyncio.Queue()
|
308
321
|
|
309
|
-
async def send():
|
322
|
+
async def send() -> NoReturn:
|
310
323
|
while True:
|
311
324
|
player, msg = await queue.get()
|
312
325
|
msg = f"玩家 {player.name}:\n" + msg
|
313
|
-
await self.players.
|
326
|
+
await self.players.killed().exclude(player).broadcast(msg)
|
327
|
+
queue.task_done()
|
328
|
+
|
329
|
+
async def recv(player: Player) -> NoReturn:
|
330
|
+
await player.killed.wait()
|
314
331
|
|
315
|
-
async def recv(player: Player):
|
316
332
|
counter = 0
|
317
333
|
|
318
|
-
def decrease():
|
334
|
+
def decrease() -> None:
|
319
335
|
nonlocal counter
|
320
336
|
counter -= 1
|
321
337
|
|
322
338
|
while True:
|
323
|
-
if not player.killed:
|
324
|
-
await asyncio.sleep(1)
|
325
|
-
continue
|
326
339
|
msg = await player.receive()
|
327
340
|
counter += 1
|
328
341
|
if counter <= 10:
|
@@ -333,13 +346,14 @@ class Game:
|
|
333
346
|
|
334
347
|
await asyncio.gather(send(), *[recv(p) for p in self.players])
|
335
348
|
|
336
|
-
async def run(self) ->
|
349
|
+
async def run(self) -> NoReturn:
|
337
350
|
# 告知玩家角色信息
|
338
351
|
await self.notify_player_role()
|
339
352
|
# 天数记录 主要用于第一晚狼人击杀的遗言
|
340
353
|
day_count = 0
|
341
354
|
|
342
|
-
|
355
|
+
# 游戏主循环
|
356
|
+
while True:
|
343
357
|
# 重置游戏状态,进入下一夜
|
344
358
|
self.state = GameState(day_count)
|
345
359
|
players = self.players.alive()
|
@@ -348,29 +362,31 @@ class Game:
|
|
348
362
|
# 狼人、预言家、守卫 同时交互,女巫在狼人后交互
|
349
363
|
await asyncio.gather(
|
350
364
|
self.select_killed(),
|
351
|
-
players.select(Role.Witch).broadcast("请等待狼人决定目标..."),
|
352
|
-
players.select(Role.Civilian).broadcast("请等待其他玩家结束交互..."),
|
353
365
|
self.interact(Role.Prophet, 60),
|
354
366
|
self.interact(Role.Guard, 60),
|
367
|
+
players.select(Role.Witch).broadcast("请等待狼人决定目标..."),
|
368
|
+
players.exclude(
|
369
|
+
RoleGroup.Werewolf, Role.Prophet, Role.Witch, Role.Guard
|
370
|
+
).broadcast("请等待其他玩家结束交互..."),
|
355
371
|
)
|
356
372
|
|
357
373
|
# 狼人击杀目标
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
+
if (
|
375
|
+
(killed := self.state.killed) # 狼人未空刀
|
376
|
+
and killed not in self.state.protected # 守卫保护
|
377
|
+
and killed not in self.state.antidote # 女巫使用解药
|
378
|
+
):
|
379
|
+
# 狼人正常击杀玩家
|
380
|
+
await killed.kill(
|
381
|
+
KillReason.Werewolf,
|
382
|
+
*players.select(RoleGroup.Werewolf),
|
383
|
+
)
|
384
|
+
|
385
|
+
# 女巫操作目标
|
386
|
+
for witch, potioned in self.state.poison:
|
387
|
+
if potioned not in self.state.protected: # 守卫未保护
|
388
|
+
# 女巫毒杀玩家
|
389
|
+
await potioned.kill(KillReason.Poison, witch)
|
374
390
|
|
375
391
|
day_count += 1
|
376
392
|
msg = UniMessage.text(f"『第{day_count}天』天亮了...\n")
|
@@ -396,8 +412,7 @@ class Game:
|
|
396
412
|
await self.post_kill(dead)
|
397
413
|
|
398
414
|
# 判断游戏状态
|
399
|
-
|
400
|
-
break
|
415
|
+
self.check_game_status()
|
401
416
|
|
402
417
|
# 公示存活玩家
|
403
418
|
await self.send(f"当前存活玩家: \n\n{self.players.alive().show()}")
|
@@ -410,37 +425,49 @@ class Game:
|
|
410
425
|
await self.send("讨论结束, 进入投票环节,限时1分钟\n请在私聊中进行投票交互")
|
411
426
|
await self.run_vote()
|
412
427
|
|
413
|
-
|
414
|
-
|
428
|
+
# 判断游戏状态
|
429
|
+
self.check_game_status()
|
430
|
+
|
431
|
+
async def handle_game_finish(self, status: GameStatus) -> None:
|
432
|
+
match status:
|
433
|
+
case GameStatus.GoodGuy:
|
434
|
+
winner = "好人"
|
435
|
+
case GameStatus.Werewolf:
|
436
|
+
winner = "狼人"
|
437
|
+
case GameStatus.Joker:
|
438
|
+
winner = "小丑"
|
439
|
+
|
415
440
|
msg = UniMessage.text(f"🎉游戏结束,{winner}获胜\n\n")
|
416
441
|
for p in sorted(self.players, key=lambda p: (p.role.value, p.user_id)):
|
417
442
|
msg.at(p.user_id).text(f": {p.role_name}\n")
|
418
443
|
await self.send(msg)
|
419
|
-
await self.send(f"
|
444
|
+
await self.send(f"📌玩家死亡报告:\n\n{self.show_killed_players()}")
|
420
445
|
|
421
|
-
def start(self):
|
422
|
-
|
446
|
+
def start(self) -> None:
|
447
|
+
finished = asyncio.Event()
|
423
448
|
game_task = asyncio.create_task(self.run())
|
424
|
-
game_task.add_done_callback(lambda _:
|
449
|
+
game_task.add_done_callback(lambda _: finished.set())
|
425
450
|
dead_channel = asyncio.create_task(self.run_dead_channel())
|
426
451
|
|
427
|
-
async def daemon():
|
428
|
-
await
|
452
|
+
async def daemon() -> None:
|
453
|
+
await finished.wait()
|
429
454
|
|
430
455
|
try:
|
431
456
|
game_task.result()
|
457
|
+
except asyncio.CancelledError:
|
458
|
+
logger.warning(f"{self.group.id} 的狼人杀游戏进程被取消")
|
459
|
+
except GameFinishedError as result:
|
460
|
+
await self.handle_game_finish(result.status)
|
432
461
|
logger.info(f"{self.group.id} 的狼人杀游戏进程正常退出")
|
433
|
-
except asyncio.CancelledError as err:
|
434
|
-
logger.warning(f"{self.group.id} 的狼人杀游戏进程被取消: {err}")
|
435
462
|
except Exception as err:
|
436
|
-
msg = f"{self.group.id}
|
463
|
+
msg = f"{self.group.id} 的狼人杀游戏进程出现未知错误: {err!r}"
|
437
464
|
logger.opt(exception=err).error(msg)
|
438
465
|
await self.send(msg)
|
439
466
|
finally:
|
440
467
|
dead_channel.cancel()
|
441
468
|
running_games.pop(self.group.id, None)
|
442
469
|
|
443
|
-
def daemon_callback(task: asyncio.Task[None]):
|
470
|
+
def daemon_callback(task: asyncio.Task[None]) -> None:
|
444
471
|
if err := task.exception():
|
445
472
|
logger.opt(exception=err).error(
|
446
473
|
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
|
|
@@ -288,24 +307,6 @@ class Witch(Player):
|
|
288
307
|
antidote: int = 1
|
289
308
|
poison: int = 1
|
290
309
|
|
291
|
-
def set_state(
|
292
|
-
self,
|
293
|
-
*,
|
294
|
-
antidote: Player | None = None,
|
295
|
-
posion: Player | None = None,
|
296
|
-
):
|
297
|
-
if antidote is not None:
|
298
|
-
self.antidote = 0
|
299
|
-
self.selected = antidote
|
300
|
-
self.game.state.potion = (antidote, (True, False))
|
301
|
-
elif posion is not None:
|
302
|
-
self.poison = 0
|
303
|
-
self.selected = posion
|
304
|
-
self.game.state.potion = (posion, (False, True))
|
305
|
-
else:
|
306
|
-
self.selected = None
|
307
|
-
self.game.state.potion = (None, (False, False))
|
308
|
-
|
309
310
|
async def handle_killed(self) -> bool:
|
310
311
|
msg = UniMessage()
|
311
312
|
if (killed := self.game.state.killed) is not None:
|
@@ -324,13 +325,12 @@ class Witch(Player):
|
|
324
325
|
text = await self.receive_text()
|
325
326
|
if text == "1":
|
326
327
|
self.antidote = 0
|
327
|
-
self.
|
328
|
+
self.game.state.antidote.add(killed)
|
328
329
|
await self.send(f"你对 {killed.name} 使用了解药,回合结束")
|
329
330
|
return True
|
330
|
-
|
331
|
+
if text == "/stop":
|
331
332
|
return False
|
332
|
-
|
333
|
-
await self.send("输入错误: 请输入 “1” 或 “/stop”")
|
333
|
+
await self.send("输入错误: 请输入 “1” 或 “/stop”")
|
334
334
|
|
335
335
|
@override
|
336
336
|
async def interact(self) -> None:
|
@@ -339,7 +339,6 @@ class Witch(Player):
|
|
339
339
|
|
340
340
|
if not self.poison:
|
341
341
|
await self.send("你没有可以使用的药水,回合结束")
|
342
|
-
self.set_state()
|
343
342
|
return
|
344
343
|
|
345
344
|
players = self.game.players.alive()
|
@@ -357,16 +356,14 @@ class Witch(Player):
|
|
357
356
|
if index is not None:
|
358
357
|
selected = index - 1
|
359
358
|
break
|
360
|
-
|
359
|
+
if text == "/stop":
|
361
360
|
await self.send("你选择不使用毒药,回合结束")
|
362
|
-
self.set_state()
|
363
361
|
return
|
364
|
-
|
365
|
-
await self.send("输入错误: 请发送玩家编号或 “/stop”")
|
362
|
+
await self.send("输入错误: 请发送玩家编号或 “/stop”")
|
366
363
|
|
367
364
|
self.poison = 0
|
368
|
-
|
369
|
-
self.
|
365
|
+
player = players[selected]
|
366
|
+
self.game.state.poison.add((self, player))
|
370
367
|
await self.send(f"当前回合选择对玩家 {player.name} 使用毒药\n回合结束")
|
371
368
|
|
372
369
|
|
@@ -379,9 +376,10 @@ class Hunter(CanShoot, Player):
|
|
379
376
|
class Guard(Player):
|
380
377
|
@override
|
381
378
|
async def interact(self) -> None:
|
382
|
-
players = self.game.players.alive()
|
379
|
+
players = self.game.players.alive()
|
383
380
|
await self.send(
|
384
|
-
UniMessage.text(
|
381
|
+
UniMessage.text("请选择需要保护的玩家:\n")
|
382
|
+
.text(players.show())
|
385
383
|
.text("\n\n发送编号选择玩家")
|
386
384
|
.text("\n发送 “/stop” 结束回合")
|
387
385
|
)
|
@@ -400,7 +398,8 @@ class Guard(Player):
|
|
400
398
|
break
|
401
399
|
await self.send("输入错误,请发送编号选择玩家")
|
402
400
|
|
403
|
-
self.
|
401
|
+
self.selected = players[selected]
|
402
|
+
self.game.state.protected.add(self.selected)
|
404
403
|
await self.send(f"本回合保护的玩家: {self.selected.name}")
|
405
404
|
|
406
405
|
|
@@ -409,17 +408,20 @@ class Idiot(Player):
|
|
409
408
|
voted: bool = False
|
410
409
|
|
411
410
|
@override
|
412
|
-
async def
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
411
|
+
async def notify_role(self) -> None:
|
412
|
+
await super().notify_role()
|
413
|
+
await self.send(
|
414
|
+
"作为白痴,你可以在首次被投票放逐时免疫放逐,但在之后的投票中无法继续投票"
|
415
|
+
)
|
416
|
+
|
417
|
+
@override
|
418
|
+
async def kill(self, reason: KillReason, *killers: Player) -> bool:
|
417
419
|
if reason == KillReason.Vote and not self.voted:
|
418
420
|
self.voted = True
|
419
421
|
await self.game.send(
|
420
422
|
UniMessage.at(self.user_id)
|
421
423
|
.text(" 的身份是白痴\n")
|
422
|
-
.text("免疫本次投票放逐,且接下来无法参与投票")
|
424
|
+
.text("免疫本次投票放逐,且接下来无法参与投票"),
|
423
425
|
)
|
424
426
|
return False
|
425
427
|
return await super().kill(reason, *killers)
|
@@ -432,6 +434,22 @@ class Idiot(Player):
|
|
432
434
|
return await super().vote(players)
|
433
435
|
|
434
436
|
|
437
|
+
@register_role(Role.Joker, RoleGroup.Others)
|
438
|
+
class Joker(Player):
|
439
|
+
@override
|
440
|
+
async def notify_role(self) -> None:
|
441
|
+
await super().notify_role()
|
442
|
+
await self.send("你的胜利条件: 被投票放逐")
|
443
|
+
|
444
|
+
@override
|
445
|
+
async def kill(self, reason: KillReason, *killers: Player) -> bool:
|
446
|
+
await super().kill(reason, *killers)
|
447
|
+
if reason == KillReason.Vote:
|
448
|
+
self.game.killed_players.append(self)
|
449
|
+
raise GameFinishedError(GameStatus.Joker)
|
450
|
+
return True
|
451
|
+
|
452
|
+
|
435
453
|
@register_role(Role.Civilian, RoleGroup.GoodGuy)
|
436
454
|
class Civilian(Player):
|
437
455
|
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.1.dist-info}/METADATA
RENAMED
@@ -1,15 +1,16 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: nonebot-plugin-werewolf
|
3
|
-
Version: 1.
|
4
|
-
Summary:
|
5
|
-
Author-
|
3
|
+
Version: 1.1.1
|
4
|
+
Summary: 适用于 Nonebot2 的狼人杀插件
|
5
|
+
Author-email: wyf7685 <wyf7685@163.com>
|
6
6
|
License: MIT
|
7
|
-
Requires-Python: >=3.
|
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
|
7
|
+
Requires-Python: >=3.11
|
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">
|
@@ -25,13 +26,21 @@ _✨ 简单的狼人杀插件 ✨_
|
|
25
26
|
|
26
27
|
[](./LICENSE)
|
27
28
|
[](https://pypi.python.org/pypi/nonebot-plugin-werewolf)
|
28
|
-
[](https://www.python.org/)
|
30
|
+
[](https://github.com/astral-sh/uv)
|
31
|
+
[](https://github.com/astral-sh/ruff)
|
31
32
|
[](https://pycqa.github.io/isort/)
|
32
33
|
[](https://github.com/psf/black)
|
33
34
|
[](https://github.com/Microsoft/pyright)
|
34
|
-
|
35
|
+
|
36
|
+
[](https://github.com/wyf7685/nonebot-plugin-werewolf/commits)
|
37
|
+
[](https://wakatime.com/badge/user/b097681b-c224-44ec-8e04-e1cf71744655/project/70a7f68d-5625-4989-9476-be6877408332)
|
38
|
+
[](https://results.pre-commit.ci/latest/github/wyf7685/nonebot-plugin-werewolf/master)
|
39
|
+
[](https://github.com/wyf7685/nonebot-plugin-werewolf/actions/workflows/pyright.yml)
|
40
|
+
[](https://github.com/wyf7685/nonebot-plugin-werewolf/actions/workflows/pypi-publish.yml)
|
41
|
+
|
42
|
+
[](https://registry.nonebot.dev/plugin/nonebot-plugin-werewolf:nonebot_plugin_werewolf)
|
43
|
+
[](https://registry.nonebot.dev/plugin/nonebot-plugin-werewolf:nonebot_plugin_werewolf)
|
35
44
|
|
36
45
|
</div>
|
37
46
|
|
@@ -41,6 +50,10 @@ _✨ 简单的狼人杀插件 ✨_
|
|
41
50
|
|
42
51
|
## 💿 安装
|
43
52
|
|
53
|
+
> [!note]
|
54
|
+
>
|
55
|
+
> 请确保 NoneBot2 使用的 Python 解释器版本 >=3.11
|
56
|
+
|
44
57
|
<details open>
|
45
58
|
<summary>使用 nb-cli 安装</summary>
|
46
59
|
在 nonebot2 项目的根目录下打开命令行, 输入以下指令即可安装
|
@@ -86,7 +99,7 @@ _✨ 简单的狼人杀插件 ✨_
|
|
86
99
|
|
87
100
|
## ⚙️ 配置
|
88
101
|
|
89
|
-
在 nonebot2
|
102
|
+
在 nonebot2 项目的 `.env` 文件中添加如下配置
|
90
103
|
|
91
104
|
| 配置项 | 必填 | 默认值 | 说明 |
|
92
105
|
| :-----------------------------: | :--: | :----: | :-----------------------------------------------------------: |
|
@@ -94,6 +107,7 @@ _✨ 简单的狼人杀插件 ✨_
|
|
94
107
|
| `werewolf__role_preset` | 否 | - | 覆写插件内置的职业预设 |
|
95
108
|
| `werewolf__werewolf_priority` | 否 | - | 自定义狼人职业优先级 |
|
96
109
|
| `werewolf__priesthood_proirity` | 否 | - | 自定义神职职业优先级 |
|
110
|
+
| `werewolf__joker_probability` | 否 | `0.0` | 小丑职业替换平民的概率, 范围`[0,1]` |
|
97
111
|
|
98
112
|
`werewolf__role_preset`, `werewolf__werewolf_priority`, `werewolf__priesthood_proirity` 的配置格式请参考 [`游戏内容`](#游戏内容) 部分
|
99
113
|
|
@@ -192,7 +206,7 @@ werewolf__werewolf_priority=[1, 2, 1, 1]
|
|
192
206
|
|
193
207
|
上述配置中,`[1, 2, 1, 1]` 表示狼人的职业优先级为 `狼人`, `狼王`, `狼人`, `狼人`
|
194
208
|
|
195
|
-
#### 配置项 `
|
209
|
+
#### 配置项 `werewolf__priesthood_proirity`
|
196
210
|
|
197
211
|
```env
|
198
212
|
werewolf__priesthood_proirity=[11, 12, 13, 14, 15]
|
@@ -226,6 +240,18 @@ werewolf__priesthood_proirity=[11, 12, 13, 14, 15]
|
|
226
240
|
|
227
241
|
<!-- CHANGELOG -->
|
228
242
|
|
243
|
+
- 2024.09.11 v1.1.1
|
244
|
+
|
245
|
+
- 修改 Python 需求为 `>=3.11`
|
246
|
+
- 优化交互结果处理 ~~_可以在一局游戏中加入多个女巫了_~~
|
247
|
+
|
248
|
+
- 2024.09.09 v1.1.0
|
249
|
+
|
250
|
+
- 新增职业 `小丑`
|
251
|
+
- 修复守卫无法保护自己的 bug
|
252
|
+
- 添加部分特殊职业的说明
|
253
|
+
- 添加游戏过程中的日志输出
|
254
|
+
|
229
255
|
- 2024.09.04 v1.0.7
|
230
256
|
|
231
257
|
- 优先使用群名片作为玩家名
|
@@ -0,0 +1,15 @@
|
|
1
|
+
nonebot_plugin_werewolf/__init__.py,sha256=I6lFyJgJ4Tz4t8gS2lrsx-7paN2h_PNoGjGR8ryvSeU,734
|
2
|
+
nonebot_plugin_werewolf/config.py,sha256=gl_ujiY-8fxQ5hN9_gjL1WpJJR5UOBIRBGofbLcQzaE,2900
|
3
|
+
nonebot_plugin_werewolf/constant.py,sha256=1XNTcTicil90WSviI2e33FX0LstWXl1aV2ugbhsDlIk,1909
|
4
|
+
nonebot_plugin_werewolf/exception.py,sha256=Ui8rB1sBg6_p8JHSvrjCpUaXJlgrM_9XsLJ09k8F1bg,391
|
5
|
+
nonebot_plugin_werewolf/game.py,sha256=Y9MQASaNdm2uuNw37IjdQEQzikWmbql_PWLHaL7lcB8,17414
|
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=ifqTBfNepQY5FCY7hZoUA6DcObkcFEiNmKEeRkitJqY,14749
|
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.1.dist-info/LICENSE,sha256=B_WbEqjGr6GYVNfEJPY31T1Opik7OtgOkhRs4Ig3e2M,1064
|
12
|
+
nonebot_plugin_werewolf-1.1.1.dist-info/METADATA,sha256=5ikh-mdZTIObaFs9w7qgzrgsx31lCxB5Ag--C8stl0g,10447
|
13
|
+
nonebot_plugin_werewolf-1.1.1.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
|
14
|
+
nonebot_plugin_werewolf-1.1.1.dist-info/top_level.txt,sha256=wLTfg8sTKbH9lLT9LtU118C9cTspEBJareLsrYM52YA,24
|
15
|
+
nonebot_plugin_werewolf-1.1.1.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.1.dist-info}/LICENSE
RENAMED
File without changes
|