nonebot-plugin-werewolf 1.1.1__py3-none-any.whl → 1.1.3__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 +8 -4
- nonebot_plugin_werewolf/_timeout.py +110 -0
- nonebot_plugin_werewolf/config.py +15 -18
- nonebot_plugin_werewolf/constant.py +18 -3
- nonebot_plugin_werewolf/exception.py +1 -1
- nonebot_plugin_werewolf/game.py +91 -110
- nonebot_plugin_werewolf/matchers/__init__.py +2 -0
- nonebot_plugin_werewolf/matchers/message_in_game.py +15 -0
- nonebot_plugin_werewolf/{ob11_ext.py → matchers/ob11_ext.py} +26 -20
- nonebot_plugin_werewolf/matchers/start_game.py +56 -0
- nonebot_plugin_werewolf/player_set.py +9 -7
- nonebot_plugin_werewolf/players/__init__.py +10 -0
- nonebot_plugin_werewolf/players/can_shoot.py +59 -0
- nonebot_plugin_werewolf/players/civilian.py +7 -0
- nonebot_plugin_werewolf/players/guard.py +37 -0
- nonebot_plugin_werewolf/players/hunter.py +8 -0
- nonebot_plugin_werewolf/players/idiot.py +44 -0
- nonebot_plugin_werewolf/players/joker.py +21 -0
- nonebot_plugin_werewolf/players/player.py +161 -0
- nonebot_plugin_werewolf/players/prophet.py +30 -0
- nonebot_plugin_werewolf/players/werewolf.py +67 -0
- nonebot_plugin_werewolf/players/witch.py +72 -0
- nonebot_plugin_werewolf/players/wolfking.py +14 -0
- nonebot_plugin_werewolf/utils.py +83 -65
- {nonebot_plugin_werewolf-1.1.1.dist-info → nonebot_plugin_werewolf-1.1.3.dist-info}/METADATA +20 -8
- nonebot_plugin_werewolf-1.1.3.dist-info/RECORD +29 -0
- {nonebot_plugin_werewolf-1.1.1.dist-info → nonebot_plugin_werewolf-1.1.3.dist-info}/WHEEL +1 -1
- nonebot_plugin_werewolf/matchers.py +0 -63
- nonebot_plugin_werewolf/player.py +0 -455
- nonebot_plugin_werewolf-1.1.1.dist-info/RECORD +0 -15
- {nonebot_plugin_werewolf-1.1.1.dist-info → nonebot_plugin_werewolf-1.1.3.dist-info}/LICENSE +0 -0
- {nonebot_plugin_werewolf-1.1.1.dist-info → nonebot_plugin_werewolf-1.1.3.dist-info}/top_level.txt +0 -0
nonebot_plugin_werewolf/game.py
CHANGED
@@ -1,34 +1,30 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import asyncio
|
4
|
-
import asyncio.timeouts
|
5
4
|
import contextlib
|
6
5
|
import secrets
|
7
|
-
from typing import TYPE_CHECKING, NoReturn
|
6
|
+
from typing import TYPE_CHECKING, ClassVar, NoReturn
|
8
7
|
|
9
8
|
from nonebot.log import logger
|
10
9
|
from nonebot_plugin_alconna import At, Target, UniMessage
|
11
10
|
|
11
|
+
from ._timeout import timeout
|
12
12
|
from .config import config
|
13
13
|
from .constant import GameState, GameStatus, KillReason, Role, RoleGroup, role_name_conv
|
14
|
-
from .exception import
|
15
|
-
from .player import Player
|
14
|
+
from .exception import GameFinished
|
16
15
|
from .player_set import PlayerSet
|
16
|
+
from .players import Player
|
17
17
|
from .utils import InputStore
|
18
18
|
|
19
19
|
if TYPE_CHECKING:
|
20
20
|
from nonebot.adapters import Bot
|
21
21
|
from nonebot_plugin_alconna.uniseg.message import Receipt
|
22
22
|
|
23
|
-
starting_games: dict[str, dict[str, str]] = {}
|
24
|
-
running_games: dict[str, Game] = {}
|
25
|
-
|
26
23
|
|
27
24
|
def init_players(bot: Bot, game: Game, players: dict[str, str]) -> PlayerSet:
|
28
25
|
logger.opt(colors=True).debug(f"初始化 <c>{game.group.id}</c> 的玩家职业")
|
29
26
|
role_preset = config.get_role_preset()
|
30
|
-
preset
|
31
|
-
if preset is None:
|
27
|
+
if (preset := role_preset.get(len(players))) is None:
|
32
28
|
raise ValueError(
|
33
29
|
f"玩家人数不符: "
|
34
30
|
f"应为 {', '.join(map(str, role_preset))} 人, 传入{len(players)}人"
|
@@ -44,34 +40,22 @@ def init_players(bot: Bot, game: Game, players: dict[str, str]) -> PlayerSet:
|
|
44
40
|
roles.remove(Role.Civilian)
|
45
41
|
roles.append(Role.Joker)
|
46
42
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
async def selector(target_: Target, b: Bot) -> bool:
|
55
|
-
return target_.self_id == bot.self_id and b is bot
|
56
|
-
|
57
|
-
return PlayerSet(
|
58
|
-
Player.new(
|
59
|
-
role,
|
60
|
-
bot,
|
61
|
-
game,
|
62
|
-
Target(
|
63
|
-
user_id,
|
64
|
-
private=True,
|
65
|
-
self_id=bot.self_id,
|
66
|
-
selector=selector,
|
67
|
-
),
|
68
|
-
players[user_id],
|
69
|
-
)
|
70
|
-
for user_id, role in zip(players, shuffled, strict=True)
|
43
|
+
def _select_role() -> Role:
|
44
|
+
return roles.pop(secrets.randbelow(len(roles)))
|
45
|
+
|
46
|
+
player_set = PlayerSet(
|
47
|
+
Player.new(_select_role(), bot, game, user_id, players[user_id])
|
48
|
+
for user_id in players
|
71
49
|
)
|
50
|
+
logger.debug(f"职业分配完成: {player_set}")
|
51
|
+
|
52
|
+
return player_set
|
72
53
|
|
73
54
|
|
74
55
|
class Game:
|
56
|
+
starting_games: ClassVar[dict[Target, dict[str, str]]] = {}
|
57
|
+
running_games: ClassVar[set[Game]] = set()
|
58
|
+
|
75
59
|
bot: Bot
|
76
60
|
group: Target
|
77
61
|
players: PlayerSet
|
@@ -112,92 +96,82 @@ class Game:
|
|
112
96
|
|
113
97
|
# 狼人数量大于其他职业数量
|
114
98
|
if w.size >= p.size:
|
115
|
-
raise
|
99
|
+
raise GameFinished(GameStatus.Werewolf)
|
116
100
|
# 屠边-村民/中立全灭
|
117
101
|
if not p.select(Role.Civilian, RoleGroup.Others).size:
|
118
|
-
raise
|
102
|
+
raise GameFinished(GameStatus.Werewolf)
|
119
103
|
# 屠边-神职全灭
|
120
104
|
if not p.exclude(Role.Civilian).size:
|
121
|
-
raise
|
105
|
+
raise GameFinished(GameStatus.Werewolf)
|
122
106
|
# 狼人全灭
|
123
107
|
if not w.size:
|
124
|
-
raise
|
108
|
+
raise GameFinished(GameStatus.GoodGuy)
|
125
109
|
|
126
110
|
def show_killed_players(self) -> str:
|
127
|
-
|
111
|
+
result: list[str] = []
|
128
112
|
|
129
113
|
for player in self.killed_players:
|
130
114
|
if player.kill_info is None:
|
131
115
|
continue
|
132
116
|
|
133
|
-
|
117
|
+
line = f"{player.name} 被 " + ", ".join(
|
134
118
|
p.name for p in player.kill_info.killers
|
135
119
|
)
|
136
120
|
match player.kill_info.reason:
|
137
121
|
case KillReason.Werewolf:
|
138
|
-
|
122
|
+
line = f"🔪 {line} 刀了"
|
139
123
|
case KillReason.Poison:
|
140
|
-
|
124
|
+
line = f"🧪 {line} 毒死"
|
141
125
|
case KillReason.Shoot:
|
142
|
-
|
126
|
+
line = f"🔫 {line} 射杀"
|
143
127
|
case KillReason.Vote:
|
144
|
-
|
145
|
-
|
128
|
+
line = f"🗳️ {line} 票出"
|
129
|
+
result.append(line)
|
146
130
|
|
147
|
-
return
|
131
|
+
return "\n\n".join(result)
|
148
132
|
|
149
133
|
async def notify_player_role(self) -> None:
|
150
134
|
preset = config.get_role_preset()[len(self.players)]
|
151
135
|
await asyncio.gather(
|
152
136
|
self.send(
|
153
137
|
self.at_all()
|
154
|
-
.text("\n\n
|
138
|
+
.text("\n\n📝正在分配职业,请注意查看私聊消息\n")
|
155
139
|
.text(f"当前玩家数: {len(self.players)}\n")
|
156
140
|
.text(f"职业分配: 狼人x{preset[0]}, 神职x{preset[1]}, 平民x{preset[2]}")
|
157
141
|
),
|
158
142
|
*[p.notify_role() for p in self.players],
|
159
143
|
)
|
160
144
|
|
161
|
-
async def wait_stop(
|
162
|
-
self,
|
163
|
-
players: Player | PlayerSet,
|
164
|
-
timeout_secs: float,
|
165
|
-
) -> None:
|
166
|
-
if isinstance(players, Player):
|
167
|
-
players = PlayerSet([players])
|
168
|
-
|
145
|
+
async def wait_stop(self, *players: Player, timeout_secs: float) -> None:
|
169
146
|
async def wait(p: Player) -> None:
|
170
147
|
while True:
|
171
148
|
msg = await InputStore.fetch(p.user_id, self.group.id)
|
172
|
-
if msg.extract_plain_text() == "/stop":
|
149
|
+
if msg.extract_plain_text().strip() == "/stop":
|
173
150
|
break
|
174
151
|
|
175
152
|
with contextlib.suppress(TimeoutError):
|
176
|
-
async with
|
153
|
+
async with timeout(timeout_secs):
|
177
154
|
await asyncio.gather(*[wait(p) for p in players])
|
178
155
|
|
179
156
|
async def interact(
|
180
157
|
self,
|
181
|
-
|
158
|
+
player_type: Player | Role | RoleGroup,
|
182
159
|
timeout_secs: float,
|
183
160
|
) -> None:
|
184
|
-
players = self.players.alive().select(
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
)
|
194
|
-
|
195
|
-
await players.broadcast(f"{text}交互开始,限时 {timeout_secs/60:.2f} 分钟")
|
161
|
+
players = self.players.alive().select(player_type)
|
162
|
+
if isinstance(player_type, Player):
|
163
|
+
text = player_type.role_name
|
164
|
+
elif isinstance(player_type, Role):
|
165
|
+
text = role_name_conv[player_type]
|
166
|
+
else: # RoleGroup
|
167
|
+
text = f"{role_name_conv[player_type]}阵营"
|
168
|
+
|
169
|
+
await players.broadcast(f"✏️{text}交互开始,限时 {timeout_secs/60:.2f} 分钟")
|
196
170
|
try:
|
197
171
|
await players.interact(timeout_secs)
|
198
172
|
except TimeoutError:
|
199
|
-
logger.opt(colors=True).debug(f"{text}交互超时 (<y>{timeout_secs}</y>s)")
|
200
|
-
await players.broadcast(f"{text}交互时间结束")
|
173
|
+
logger.opt(colors=True).debug(f"⚠️{text}交互超时 (<y>{timeout_secs}</y>s)")
|
174
|
+
await players.broadcast(f"ℹ️{text}交互时间结束")
|
201
175
|
|
202
176
|
async def select_killed(self) -> None:
|
203
177
|
players = self.players.alive()
|
@@ -207,9 +181,9 @@ class Game:
|
|
207
181
|
await self.interact(RoleGroup.Werewolf, 120)
|
208
182
|
if (s := w.player_selected()).size == 1:
|
209
183
|
self.state.killed = s.pop()
|
210
|
-
await w.broadcast(f"
|
184
|
+
await w.broadcast(f"🔪今晚选择的目标为: {self.state.killed.name}")
|
211
185
|
else:
|
212
|
-
await w.broadcast("
|
186
|
+
await w.broadcast("⚠️狼人阵营意见未统一,此晚空刀")
|
213
187
|
|
214
188
|
# 如果女巫存活,正常交互,限时1分钟
|
215
189
|
if players.include(Role.Witch):
|
@@ -226,12 +200,12 @@ class Game:
|
|
226
200
|
|
227
201
|
await asyncio.gather(
|
228
202
|
players.broadcast(
|
229
|
-
"
|
203
|
+
"ℹ️你已加入死者频道,请勿在群内继续发言\n"
|
230
204
|
"私聊发送消息将转发至其他已死亡玩家"
|
231
205
|
),
|
232
206
|
self.players.dead()
|
233
207
|
.exclude(*players)
|
234
|
-
.broadcast(f"
|
208
|
+
.broadcast(f"ℹ️玩家 {', '.join(p.name for p in players)} 加入了死者频道"),
|
235
209
|
)
|
236
210
|
|
237
211
|
async def post_kill(self, players: Player | PlayerSet) -> None:
|
@@ -253,7 +227,7 @@ class Game:
|
|
253
227
|
.text(f" 被{shooter.role_name}射杀, 请发表遗言\n")
|
254
228
|
.text("限时1分钟, 发送 “/stop” 结束发言")
|
255
229
|
)
|
256
|
-
await self.wait_stop(shoot, 60)
|
230
|
+
await self.wait_stop(shoot, timeout_secs=60)
|
257
231
|
self.state.shoot = (None, None)
|
258
232
|
await self.post_kill(shoot)
|
259
233
|
|
@@ -271,34 +245,35 @@ class Game:
|
|
271
245
|
logger.debug(f"投票结果: {vote_result}")
|
272
246
|
|
273
247
|
# 投票结果公示
|
274
|
-
msg = UniMessage.text("
|
248
|
+
msg = UniMessage.text("📊投票结果:\n")
|
275
249
|
for p, v in sorted(vote_result.items(), key=lambda x: len(x[1]), reverse=True):
|
276
250
|
if p is not None:
|
277
251
|
msg.at(p.user_id).text(f": {len(v)} 票\n")
|
278
252
|
vote_reversed[len(v)] = [*vote_reversed.get(len(v), []), p]
|
279
253
|
if v := (len(players) - total_votes):
|
280
|
-
msg.text(f"弃票: {v} 票\n")
|
281
|
-
await self.send(msg)
|
254
|
+
msg.text(f"弃票: {v} 票\n\n")
|
282
255
|
|
283
256
|
# 全员弃票 # 不是哥们?
|
284
257
|
if total_votes == 0:
|
285
|
-
await self.send("
|
258
|
+
await self.send(msg.text("🔨没有人被票出"))
|
286
259
|
return
|
287
260
|
|
288
261
|
# 弃票大于最高票
|
289
262
|
if (len(players) - total_votes) >= max(vote_reversed.keys()):
|
290
|
-
await self.send("
|
263
|
+
await self.send(msg.text("🔨弃票数大于最高票数, 没有人被票出"))
|
291
264
|
return
|
292
265
|
|
293
266
|
# 平票
|
294
267
|
if len(vs := vote_reversed[max(vote_reversed.keys())]) != 1:
|
295
268
|
await self.send(
|
296
|
-
|
269
|
+
msg.text("🔨玩家 ")
|
297
270
|
.text(", ".join(p.name for p in vs))
|
298
271
|
.text(" 平票, 没有人被票出")
|
299
272
|
)
|
300
273
|
return
|
301
274
|
|
275
|
+
await self.send(msg)
|
276
|
+
|
302
277
|
# 仅有一名玩家票数最高
|
303
278
|
voted = vs.pop()
|
304
279
|
if not await voted.kill(KillReason.Vote, *vote_result[voted]):
|
@@ -307,15 +282,15 @@ class Game:
|
|
307
282
|
|
308
283
|
# 遗言
|
309
284
|
await self.send(
|
310
|
-
UniMessage.text("
|
285
|
+
UniMessage.text("🔨玩家 ")
|
311
286
|
.at(voted.user_id)
|
312
287
|
.text(" 被投票放逐, 请发表遗言\n")
|
313
288
|
.text("限时1分钟, 发送 “/stop” 结束发言")
|
314
289
|
)
|
315
|
-
await self.wait_stop(voted, 60)
|
290
|
+
await self.wait_stop(voted, timeout_secs=60)
|
316
291
|
await self.post_kill(voted)
|
317
292
|
|
318
|
-
async def run_dead_channel(self) ->
|
293
|
+
async def run_dead_channel(self) -> NoReturn:
|
319
294
|
loop = asyncio.get_event_loop()
|
320
295
|
queue: asyncio.Queue[tuple[Player, UniMessage]] = asyncio.Queue()
|
321
296
|
|
@@ -342,7 +317,7 @@ class Game:
|
|
342
317
|
await queue.put((player, msg))
|
343
318
|
loop.call_later(60, decrease)
|
344
319
|
else:
|
345
|
-
await player.send("
|
320
|
+
await player.send("❌发言频率超过限制, 该消息被屏蔽")
|
346
321
|
|
347
322
|
await asyncio.gather(send(), *[recv(p) for p in self.players])
|
348
323
|
|
@@ -357,22 +332,22 @@ class Game:
|
|
357
332
|
# 重置游戏状态,进入下一夜
|
358
333
|
self.state = GameState(day_count)
|
359
334
|
players = self.players.alive()
|
360
|
-
await self.send("
|
335
|
+
await self.send("🌙天黑请闭眼...")
|
361
336
|
|
362
337
|
# 狼人、预言家、守卫 同时交互,女巫在狼人后交互
|
363
338
|
await asyncio.gather(
|
364
339
|
self.select_killed(),
|
365
340
|
self.interact(Role.Prophet, 60),
|
366
341
|
self.interact(Role.Guard, 60),
|
367
|
-
players.select(Role.Witch).broadcast("
|
342
|
+
players.select(Role.Witch).broadcast("ℹ️请等待狼人决定目标..."),
|
368
343
|
players.exclude(
|
369
344
|
RoleGroup.Werewolf, Role.Prophet, Role.Witch, Role.Guard
|
370
|
-
).broadcast("
|
345
|
+
).broadcast("ℹ️请等待其他玩家结束交互..."),
|
371
346
|
)
|
372
347
|
|
373
348
|
# 狼人击杀目标
|
374
349
|
if (
|
375
|
-
(killed := self.state.killed) # 狼人未空刀
|
350
|
+
(killed := self.state.killed) is not None # 狼人未空刀
|
376
351
|
and killed not in self.state.protected # 守卫保护
|
377
352
|
and killed not in self.state.antidote # 女巫使用解药
|
378
353
|
):
|
@@ -383,46 +358,52 @@ class Game:
|
|
383
358
|
)
|
384
359
|
|
385
360
|
# 女巫操作目标
|
386
|
-
for witch
|
387
|
-
if
|
361
|
+
for witch in self.state.poison:
|
362
|
+
if witch.selected is None:
|
363
|
+
continue
|
364
|
+
if witch.selected not in self.state.protected: # 守卫未保护
|
388
365
|
# 女巫毒杀玩家
|
389
|
-
await
|
366
|
+
await witch.selected.kill(KillReason.Poison, witch)
|
390
367
|
|
391
368
|
day_count += 1
|
392
|
-
msg = UniMessage.text(f"『第{day_count}
|
369
|
+
msg = UniMessage.text(f"『第{day_count}天』☀️天亮了...\n")
|
393
370
|
# 没有玩家死亡,平安夜
|
394
371
|
if not (dead := players.dead()):
|
395
372
|
await self.send(msg.text("昨晚是平安夜"))
|
396
373
|
# 有玩家死亡,公布死者名单
|
397
374
|
else:
|
398
|
-
msg.text("
|
399
|
-
for p in dead.sorted
|
375
|
+
msg.text("☠️昨晚的死者是:")
|
376
|
+
for p in dead.sorted:
|
400
377
|
msg.text("\n").at(p.user_id)
|
401
378
|
await self.send(msg)
|
402
379
|
|
403
380
|
# 第一晚被狼人杀死的玩家发表遗言
|
404
381
|
if day_count == 1 and killed is not None and not killed.alive:
|
405
382
|
await self.send(
|
406
|
-
UniMessage.text("
|
383
|
+
UniMessage.text("⚙️当前为第一天\n请被狼人杀死的 ")
|
407
384
|
.at(killed.user_id)
|
408
385
|
.text(" 发表遗言\n")
|
409
386
|
.text("限时1分钟, 发送 “/stop” 结束发言")
|
410
387
|
)
|
411
|
-
await self.wait_stop(killed, 60)
|
388
|
+
await self.wait_stop(killed, timeout_secs=60)
|
412
389
|
await self.post_kill(dead)
|
413
390
|
|
414
391
|
# 判断游戏状态
|
415
392
|
self.check_game_status()
|
416
393
|
|
417
394
|
# 公示存活玩家
|
418
|
-
await self.send(f"
|
395
|
+
await self.send(f"📝当前存活玩家: \n\n{self.players.alive().show()}")
|
419
396
|
|
420
397
|
# 开始自由讨论
|
421
|
-
await self.send(
|
422
|
-
|
398
|
+
await self.send(
|
399
|
+
"💬接下来开始自由讨论\n限时2分钟, 全员发送 “/stop” 结束发言"
|
400
|
+
)
|
401
|
+
await self.wait_stop(*self.players.alive(), timeout_secs=120)
|
423
402
|
|
424
403
|
# 开始投票
|
425
|
-
await self.send(
|
404
|
+
await self.send(
|
405
|
+
"🗳️讨论结束, 进入投票环节,限时1分钟\n请在私聊中进行投票交互"
|
406
|
+
)
|
426
407
|
await self.run_vote()
|
427
408
|
|
428
409
|
# 判断游戏状态
|
@@ -456,22 +437,22 @@ class Game:
|
|
456
437
|
game_task.result()
|
457
438
|
except asyncio.CancelledError:
|
458
439
|
logger.warning(f"{self.group.id} 的狼人杀游戏进程被取消")
|
459
|
-
except
|
440
|
+
except GameFinished as result:
|
460
441
|
await self.handle_game_finish(result.status)
|
461
442
|
logger.info(f"{self.group.id} 的狼人杀游戏进程正常退出")
|
462
443
|
except Exception as err:
|
463
444
|
msg = f"{self.group.id} 的狼人杀游戏进程出现未知错误: {err!r}"
|
464
445
|
logger.opt(exception=err).error(msg)
|
465
|
-
await self.send(
|
446
|
+
await self.send(f"❌狼人杀游戏进程出现未知错误: {err!r}")
|
466
447
|
finally:
|
467
448
|
dead_channel.cancel()
|
468
|
-
running_games.
|
449
|
+
self.running_games.discard(self)
|
469
450
|
|
470
451
|
def daemon_callback(task: asyncio.Task[None]) -> None:
|
471
452
|
if err := task.exception():
|
472
|
-
|
473
|
-
|
474
|
-
)
|
453
|
+
msg = f"{self.group.id} 的狼人杀守护进程出现错误: {err!r}"
|
454
|
+
logger.opt(exception=err).error(msg)
|
475
455
|
|
476
|
-
|
477
|
-
asyncio.create_task(daemon())
|
456
|
+
self.running_games.add(self)
|
457
|
+
daemon_task = asyncio.create_task(daemon())
|
458
|
+
daemon_task.add_done_callback(daemon_callback)
|
@@ -0,0 +1,15 @@
|
|
1
|
+
from nonebot import on_message
|
2
|
+
from nonebot.adapters import Event
|
3
|
+
from nonebot_plugin_alconna import MsgTarget, UniMsg
|
4
|
+
|
5
|
+
from ..utils import InputStore, rule_in_game
|
6
|
+
|
7
|
+
message_in_game = on_message(rule=rule_in_game)
|
8
|
+
|
9
|
+
|
10
|
+
@message_in_game.handle()
|
11
|
+
async def handle_input(event: Event, target: MsgTarget, msg: UniMsg) -> None:
|
12
|
+
if target.private:
|
13
|
+
InputStore.put(msg, target.id)
|
14
|
+
else:
|
15
|
+
InputStore.put(msg, event.get_user_id(), target.id)
|
@@ -1,12 +1,12 @@
|
|
1
1
|
import contextlib
|
2
2
|
|
3
|
-
from nonebot import
|
3
|
+
from nonebot import on_notice
|
4
4
|
from nonebot.internal.matcher import current_bot
|
5
|
-
from nonebot_plugin_alconna import UniMessage
|
5
|
+
from nonebot_plugin_alconna import MsgTarget, UniMessage
|
6
6
|
|
7
|
-
from
|
8
|
-
from
|
9
|
-
from
|
7
|
+
from ..config import config
|
8
|
+
from ..game import Game
|
9
|
+
from ..utils import InputStore, user_in_game
|
10
10
|
|
11
11
|
|
12
12
|
def ob11_ext_enabled() -> bool:
|
@@ -14,11 +14,11 @@ def ob11_ext_enabled() -> bool:
|
|
14
14
|
|
15
15
|
|
16
16
|
with contextlib.suppress(ImportError):
|
17
|
-
from nonebot.adapters.onebot.v11 import Bot
|
17
|
+
from nonebot.adapters.onebot.v11 import Bot
|
18
18
|
from nonebot.adapters.onebot.v11.event import PokeNotifyEvent
|
19
19
|
|
20
20
|
# 游戏内戳一戳等效 "/stop"
|
21
|
-
async def
|
21
|
+
async def _rule_poke_stop(bot: Bot, event: PokeNotifyEvent) -> bool:
|
22
22
|
if not config.enable_poke:
|
23
23
|
return False
|
24
24
|
|
@@ -27,19 +27,21 @@ with contextlib.suppress(ImportError):
|
|
27
27
|
return (
|
28
28
|
config.enable_poke
|
29
29
|
and (event.target_id == event.self_id)
|
30
|
-
and user_in_game(user_id, group_id)
|
30
|
+
and user_in_game(bot.self_id, user_id, group_id)
|
31
31
|
)
|
32
32
|
|
33
|
-
@
|
34
|
-
async def
|
33
|
+
@on_notice(rule=_rule_poke_stop).handle()
|
34
|
+
async def handle_poke_stop(event: PokeNotifyEvent) -> None:
|
35
35
|
InputStore.put(
|
36
|
+
msg=UniMessage.text("/stop"),
|
36
37
|
user_id=str(event.user_id),
|
37
38
|
group_id=str(event.group_id) if event.group_id is not None else None,
|
38
|
-
msg=UniMessage.text("/stop"),
|
39
39
|
)
|
40
40
|
|
41
41
|
# 准备阶段戳一戳等效加入游戏
|
42
|
-
async def
|
42
|
+
async def _rule_poke_join(
|
43
|
+
bot: Bot, event: PokeNotifyEvent, target: MsgTarget
|
44
|
+
) -> bool:
|
43
45
|
if not config.enable_poke or event.group_id is None:
|
44
46
|
return False
|
45
47
|
|
@@ -47,15 +49,19 @@ with contextlib.suppress(ImportError):
|
|
47
49
|
group_id = str(event.group_id)
|
48
50
|
return (
|
49
51
|
(event.target_id == event.self_id)
|
50
|
-
and not user_in_game(user_id, group_id)
|
51
|
-
and
|
52
|
+
and not user_in_game(bot.self_id, user_id, group_id)
|
53
|
+
and any(target.verify(group) for group in Game.starting_games)
|
52
54
|
)
|
53
55
|
|
54
|
-
@
|
55
|
-
async def
|
56
|
-
|
57
|
-
|
58
|
-
|
56
|
+
@on_notice(rule=_rule_poke_join).handle()
|
57
|
+
async def handle_poke_join(
|
58
|
+
bot: Bot,
|
59
|
+
event: PokeNotifyEvent,
|
60
|
+
target: MsgTarget,
|
61
|
+
) -> None:
|
62
|
+
user_id = event.get_user_id()
|
63
|
+
group_id = target.id
|
64
|
+
players = next(p for g, p in Game.starting_games.items() if target.verify(g))
|
59
65
|
|
60
66
|
if user_id not in players:
|
61
67
|
res: dict[str, str] = await bot.get_group_member_info(
|
@@ -63,7 +69,7 @@ with contextlib.suppress(ImportError):
|
|
63
69
|
user_id=int(user_id),
|
64
70
|
)
|
65
71
|
players[user_id] = res.get("card") or res.get("nickname") or user_id
|
66
|
-
await
|
72
|
+
await UniMessage.at(user_id).text("\n✅成功加入游戏").send(target, bot)
|
67
73
|
|
68
74
|
def ob11_ext_enabled() -> bool:
|
69
75
|
if not config.enable_poke:
|
@@ -0,0 +1,56 @@
|
|
1
|
+
from nonebot import on_command
|
2
|
+
from nonebot.adapters import Bot, Event
|
3
|
+
from nonebot.rule import to_me
|
4
|
+
from nonebot_plugin_alconna import MsgTarget, UniMessage
|
5
|
+
from nonebot_plugin_uninfo import Uninfo
|
6
|
+
|
7
|
+
from .._timeout import timeout
|
8
|
+
from ..game import Game
|
9
|
+
from ..utils import prepare_game, rule_not_in_game
|
10
|
+
from .ob11_ext import ob11_ext_enabled
|
11
|
+
|
12
|
+
start_game = on_command(
|
13
|
+
"werewolf",
|
14
|
+
rule=to_me() & rule_not_in_game,
|
15
|
+
aliases={"狼人杀"},
|
16
|
+
)
|
17
|
+
|
18
|
+
|
19
|
+
@start_game.handle()
|
20
|
+
async def handle_start_warning(target: MsgTarget) -> None:
|
21
|
+
if target.private:
|
22
|
+
await UniMessage("⚠️请在群组中创建新游戏").finish(reply_to=True)
|
23
|
+
|
24
|
+
|
25
|
+
@start_game.handle()
|
26
|
+
async def handle_start(
|
27
|
+
bot: Bot,
|
28
|
+
event: Event,
|
29
|
+
target: MsgTarget,
|
30
|
+
session: Uninfo,
|
31
|
+
) -> None:
|
32
|
+
admin_id = event.get_user_id()
|
33
|
+
msg = (
|
34
|
+
UniMessage.at(admin_id)
|
35
|
+
.text("\n🎉成功创建游戏\n\n")
|
36
|
+
.text(" 玩家请 @我 发送 “加入游戏”、“退出游戏”\n")
|
37
|
+
.text(" 玩家 @我 发送 “当前玩家” 可查看玩家列表\n")
|
38
|
+
.text(" 游戏发起者 @我 发送 “结束游戏” 可结束当前游戏\n")
|
39
|
+
.text(" 玩家均加入后,游戏发起者请 @我 发送 “开始游戏”\n")
|
40
|
+
)
|
41
|
+
if ob11_ext_enabled():
|
42
|
+
msg.text("\n可使用戳一戳代替游戏交互中的 “/stop” 命令\n")
|
43
|
+
await msg.text("\n游戏准备阶段限时5分钟,超时将自动结束").send()
|
44
|
+
|
45
|
+
admin_name = session.user.nick or session.user.name or admin_id
|
46
|
+
if session.member:
|
47
|
+
admin_name = session.member.nick or admin_name
|
48
|
+
players = {admin_id: admin_name}
|
49
|
+
|
50
|
+
try:
|
51
|
+
async with timeout(5 * 60):
|
52
|
+
await prepare_game(event, players)
|
53
|
+
except TimeoutError:
|
54
|
+
await UniMessage.text("⚠️游戏准备超时,已自动结束").finish()
|
55
|
+
|
56
|
+
Game(bot, target, players).start()
|
@@ -1,10 +1,11 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import asyncio
|
4
|
-
import
|
4
|
+
import functools
|
5
5
|
from typing import TYPE_CHECKING
|
6
6
|
|
7
|
-
from .
|
7
|
+
from ._timeout import timeout
|
8
|
+
from .players import Player
|
8
9
|
|
9
10
|
if TYPE_CHECKING:
|
10
11
|
from nonebot_plugin_alconna.uniseg import UniMessage
|
@@ -50,20 +51,21 @@ class PlayerSet(set[Player]):
|
|
50
51
|
def player_selected(self) -> PlayerSet:
|
51
52
|
return PlayerSet(p.selected for p in self.alive() if p.selected is not None)
|
52
53
|
|
54
|
+
@functools.cached_property
|
53
55
|
def sorted(self) -> list[Player]:
|
54
56
|
return sorted(self, key=lambda p: p.user_id)
|
55
57
|
|
56
58
|
async def interact(self, timeout_secs: float = 60) -> None:
|
57
|
-
async with
|
59
|
+
async with timeout(timeout_secs):
|
58
60
|
await asyncio.gather(*[p.interact() for p in self.alive()])
|
59
61
|
|
60
62
|
async def vote(self, timeout_secs: float = 60) -> dict[Player, list[Player]]:
|
61
63
|
async def vote(player: Player) -> tuple[Player, Player] | None:
|
62
64
|
try:
|
63
|
-
async with
|
65
|
+
async with timeout(timeout_secs):
|
64
66
|
return await player.vote(self)
|
65
67
|
except TimeoutError:
|
66
|
-
await player.send("
|
68
|
+
await player.send("⚠️投票超时,将视为弃票")
|
67
69
|
return None
|
68
70
|
|
69
71
|
result: dict[Player, list[Player]] = {}
|
@@ -77,7 +79,7 @@ class PlayerSet(set[Player]):
|
|
77
79
|
await asyncio.gather(*[p.send(message) for p in self])
|
78
80
|
|
79
81
|
def show(self) -> str:
|
80
|
-
return "\n".join(f"{i}. {p.name}" for i, p in enumerate(self.sorted
|
82
|
+
return "\n".join(f"{i}. {p.name}" for i, p in enumerate(self.sorted, 1))
|
81
83
|
|
82
84
|
def __getitem__(self, __index: int) -> Player:
|
83
|
-
return self.sorted
|
85
|
+
return self.sorted[__index]
|
@@ -0,0 +1,10 @@
|
|
1
|
+
from .civilian import Civilian as Civilian
|
2
|
+
from .guard import Guard as Guard
|
3
|
+
from .hunter import Hunter as Hunter
|
4
|
+
from .idiot import Idiot as Idiot
|
5
|
+
from .joker import Joker as Joker
|
6
|
+
from .player import Player as Player
|
7
|
+
from .prophet import Prophet as Prophet
|
8
|
+
from .werewolf import Werewolf as Werewolf
|
9
|
+
from .witch import Witch as Witch
|
10
|
+
from .wolfking import WolfKing as WolfKing
|