nonebot-plugin-werewolf 1.1.10__py3-none-any.whl → 1.1.12__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 +57 -35
- nonebot_plugin_werewolf/constant.py +0 -19
- nonebot_plugin_werewolf/dead_channel.py +79 -0
- nonebot_plugin_werewolf/game.py +48 -117
- nonebot_plugin_werewolf/matchers/_prepare_game.py +223 -0
- nonebot_plugin_werewolf/matchers/depends.py +4 -4
- nonebot_plugin_werewolf/matchers/edit_behavior.py +2 -2
- nonebot_plugin_werewolf/matchers/edit_preset.py +20 -20
- nonebot_plugin_werewolf/matchers/message_in_game.py +5 -1
- nonebot_plugin_werewolf/matchers/poke/__init__.py +2 -1
- nonebot_plugin_werewolf/matchers/poke/chronocat_poke.py +7 -12
- nonebot_plugin_werewolf/matchers/poke/ob11_poke.py +8 -11
- nonebot_plugin_werewolf/matchers/start_game.py +23 -233
- nonebot_plugin_werewolf/matchers/superuser_ops.py +16 -12
- nonebot_plugin_werewolf/models.py +19 -0
- nonebot_plugin_werewolf/player.py +35 -31
- nonebot_plugin_werewolf/player_set.py +10 -0
- nonebot_plugin_werewolf/players/guard.py +2 -2
- nonebot_plugin_werewolf/players/prophet.py +2 -2
- nonebot_plugin_werewolf/players/shooter.py +2 -2
- nonebot_plugin_werewolf/players/werewolf.py +18 -19
- nonebot_plugin_werewolf/players/witch.py +4 -4
- nonebot_plugin_werewolf/utils.py +18 -56
- {nonebot_plugin_werewolf-1.1.10.dist-info → nonebot_plugin_werewolf-1.1.12.dist-info}/METADATA +122 -58
- nonebot_plugin_werewolf-1.1.12.dist-info/RECORD +37 -0
- {nonebot_plugin_werewolf-1.1.10.dist-info → nonebot_plugin_werewolf-1.1.12.dist-info}/WHEEL +1 -1
- nonebot_plugin_werewolf-1.1.10.dist-info/RECORD +0 -35
- {nonebot_plugin_werewolf-1.1.10.dist-info → nonebot_plugin_werewolf-1.1.12.dist-info}/licenses/LICENSE +0 -0
- {nonebot_plugin_werewolf-1.1.10.dist-info → nonebot_plugin_werewolf-1.1.12.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
import json
|
2
2
|
from pathlib import Path
|
3
|
-
from typing import Any, ClassVar, Final
|
3
|
+
from typing import Any, ClassVar, Final, Literal
|
4
4
|
from typing_extensions import Self
|
5
5
|
|
6
6
|
import nonebot
|
@@ -12,37 +12,36 @@ from .constant import (
|
|
12
12
|
DEFAULT_PRIESTHOOD_PRIORITY,
|
13
13
|
DEFAULT_ROLE_PRESET,
|
14
14
|
DEFAULT_WEREWOLF_PRIORITY,
|
15
|
-
stop_command_prompt,
|
16
15
|
)
|
17
16
|
from .models import Role
|
18
17
|
|
19
18
|
|
20
19
|
class ConfigFile(BaseModel):
|
21
|
-
|
22
|
-
|
20
|
+
_file_: ClassVar[Path]
|
21
|
+
_cache_: ClassVar[Self | None] = None
|
23
22
|
|
24
23
|
def __init_subclass__(cls, **kwargs: Any) -> None: # noqa: ANN401
|
25
24
|
super().__init_subclass__(**kwargs)
|
26
|
-
if not cls.
|
25
|
+
if not cls._file_.exists():
|
27
26
|
cls().save()
|
28
27
|
|
29
28
|
@classmethod
|
30
29
|
def load(cls) -> Self:
|
31
|
-
return type_validate_json(cls, cls.
|
30
|
+
return type_validate_json(cls, cls._file_.read_text())
|
32
31
|
|
33
32
|
@classmethod
|
34
33
|
def get(cls, *, use_cache: bool = True) -> Self:
|
35
|
-
if cls.
|
36
|
-
cls.
|
37
|
-
return cls.
|
34
|
+
if cls._cache_ is None or not use_cache:
|
35
|
+
cls._cache_ = cls.load()
|
36
|
+
return cls._cache_
|
38
37
|
|
39
38
|
def save(self) -> None:
|
40
|
-
self.
|
41
|
-
type(self).
|
39
|
+
self._file_.write_text(json.dumps(model_dump(self)))
|
40
|
+
type(self)._cache_ = self
|
42
41
|
|
43
42
|
|
44
43
|
class PresetData(ConfigFile):
|
45
|
-
|
44
|
+
_file_: ClassVar[Path] = get_plugin_data_file("preset.json")
|
46
45
|
|
47
46
|
role_preset: dict[int, tuple[int, int, int]] = DEFAULT_ROLE_PRESET.copy()
|
48
47
|
werewolf_priority: list[Role] = DEFAULT_WEREWOLF_PRIORITY.copy()
|
@@ -50,43 +49,56 @@ class PresetData(ConfigFile):
|
|
50
49
|
jester_probability: float = Field(default=0.0, ge=0.0, le=1.0)
|
51
50
|
|
52
51
|
|
52
|
+
class _Timeout(BaseModel):
|
53
|
+
prepare: int = Field(default=5 * 60, ge=5 * 60)
|
54
|
+
speak: int = Field(default=60, ge=60)
|
55
|
+
group_speak: int = Field(default=120, ge=120)
|
56
|
+
interact: int = Field(default=60, ge=60)
|
57
|
+
vote: int = Field(default=60, ge=60)
|
58
|
+
werewolf: int = Field(default=120, ge=120)
|
59
|
+
|
60
|
+
@property
|
61
|
+
def speak_timeout_prompt(self) -> str:
|
62
|
+
return f"限时{self.speak / 60:.1f}分钟, 发送 “{stop_command_prompt}” 结束发言"
|
63
|
+
|
64
|
+
@property
|
65
|
+
def group_speak_timeout_prompt(self) -> str:
|
66
|
+
return (
|
67
|
+
f"限时{self.group_speak / 60:.1f}分钟, "
|
68
|
+
f"全员发送 “{stop_command_prompt}” 结束发言"
|
69
|
+
)
|
70
|
+
|
71
|
+
|
53
72
|
class GameBehavior(ConfigFile):
|
54
|
-
|
73
|
+
_file_: ClassVar[Path] = get_plugin_data_file("behavior.json")
|
55
74
|
|
56
75
|
show_roles_list_on_start: bool = False
|
57
76
|
speak_in_turn: bool = False
|
58
77
|
dead_channel_rate_limit: int = 8 # per minute
|
59
78
|
werewolf_multi_select: bool = False
|
79
|
+
timeout: Final[_Timeout] = _Timeout()
|
60
80
|
|
61
|
-
class _Timeout(BaseModel):
|
62
|
-
prepare: int = Field(default=5 * 60, ge=5 * 60)
|
63
|
-
speak: int = Field(default=60, ge=60)
|
64
|
-
group_speak: int = Field(default=120, ge=120)
|
65
|
-
interact: int = Field(default=60, ge=60)
|
66
|
-
vote: int = Field(default=60, ge=60)
|
67
|
-
werewolf: int = Field(default=120, ge=120)
|
68
|
-
|
69
|
-
@property
|
70
|
-
def speak_timeout_prompt(self) -> str:
|
71
|
-
return (
|
72
|
-
f"限时{self.speak / 60:.1f}分钟, "
|
73
|
-
f"发送 “{stop_command_prompt()}” 结束发言"
|
74
|
-
)
|
75
|
-
|
76
|
-
@property
|
77
|
-
def group_speak_timeout_prompt(self) -> str:
|
78
|
-
return (
|
79
|
-
f"限时{self.group_speak / 60:.1f}分钟, "
|
80
|
-
f"全员发送 “{stop_command_prompt()}” 结束发言"
|
81
|
-
)
|
82
81
|
|
83
|
-
|
82
|
+
class RequireAtConfig(BaseModel):
|
83
|
+
start: bool = True
|
84
|
+
terminate: bool = True
|
85
|
+
|
86
|
+
|
87
|
+
class MatcherPriorityConfig(BaseModel):
|
88
|
+
start: int = 1
|
89
|
+
terminate: int = 1
|
90
|
+
preset: int = 1
|
91
|
+
behavior: int = 1
|
92
|
+
in_game: int = 10
|
93
|
+
stop: int = 10
|
84
94
|
|
85
95
|
|
86
96
|
class PluginConfig(BaseModel):
|
87
97
|
enable_poke: bool = True
|
88
98
|
enable_button: bool = False
|
89
99
|
stop_command: str | set[str] = "stop"
|
100
|
+
require_at: bool | RequireAtConfig = True
|
101
|
+
matcher_priority: MatcherPriorityConfig = MatcherPriorityConfig()
|
90
102
|
|
91
103
|
def get_stop_command(self) -> list[str]:
|
92
104
|
return (
|
@@ -95,6 +107,11 @@ class PluginConfig(BaseModel):
|
|
95
107
|
else sorted(self.stop_command, key=len)
|
96
108
|
)
|
97
109
|
|
110
|
+
def get_require_at(self, cmd: Literal["start", "terminate"]) -> bool:
|
111
|
+
if isinstance(self.require_at, bool):
|
112
|
+
return self.require_at
|
113
|
+
return getattr(self.require_at, cmd)
|
114
|
+
|
98
115
|
|
99
116
|
class Config(BaseModel):
|
100
117
|
werewolf: PluginConfig = PluginConfig()
|
@@ -102,3 +119,8 @@ class Config(BaseModel):
|
|
102
119
|
|
103
120
|
config = nonebot.get_plugin_config(Config).werewolf
|
104
121
|
nonebot.logger.debug(f"加载插件配置: {config}")
|
122
|
+
|
123
|
+
stop_command_prompt = (
|
124
|
+
next(iter(sorted(nonebot.get_driver().config.command_start, key=len)), "")
|
125
|
+
+ config.get_stop_command()[0]
|
126
|
+
)
|
@@ -1,25 +1,6 @@
|
|
1
|
-
import functools
|
2
|
-
from typing import TYPE_CHECKING
|
3
|
-
|
4
|
-
import nonebot
|
5
|
-
|
6
1
|
from .models import GameStatus, KillReason, Role, RoleGroup
|
7
2
|
|
8
3
|
STOP_COMMAND = "{{stop}}"
|
9
|
-
COMMAND_START = next(
|
10
|
-
iter(sorted(nonebot.get_driver().config.command_start, key=len)), ""
|
11
|
-
)
|
12
|
-
|
13
|
-
|
14
|
-
def stop_command_prompt() -> str:
|
15
|
-
from .config import config # circular import
|
16
|
-
|
17
|
-
return COMMAND_START + config.get_stop_command()[0]
|
18
|
-
|
19
|
-
|
20
|
-
if not TYPE_CHECKING:
|
21
|
-
stop_command_prompt = functools.cache(stop_command_prompt)
|
22
|
-
del TYPE_CHECKING
|
23
4
|
|
24
5
|
|
25
6
|
ROLE_NAME_CONV: dict[Role | RoleGroup, str] = {
|
@@ -0,0 +1,79 @@
|
|
1
|
+
import contextlib
|
2
|
+
from typing import NoReturn
|
3
|
+
|
4
|
+
import anyio
|
5
|
+
import anyio.lowlevel
|
6
|
+
from nonebot_plugin_alconna import UniMessage
|
7
|
+
|
8
|
+
from .config import GameBehavior
|
9
|
+
from .player import Player
|
10
|
+
from .player_set import PlayerSet
|
11
|
+
|
12
|
+
|
13
|
+
class DeadChannel:
|
14
|
+
players: PlayerSet
|
15
|
+
finished: anyio.Event
|
16
|
+
counter: dict[str, int]
|
17
|
+
|
18
|
+
def __init__(self, players: PlayerSet, finished: anyio.Event) -> None:
|
19
|
+
self.players = players
|
20
|
+
self.finished = finished
|
21
|
+
self.counter = {p.user_id: 0 for p in players}
|
22
|
+
|
23
|
+
async def _decrease(self, user_id: str) -> None:
|
24
|
+
await anyio.sleep(60)
|
25
|
+
self.counter[user_id] -= 1
|
26
|
+
|
27
|
+
async def _wait_finished(self) -> None:
|
28
|
+
await self.finished.wait()
|
29
|
+
self._task_group.cancel_scope.cancel()
|
30
|
+
|
31
|
+
async def _broadcast(self) -> NoReturn:
|
32
|
+
stream = self.stream[1]
|
33
|
+
while True:
|
34
|
+
player, msg = await stream.receive()
|
35
|
+
msg = UniMessage.text(f"玩家 {player.name}:\n") + msg
|
36
|
+
target = self.players.killed().exclude(player)
|
37
|
+
try:
|
38
|
+
await target.broadcast(msg)
|
39
|
+
except Exception as err:
|
40
|
+
with contextlib.suppress(Exception):
|
41
|
+
await player.send(f"消息转发失败: {err!r}")
|
42
|
+
|
43
|
+
async def _receive(self, player: Player) -> NoReturn:
|
44
|
+
await player.killed.wait()
|
45
|
+
await anyio.lowlevel.checkpoint()
|
46
|
+
user_id = player.user_id
|
47
|
+
stream = self.stream[0]
|
48
|
+
|
49
|
+
await player.send(
|
50
|
+
"ℹ️你已加入死者频道,请勿在群组内继续发言\n"
|
51
|
+
"私聊发送消息将转发至其他已死亡玩家",
|
52
|
+
)
|
53
|
+
await (
|
54
|
+
self.players.killed()
|
55
|
+
.exclude(player)
|
56
|
+
.broadcast(f"ℹ️玩家 {player.name} 加入了死者频道")
|
57
|
+
)
|
58
|
+
|
59
|
+
while True:
|
60
|
+
msg = await player.receive()
|
61
|
+
self.counter[user_id] += 1
|
62
|
+
self._task_group.start_soon(self._decrease, user_id)
|
63
|
+
|
64
|
+
# 发言频率限制
|
65
|
+
if self.counter[user_id] > GameBehavior.get().dead_channel_rate_limit:
|
66
|
+
await player.send("❌发言频率超过限制, 该消息被屏蔽")
|
67
|
+
continue
|
68
|
+
|
69
|
+
# 推送消息
|
70
|
+
await stream.send((player, msg))
|
71
|
+
|
72
|
+
async def run(self) -> None:
|
73
|
+
self.stream = anyio.create_memory_object_stream[tuple[Player, UniMessage]](16)
|
74
|
+
send, recv = self.stream
|
75
|
+
async with send, recv, anyio.create_task_group() as self._task_group:
|
76
|
+
self._task_group.start_soon(self._wait_finished)
|
77
|
+
self._task_group.start_soon(self._broadcast)
|
78
|
+
for p in self.players:
|
79
|
+
self._task_group.start_soon(self._receive, p)
|
nonebot_plugin_werewolf/game.py
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
import contextlib
|
2
1
|
import functools
|
3
2
|
import secrets
|
4
3
|
from collections import Counter
|
@@ -10,36 +9,27 @@ import nonebot
|
|
10
9
|
from nonebot.adapters import Bot
|
11
10
|
from nonebot.utils import escape_tag
|
12
11
|
from nonebot_plugin_alconna import At, Target, UniMessage
|
13
|
-
from nonebot_plugin_alconna.uniseg.
|
14
|
-
from nonebot_plugin_uninfo import
|
12
|
+
from nonebot_plugin_alconna.uniseg.receipt import Receipt
|
13
|
+
from nonebot_plugin_uninfo import Scene, SceneType, get_interface
|
15
14
|
|
16
15
|
from .config import GameBehavior, PresetData
|
17
|
-
from .constant import GAME_STATUS_CONV, REPORT_TEXT
|
16
|
+
from .constant import GAME_STATUS_CONV, REPORT_TEXT
|
17
|
+
from .dead_channel import DeadChannel
|
18
18
|
from .exception import GameFinished
|
19
19
|
from .models import GameState, GameStatus, KillInfo, KillReason, Role, RoleGroup
|
20
20
|
from .player import Player
|
21
21
|
from .player_set import PlayerSet
|
22
|
-
from .utils import InputStore,
|
22
|
+
from .utils import InputStore, SendHandler, add_stop_button, link
|
23
23
|
|
24
24
|
logger = nonebot.logger.opt(colors=True)
|
25
|
-
|
26
|
-
running_games: set["Game"] = set()
|
25
|
+
running_games: dict[Target, "Game"] = {}
|
27
26
|
|
28
27
|
|
29
|
-
def
|
30
|
-
return starting_games
|
31
|
-
|
32
|
-
|
33
|
-
def get_running_games() -> set["Game"]:
|
28
|
+
def get_running_games() -> dict[Target, "Game"]:
|
34
29
|
return running_games
|
35
30
|
|
36
31
|
|
37
|
-
async def init_players(
|
38
|
-
bot: Bot,
|
39
|
-
game: "Game",
|
40
|
-
players: set[str],
|
41
|
-
interface: Interface,
|
42
|
-
) -> PlayerSet:
|
32
|
+
async def init_players(bot: Bot, game: "Game", players: set[str]) -> PlayerSet:
|
43
33
|
logger.debug(f"初始化 {game.colored_name} 的玩家职业")
|
44
34
|
|
45
35
|
preset_data = PresetData.get()
|
@@ -51,21 +41,20 @@ async def init_players(
|
|
51
41
|
)
|
52
42
|
|
53
43
|
w, p, c = preset
|
54
|
-
roles
|
55
|
-
|
56
|
-
|
57
|
-
|
44
|
+
roles = [
|
45
|
+
*preset_data.werewolf_priority[:w],
|
46
|
+
*preset_data.priesthood_proirity[:p],
|
47
|
+
*([Role.CIVILIAN] * c),
|
48
|
+
]
|
58
49
|
|
59
50
|
if c >= 2 and secrets.randbelow(100) <= preset_data.jester_probability * 100:
|
60
51
|
roles.remove(Role.CIVILIAN)
|
61
52
|
roles.append(Role.JESTER)
|
62
53
|
|
63
|
-
def _select_role() -> Role:
|
64
|
-
return roles.pop(secrets.randbelow(len(roles)))
|
65
|
-
|
66
54
|
player_set = PlayerSet()
|
67
55
|
for user_id in players:
|
68
|
-
|
56
|
+
role = roles.pop(secrets.randbelow(len(roles)))
|
57
|
+
player_set.add(await Player.new(role, bot, game, user_id))
|
69
58
|
|
70
59
|
logger.debug(f"职业分配完成: <e>{escape_tag(str(player_set))}</e>")
|
71
60
|
return player_set
|
@@ -82,73 +71,6 @@ class _SendHandler(SendHandler[str | None]):
|
|
82
71
|
return msg
|
83
72
|
|
84
73
|
|
85
|
-
class DeadChannel:
|
86
|
-
players: PlayerSet
|
87
|
-
finished: anyio.Event
|
88
|
-
counter: dict[str, int]
|
89
|
-
stream: ObjectStream[tuple[Player, UniMessage]]
|
90
|
-
|
91
|
-
def __init__(self, players: PlayerSet, finished: anyio.Event) -> None:
|
92
|
-
self.players = players
|
93
|
-
self.finished = finished
|
94
|
-
self.counter = {p.user_id: 0 for p in players}
|
95
|
-
self.stream = ObjectStream[tuple[Player, UniMessage]](16)
|
96
|
-
|
97
|
-
async def _decrease(self, user_id: str) -> None:
|
98
|
-
await anyio.sleep(60)
|
99
|
-
self.counter[user_id] -= 1
|
100
|
-
|
101
|
-
async def _wait_finished(self) -> None:
|
102
|
-
await self.finished.wait()
|
103
|
-
self._task_group.cancel_scope.cancel()
|
104
|
-
|
105
|
-
async def _broadcast(self) -> NoReturn:
|
106
|
-
while True:
|
107
|
-
player, msg = await self.stream.recv()
|
108
|
-
msg = f"玩家 {player.name}:\n" + msg
|
109
|
-
target = self.players.killed().exclude(player)
|
110
|
-
try:
|
111
|
-
await target.broadcast(msg)
|
112
|
-
except Exception as err:
|
113
|
-
with contextlib.suppress(Exception):
|
114
|
-
await player.send(f"消息转发失败: {err!r}")
|
115
|
-
|
116
|
-
async def _receive(self, player: Player) -> NoReturn:
|
117
|
-
await player.killed.wait()
|
118
|
-
user_id = player.user_id
|
119
|
-
|
120
|
-
await player.send(
|
121
|
-
"ℹ️你已加入死者频道,请勿在群组内继续发言\n"
|
122
|
-
"私聊发送消息将转发至其他已死亡玩家",
|
123
|
-
)
|
124
|
-
await (
|
125
|
-
self.players.killed()
|
126
|
-
.exclude(player)
|
127
|
-
.broadcast(f"ℹ️玩家 {player.name} 加入了死者频道")
|
128
|
-
)
|
129
|
-
|
130
|
-
while True:
|
131
|
-
msg = await player.receive()
|
132
|
-
self.counter[user_id] += 1
|
133
|
-
self._task_group.start_soon(self._decrease, user_id)
|
134
|
-
|
135
|
-
# 发言频率限制
|
136
|
-
if self.counter[user_id] > GameBehavior.get().dead_channel_rate_limit:
|
137
|
-
await player.send("❌发言频率超过限制, 该消息被屏蔽")
|
138
|
-
continue
|
139
|
-
|
140
|
-
# 推送消息
|
141
|
-
await self.stream.send((player, msg))
|
142
|
-
|
143
|
-
async def run(self) -> None:
|
144
|
-
async with anyio.create_task_group() as tg:
|
145
|
-
self._task_group = tg
|
146
|
-
tg.start_soon(self._wait_finished)
|
147
|
-
tg.start_soon(self._broadcast)
|
148
|
-
for p in self.players:
|
149
|
-
tg.start_soon(self._receive, p)
|
150
|
-
|
151
|
-
|
152
74
|
class Game:
|
153
75
|
bot: Bot
|
154
76
|
group: Target
|
@@ -163,7 +85,7 @@ class Game:
|
|
163
85
|
self.killed_players = []
|
164
86
|
self._player_map: dict[str, Player] = {}
|
165
87
|
self._shuffled: list[Player] = []
|
166
|
-
self._scene = None
|
88
|
+
self._scene: Scene | None = None
|
167
89
|
self._finished = self._task_group = None
|
168
90
|
self._send_handler = _SendHandler(group, bot)
|
169
91
|
|
@@ -174,15 +96,15 @@ class Game:
|
|
174
96
|
bot: Bot,
|
175
97
|
group: Target,
|
176
98
|
players: set[str],
|
177
|
-
interface: Interface,
|
178
99
|
) -> Self:
|
179
100
|
self = cls(bot, group)
|
180
101
|
|
181
|
-
|
182
|
-
|
183
|
-
self._scene
|
102
|
+
if interface := get_interface(bot):
|
103
|
+
self._scene = await interface.get_scene(SceneType.GROUP, self.group_id)
|
104
|
+
if self._scene is None:
|
105
|
+
self._scene = await interface.get_scene(SceneType.GUILD, self.group_id)
|
184
106
|
|
185
|
-
self.players = await init_players(bot, self, players
|
107
|
+
self.players = await init_players(bot, self, players)
|
186
108
|
self._player_map |= {p.user_id: p for p in self.players}
|
187
109
|
self._shuffled = self.players.shuffled
|
188
110
|
|
@@ -261,7 +183,7 @@ class Game:
|
|
261
183
|
msg.text("\n\n📚职业列表:\n")
|
262
184
|
counter = Counter(p.role for p in self.players)
|
263
185
|
for role, cnt in sorted(counter.items(), key=lambda x: x[0].value):
|
264
|
-
msg.text(f"- {
|
186
|
+
msg.text(f"- {role.emoji}{role.display}x{cnt}\n")
|
265
187
|
|
266
188
|
async with anyio.create_task_group() as tg:
|
267
189
|
tg.start_soon(self.send, msg)
|
@@ -304,7 +226,7 @@ class Game:
|
|
304
226
|
self.state.shooter = shooter.selected = None
|
305
227
|
await self.post_kill(shoot)
|
306
228
|
|
307
|
-
async def run_night(self, players: PlayerSet) ->
|
229
|
+
async def run_night(self, players: PlayerSet) -> None:
|
308
230
|
async with anyio.create_task_group() as tg:
|
309
231
|
for p in players:
|
310
232
|
tg.start_soon(p.interact)
|
@@ -321,17 +243,18 @@ class Game:
|
|
321
243
|
*players.select(RoleGroup.WEREWOLF),
|
322
244
|
)
|
323
245
|
else:
|
324
|
-
killed = None
|
246
|
+
self.state.killed = None
|
325
247
|
|
326
248
|
# 女巫操作目标
|
327
249
|
for witch in self.state.poison:
|
328
|
-
if
|
329
|
-
|
330
|
-
|
250
|
+
if (
|
251
|
+
(selected := witch.selected) is not None # 理论上不会是 None (
|
252
|
+
and selected not in self.state.protected # 守卫保护
|
253
|
+
# 虽然应该没什么人会加多个女巫玩...但还是加上判断比较好
|
254
|
+
and selected not in self.state.antidote # 女巫使用解药
|
255
|
+
):
|
331
256
|
# 女巫毒杀玩家
|
332
|
-
await
|
333
|
-
|
334
|
-
return killed
|
257
|
+
await selected.kill(KillReason.POISON, witch)
|
335
258
|
|
336
259
|
async def run_discussion(self) -> None:
|
337
260
|
timeout = self.behavior.timeout
|
@@ -434,8 +357,8 @@ class Game:
|
|
434
357
|
await self.send("🌙天黑请闭眼...")
|
435
358
|
players = self.players.alive()
|
436
359
|
|
437
|
-
#
|
438
|
-
|
360
|
+
# 夜间交互
|
361
|
+
await self.run_night(players)
|
439
362
|
|
440
363
|
# 公告
|
441
364
|
self.state.day += 1
|
@@ -452,7 +375,11 @@ class Game:
|
|
452
375
|
await self.send(msg)
|
453
376
|
|
454
377
|
# 第一晚被狼人杀死的玩家发表遗言
|
455
|
-
if
|
378
|
+
if (
|
379
|
+
self.state.day == 1 # 仅第一晚
|
380
|
+
and (killed := self.state.killed) is not None # 狼人未空刀且未保护
|
381
|
+
and not killed.alive # kill 成功
|
382
|
+
):
|
456
383
|
await self.send(
|
457
384
|
UniMessage.text("⚙️当前为第一天\n请被狼人杀死的 ")
|
458
385
|
.at(killed.user_id)
|
@@ -474,7 +401,9 @@ class Game:
|
|
474
401
|
|
475
402
|
# 开始投票
|
476
403
|
await self.send(
|
477
|
-
"🗳️讨论结束,
|
404
|
+
"🗳️讨论结束, 进入投票环节, "
|
405
|
+
f"限时{self.behavior.timeout.vote / 60:.1f}分钟\n"
|
406
|
+
"请在私聊中进行投票交互"
|
478
407
|
)
|
479
408
|
self.state.state = GameState.State.VOTE
|
480
409
|
await self.run_vote()
|
@@ -499,6 +428,7 @@ class Game:
|
|
499
428
|
await self.mainloop()
|
500
429
|
except anyio.get_cancelled_exc_class():
|
501
430
|
logger.warning(f"{self.colored_name} 的狼人杀游戏进程被取消")
|
431
|
+
raise
|
502
432
|
except GameFinished as result:
|
503
433
|
await self.handle_game_finish(result.status)
|
504
434
|
logger.info(f"{self.colored_name} 的狼人杀游戏进程正常退出")
|
@@ -509,26 +439,27 @@ class Game:
|
|
509
439
|
if self._finished is not None:
|
510
440
|
self._finished.set()
|
511
441
|
|
512
|
-
async def
|
442
|
+
async def run(self) -> None:
|
513
443
|
self._finished = anyio.Event()
|
514
444
|
dead_channel = DeadChannel(self.players, self._finished)
|
515
|
-
get_running_games().
|
445
|
+
get_running_games()[self.group] = self
|
516
446
|
|
517
447
|
try:
|
518
448
|
async with anyio.create_task_group() as self._task_group:
|
519
449
|
self._task_group.start_soon(self.run_daemon)
|
520
450
|
self._task_group.start_soon(dead_channel.run)
|
521
|
-
except anyio.get_cancelled_exc_class():
|
522
|
-
logger.warning(f"{self.colored_name} 的狼人杀游戏进程被取消")
|
523
451
|
except Exception as err:
|
524
452
|
msg = f"{self.colored_name} 的狼人杀守护进程出现错误: {err!r}"
|
525
453
|
logger.opt(exception=err).error(msg)
|
526
454
|
finally:
|
527
455
|
self._finished = None
|
528
456
|
self._task_group = None
|
529
|
-
get_running_games().
|
457
|
+
get_running_games().pop(self.group, None)
|
530
458
|
InputStore.cleanup(self._player_map.keys(), self.group_id)
|
531
459
|
|
460
|
+
def start(self) -> None:
|
461
|
+
nonebot.get_driver().task_group.start_soon(self.run)
|
462
|
+
|
532
463
|
def terminate(self) -> None:
|
533
464
|
if self._task_group is not None:
|
534
465
|
logger.warning(f"中止 {self.colored_name} 的狼人杀游戏进程")
|