nonebot-plugin-werewolf 1.0.1__py3-none-any.whl → 1.0.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 +1 -1
- nonebot_plugin_werewolf/constant.py +1 -0
- nonebot_plugin_werewolf/game.py +64 -32
- nonebot_plugin_werewolf/matchers.py +21 -195
- nonebot_plugin_werewolf/ob11_ext.py +72 -0
- nonebot_plugin_werewolf/player.py +65 -128
- nonebot_plugin_werewolf/player_set.py +87 -0
- nonebot_plugin_werewolf/utils.py +158 -4
- {nonebot_plugin_werewolf-1.0.1.dist-info → nonebot_plugin_werewolf-1.0.3.dist-info}/METADATA +12 -6
- nonebot_plugin_werewolf-1.0.3.dist-info/RECORD +13 -0
- nonebot_plugin_werewolf-1.0.1.dist-info/RECORD +0 -11
- {nonebot_plugin_werewolf-1.0.1.dist-info → nonebot_plugin_werewolf-1.0.3.dist-info}/WHEEL +0 -0
- {nonebot_plugin_werewolf-1.0.1.dist-info → nonebot_plugin_werewolf-1.0.3.dist-info}/licenses/LICENSE +0 -0
nonebot_plugin_werewolf/game.py
CHANGED
@@ -1,13 +1,16 @@
|
|
1
1
|
import asyncio
|
2
2
|
import asyncio.timeouts
|
3
3
|
import random
|
4
|
-
from collections.abc import Callable
|
5
4
|
|
6
5
|
from nonebot.adapters import Bot
|
7
6
|
from nonebot_plugin_alconna import Target, UniMessage
|
8
7
|
|
9
8
|
from .constant import GameState, GameStatus, KillReason, Role, RoleGroup, player_preset
|
10
|
-
from .player import Player
|
9
|
+
from .player import Player
|
10
|
+
from .player_set import PlayerSet
|
11
|
+
|
12
|
+
starting_games: dict[str, dict[str, str]] = {}
|
13
|
+
running_games: dict[str, tuple["Game", asyncio.Task[None]]] = {}
|
11
14
|
|
12
15
|
|
13
16
|
def init_players(bot: Bot, game: "Game", players: dict[str, str]) -> PlayerSet:
|
@@ -15,7 +18,7 @@ def init_players(bot: Bot, game: "Game", players: dict[str, str]) -> PlayerSet:
|
|
15
18
|
if preset is None:
|
16
19
|
raise ValueError(
|
17
20
|
f"玩家人数不符: "
|
18
|
-
f"应为{
|
21
|
+
f"应为 {', '.join(map(str, player_preset))} 人, 传入{len(players)}人"
|
19
22
|
)
|
20
23
|
|
21
24
|
roles: list[Role] = []
|
@@ -50,20 +53,18 @@ class Game:
|
|
50
53
|
group: Target
|
51
54
|
players: PlayerSet
|
52
55
|
state: GameState
|
53
|
-
|
56
|
+
killed_players: list[Player]
|
54
57
|
|
55
58
|
def __init__(
|
56
59
|
self,
|
57
60
|
bot: Bot,
|
58
61
|
group: Target,
|
59
62
|
players: dict[str, str],
|
60
|
-
on_exit: Callable[[], None],
|
61
63
|
) -> None:
|
62
64
|
self.bot = bot
|
63
65
|
self.group = group
|
64
66
|
self.players = init_players(bot, self, players)
|
65
|
-
self.state = GameState()
|
66
|
-
self._on_exit = on_exit
|
67
|
+
self.state = GameState(0)
|
67
68
|
|
68
69
|
async def send(self, message: str | UniMessage):
|
69
70
|
if isinstance(message, str):
|
@@ -78,27 +79,49 @@ class Game:
|
|
78
79
|
|
79
80
|
def check_game_status(self) -> GameStatus:
|
80
81
|
players = self.players.alive()
|
81
|
-
|
82
82
|
w = players.select(RoleGroup.狼人)
|
83
|
-
if not w.size:
|
84
|
-
return GameStatus.Good
|
85
|
-
|
86
83
|
p = players.exclude(RoleGroup.狼人)
|
84
|
+
|
87
85
|
if w.size >= p.size:
|
88
86
|
return GameStatus.Bad
|
89
|
-
if not
|
87
|
+
if not p.select(Role.平民):
|
90
88
|
return GameStatus.Bad
|
91
|
-
if not
|
89
|
+
if not p.exclude(Role.平民):
|
92
90
|
return GameStatus.Bad
|
91
|
+
if not w.size:
|
92
|
+
return GameStatus.Good
|
93
93
|
|
94
94
|
return GameStatus.Unset
|
95
95
|
|
96
|
+
def show_killed_players(self) -> str:
|
97
|
+
msg = ""
|
98
|
+
|
99
|
+
for player in self.killed_players:
|
100
|
+
if player.kill_info is None:
|
101
|
+
continue
|
102
|
+
|
103
|
+
msg += f"{player.name} 被 " + ", ".join(
|
104
|
+
p.name for p in player.kill_info.killers
|
105
|
+
)
|
106
|
+
match player.kill_info.reason:
|
107
|
+
case KillReason.Kill:
|
108
|
+
msg += " 刀了"
|
109
|
+
case KillReason.Poison:
|
110
|
+
msg += " 毒死"
|
111
|
+
case KillReason.Shoot:
|
112
|
+
msg += " 射杀"
|
113
|
+
case KillReason.Vote:
|
114
|
+
msg += " 投票放逐"
|
115
|
+
msg += "\n\n"
|
116
|
+
|
117
|
+
return msg.strip()
|
118
|
+
|
96
119
|
async def notify_player_role(self) -> None:
|
97
120
|
preset = player_preset[len(self.players)]
|
98
121
|
await asyncio.gather(
|
99
122
|
self.send(
|
100
123
|
self.at_all()
|
101
|
-
.text("\n正在分配职业,请注意查看私聊消息\n")
|
124
|
+
.text("\n\n正在分配职业,请注意查看私聊消息\n")
|
102
125
|
.text(f"当前玩家数: {len(self.players)}\n")
|
103
126
|
.text(f"职业分配: 狼人x{preset[0]}, 神职x{preset[1]}, 平民x{preset[2]}")
|
104
127
|
),
|
@@ -125,8 +148,10 @@ class Game:
|
|
125
148
|
type_.role.name # Player
|
126
149
|
if isinstance(type_, Player)
|
127
150
|
else (
|
128
|
-
type_.name
|
129
|
-
|
151
|
+
type_.name # Role
|
152
|
+
if isinstance(type_, Role)
|
153
|
+
else f"{type_.name}阵营" # RoleGroup
|
154
|
+
)
|
130
155
|
)
|
131
156
|
|
132
157
|
await players.broadcast(f"{text}交互开始,限时 {timeout_secs/60:.2f} 分钟")
|
@@ -176,9 +201,10 @@ class Game:
|
|
176
201
|
if not players:
|
177
202
|
return
|
178
203
|
|
179
|
-
for player in players:
|
204
|
+
for player in players.dead():
|
180
205
|
await player.post_kill()
|
181
206
|
await self.handle_new_dead(player)
|
207
|
+
self.killed_players.append(player)
|
182
208
|
|
183
209
|
(shooter, shoot) = self.state.shoot
|
184
210
|
if shooter is not None and shoot is not None:
|
@@ -189,27 +215,32 @@ class Game:
|
|
189
215
|
.text("限时1分钟, 发送 “/stop” 结束发言")
|
190
216
|
)
|
191
217
|
await self.wait_stop(shoot, 60)
|
218
|
+
await self.post_kill(shoot)
|
192
219
|
self.state.shoot = (None, None)
|
193
220
|
|
194
221
|
async def run_vote(self) -> None:
|
195
222
|
# 统计投票结果
|
196
|
-
|
223
|
+
players = self.players.alive()
|
224
|
+
|
225
|
+
# 被票玩家: [投票玩家]
|
226
|
+
vote_result: dict[Player, list[Player]] = await players.vote(60)
|
227
|
+
# 票数: [被票玩家]
|
197
228
|
vote_reversed: dict[int, list[Player]] = {}
|
198
|
-
|
199
|
-
|
229
|
+
# 收集到的总票数
|
230
|
+
total_votes = sum(map(len, vote_result.values()))
|
200
231
|
|
201
232
|
# 投票结果公示
|
202
233
|
msg = UniMessage.text("投票结果:\n")
|
203
234
|
for p, v in sorted(vote_result.items(), key=lambda x: x[1], reverse=True):
|
204
235
|
if p is not None:
|
205
236
|
msg.at(p.user_id).text(f": {v} 票\n")
|
206
|
-
vote_reversed[v] = [*vote_reversed.get(v, []), p]
|
207
|
-
if v :=
|
237
|
+
vote_reversed[len(v)] = [*vote_reversed.get(len(v), []), p]
|
238
|
+
if v := (len(players) - total_votes):
|
208
239
|
msg.text(f"弃票: {v} 票\n")
|
209
240
|
await self.send(msg)
|
210
241
|
|
211
242
|
# 全员弃票 # 不是哥们?
|
212
|
-
if
|
243
|
+
if total_votes == 0:
|
213
244
|
await self.send("没有人被票出")
|
214
245
|
return
|
215
246
|
|
@@ -224,7 +255,7 @@ class Game:
|
|
224
255
|
|
225
256
|
# 仅有一名玩家票数最高
|
226
257
|
voted = vs.pop()
|
227
|
-
if not await voted.kill(KillReason.Vote):
|
258
|
+
if not await voted.kill(KillReason.Vote, *vote_result[voted]):
|
228
259
|
# 投票放逐失败 (例: 白痴)
|
229
260
|
return
|
230
261
|
|
@@ -267,7 +298,7 @@ class Game:
|
|
267
298
|
|
268
299
|
while self.check_game_status() == GameStatus.Unset:
|
269
300
|
# 重置游戏状态,进入下一夜
|
270
|
-
self.state = GameState()
|
301
|
+
self.state = GameState(day_count)
|
271
302
|
players = self.players.alive()
|
272
303
|
await self.send("天黑请闭眼...")
|
273
304
|
|
@@ -290,19 +321,19 @@ class Game:
|
|
290
321
|
if killed is not None:
|
291
322
|
# 除非守卫保护或女巫使用解药,否则狼人正常击杀玩家
|
292
323
|
if not ((killed is protected) or (antidote and potioned is killed)):
|
293
|
-
await killed.kill(KillReason.Kill)
|
324
|
+
await killed.kill(KillReason.Kill, *players.select(RoleGroup.狼人))
|
294
325
|
# 如果女巫使用毒药且守卫未保护,杀死该玩家
|
295
326
|
if poison and (potioned is not None) and (potioned is not protected):
|
296
|
-
await potioned.kill(KillReason.Poison)
|
327
|
+
await potioned.kill(KillReason.Poison, *players.select(Role.女巫))
|
297
328
|
|
298
329
|
day_count += 1
|
299
|
-
msg = UniMessage.text(f"
|
330
|
+
msg = UniMessage.text(f"『第{day_count}天』天亮了...\n")
|
300
331
|
# 没有玩家死亡,平安夜
|
301
332
|
if not (dead := players.dead()):
|
302
333
|
await self.send(msg.text("昨晚是平安夜"))
|
303
334
|
# 有玩家死亡,执行死亡流程
|
304
335
|
else:
|
305
|
-
#
|
336
|
+
# 公布死者名单
|
306
337
|
msg.text("昨晚的死者是:")
|
307
338
|
for p in dead.sorted():
|
308
339
|
msg.text("\n").at(p.user_id)
|
@@ -336,8 +367,9 @@ class Game:
|
|
336
367
|
# 游戏结束
|
337
368
|
dead_channel.cancel()
|
338
369
|
winner = "好人" if self.check_game_status() == GameStatus.Good else "狼人"
|
339
|
-
msg = UniMessage.text(f"
|
340
|
-
for p in sorted(self.players, key=lambda p: (p.role.
|
370
|
+
msg = UniMessage.text(f"🎉游戏结束,{winner}获胜\n\n")
|
371
|
+
for p in sorted(self.players, key=lambda p: (p.role.value, p.user_id)):
|
341
372
|
msg.at(p.user_id).text(f": {p.role.name}\n")
|
373
|
+
msg.text(f"\n{self.show_killed_players()}")
|
342
374
|
await self.send(msg)
|
343
|
-
self.
|
375
|
+
running_games.pop(self.group.id, None)
|
@@ -1,47 +1,26 @@
|
|
1
1
|
import asyncio
|
2
2
|
import asyncio.timeouts
|
3
|
-
import contextlib
|
4
3
|
from typing import Annotated
|
5
4
|
|
6
|
-
import
|
7
|
-
from nonebot import on_command, on_message, on_type
|
5
|
+
from nonebot import on_command, on_message
|
8
6
|
from nonebot.adapters import Bot, Event
|
9
7
|
from nonebot.rule import to_me
|
10
8
|
from nonebot_plugin_alconna import MsgTarget, UniMessage, UniMsg
|
11
9
|
from nonebot_plugin_userinfo import EventUserInfo, UserInfo
|
12
10
|
|
13
|
-
from .
|
14
|
-
from .
|
15
|
-
from .utils import InputStore
|
11
|
+
from .game import Game, running_games, starting_games
|
12
|
+
from .ob11_ext import ob11_ext_enabled
|
13
|
+
from .utils import InputStore, is_group, prepare_game, rule_in_game, rule_not_in_game
|
16
14
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
return False
|
24
|
-
games = running_games.values() if group_id is None else [running_games[group_id]]
|
25
|
-
for game, _ in games:
|
26
|
-
return any(user_id == player.user_id for player in game.players)
|
27
|
-
return False
|
28
|
-
|
29
|
-
|
30
|
-
async def rule_in_game(event: Event, target: MsgTarget) -> bool:
|
31
|
-
if not running_games:
|
32
|
-
return False
|
33
|
-
if target.private:
|
34
|
-
return user_in_game(target.id, None)
|
35
|
-
elif target.id in running_games:
|
36
|
-
return user_in_game(event.get_user_id(), target.id)
|
37
|
-
return False
|
38
|
-
|
39
|
-
|
40
|
-
async def rule_not_in_game(event: Event, target: MsgTarget) -> bool:
|
41
|
-
return not await rule_in_game(event, target)
|
15
|
+
in_game_message = on_message(rule=rule_in_game)
|
16
|
+
start_game = on_command(
|
17
|
+
"werewolf",
|
18
|
+
rule=to_me() & is_group & rule_not_in_game,
|
19
|
+
aliases={"狼人杀"},
|
20
|
+
)
|
42
21
|
|
43
22
|
|
44
|
-
@
|
23
|
+
@in_game_message.handle()
|
45
24
|
async def handle_input(event: Event, target: MsgTarget, msg: UniMsg) -> None:
|
46
25
|
if target.private:
|
47
26
|
InputStore.put(target.id, None, msg)
|
@@ -49,87 +28,16 @@ async def handle_input(event: Event, target: MsgTarget, msg: UniMsg) -> None:
|
|
49
28
|
InputStore.put(event.get_user_id(), target.id, msg)
|
50
29
|
|
51
30
|
|
52
|
-
|
53
|
-
return not target.private
|
54
|
-
|
55
|
-
|
56
|
-
async def prepare_game(
|
57
|
-
wait: waiter.Waiter[tuple[str, str, str]],
|
58
|
-
players: dict[str, str],
|
59
|
-
group_id: str,
|
60
|
-
admin_id: str,
|
61
|
-
):
|
62
|
-
async for user, name, text in wait(default=(None, "", "")):
|
63
|
-
if user is None:
|
64
|
-
continue
|
65
|
-
msg = UniMessage.at(user)
|
66
|
-
|
67
|
-
match (text, user == admin_id):
|
68
|
-
case ("开始游戏", True):
|
69
|
-
if len(players) < min(player_preset):
|
70
|
-
await (
|
71
|
-
msg.text(f"游戏至少需要 {min(player_preset)} 人, ")
|
72
|
-
.text(f"当前已有 {len(players)} 人")
|
73
|
-
.send()
|
74
|
-
)
|
75
|
-
elif len(players) > max(player_preset):
|
76
|
-
await (
|
77
|
-
msg.text(f"游戏最多需要 {max(player_preset)} 人, ")
|
78
|
-
.text(f"当前已有 {len(players)} 人")
|
79
|
-
.send()
|
80
|
-
)
|
81
|
-
else:
|
82
|
-
await msg.text("游戏即将开始...").send()
|
83
|
-
return
|
84
|
-
|
85
|
-
case ("开始游戏", False):
|
86
|
-
await msg.text("只有游戏发起者可以开始游戏").send()
|
87
|
-
|
88
|
-
case ("结束游戏", True):
|
89
|
-
del starting_games[group_id]
|
90
|
-
await msg.text("已结束当前游戏").finish()
|
91
|
-
|
92
|
-
case ("结束游戏", False):
|
93
|
-
await msg.text("只有游戏发起者可以结束游戏").send()
|
94
|
-
|
95
|
-
case ("加入游戏", True):
|
96
|
-
await msg.text("游戏发起者已经加入游戏了").send()
|
97
|
-
|
98
|
-
case ("加入游戏", False):
|
99
|
-
if user not in players:
|
100
|
-
players[user] = name
|
101
|
-
await msg.text("成功加入游戏").send()
|
102
|
-
else:
|
103
|
-
await msg.text("你已经加入游戏了").send()
|
104
|
-
|
105
|
-
case ("退出游戏", True):
|
106
|
-
await msg.text("游戏发起者无法退出游戏").send()
|
107
|
-
|
108
|
-
case ("退出游戏", False):
|
109
|
-
if user in players:
|
110
|
-
del players[user]
|
111
|
-
await msg.text("成功退出游戏").send()
|
112
|
-
else:
|
113
|
-
await msg.text("你还没有加入游戏").send()
|
114
|
-
|
115
|
-
case ("当前玩家", _):
|
116
|
-
msg.text("\n当前玩家:\n")
|
117
|
-
for name in players.values():
|
118
|
-
msg.text(f"\n{name}")
|
119
|
-
await msg.send()
|
120
|
-
|
121
|
-
|
122
|
-
@on_command(
|
123
|
-
"werewolf",
|
124
|
-
rule=to_me() & is_group & rule_not_in_game,
|
125
|
-
aliases={"狼人杀"},
|
126
|
-
).handle()
|
31
|
+
@start_game.handle()
|
127
32
|
async def handle_start(
|
128
33
|
bot: Bot,
|
129
34
|
event: Event,
|
130
35
|
target: MsgTarget,
|
131
36
|
admin_info: Annotated[UserInfo, EventUserInfo()],
|
132
37
|
) -> None:
|
38
|
+
if target.id in running_games:
|
39
|
+
await UniMessage.text("当前群聊内有正在进行的游戏,无法创建游戏").finish()
|
40
|
+
|
133
41
|
admin_id = event.get_user_id()
|
134
42
|
msg = (
|
135
43
|
UniMessage.at(admin_id)
|
@@ -139,102 +47,20 @@ async def handle_start(
|
|
139
47
|
.text("游戏发起者 @我 发送 “结束游戏” 可结束当前游戏\n")
|
140
48
|
.text("玩家均加入后,游戏发起者请 @我 发送 “开始游戏”\n")
|
141
49
|
)
|
142
|
-
if (
|
143
|
-
config.enable_poke
|
144
|
-
and OneBotV11Available
|
145
|
-
and bot.adapter.get_name() == "OneBot V11"
|
146
|
-
):
|
50
|
+
if ob11_ext_enabled():
|
147
51
|
msg.text("\n可使用戳一戳代替游戏交互中的 “/stop” 命令")
|
148
52
|
await msg.text("\n\n游戏准备阶段限时5分钟,超时将自动结束").send()
|
149
53
|
|
150
|
-
|
151
|
-
return not target_.private and target_.id == target.id
|
152
|
-
|
153
|
-
@waiter.waiter(
|
154
|
-
waits=[event.get_type()],
|
155
|
-
keep_session=False,
|
156
|
-
rule=to_me() & rule & rule_not_in_game,
|
157
|
-
)
|
158
|
-
def wait(
|
159
|
-
event: Event,
|
160
|
-
info: Annotated[UserInfo | None, EventUserInfo()],
|
161
|
-
msg: UniMsg,
|
162
|
-
):
|
163
|
-
return (
|
164
|
-
event.get_user_id(),
|
165
|
-
info.user_name if info is not None else event.get_user_id(),
|
166
|
-
msg.extract_plain_text().strip(),
|
167
|
-
)
|
168
|
-
|
169
|
-
starting_games[target.id] = players = {admin_id: admin_info.user_name}
|
54
|
+
players = starting_games[target.id] = {admin_id: admin_info.user_name}
|
170
55
|
|
171
56
|
try:
|
172
57
|
async with asyncio.timeouts.timeout(5 * 60):
|
173
|
-
await prepare_game(
|
58
|
+
await prepare_game(event, players)
|
174
59
|
except TimeoutError:
|
175
|
-
del starting_games[target.id]
|
176
60
|
await UniMessage.text("游戏准备超时,已自动结束").finish()
|
61
|
+
finally:
|
62
|
+
del starting_games[target.id]
|
177
63
|
|
178
|
-
game = Game(
|
179
|
-
bot=bot,
|
180
|
-
group=target,
|
181
|
-
players=players,
|
182
|
-
on_exit=lambda: running_games.pop(target.id, None) and None,
|
183
|
-
)
|
64
|
+
game = Game(bot=bot, group=target, players=players)
|
184
65
|
task = asyncio.create_task(game.run())
|
185
66
|
running_games[target.id] = (game, task)
|
186
|
-
del starting_games[target.id]
|
187
|
-
|
188
|
-
|
189
|
-
# OneBot V11 扩展
|
190
|
-
OneBotV11Available = False
|
191
|
-
with contextlib.suppress(ImportError, RuntimeError):
|
192
|
-
if not config.enable_poke:
|
193
|
-
raise RuntimeError
|
194
|
-
|
195
|
-
from nonebot.adapters.onebot.v11 import Bot as V11Bot
|
196
|
-
from nonebot.adapters.onebot.v11 import MessageSegment
|
197
|
-
from nonebot.adapters.onebot.v11.event import PokeNotifyEvent
|
198
|
-
|
199
|
-
OneBotV11Available = True
|
200
|
-
|
201
|
-
# 游戏内戳一戳等效 "/stop"
|
202
|
-
async def _rule_poke_1(event: PokeNotifyEvent) -> bool:
|
203
|
-
user_id = str(event.user_id)
|
204
|
-
group_id = str(event.group_id) if event.group_id is not None else None
|
205
|
-
return (event.target_id == event.self_id) and user_in_game(user_id, group_id)
|
206
|
-
|
207
|
-
@on_type(PokeNotifyEvent, rule=_rule_poke_1).handle()
|
208
|
-
async def handle_poke_1(event: PokeNotifyEvent) -> None:
|
209
|
-
InputStore.put(
|
210
|
-
user_id=str(event.user_id),
|
211
|
-
group_id=str(event.group_id) if event.group_id is not None else None,
|
212
|
-
msg=UniMessage.text("/stop"),
|
213
|
-
)
|
214
|
-
|
215
|
-
# 准备阶段戳一戳等效加入游戏
|
216
|
-
async def _rule_poke_2(event: PokeNotifyEvent) -> bool:
|
217
|
-
if event.group_id is None:
|
218
|
-
return False
|
219
|
-
|
220
|
-
user_id = str(event.user_id)
|
221
|
-
group_id = str(event.group_id)
|
222
|
-
return (
|
223
|
-
(event.target_id == event.self_id)
|
224
|
-
and not user_in_game(user_id, group_id)
|
225
|
-
and group_id in starting_games
|
226
|
-
)
|
227
|
-
|
228
|
-
@on_type(PokeNotifyEvent, rule=_rule_poke_2).handle()
|
229
|
-
async def handle_poke_2(bot: V11Bot, event: PokeNotifyEvent) -> None:
|
230
|
-
user_id = str(event.user_id)
|
231
|
-
group_id = str(event.group_id)
|
232
|
-
players = starting_games[group_id]
|
233
|
-
|
234
|
-
if user_id not in players:
|
235
|
-
res: dict[str, str] = await bot.get_group_member_info(
|
236
|
-
group_id=int(group_id),
|
237
|
-
user_id=int(user_id),
|
238
|
-
)
|
239
|
-
players[user_id] = res.get("nickname") or user_id
|
240
|
-
await bot.send(event, MessageSegment.at(user_id) + "成功加入游戏")
|
@@ -0,0 +1,72 @@
|
|
1
|
+
import contextlib
|
2
|
+
|
3
|
+
from nonebot import on_type
|
4
|
+
from nonebot.internal.matcher import current_bot
|
5
|
+
from nonebot_plugin_alconna import UniMessage
|
6
|
+
|
7
|
+
from .config import config
|
8
|
+
from .game import starting_games
|
9
|
+
from .utils import InputStore, user_in_game
|
10
|
+
|
11
|
+
|
12
|
+
def ob11_ext_enabled() -> bool:
|
13
|
+
return False
|
14
|
+
|
15
|
+
|
16
|
+
with contextlib.suppress(ImportError):
|
17
|
+
from nonebot.adapters.onebot.v11 import Bot, MessageSegment
|
18
|
+
from nonebot.adapters.onebot.v11.event import PokeNotifyEvent
|
19
|
+
|
20
|
+
# 游戏内戳一戳等效 "/stop"
|
21
|
+
async def _rule_poke_1(event: PokeNotifyEvent) -> bool:
|
22
|
+
if not config.enable_poke:
|
23
|
+
return False
|
24
|
+
|
25
|
+
user_id = str(event.user_id)
|
26
|
+
group_id = str(event.group_id) if event.group_id is not None else None
|
27
|
+
return (
|
28
|
+
config.enable_poke
|
29
|
+
and (event.target_id == event.self_id)
|
30
|
+
and user_in_game(user_id, group_id)
|
31
|
+
)
|
32
|
+
|
33
|
+
@on_type(PokeNotifyEvent, rule=_rule_poke_1).handle()
|
34
|
+
async def handle_poke_1(event: PokeNotifyEvent) -> None:
|
35
|
+
InputStore.put(
|
36
|
+
user_id=str(event.user_id),
|
37
|
+
group_id=str(event.group_id) if event.group_id is not None else None,
|
38
|
+
msg=UniMessage.text("/stop"),
|
39
|
+
)
|
40
|
+
|
41
|
+
# 准备阶段戳一戳等效加入游戏
|
42
|
+
async def _rule_poke_2(event: PokeNotifyEvent) -> bool:
|
43
|
+
if not config.enable_poke or event.group_id is None:
|
44
|
+
return False
|
45
|
+
|
46
|
+
user_id = str(event.user_id)
|
47
|
+
group_id = str(event.group_id)
|
48
|
+
return (
|
49
|
+
(event.target_id == event.self_id)
|
50
|
+
and not user_in_game(user_id, group_id)
|
51
|
+
and group_id in starting_games
|
52
|
+
)
|
53
|
+
|
54
|
+
@on_type(PokeNotifyEvent, rule=_rule_poke_2).handle()
|
55
|
+
async def handle_poke_2(bot: Bot, event: PokeNotifyEvent) -> None:
|
56
|
+
user_id = str(event.user_id)
|
57
|
+
group_id = str(event.group_id)
|
58
|
+
players = starting_games[group_id]
|
59
|
+
|
60
|
+
if user_id not in players:
|
61
|
+
res: dict[str, str] = await bot.get_group_member_info(
|
62
|
+
group_id=int(group_id),
|
63
|
+
user_id=int(user_id),
|
64
|
+
)
|
65
|
+
players[user_id] = res.get("nickname") or user_id
|
66
|
+
await bot.send(event, MessageSegment.at(user_id) + "成功加入游戏")
|
67
|
+
|
68
|
+
def ob11_ext_enabled() -> bool:
|
69
|
+
if not config.enable_poke:
|
70
|
+
return False
|
71
|
+
|
72
|
+
return isinstance(current_bot.get(), Bot)
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import asyncio
|
2
2
|
import asyncio.timeouts
|
3
|
-
import
|
4
|
-
from typing import TYPE_CHECKING, ClassVar,
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from typing import TYPE_CHECKING, ClassVar, TypeVar
|
5
5
|
from typing_extensions import override
|
6
6
|
|
7
7
|
from nonebot.adapters import Bot
|
@@ -12,15 +12,24 @@ from .utils import InputStore, check_index
|
|
12
12
|
|
13
13
|
if TYPE_CHECKING:
|
14
14
|
from .game import Game
|
15
|
+
from .player_set import PlayerSet
|
15
16
|
|
16
|
-
PlayerClass: dict[Role, type["Player"]] = {}
|
17
17
|
|
18
|
+
P = TypeVar("P", bound=type["Player"])
|
19
|
+
PLAYER_CLASS: dict[Role, type["Player"]] = {}
|
18
20
|
|
19
|
-
|
20
|
-
|
21
|
+
|
22
|
+
def register_role(cls: P) -> P:
|
23
|
+
PLAYER_CLASS[cls.role] = cls
|
21
24
|
return cls
|
22
25
|
|
23
26
|
|
27
|
+
@dataclass
|
28
|
+
class KillInfo:
|
29
|
+
reason: KillReason
|
30
|
+
killers: "PlayerSet"
|
31
|
+
|
32
|
+
|
24
33
|
class Player:
|
25
34
|
role: ClassVar[Role]
|
26
35
|
role_group: ClassVar[RoleGroup]
|
@@ -31,9 +40,15 @@ class Player:
|
|
31
40
|
name: str
|
32
41
|
alive: bool = True
|
33
42
|
killed: bool = False
|
34
|
-
|
43
|
+
kill_info: KillInfo | None = None
|
35
44
|
selected: "Player | None" = None
|
36
45
|
|
46
|
+
def __init__(self, bot: Bot, game: "Game", user: Target, name: str) -> None:
|
47
|
+
self.bot = bot
|
48
|
+
self.game = game
|
49
|
+
self.user = user
|
50
|
+
self.name = name
|
51
|
+
|
37
52
|
@classmethod
|
38
53
|
def new(
|
39
54
|
cls,
|
@@ -43,20 +58,18 @@ class Player:
|
|
43
58
|
user: Target,
|
44
59
|
name: str,
|
45
60
|
) -> "Player":
|
46
|
-
if role not in
|
61
|
+
if role not in PLAYER_CLASS:
|
47
62
|
raise ValueError(f"Unexpected role: {role!r}")
|
48
63
|
|
49
|
-
|
50
|
-
player.bot = bot
|
51
|
-
player.game = game
|
52
|
-
player.user = user
|
53
|
-
player.name = name
|
54
|
-
player.game = game
|
55
|
-
return player
|
64
|
+
return PLAYER_CLASS[role](bot, game, user, name)
|
56
65
|
|
57
66
|
def __repr__(self) -> str:
|
58
67
|
return f"<{self.role.name}: user={self.user} alive={self.alive}>"
|
59
68
|
|
69
|
+
@property
|
70
|
+
def user_id(self) -> str:
|
71
|
+
return self.user.id
|
72
|
+
|
60
73
|
async def send(self, message: str | UniMessage) -> Receipt:
|
61
74
|
if isinstance(message, str):
|
62
75
|
message = UniMessage.text(message)
|
@@ -68,21 +81,28 @@ class Player:
|
|
68
81
|
await self.send(prompt)
|
69
82
|
return await InputStore.fetch(self.user.id)
|
70
83
|
|
84
|
+
async def receive_text(self) -> str:
|
85
|
+
return (await self.receive()).extract_plain_text()
|
86
|
+
|
71
87
|
async def interact(self) -> None:
|
72
88
|
return
|
73
89
|
|
74
90
|
async def notify_role(self) -> None:
|
75
91
|
await self.send(f"你的身份: {self.role.name}")
|
76
92
|
|
77
|
-
async def kill(
|
93
|
+
async def kill(
|
94
|
+
self,
|
95
|
+
reason: KillReason,
|
96
|
+
*killers: "Player",
|
97
|
+
) -> bool:
|
78
98
|
self.alive = False
|
79
|
-
self.
|
99
|
+
self.kill_info = KillInfo(reason, PlayerSet(killers))
|
80
100
|
return True
|
81
101
|
|
82
102
|
async def post_kill(self) -> None:
|
83
103
|
self.killed = True
|
84
104
|
|
85
|
-
async def vote(self, players: "PlayerSet") -> "Player | None":
|
105
|
+
async def vote(self, players: "PlayerSet") -> "tuple[Player, Player] | None":
|
86
106
|
await self.send(
|
87
107
|
f"请选择需要投票的玩家:\n{players.show()}"
|
88
108
|
"\n\n发送编号选择玩家\n发送 “/stop” 弃票"
|
@@ -100,17 +120,13 @@ class Player:
|
|
100
120
|
|
101
121
|
player = players[selected]
|
102
122
|
await self.send(f"投票的玩家: {player.name}")
|
103
|
-
return player
|
104
|
-
|
105
|
-
@property
|
106
|
-
def user_id(self) -> str:
|
107
|
-
return self.user.id
|
123
|
+
return self, player
|
108
124
|
|
109
125
|
|
110
126
|
class CanShoot(Player):
|
111
127
|
@override
|
112
128
|
async def post_kill(self) -> None:
|
113
|
-
if self.
|
129
|
+
if self.kill_info and self.kill_info.reason == KillReason.Poison:
|
114
130
|
await self.send("你昨晚被女巫毒杀,无法使用技能")
|
115
131
|
return await super().post_kill()
|
116
132
|
|
@@ -122,18 +138,16 @@ class CanShoot(Player):
|
|
122
138
|
|
123
139
|
self.game.state.shoot = (None, None)
|
124
140
|
shoot = await self.shoot()
|
125
|
-
if shoot is not None:
|
126
|
-
self.game.state.shoot = (self, shoot)
|
127
141
|
|
128
142
|
if shoot is not None:
|
143
|
+
self.game.state.shoot = (self, shoot)
|
129
144
|
await self.send(
|
130
145
|
UniMessage.text(f"{self.role.name} ")
|
131
146
|
.at(self.user_id)
|
132
147
|
.text(" 射杀了玩家 ")
|
133
148
|
.at(shoot.user_id)
|
134
149
|
)
|
135
|
-
await shoot.kill(KillReason.Shoot)
|
136
|
-
await shoot.post_kill()
|
150
|
+
await shoot.kill(KillReason.Shoot, self)
|
137
151
|
else:
|
138
152
|
await self.send(f"{self.role.name}选择了取消技能")
|
139
153
|
return await super().post_kill()
|
@@ -148,7 +162,7 @@ class CanShoot(Player):
|
|
148
162
|
)
|
149
163
|
|
150
164
|
while True:
|
151
|
-
text =
|
165
|
+
text = await self.receive_text()
|
152
166
|
if text == "/stop":
|
153
167
|
await self.send("已取消技能")
|
154
168
|
return
|
@@ -242,7 +256,7 @@ class 预言家(Player):
|
|
242
256
|
)
|
243
257
|
|
244
258
|
while True:
|
245
|
-
text =
|
259
|
+
text = await self.receive_text()
|
246
260
|
index = check_index(text, len(players))
|
247
261
|
if index is not None:
|
248
262
|
selected = index - 1
|
@@ -262,7 +276,10 @@ class 女巫(Player):
|
|
262
276
|
poison: int = 1
|
263
277
|
|
264
278
|
def set_state(
|
265
|
-
self,
|
279
|
+
self,
|
280
|
+
*,
|
281
|
+
antidote: Player | None = None,
|
282
|
+
posion: Player | None = None,
|
266
283
|
):
|
267
284
|
if antidote is not None:
|
268
285
|
self.antidote = 0
|
@@ -273,33 +290,29 @@ class 女巫(Player):
|
|
273
290
|
self.selected = posion
|
274
291
|
self.game.state.potion = (posion, (False, True))
|
275
292
|
else:
|
293
|
+
self.selected = None
|
276
294
|
self.game.state.potion = (None, (False, False))
|
277
295
|
|
278
|
-
@staticmethod
|
279
|
-
def potion_str(potion: Literal[1, 2]) -> str:
|
280
|
-
return "解药" if potion == 1 else "毒药"
|
281
|
-
|
282
296
|
async def handle_killed(self) -> bool:
|
283
|
-
|
284
|
-
|
297
|
+
msg = UniMessage()
|
298
|
+
if (killed := self.game.state.killed) is not None:
|
299
|
+
msg.text(f"今晚 {killed} 被刀了\n\n")
|
285
300
|
else:
|
286
301
|
await self.send("今晚没有人被刀")
|
287
302
|
return False
|
288
303
|
|
289
304
|
if not self.antidote:
|
290
|
-
await self.send("你已经用过解药了")
|
305
|
+
await self.send(msg.text("你已经用过解药了"))
|
291
306
|
return False
|
292
307
|
|
293
|
-
await self.send("使用解药请发送 “1”\n不使用解药请发送 “/stop”")
|
308
|
+
await self.send(msg.text("使用解药请发送 “1”\n不使用解药请发送 “/stop”"))
|
294
309
|
|
295
310
|
while True:
|
296
|
-
text =
|
311
|
+
text = await self.receive_text()
|
297
312
|
if text == "1":
|
298
313
|
self.antidote = 0
|
299
|
-
self.set_state(antidote=
|
300
|
-
await self.send(
|
301
|
-
f"你对 {self.game.state.killed.name} 使用了解药,回合结束"
|
302
|
-
)
|
314
|
+
self.set_state(antidote=killed)
|
315
|
+
await self.send(f"你对 {killed.name} 使用了解药,回合结束")
|
303
316
|
return True
|
304
317
|
elif text == "/stop":
|
305
318
|
return False
|
@@ -326,7 +339,7 @@ class 女巫(Player):
|
|
326
339
|
)
|
327
340
|
|
328
341
|
while True:
|
329
|
-
text =
|
342
|
+
text = await self.receive_text()
|
330
343
|
index = check_index(text, len(players))
|
331
344
|
if index is not None:
|
332
345
|
selected = index - 1
|
@@ -365,7 +378,7 @@ class 守卫(Player):
|
|
365
378
|
)
|
366
379
|
|
367
380
|
while True:
|
368
|
-
text =
|
381
|
+
text = await self.receive_text()
|
369
382
|
if text == "/stop":
|
370
383
|
await self.send("你选择了取消,回合结束")
|
371
384
|
return
|
@@ -389,7 +402,11 @@ class 白痴(Player):
|
|
389
402
|
voted: bool = False
|
390
403
|
|
391
404
|
@override
|
392
|
-
async def kill(
|
405
|
+
async def kill(
|
406
|
+
self,
|
407
|
+
reason: KillReason,
|
408
|
+
*killers: Player,
|
409
|
+
) -> bool:
|
393
410
|
if reason == KillReason.Vote and not self.voted:
|
394
411
|
self.voted = True
|
395
412
|
await self.game.send(
|
@@ -398,10 +415,10 @@ class 白痴(Player):
|
|
398
415
|
.text("免疫本次投票放逐,且接下来无法参与投票")
|
399
416
|
)
|
400
417
|
return False
|
401
|
-
return await super().kill(reason)
|
418
|
+
return await super().kill(reason, *killers)
|
402
419
|
|
403
420
|
@override
|
404
|
-
async def vote(self, players: "PlayerSet") -> "Player | None":
|
421
|
+
async def vote(self, players: "PlayerSet") -> "tuple[Player, Player] | None":
|
405
422
|
if self.voted:
|
406
423
|
await self.send("你已经发动过白痴身份的技能,无法参与本次投票")
|
407
424
|
return None
|
@@ -412,83 +429,3 @@ class 白痴(Player):
|
|
412
429
|
class 平民(Player):
|
413
430
|
role: ClassVar[Role] = Role.平民
|
414
431
|
role_group: ClassVar[RoleGroup] = RoleGroup.好人
|
415
|
-
|
416
|
-
|
417
|
-
class PlayerSet(set[Player]):
|
418
|
-
@property
|
419
|
-
def size(self) -> int:
|
420
|
-
return len(self)
|
421
|
-
|
422
|
-
def alive(self) -> "PlayerSet":
|
423
|
-
return PlayerSet(p for p in self if p.alive)
|
424
|
-
|
425
|
-
def dead(self) -> "PlayerSet":
|
426
|
-
return PlayerSet(p for p in self if not p.alive)
|
427
|
-
|
428
|
-
def include(self, *types: Player | Role | RoleGroup) -> "PlayerSet":
|
429
|
-
return PlayerSet(
|
430
|
-
player
|
431
|
-
for player in self
|
432
|
-
if (player in types or player.role in types or player.role_group in types)
|
433
|
-
)
|
434
|
-
|
435
|
-
def select(self, *types: Player | Role | RoleGroup) -> "PlayerSet":
|
436
|
-
return self.include(*types)
|
437
|
-
|
438
|
-
def exclude(self, *types: Player | Role | RoleGroup) -> "PlayerSet":
|
439
|
-
return PlayerSet(
|
440
|
-
player
|
441
|
-
for player in self
|
442
|
-
if (
|
443
|
-
player not in types
|
444
|
-
and player.role not in types
|
445
|
-
and player.role_group not in types
|
446
|
-
)
|
447
|
-
)
|
448
|
-
|
449
|
-
def player_selected(self) -> "PlayerSet":
|
450
|
-
return PlayerSet(p.selected for p in self.alive() if p.selected is not None)
|
451
|
-
|
452
|
-
def sorted(self) -> list[Player]:
|
453
|
-
return sorted(self, key=lambda p: p.user_id)
|
454
|
-
|
455
|
-
async def interact(self, timeout_secs: float = 60) -> None:
|
456
|
-
async with asyncio.timeouts.timeout(timeout_secs):
|
457
|
-
await asyncio.gather(*[p.interact() for p in self.alive()])
|
458
|
-
|
459
|
-
async def vote(self, timeout_secs: float = 60) -> list[Player]:
|
460
|
-
async def vote(player: Player) -> "Player | None":
|
461
|
-
try:
|
462
|
-
async with asyncio.timeouts.timeout(timeout_secs):
|
463
|
-
return await player.vote(self)
|
464
|
-
except TimeoutError:
|
465
|
-
await player.send("投票超时,将自动弃票")
|
466
|
-
|
467
|
-
return [
|
468
|
-
p
|
469
|
-
for p in await asyncio.gather(*[vote(p) for p in self.alive()])
|
470
|
-
if p is not None
|
471
|
-
]
|
472
|
-
|
473
|
-
async def post_kill(self) -> None:
|
474
|
-
await asyncio.gather(*[p.post_kill() for p in self])
|
475
|
-
|
476
|
-
async def broadcast(self, message: str | UniMessage) -> None:
|
477
|
-
await asyncio.gather(*[p.send(message) for p in self])
|
478
|
-
|
479
|
-
async def wait_group_stop(self, group_id: str, timeout_secs: float) -> None:
|
480
|
-
async def wait(p: Player):
|
481
|
-
while True:
|
482
|
-
msg = await InputStore.fetch(p.user_id, group_id)
|
483
|
-
if msg.extract_plain_text() == "/stop":
|
484
|
-
break
|
485
|
-
|
486
|
-
with contextlib.suppress(TimeoutError):
|
487
|
-
async with asyncio.timeouts.timeout(timeout_secs):
|
488
|
-
await asyncio.gather(*[wait(p) for p in self])
|
489
|
-
|
490
|
-
def show(self) -> str:
|
491
|
-
return "\n".join(f"{i}. {p.name}" for i, p in enumerate(self.sorted(), 1))
|
492
|
-
|
493
|
-
def __getitem__(self, __index: int) -> Player:
|
494
|
-
return self.sorted()[__index]
|
@@ -0,0 +1,87 @@
|
|
1
|
+
import asyncio
|
2
|
+
import asyncio.timeouts
|
3
|
+
import contextlib
|
4
|
+
|
5
|
+
from nonebot_plugin_alconna.uniseg import UniMessage
|
6
|
+
|
7
|
+
from .constant import Role, RoleGroup
|
8
|
+
from .player import Player
|
9
|
+
from .utils import InputStore
|
10
|
+
|
11
|
+
|
12
|
+
class PlayerSet(set[Player]):
|
13
|
+
@property
|
14
|
+
def size(self) -> int:
|
15
|
+
return len(self)
|
16
|
+
|
17
|
+
def alive(self) -> "PlayerSet":
|
18
|
+
return PlayerSet(p for p in self if p.alive)
|
19
|
+
|
20
|
+
def dead(self) -> "PlayerSet":
|
21
|
+
return PlayerSet(p for p in self if not p.alive)
|
22
|
+
|
23
|
+
def include(self, *types: Player | Role | RoleGroup) -> "PlayerSet":
|
24
|
+
return PlayerSet(
|
25
|
+
player
|
26
|
+
for player in self
|
27
|
+
if (player in types or player.role in types or player.role_group in types)
|
28
|
+
)
|
29
|
+
|
30
|
+
def select(self, *types: Player | Role | RoleGroup) -> "PlayerSet":
|
31
|
+
return self.include(*types)
|
32
|
+
|
33
|
+
def exclude(self, *types: Player | Role | RoleGroup) -> "PlayerSet":
|
34
|
+
return PlayerSet(
|
35
|
+
player
|
36
|
+
for player in self
|
37
|
+
if (
|
38
|
+
player not in types
|
39
|
+
and player.role not in types
|
40
|
+
and player.role_group not in types
|
41
|
+
)
|
42
|
+
)
|
43
|
+
|
44
|
+
def player_selected(self) -> "PlayerSet":
|
45
|
+
return PlayerSet(p.selected for p in self.alive() if p.selected is not None)
|
46
|
+
|
47
|
+
def sorted(self) -> list[Player]:
|
48
|
+
return sorted(self, key=lambda p: p.user_id)
|
49
|
+
|
50
|
+
async def interact(self, timeout_secs: float = 60) -> None:
|
51
|
+
async with asyncio.timeouts.timeout(timeout_secs):
|
52
|
+
await asyncio.gather(*[p.interact() for p in self.alive()])
|
53
|
+
|
54
|
+
async def vote(self, timeout_secs: float = 60) -> dict[Player, list[Player]]:
|
55
|
+
async def vote(player: Player) -> "tuple[Player, Player] | None":
|
56
|
+
try:
|
57
|
+
async with asyncio.timeouts.timeout(timeout_secs):
|
58
|
+
return await player.vote(self)
|
59
|
+
except TimeoutError:
|
60
|
+
await player.send("投票超时,将视为弃票")
|
61
|
+
|
62
|
+
result: dict[Player, list[Player]] = {}
|
63
|
+
for item in await asyncio.gather(*[vote(p) for p in self.alive()]):
|
64
|
+
if item is not None:
|
65
|
+
player, voted = item
|
66
|
+
result[voted] = [*result.get(voted, []), player]
|
67
|
+
return result
|
68
|
+
|
69
|
+
async def broadcast(self, message: str | UniMessage) -> None:
|
70
|
+
await asyncio.gather(*[p.send(message) for p in self])
|
71
|
+
|
72
|
+
async def wait_group_stop(self, group_id: str, timeout_secs: float) -> None:
|
73
|
+
async def wait(p: Player):
|
74
|
+
while True:
|
75
|
+
msg = await InputStore.fetch(p.user_id, group_id)
|
76
|
+
if msg.extract_plain_text() == "/stop":
|
77
|
+
break
|
78
|
+
|
79
|
+
with contextlib.suppress(TimeoutError):
|
80
|
+
async with asyncio.timeouts.timeout(timeout_secs):
|
81
|
+
await asyncio.gather(*[wait(p) for p in self])
|
82
|
+
|
83
|
+
def show(self) -> str:
|
84
|
+
return "\n".join(f"{i}. {p.name}" for i, p in enumerate(self.sorted(), 1))
|
85
|
+
|
86
|
+
def __getitem__(self, __index: int) -> Player:
|
87
|
+
return self.sorted()[__index]
|
nonebot_plugin_werewolf/utils.py
CHANGED
@@ -1,8 +1,15 @@
|
|
1
1
|
import asyncio
|
2
|
+
import asyncio.timeouts
|
2
3
|
from collections import defaultdict
|
3
|
-
from typing import ClassVar
|
4
|
+
from typing import Annotated, Any, ClassVar
|
4
5
|
|
5
|
-
|
6
|
+
import nonebot_plugin_waiter as waiter
|
7
|
+
from nonebot.adapters import Event
|
8
|
+
from nonebot.rule import to_me
|
9
|
+
from nonebot_plugin_alconna import MsgTarget, UniMessage, UniMsg
|
10
|
+
from nonebot_plugin_userinfo import EventUserInfo, UserInfo
|
11
|
+
|
12
|
+
from .game import player_preset, running_games
|
6
13
|
|
7
14
|
|
8
15
|
def check_index(text: str, arrlen: int) -> int | None:
|
@@ -18,7 +25,7 @@ class InputStore:
|
|
18
25
|
futures: ClassVar[dict[str, asyncio.Future[UniMessage]]] = {}
|
19
26
|
|
20
27
|
@classmethod
|
21
|
-
async def fetch(cls, user_id: str, group_id: str | None = None):
|
28
|
+
async def fetch(cls, user_id: str, group_id: str | None = None) -> UniMessage[Any]:
|
22
29
|
key = f"{group_id}_{user_id}"
|
23
30
|
async with cls.locks[key]:
|
24
31
|
cls.futures[key] = asyncio.get_event_loop().create_future()
|
@@ -28,7 +35,154 @@ class InputStore:
|
|
28
35
|
del cls.futures[key]
|
29
36
|
|
30
37
|
@classmethod
|
31
|
-
def put(cls, user_id: str, group_id: str | None, msg: UniMessage):
|
38
|
+
def put(cls, user_id: str, group_id: str | None, msg: UniMessage) -> None:
|
32
39
|
key = f"{group_id}_{user_id}"
|
33
40
|
if future := cls.futures.get(key):
|
34
41
|
future.set_result(msg)
|
42
|
+
|
43
|
+
|
44
|
+
def user_in_game(user_id: str, group_id: str | None) -> bool:
|
45
|
+
if group_id is not None and group_id not in running_games:
|
46
|
+
return False
|
47
|
+
games = running_games.values() if group_id is None else [running_games[group_id]]
|
48
|
+
for game, _ in games:
|
49
|
+
return any(user_id == player.user_id for player in game.players)
|
50
|
+
return False
|
51
|
+
|
52
|
+
|
53
|
+
async def rule_in_game(event: Event, target: MsgTarget) -> bool:
|
54
|
+
if not running_games:
|
55
|
+
return False
|
56
|
+
if target.private:
|
57
|
+
return user_in_game(target.id, None)
|
58
|
+
elif target.id in running_games:
|
59
|
+
return user_in_game(event.get_user_id(), target.id)
|
60
|
+
return False
|
61
|
+
|
62
|
+
|
63
|
+
async def rule_not_in_game(event: Event, target: MsgTarget) -> bool:
|
64
|
+
return not await rule_in_game(event, target)
|
65
|
+
|
66
|
+
|
67
|
+
async def is_group(target: MsgTarget) -> bool:
|
68
|
+
return not target.private
|
69
|
+
|
70
|
+
|
71
|
+
async def _prepare_game_receive(
|
72
|
+
queue: asyncio.Queue[tuple[str, str, str]],
|
73
|
+
event: Event,
|
74
|
+
group_id: str,
|
75
|
+
) -> None:
|
76
|
+
async def rule(target_: MsgTarget) -> bool:
|
77
|
+
return not target_.private and target_.id == group_id
|
78
|
+
|
79
|
+
@waiter.waiter(
|
80
|
+
waits=[event.get_type()],
|
81
|
+
keep_session=False,
|
82
|
+
rule=to_me() & rule & rule_not_in_game,
|
83
|
+
)
|
84
|
+
def wait(
|
85
|
+
event: Event,
|
86
|
+
info: Annotated[UserInfo | None, EventUserInfo()],
|
87
|
+
msg: UniMsg,
|
88
|
+
) -> tuple[str, str, str]:
|
89
|
+
return (
|
90
|
+
event.get_user_id(),
|
91
|
+
info.user_name if info is not None else event.get_user_id(),
|
92
|
+
msg.extract_plain_text().strip(),
|
93
|
+
)
|
94
|
+
|
95
|
+
async for user, name, text in wait(default=(None, "", "")):
|
96
|
+
if user is None:
|
97
|
+
continue
|
98
|
+
await queue.put((user, name, text))
|
99
|
+
|
100
|
+
|
101
|
+
async def _prepare_game_handle(
|
102
|
+
queue: asyncio.Queue[tuple[str, str, str]],
|
103
|
+
players: dict[str, str],
|
104
|
+
admin_id: str,
|
105
|
+
) -> None:
|
106
|
+
while True:
|
107
|
+
user, name, text = await queue.get()
|
108
|
+
msg = UniMessage.at(user)
|
109
|
+
|
110
|
+
match (text, user == admin_id):
|
111
|
+
case ("开始游戏", True):
|
112
|
+
player_num = len(players)
|
113
|
+
if player_num < min(player_preset):
|
114
|
+
await (
|
115
|
+
msg.text(f"游戏至少需要 {min(player_preset)} 人, ")
|
116
|
+
.text(f"当前已有 {player_num} 人")
|
117
|
+
.send()
|
118
|
+
)
|
119
|
+
elif player_num > max(player_preset):
|
120
|
+
await (
|
121
|
+
msg.text(f"游戏最多需要 {max(player_preset)} 人, ")
|
122
|
+
.text(f"当前已有 {player_num} 人")
|
123
|
+
.send()
|
124
|
+
)
|
125
|
+
elif player_num not in player_preset:
|
126
|
+
await (
|
127
|
+
msg.text(f"不存在总人数为 {player_num} 的预设, ")
|
128
|
+
.text("无法开始游戏")
|
129
|
+
.send()
|
130
|
+
)
|
131
|
+
else:
|
132
|
+
await msg.text("游戏即将开始...").send()
|
133
|
+
return
|
134
|
+
|
135
|
+
case ("开始游戏", False):
|
136
|
+
await msg.text("只有游戏发起者可以开始游戏").send()
|
137
|
+
|
138
|
+
case ("结束游戏", True):
|
139
|
+
await msg.text("已结束当前游戏").finish()
|
140
|
+
|
141
|
+
case ("结束游戏", False):
|
142
|
+
await msg.text("只有游戏发起者可以结束游戏").send()
|
143
|
+
|
144
|
+
case ("加入游戏", True):
|
145
|
+
await msg.text("游戏发起者已经加入游戏了").send()
|
146
|
+
|
147
|
+
case ("加入游戏", False):
|
148
|
+
if user not in players:
|
149
|
+
players[user] = name
|
150
|
+
await msg.text("成功加入游戏").send()
|
151
|
+
else:
|
152
|
+
await msg.text("你已经加入游戏了").send()
|
153
|
+
|
154
|
+
case ("退出游戏", True):
|
155
|
+
await msg.text("游戏发起者无法退出游戏").send()
|
156
|
+
|
157
|
+
case ("退出游戏", False):
|
158
|
+
if user in players:
|
159
|
+
del players[user]
|
160
|
+
await msg.text("成功退出游戏").send()
|
161
|
+
else:
|
162
|
+
await msg.text("你还没有加入游戏").send()
|
163
|
+
|
164
|
+
case ("当前玩家", _):
|
165
|
+
msg.text("\n当前玩家:\n")
|
166
|
+
for name in players.values():
|
167
|
+
msg.text(f"\n{name}")
|
168
|
+
await msg.send()
|
169
|
+
|
170
|
+
|
171
|
+
async def prepare_game(event: Event, players: dict[str, str]) -> None:
|
172
|
+
queue = asyncio.Queue()
|
173
|
+
task_receive = asyncio.create_task(
|
174
|
+
_prepare_game_receive(
|
175
|
+
queue,
|
176
|
+
event,
|
177
|
+
UniMessage.get_target().id,
|
178
|
+
)
|
179
|
+
)
|
180
|
+
|
181
|
+
try:
|
182
|
+
await _prepare_game_handle(
|
183
|
+
queue,
|
184
|
+
players,
|
185
|
+
event.get_user_id(),
|
186
|
+
)
|
187
|
+
finally:
|
188
|
+
task_receive.cancel()
|
{nonebot_plugin_werewolf-1.0.1.dist-info → nonebot_plugin_werewolf-1.0.3.dist-info}/METADATA
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: nonebot-plugin-werewolf
|
3
|
-
Version: 1.0.
|
3
|
+
Version: 1.0.3
|
4
4
|
Summary: Default template for PDM package
|
5
5
|
Author-Email: wyf7685 <wyf7685@163.com>
|
6
6
|
License: MIT
|
@@ -12,9 +12,9 @@ Requires-Dist: nonebot-plugin-waiter>=0.7.1
|
|
12
12
|
Description-Content-Type: text/markdown
|
13
13
|
|
14
14
|
<div align="center">
|
15
|
-
<a href="https://v2.nonebot.dev/store"
|
16
|
-
|
17
|
-
|
15
|
+
<a href="https://v2.nonebot.dev/store">
|
16
|
+
<img src="https://github.com/wyf7685/wyf7685/blob/main/assets/NoneBotPlugin.svg" width="300" alt="logo">
|
17
|
+
</a>
|
18
18
|
</div>
|
19
19
|
|
20
20
|
<div align="center">
|
@@ -24,7 +24,7 @@ Description-Content-Type: text/markdown
|
|
24
24
|
_✨ 简单的狼人杀插件 ✨_
|
25
25
|
|
26
26
|
[](./LICENSE)
|
27
|
-
[](https://pypi.python.org/pypi/nonebot-plugin-
|
27
|
+
[](https://pypi.python.org/pypi/nonebot-plugin-werewolf)
|
28
28
|
[](https://www.python.org/)
|
29
29
|
|
30
30
|
[](https://pdm-project.org)
|
@@ -143,7 +143,9 @@ _✨ 简单的狼人杀插件 ✨_
|
|
143
143
|
职业预设可以通过配置项 `werewolf__override_preset` 修改
|
144
144
|
|
145
145
|
<details>
|
146
|
-
|
146
|
+
<summary>示例</summary>
|
147
|
+
|
148
|
+
配置项 `werewolf__override_preset`
|
147
149
|
|
148
150
|
```env
|
149
151
|
werewolf__override_preset='
|
@@ -169,6 +171,10 @@ werewolf__override_preset='
|
|
169
171
|
<details>
|
170
172
|
<summary>更新日志</summary>
|
171
173
|
|
174
|
+
- 2024.09.01 v1.0.3
|
175
|
+
|
176
|
+
- 优化玩家交互体验
|
177
|
+
|
172
178
|
- 2024.08.31 v1.0.1
|
173
179
|
|
174
180
|
- 允许通过配置项修改职业预设
|
@@ -0,0 +1,13 @@
|
|
1
|
+
nonebot_plugin_werewolf-1.0.3.dist-info/METADATA,sha256=0co-Cgj6W-98W7ZyDOR8u7phucnJUIstiq0vtwQbLf8,5815
|
2
|
+
nonebot_plugin_werewolf-1.0.3.dist-info/WHEEL,sha256=rSwsxJWe3vzyR5HCwjWXQruDgschpei4h_giTm0dJVE,90
|
3
|
+
nonebot_plugin_werewolf-1.0.3.dist-info/licenses/LICENSE,sha256=B_WbEqjGr6GYVNfEJPY31T1Opik7OtgOkhRs4Ig3e2M,1064
|
4
|
+
nonebot_plugin_werewolf/__init__.py,sha256=CajWxyaRax2kotzc4w5IcJbdmc07ysCbSNGKve5wIvw,734
|
5
|
+
nonebot_plugin_werewolf/config.py,sha256=3O63P1pjvJEwgOwxApAHbKQzvCR1zoG5UjTClZ_OJts,315
|
6
|
+
nonebot_plugin_werewolf/constant.py,sha256=kMn5DHXdYuOXeGpJjnG_WX9AD-OytLKXSjrww0T5OYg,1234
|
7
|
+
nonebot_plugin_werewolf/game.py,sha256=PI6j-T70Logax87fpmf8HBS3LihF7J40nespvoNFyus,13449
|
8
|
+
nonebot_plugin_werewolf/matchers.py,sha256=o0LdO7AOUjTPcNaNTHbmw5d0CcnMc7mX8oko0JT8Np4,2351
|
9
|
+
nonebot_plugin_werewolf/ob11_ext.py,sha256=I6bPCv5SgAStTJuvBl5F7wqgiksWeFkb4R7n06jXprA,2399
|
10
|
+
nonebot_plugin_werewolf/player.py,sha256=z_yVjqfDOxnQlDtDxSbdmvEapIQikkho2hCjb46zqKY,13799
|
11
|
+
nonebot_plugin_werewolf/player_set.py,sha256=ptNZb13H8DgcjQ9tm3uT2ISXnnWioQG1W4R506HlaMU,3057
|
12
|
+
nonebot_plugin_werewolf/utils.py,sha256=KfCBdiy5pxkOf1k1EOg8NLe5-viiCCm72vPP6lobudQ,6127
|
13
|
+
nonebot_plugin_werewolf-1.0.3.dist-info/RECORD,,
|
@@ -1,11 +0,0 @@
|
|
1
|
-
nonebot_plugin_werewolf-1.0.1.dist-info/METADATA,sha256=tQZAz__0XgsrQTiovdGfV-glhlS6TESEMfwhxvYF1sc,5944
|
2
|
-
nonebot_plugin_werewolf-1.0.1.dist-info/WHEEL,sha256=rSwsxJWe3vzyR5HCwjWXQruDgschpei4h_giTm0dJVE,90
|
3
|
-
nonebot_plugin_werewolf-1.0.1.dist-info/licenses/LICENSE,sha256=B_WbEqjGr6GYVNfEJPY31T1Opik7OtgOkhRs4Ig3e2M,1064
|
4
|
-
nonebot_plugin_werewolf/__init__.py,sha256=dGaXi6VOQUYCjbe2yfwTLWbHvRRRq11xUeDCgPqnjPE,734
|
5
|
-
nonebot_plugin_werewolf/config.py,sha256=3O63P1pjvJEwgOwxApAHbKQzvCR1zoG5UjTClZ_OJts,315
|
6
|
-
nonebot_plugin_werewolf/constant.py,sha256=Q1MKe5hSzg88jL8LQ5SvC2Pr6QwJ9PHrrj1qpOSYCZg,1221
|
7
|
-
nonebot_plugin_werewolf/game.py,sha256=KD57WxN16VcLJbflxxPw4EXGfjEaXvvBAB8DiK4RMwY,12322
|
8
|
-
nonebot_plugin_werewolf/matchers.py,sha256=8bGM1gESjL9JMFQRv7611YGM0TZFfg0hQjlOrzS3zRg,8336
|
9
|
-
nonebot_plugin_werewolf/player.py,sha256=ckviPuXv9JvgcwzD9xcHSVBj7JdRtqGge7WFpynonVY,16355
|
10
|
-
nonebot_plugin_werewolf/utils.py,sha256=mzFVuzkayfY-8wXnFASPqo2dtJkNOhycfoceIyRGuZg,1023
|
11
|
-
nonebot_plugin_werewolf-1.0.1.dist-info/RECORD,,
|
File without changes
|
{nonebot_plugin_werewolf-1.0.1.dist-info → nonebot_plugin_werewolf-1.0.3.dist-info}/licenses/LICENSE
RENAMED
File without changes
|