nonebot-plugin-werewolf 1.1.5__py3-none-any.whl → 1.1.7__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 +2 -1
- nonebot_plugin_werewolf/config.py +21 -58
- nonebot_plugin_werewolf/constant.py +14 -74
- nonebot_plugin_werewolf/exception.py +1 -1
- nonebot_plugin_werewolf/game.py +217 -226
- nonebot_plugin_werewolf/matchers/__init__.py +2 -0
- nonebot_plugin_werewolf/matchers/depends.py +17 -5
- nonebot_plugin_werewolf/matchers/edit_preset.py +263 -0
- nonebot_plugin_werewolf/matchers/message_in_game.py +8 -3
- nonebot_plugin_werewolf/matchers/start_game.py +140 -48
- nonebot_plugin_werewolf/matchers/superuser_ops.py +24 -0
- nonebot_plugin_werewolf/models.py +73 -0
- nonebot_plugin_werewolf/player_set.py +1 -1
- nonebot_plugin_werewolf/players/can_shoot.py +4 -3
- nonebot_plugin_werewolf/players/civilian.py +1 -1
- nonebot_plugin_werewolf/players/guard.py +2 -1
- nonebot_plugin_werewolf/players/hunter.py +1 -1
- nonebot_plugin_werewolf/players/idiot.py +1 -1
- nonebot_plugin_werewolf/players/joker.py +6 -2
- nonebot_plugin_werewolf/players/player.py +18 -25
- nonebot_plugin_werewolf/players/prophet.py +2 -1
- nonebot_plugin_werewolf/players/werewolf.py +25 -26
- nonebot_plugin_werewolf/players/witch.py +2 -1
- nonebot_plugin_werewolf/players/wolfking.py +1 -1
- nonebot_plugin_werewolf/utils.py +69 -5
- {nonebot_plugin_werewolf-1.1.5.dist-info → nonebot_plugin_werewolf-1.1.7.dist-info}/METADATA +71 -67
- nonebot_plugin_werewolf-1.1.7.dist-info/RECORD +34 -0
- {nonebot_plugin_werewolf-1.1.5.dist-info → nonebot_plugin_werewolf-1.1.7.dist-info}/WHEEL +1 -1
- nonebot_plugin_werewolf-1.1.5.dist-info/RECORD +0 -31
- {nonebot_plugin_werewolf-1.1.5.dist-info → nonebot_plugin_werewolf-1.1.7.dist-info}/LICENSE +0 -0
- {nonebot_plugin_werewolf-1.1.5.dist-info → nonebot_plugin_werewolf-1.1.7.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,263 @@
|
|
1
|
+
from typing import Any, NoReturn
|
2
|
+
|
3
|
+
import nonebot_plugin_waiter as waiter
|
4
|
+
from arclet.alconna import AllParam
|
5
|
+
from nonebot.permission import SUPERUSER
|
6
|
+
from nonebot.typing import T_State
|
7
|
+
from nonebot_plugin_alconna import (
|
8
|
+
Alconna,
|
9
|
+
Args,
|
10
|
+
CommandMeta,
|
11
|
+
Match,
|
12
|
+
Subcommand,
|
13
|
+
UniMessage,
|
14
|
+
on_alconna,
|
15
|
+
)
|
16
|
+
|
17
|
+
from ..config import PresetData
|
18
|
+
from ..constant import role_name_conv
|
19
|
+
from ..models import Role
|
20
|
+
|
21
|
+
alc = Alconna(
|
22
|
+
"狼人杀预设",
|
23
|
+
Subcommand(
|
24
|
+
"role",
|
25
|
+
Args["total#总人数", int],
|
26
|
+
Args["werewolf#狼人数量", int],
|
27
|
+
Args["priesthood#神职数量", int],
|
28
|
+
Args["civilian#平民数量", int],
|
29
|
+
alias={"职业"},
|
30
|
+
help_text="设置总人数为 <total> 的职业分配预设",
|
31
|
+
),
|
32
|
+
Subcommand(
|
33
|
+
"del",
|
34
|
+
Args["total#总人数", int],
|
35
|
+
alias={"删除"},
|
36
|
+
help_text="删除总人数为 <total> 的职业分配预设",
|
37
|
+
),
|
38
|
+
Subcommand(
|
39
|
+
"werewolf",
|
40
|
+
Args["roles?#职业", AllParam],
|
41
|
+
alias={"狼人"},
|
42
|
+
help_text="设置狼人优先级",
|
43
|
+
),
|
44
|
+
Subcommand(
|
45
|
+
"priesthood",
|
46
|
+
Args["roles?#职业", AllParam],
|
47
|
+
alias={"神职"},
|
48
|
+
help_text="设置神职优先级",
|
49
|
+
),
|
50
|
+
Subcommand(
|
51
|
+
"joker",
|
52
|
+
Args["probability?#概率", float],
|
53
|
+
alias={"小丑"},
|
54
|
+
help_text="设置小丑概率",
|
55
|
+
),
|
56
|
+
Subcommand("reset", alias={"重置"}, help_text="重置为默认预设"),
|
57
|
+
meta=CommandMeta(
|
58
|
+
description="编辑狼人杀游戏预设",
|
59
|
+
usage="狼人杀预设 --help",
|
60
|
+
example=(
|
61
|
+
"狼人杀预设\n"
|
62
|
+
"狼人杀预设 职业 6 1 2 3\n"
|
63
|
+
"狼人杀预设 删除 6\n"
|
64
|
+
"狼人杀预设 狼人 狼 狼 狼王 狼 狼\n"
|
65
|
+
"狼人杀预设 神职 巫 预 猎 守卫 白痴\n"
|
66
|
+
"狼人杀预设 小丑 15\n"
|
67
|
+
"狼人杀预设 重置"
|
68
|
+
),
|
69
|
+
author="wyf7685",
|
70
|
+
),
|
71
|
+
)
|
72
|
+
|
73
|
+
edit_preset = on_alconna(
|
74
|
+
alc,
|
75
|
+
permission=SUPERUSER,
|
76
|
+
use_cmd_start=True,
|
77
|
+
)
|
78
|
+
|
79
|
+
|
80
|
+
async def finish(text: str) -> NoReturn:
|
81
|
+
await UniMessage.text(text).finish(reply_to=True)
|
82
|
+
|
83
|
+
|
84
|
+
def display_roles(roles: list[Role]) -> str:
|
85
|
+
role_name = role_name_conv.__getitem__
|
86
|
+
return ", ".join(map(role_name, roles))
|
87
|
+
|
88
|
+
|
89
|
+
@edit_preset.assign("role")
|
90
|
+
async def assign_role(
|
91
|
+
total: Match[int],
|
92
|
+
werewolf: Match[int],
|
93
|
+
priesthood: Match[int],
|
94
|
+
civilian: Match[int],
|
95
|
+
) -> None:
|
96
|
+
preset = (
|
97
|
+
werewolf.result,
|
98
|
+
priesthood.result,
|
99
|
+
civilian.result,
|
100
|
+
)
|
101
|
+
if sum(preset) != total.result:
|
102
|
+
await finish("总人数与职业数量不匹配")
|
103
|
+
|
104
|
+
data = PresetData.load()
|
105
|
+
if werewolf.result > len(data.werewolf_priority):
|
106
|
+
await finish("狼人数量超出优先级列表长度,请先设置足够多的狼人预设")
|
107
|
+
if priesthood.result > len(data.priesthood_proirity):
|
108
|
+
await finish("神职数量超出优先级列表长度,请先设置足够多的神职预设")
|
109
|
+
|
110
|
+
data.role_preset[total.result] = preset
|
111
|
+
data.save()
|
112
|
+
await finish(
|
113
|
+
f"设置成功\n{total.result} 人: "
|
114
|
+
f"狼人x{werewolf.result}, 神职x{priesthood.result}, 平民x{civilian.result}"
|
115
|
+
)
|
116
|
+
|
117
|
+
|
118
|
+
@edit_preset.assign("del")
|
119
|
+
async def delete_role(total: Match[int]) -> None:
|
120
|
+
data = PresetData.load()
|
121
|
+
if total.result not in data.role_preset:
|
122
|
+
await finish("未找到对应预设")
|
123
|
+
del data.role_preset[total.result]
|
124
|
+
data.save()
|
125
|
+
await finish("删除成功")
|
126
|
+
|
127
|
+
|
128
|
+
@edit_preset.assign("werewolf")
|
129
|
+
async def handle_werewolf_input_roles(roles: Match[Any], state: T_State) -> None:
|
130
|
+
if roles.available:
|
131
|
+
state["roles"] = UniMessage(roles.result).extract_plain_text().split(" ")
|
132
|
+
return
|
133
|
+
|
134
|
+
result = await waiter.prompt(
|
135
|
+
"请发送狼人优先级列表,以空格隔开\n发送 “取消” 取消操作"
|
136
|
+
)
|
137
|
+
if result is None:
|
138
|
+
await finish("发送超时,已自动取消")
|
139
|
+
|
140
|
+
text = result.extract_plain_text()
|
141
|
+
if text == "取消":
|
142
|
+
await finish("已取消操作")
|
143
|
+
|
144
|
+
state["roles"] = text.split(" ")
|
145
|
+
|
146
|
+
|
147
|
+
@edit_preset.assign("werewolf")
|
148
|
+
async def assign_werewolf(state: T_State) -> None:
|
149
|
+
roles: list[str] = state["roles"]
|
150
|
+
result: list[Role] = []
|
151
|
+
|
152
|
+
for role in roles:
|
153
|
+
match role:
|
154
|
+
case "狼人" | "狼":
|
155
|
+
result.append(Role.Werewolf)
|
156
|
+
case "狼王":
|
157
|
+
result.append(Role.WolfKing)
|
158
|
+
case x:
|
159
|
+
await finish(f"未知职业: {x}")
|
160
|
+
|
161
|
+
data = PresetData.load()
|
162
|
+
min_length = max(w for w, _, _ in data.role_preset.values())
|
163
|
+
if len(result) < min_length:
|
164
|
+
await finish(f"狼人数量不足,至少需要 {min_length} 个狼人")
|
165
|
+
|
166
|
+
data.werewolf_priority = result
|
167
|
+
data.save()
|
168
|
+
await finish("设置成功: " + display_roles(result))
|
169
|
+
|
170
|
+
|
171
|
+
@edit_preset.assign("priesthood")
|
172
|
+
async def handle_priesthood_input_roles(roles: Match[Any], state: T_State) -> None:
|
173
|
+
if roles.available:
|
174
|
+
state["roles"] = UniMessage(roles.result).extract_plain_text().split(" ")
|
175
|
+
return
|
176
|
+
|
177
|
+
result = await waiter.prompt(
|
178
|
+
"请发送神职优先级列表,以空格隔开\n发送 “取消” 取消操作"
|
179
|
+
)
|
180
|
+
if result is None:
|
181
|
+
await finish("发送超时,已自动取消")
|
182
|
+
|
183
|
+
text = result.extract_plain_text()
|
184
|
+
if text == "取消":
|
185
|
+
await finish("已取消操作")
|
186
|
+
|
187
|
+
state["roles"] = text.split(" ")
|
188
|
+
|
189
|
+
|
190
|
+
@edit_preset.assign("priesthood")
|
191
|
+
async def assign_priesthood(state: T_State) -> None:
|
192
|
+
roles: list[str] = state["roles"]
|
193
|
+
result: list[Role] = []
|
194
|
+
|
195
|
+
for role in roles:
|
196
|
+
match role:
|
197
|
+
case "预言家" | "预言" | "预":
|
198
|
+
result.append(Role.Prophet)
|
199
|
+
case "女巫" | "巫":
|
200
|
+
result.append(Role.Witch)
|
201
|
+
case "猎人" | "猎":
|
202
|
+
result.append(Role.Hunter)
|
203
|
+
case "守卫":
|
204
|
+
result.append(Role.Guard)
|
205
|
+
case "白痴":
|
206
|
+
result.append(Role.Idiot)
|
207
|
+
case x:
|
208
|
+
await finish(f"未知职业: {x}")
|
209
|
+
|
210
|
+
data = PresetData.load()
|
211
|
+
min_length = max(p for _, p, _ in data.role_preset.values())
|
212
|
+
if len(result) < min_length:
|
213
|
+
await finish(f"神职数量不足,至少需要 {min_length} 个神职")
|
214
|
+
|
215
|
+
data.priesthood_proirity = result
|
216
|
+
data.save()
|
217
|
+
await finish("设置成功: " + display_roles(result))
|
218
|
+
|
219
|
+
|
220
|
+
@edit_preset.assign("joker")
|
221
|
+
async def assign_joker(probability: Match[float]) -> None:
|
222
|
+
if not probability.available:
|
223
|
+
result = await waiter.prompt_until(
|
224
|
+
message="请发送小丑概率,范围 0-100\n发送 “取消” 取消操作",
|
225
|
+
checker=lambda m: (s := m.extract_plain_text()).isdigit() or s == "取消",
|
226
|
+
retry_prompt="输入错误,请重新输入一个正确的数字。\n剩余次数:{count}",
|
227
|
+
)
|
228
|
+
if result is None:
|
229
|
+
await finish("发送超时,已自动取消")
|
230
|
+
text = result.extract_plain_text()
|
231
|
+
if text == "取消":
|
232
|
+
await finish("已取消操作")
|
233
|
+
probability.result = float(text)
|
234
|
+
|
235
|
+
if not 0 <= probability.result <= 100:
|
236
|
+
await finish("输入错误,概率应在 0 到 100 之间")
|
237
|
+
|
238
|
+
data = PresetData.load()
|
239
|
+
data.joker_probability = probability.result / 100
|
240
|
+
data.save()
|
241
|
+
await finish(f"设置成功: 小丑概率 {probability.result:.1f}%")
|
242
|
+
|
243
|
+
|
244
|
+
@edit_preset.assign("reset")
|
245
|
+
async def reset_preset() -> None:
|
246
|
+
PresetData().save()
|
247
|
+
await finish("已重置为默认预设")
|
248
|
+
|
249
|
+
|
250
|
+
@edit_preset.handle()
|
251
|
+
async def handle_default() -> None:
|
252
|
+
lines = ["当前游戏预设:", ""]
|
253
|
+
data = PresetData.load()
|
254
|
+
|
255
|
+
for total, (w, p, c) in data.role_preset.items():
|
256
|
+
lines.append(f"{total} 人: 狼人x{w}, 神职x{p}, 平民x{c}")
|
257
|
+
lines.append("")
|
258
|
+
|
259
|
+
lines.append("狼人优先级: " + display_roles(data.werewolf_priority))
|
260
|
+
lines.append("神职优先级: " + display_roles(data.priesthood_proirity))
|
261
|
+
lines.append(f"小丑概率: {data.joker_probability:.0%}")
|
262
|
+
|
263
|
+
await finish("\n".join(lines))
|
@@ -1,6 +1,6 @@
|
|
1
|
-
from nonebot import
|
1
|
+
from nonebot import on_message
|
2
2
|
from nonebot.adapters import Event
|
3
|
-
from nonebot_plugin_alconna import MsgTarget, UniMessage, UniMsg
|
3
|
+
from nonebot_plugin_alconna import Alconna, MsgTarget, UniMessage, UniMsg, on_alconna
|
4
4
|
|
5
5
|
from ..constant import STOP_COMMAND
|
6
6
|
from ..utils import InputStore
|
@@ -17,7 +17,12 @@ async def handle_input(event: Event, target: MsgTarget, msg: UniMsg) -> None:
|
|
17
17
|
InputStore.put(msg, event.get_user_id(), target.id)
|
18
18
|
|
19
19
|
|
20
|
-
stopcmd =
|
20
|
+
stopcmd = on_alconna(
|
21
|
+
Alconna("stop"),
|
22
|
+
rule=rule_in_game,
|
23
|
+
block=True,
|
24
|
+
use_cmd_start=True,
|
25
|
+
)
|
21
26
|
|
22
27
|
|
23
28
|
@stopcmd.handle()
|
@@ -1,73 +1,128 @@
|
|
1
|
+
import json
|
1
2
|
import re
|
2
3
|
|
3
4
|
import anyio
|
4
5
|
import nonebot
|
5
6
|
import nonebot_plugin_waiter as waiter
|
6
|
-
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
|
7
|
-
from nonebot import on_command
|
8
7
|
from nonebot.adapters import Bot, Event
|
9
|
-
from nonebot.
|
8
|
+
from nonebot.internal.matcher import current_bot
|
9
|
+
from nonebot.permission import SUPERUSER
|
10
|
+
from nonebot.rule import Rule, to_me
|
11
|
+
from nonebot.typing import T_State
|
10
12
|
from nonebot.utils import escape_tag
|
11
|
-
from nonebot_plugin_alconna import
|
13
|
+
from nonebot_plugin_alconna import (
|
14
|
+
Alconna,
|
15
|
+
Button,
|
16
|
+
FallbackStrategy,
|
17
|
+
MsgTarget,
|
18
|
+
Option,
|
19
|
+
Target,
|
20
|
+
UniMessage,
|
21
|
+
UniMsg,
|
22
|
+
on_alconna,
|
23
|
+
)
|
24
|
+
from nonebot_plugin_localstore import get_plugin_data_file
|
12
25
|
from nonebot_plugin_uninfo import QryItrface, Uninfo
|
13
26
|
|
14
|
-
from ..config import config
|
27
|
+
from ..config import PresetData, config
|
15
28
|
from ..constant import STOP_COMMAND_PROMPT
|
16
29
|
from ..game import Game
|
17
|
-
from ..utils import extract_session_member_nick
|
30
|
+
from ..utils import ObjectStream, extract_session_member_nick
|
18
31
|
from .depends import rule_not_in_game
|
19
32
|
from .poke import poke_enabled
|
20
33
|
|
21
|
-
start_game =
|
22
|
-
|
34
|
+
start_game = on_alconna(
|
35
|
+
Alconna(
|
36
|
+
"werewolf",
|
37
|
+
Option("restart|--restart|重开", dest="restart"),
|
38
|
+
),
|
23
39
|
rule=to_me() & rule_not_in_game,
|
24
40
|
aliases={"狼人杀"},
|
41
|
+
use_cmd_start=True,
|
25
42
|
)
|
43
|
+
player_data_file = get_plugin_data_file("players.json")
|
44
|
+
if not player_data_file.exists():
|
45
|
+
player_data_file.write_text("[]")
|
26
46
|
|
27
47
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
48
|
+
def dump_players(target: Target, players: dict[str, str]) -> None:
|
49
|
+
data: list[dict] = json.loads(player_data_file.read_text(encoding="utf-8"))
|
50
|
+
|
51
|
+
for item in data:
|
52
|
+
if Target.load(item["target"]).verify(target):
|
53
|
+
item["players"] = players
|
54
|
+
break
|
55
|
+
else:
|
56
|
+
data.append({"target": target.dump(), "players": players})
|
57
|
+
|
58
|
+
player_data_file.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
|
59
|
+
|
60
|
+
|
61
|
+
def load_players(target: Target) -> dict[str, str] | None:
|
62
|
+
data: list[dict] = json.loads(player_data_file.read_text(encoding="utf-8"))
|
63
|
+
|
64
|
+
for item in data:
|
65
|
+
if Target.load(item["target"]).verify(target):
|
66
|
+
return item["players"]
|
67
|
+
return None
|
68
|
+
|
69
|
+
|
70
|
+
def solve_button(msg: UniMessage) -> UniMessage:
|
71
|
+
if config.enable_button:
|
72
|
+
msg.keyboard(
|
73
|
+
*[
|
74
|
+
Button("input", i, text=i)
|
75
|
+
for i in ["加入游戏", "退出游戏", "当前玩家", "开始游戏", "结束游戏"]
|
76
|
+
]
|
77
|
+
)
|
78
|
+
return msg
|
32
79
|
|
33
80
|
|
34
81
|
async def _prepare_receive(
|
35
|
-
stream:
|
36
|
-
|
82
|
+
stream: ObjectStream[tuple[Event, str, str]],
|
83
|
+
event_type: str,
|
37
84
|
group: Target,
|
38
85
|
) -> None:
|
86
|
+
@Rule
|
39
87
|
async def same_group(target: MsgTarget) -> bool:
|
40
88
|
return group.verify(target)
|
41
89
|
|
42
90
|
@waiter.waiter(
|
43
|
-
waits=[
|
91
|
+
waits=[event_type],
|
44
92
|
keep_session=False,
|
45
|
-
rule=
|
93
|
+
rule=same_group & rule_not_in_game,
|
46
94
|
)
|
47
95
|
def wait(event: Event, msg: UniMsg, session: Uninfo) -> tuple[Event, str, str]:
|
48
96
|
text = msg.extract_plain_text().strip()
|
49
97
|
name = extract_session_member_nick(session) or event.get_user_id()
|
50
|
-
return (event, text, name)
|
98
|
+
return (event, text, re.sub(r"[\u2066-\u2069]", "", name))
|
51
99
|
|
52
|
-
async for
|
53
|
-
if
|
100
|
+
async for event, text, name in wait(default=(None, "", "")):
|
101
|
+
if event is None:
|
54
102
|
continue
|
55
|
-
await stream.send((
|
103
|
+
await stream.send((event, text, name))
|
56
104
|
|
57
105
|
|
58
106
|
async def _prepare_handle(
|
59
|
-
stream:
|
107
|
+
stream: ObjectStream[tuple[Event, str, str]],
|
60
108
|
players: dict[str, str],
|
61
109
|
admin_id: str,
|
62
|
-
finished: anyio.Event,
|
63
110
|
) -> None:
|
64
111
|
logger = nonebot.logger.opt(colors=True)
|
65
112
|
|
66
|
-
async def send(msg: str,
|
67
|
-
|
113
|
+
async def send(msg: str, /, *, button: bool = True) -> None:
|
114
|
+
message = UniMessage.text(msg)
|
115
|
+
if button:
|
116
|
+
message = solve_button(message)
|
117
|
+
|
118
|
+
await message.send(
|
119
|
+
target=event,
|
120
|
+
reply_to=True,
|
121
|
+
fallback=FallbackStrategy.ignore,
|
122
|
+
)
|
68
123
|
|
69
|
-
while
|
70
|
-
event, text, name = await stream.
|
124
|
+
while not stream.closed:
|
125
|
+
event, text, name = await stream.recv()
|
71
126
|
user_id = event.get_user_id()
|
72
127
|
colored = f"<y>{escape_tag(name)}</y>(<c>{escape_tag(user_id)}</c>)"
|
73
128
|
|
@@ -80,7 +135,7 @@ async def _prepare_handle(
|
|
80
135
|
match (text, user_id == admin_id):
|
81
136
|
case ("开始游戏", True):
|
82
137
|
player_num = len(players)
|
83
|
-
role_preset =
|
138
|
+
role_preset = PresetData.load().role_preset
|
84
139
|
if player_num < min(role_preset):
|
85
140
|
await send(
|
86
141
|
f"⚠️游戏至少需要 {min(role_preset)} 人, "
|
@@ -96,7 +151,7 @@ async def _prepare_handle(
|
|
96
151
|
else:
|
97
152
|
await send("✏️游戏即将开始...")
|
98
153
|
logger.info(f"游戏发起者 {colored} 开始游戏")
|
99
|
-
|
154
|
+
stream.close()
|
100
155
|
players["#$start_game$#"] = user_id
|
101
156
|
return
|
102
157
|
|
@@ -105,12 +160,17 @@ async def _prepare_handle(
|
|
105
160
|
|
106
161
|
case ("结束游戏", True):
|
107
162
|
logger.info(f"游戏发起者 {colored} 结束游戏")
|
108
|
-
await send("ℹ️已结束当前游戏")
|
109
|
-
|
163
|
+
await send("ℹ️已结束当前游戏", button=False)
|
164
|
+
stream.close()
|
110
165
|
return
|
111
166
|
|
112
167
|
case ("结束游戏", False):
|
113
|
-
await
|
168
|
+
if await SUPERUSER(current_bot.get(), event):
|
169
|
+
logger.info(f"超级用户 {colored} 结束游戏")
|
170
|
+
await send("ℹ️已结束当前游戏", button=False)
|
171
|
+
stream.close()
|
172
|
+
return
|
173
|
+
await send("⚠️只有游戏发起者或超级用户可以结束游戏")
|
114
174
|
|
115
175
|
case ("加入游戏", True):
|
116
176
|
await send("ℹ️游戏发起者已经加入游戏了")
|
@@ -149,18 +209,17 @@ async def prepare_game(event: Event, players: dict[str, str]) -> None:
|
|
149
209
|
group = UniMessage.get_target(event)
|
150
210
|
Game.starting_games[group] = players
|
151
211
|
|
152
|
-
|
153
|
-
send, recv = anyio.create_memory_object_stream[tuple[Event, str, str]]()
|
212
|
+
stream = ObjectStream[tuple[Event, str, str]](16)
|
154
213
|
|
155
214
|
async def _handle_cancel() -> None:
|
156
|
-
await
|
215
|
+
await stream.wait_closed()
|
157
216
|
tg.cancel_scope.cancel()
|
158
217
|
|
159
218
|
try:
|
160
219
|
async with anyio.create_task_group() as tg:
|
161
220
|
tg.start_soon(_handle_cancel)
|
162
|
-
tg.start_soon(_prepare_receive,
|
163
|
-
tg.start_soon(_prepare_handle,
|
221
|
+
tg.start_soon(_prepare_receive, stream, event.get_type(), group)
|
222
|
+
tg.start_soon(_prepare_handle, stream, players, admin_id)
|
164
223
|
except Exception as err:
|
165
224
|
await UniMessage(f"狼人杀准备阶段出现未知错误: {err!r}").send()
|
166
225
|
|
@@ -169,6 +228,47 @@ async def prepare_game(event: Event, players: dict[str, str]) -> None:
|
|
169
228
|
await start_game.finish()
|
170
229
|
|
171
230
|
|
231
|
+
@start_game.handle()
|
232
|
+
async def handle_notice(target: MsgTarget, state: T_State) -> None:
|
233
|
+
if target.private:
|
234
|
+
await UniMessage("⚠️请在群组中创建新游戏").finish(reply_to=True)
|
235
|
+
if any(target.verify(g.group) for g in Game.running_games):
|
236
|
+
await (
|
237
|
+
UniMessage.text("⚠️当前群组内有正在进行的游戏\n")
|
238
|
+
.text("无法开始新游戏")
|
239
|
+
.finish(reply_to=True)
|
240
|
+
)
|
241
|
+
|
242
|
+
msg = (
|
243
|
+
UniMessage.text("🎉成功创建游戏\n\n")
|
244
|
+
.text(" 玩家请发送 “加入游戏”、“退出游戏”\n")
|
245
|
+
.text(" 玩家发送 “当前玩家” 可查看玩家列表\n")
|
246
|
+
.text(" 游戏发起者发送 “结束游戏” 可结束当前游戏\n")
|
247
|
+
.text(" 玩家均加入后,游戏发起者请发送 “开始游戏”\n")
|
248
|
+
)
|
249
|
+
if poke_enabled():
|
250
|
+
msg.text(f"\n💫可使用戳一戳代替游戏交互中的 “{STOP_COMMAND_PROMPT}” 命令\n")
|
251
|
+
msg.text("\nℹ️游戏准备阶段限时5分钟,超时将自动结束")
|
252
|
+
await solve_button(msg).send(reply_to=True, fallback=FallbackStrategy.ignore)
|
253
|
+
|
254
|
+
state["players"] = {}
|
255
|
+
|
256
|
+
|
257
|
+
@start_game.assign("restart")
|
258
|
+
async def handle_restart(target: MsgTarget, state: T_State) -> None:
|
259
|
+
players = load_players(target)
|
260
|
+
if players is None:
|
261
|
+
await UniMessage.text("ℹ️未找到历史游戏记录,将创建新游戏").send()
|
262
|
+
return
|
263
|
+
|
264
|
+
msg = UniMessage.text("🎉成功加载上次游戏:\n")
|
265
|
+
for user in players:
|
266
|
+
msg.text("\n- ").at(user)
|
267
|
+
await msg.send()
|
268
|
+
|
269
|
+
state["players"] = players
|
270
|
+
|
271
|
+
|
172
272
|
@start_game.handle()
|
173
273
|
async def handle_start(
|
174
274
|
bot: Bot,
|
@@ -176,21 +276,12 @@ async def handle_start(
|
|
176
276
|
target: MsgTarget,
|
177
277
|
session: Uninfo,
|
178
278
|
interface: QryItrface,
|
279
|
+
state: T_State,
|
179
280
|
) -> None:
|
281
|
+
players: dict[str, str] = state["players"]
|
180
282
|
admin_id = event.get_user_id()
|
181
|
-
msg = (
|
182
|
-
UniMessage.text("🎉成功创建游戏\n\n")
|
183
|
-
.text(" 玩家请 @我 发送 “加入游戏”、“退出游戏”\n")
|
184
|
-
.text(" 玩家 @我 发送 “当前玩家” 可查看玩家列表\n")
|
185
|
-
.text(" 游戏发起者 @我 发送 “结束游戏” 可结束当前游戏\n")
|
186
|
-
.text(" 玩家均加入后,游戏发起者请 @我 发送 “开始游戏”\n")
|
187
|
-
)
|
188
|
-
if poke_enabled():
|
189
|
-
msg.text(f"\n可使用戳一戳代替游戏交互中的 “{STOP_COMMAND_PROMPT}” 命令\n")
|
190
|
-
await msg.text("\n游戏准备阶段限时5分钟,超时将自动结束").send(reply_to=True)
|
191
|
-
|
192
283
|
admin_name = extract_session_member_nick(session) or admin_id
|
193
|
-
players =
|
284
|
+
players[admin_id] = admin_name
|
194
285
|
|
195
286
|
try:
|
196
287
|
with anyio.fail_after(5 * 60):
|
@@ -198,5 +289,6 @@ async def handle_start(
|
|
198
289
|
except TimeoutError:
|
199
290
|
await UniMessage.text("⚠️游戏准备超时,已自动结束").finish()
|
200
291
|
|
292
|
+
dump_players(target, players)
|
201
293
|
game = Game(bot, target, set(players), interface)
|
202
294
|
await game.start()
|
@@ -0,0 +1,24 @@
|
|
1
|
+
from nonebot.permission import SUPERUSER
|
2
|
+
from nonebot.rule import to_me
|
3
|
+
from nonebot_plugin_alconna import Alconna, MsgTarget, UniMessage, on_alconna
|
4
|
+
|
5
|
+
from ..game import Game
|
6
|
+
|
7
|
+
|
8
|
+
def rule_game_running(target: MsgTarget) -> bool:
|
9
|
+
return any(target.verify(g.group) for g in Game.running_games)
|
10
|
+
|
11
|
+
|
12
|
+
force_stop = on_alconna(
|
13
|
+
Alconna("中止游戏"),
|
14
|
+
rule=to_me() & rule_game_running,
|
15
|
+
permission=SUPERUSER,
|
16
|
+
use_cmd_start=True,
|
17
|
+
)
|
18
|
+
|
19
|
+
|
20
|
+
@force_stop.handle()
|
21
|
+
async def _(target: MsgTarget) -> None:
|
22
|
+
game = next(g for g in Game.running_games if target.verify(g.group))
|
23
|
+
game.terminate()
|
24
|
+
await UniMessage.text("已中止当前群组的游戏进程").finish(reply_to=True)
|
@@ -0,0 +1,73 @@
|
|
1
|
+
import dataclasses
|
2
|
+
from enum import Enum, auto
|
3
|
+
from typing import TYPE_CHECKING
|
4
|
+
|
5
|
+
if TYPE_CHECKING:
|
6
|
+
from .players import Player
|
7
|
+
|
8
|
+
|
9
|
+
class Role(int, Enum):
|
10
|
+
# 狼人
|
11
|
+
Werewolf = 1
|
12
|
+
WolfKing = 2
|
13
|
+
|
14
|
+
# 神职
|
15
|
+
Prophet = 11
|
16
|
+
Witch = 12
|
17
|
+
Hunter = 13
|
18
|
+
Guard = 14
|
19
|
+
Idiot = 15
|
20
|
+
|
21
|
+
# 其他
|
22
|
+
Joker = 51
|
23
|
+
|
24
|
+
# 平民
|
25
|
+
Civilian = 0
|
26
|
+
|
27
|
+
|
28
|
+
class RoleGroup(Enum):
|
29
|
+
Werewolf = auto()
|
30
|
+
GoodGuy = auto()
|
31
|
+
Others = auto()
|
32
|
+
|
33
|
+
|
34
|
+
class KillReason(Enum):
|
35
|
+
Werewolf = auto()
|
36
|
+
Poison = auto()
|
37
|
+
Shoot = auto()
|
38
|
+
Vote = auto()
|
39
|
+
|
40
|
+
|
41
|
+
class GameStatus(Enum):
|
42
|
+
GoodGuy = auto()
|
43
|
+
Werewolf = auto()
|
44
|
+
Joker = auto()
|
45
|
+
|
46
|
+
|
47
|
+
@dataclasses.dataclass
|
48
|
+
class GameState:
|
49
|
+
day: int
|
50
|
+
"""当前天数记录, 不会被 `reset()` 重置"""
|
51
|
+
killed: "Player | None" = None
|
52
|
+
"""当晚狼人击杀目标, `None` 则为空刀"""
|
53
|
+
shoot: "Player | None" = None
|
54
|
+
"""当前执行射杀操作的玩家"""
|
55
|
+
antidote: set["Player"] = dataclasses.field(default_factory=set)
|
56
|
+
"""当晚女巫使用解药的目标"""
|
57
|
+
poison: set["Player"] = dataclasses.field(default_factory=set)
|
58
|
+
"""当晚使用了毒药的女巫"""
|
59
|
+
protected: set["Player"] = dataclasses.field(default_factory=set)
|
60
|
+
"""当晚守卫保护的目标"""
|
61
|
+
|
62
|
+
def reset(self) -> None:
|
63
|
+
self.killed = None
|
64
|
+
self.shoot = None
|
65
|
+
self.antidote = set()
|
66
|
+
self.poison = set()
|
67
|
+
self.protected = set()
|
68
|
+
|
69
|
+
|
70
|
+
@dataclasses.dataclass
|
71
|
+
class KillInfo:
|
72
|
+
reason: KillReason
|
73
|
+
killers: list[str]
|