-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcli.py
More file actions
659 lines (533 loc) · 21.3 KB
/
Copy pathcli.py
File metadata and controls
659 lines (533 loc) · 21.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
# -*- coding: utf-8 -*-
"""剧本杀命令行界面 - 本地直接运行游戏"""
import asyncio
import os
import random
from dotenv import load_dotenv
load_dotenv()
from models import StoryArchive, GameState, PlayerState
from generator import (
create_deepseek_client,
generate_murder_case,
parse_case_to_models,
save_story,
load_story,
list_stories,
delete_story,
ensure_stories_dir
)
from game_agent import ScriptCharacterAgent
from game_hub import ScriptKillHub
from user_agent import UserAgent
from agentscope.model import DashScopeChatModel
def show_banner():
"""显示游戏banner"""
banner = """
+==========================================================+
| |
| Script Kill - AgentScope Version |
| |
| 剧本杀 - AgentScope 版本 |
| |
+==========================================================+
"""
print(banner)
def show_menu():
"""显示主菜单"""
print("\n【主菜单】")
print(" 1. 新游戏 - 输入主题生成新剧本")
print(" 2. 加载故事 - 从存档列表选择一个故事")
print(" 3. 删除存档 - 管理故事存档")
print(" 4. 退出")
def print_story_list(stories: list[dict]):
"""打印故事列表"""
if not stories:
print("\n暂无存档")
return
print("\n【故事列表】")
for i, story in enumerate(stories, 1):
print(f"\n{i}. {story['title']}")
print(f" 主题: {story['topic']}")
print(f" 角色数: {story['num_characters']}")
print(f" 创建时间: {story['created_at']}")
print(f" ID: {story['id']}")
# ==================== 阶段执行函数 ====================
def display_clue_board(hub: ScriptKillHub, human_id: str):
"""显示线索看板"""
# 场景公开线索 - 一开始就对所有人可见
scene_public = [c for c in hub.archive.clues if c.holder_id == "scene"]
state = hub.game_state.player_states.get(human_id)
# 玩家已获得的线索(从 state.known_clues 获取)
owned = []
if state:
for clue_id in state.known_clues:
clue = hub.archive.get_clue(clue_id)
if clue and clue.holder_id != "scene": # 排除场景线索(已单独显示)
owned.append(clue)
if scene_public:
print("\n【公开线索】(场景)")
for clue in scene_public:
print(f" [{clue.type}] {clue.content}")
if owned:
print("\n【已获得线索】")
for clue in owned:
holder_name = "场景"
if clue.holder_id != "scene":
holder = hub._get_character_by_id(clue.holder_id)
if holder:
holder_name = holder.name
print(f" [{clue.type}] {clue.content}")
print(f" (持有者: {holder_name})")
else:
if not scene_public:
print("\n暂无线索")
async def run_introduction_phase(hub: ScriptKillHub, human_id: str):
"""执行自我介绍阶段"""
print(f"\n{'='*60}")
print("【阶段1:自我介绍】")
print(f"{'='*60}")
human_char = hub.archive.get_character(human_id)
if not human_char:
return
print(f"\n【你扮演的角色】{human_char.name}")
print(f"身份: {human_char.public_identity}")
print(f"外貌: {human_char.appearance}")
print(f"背景: {human_char.backstory}")
print("\n请输入你的自我介绍(可以直接按回车跳过):")
user_intro = input("> ").strip()
if not user_intro:
user_intro = f"大家好,我是{human_char.name},{human_char.public_identity}。"
print(f"\n你: {user_intro}")
# AI角色自我介绍
for agent in hub.characters:
char_id = agent.character_data.get("id")
if char_id and hub.game_state.player_states.get(char_id):
state = hub.game_state.player_states[char_id]
if state.is_alive:
print(f"\n{agent.name} 正在自我介绍...")
response = await agent.introduction()
print(response)
input("\n按回车继续...")
async def run_investigation_phase(hub: ScriptKillHub, human_id: str):
"""执行搜证阶段"""
print(f"\n{'='*60}")
print("【阶段2:搜证】")
print(f"{'='*60}")
human_char = hub.archive.get_character(human_id)
# 初始分配1条线索给玩家
distributed = hub._distribute_random_clues(human_id, 1)
if distributed:
print("\n你获得了以下线索:")
for clue in distributed:
print(f" - [{clue.type}] {clue.content}")
# 如果是角色持有的线索,让AI回应
if clue.holder_id != "scene":
holder_agent = hub._get_character_agent(clue.holder_id)
if holder_agent:
known_clues = hub._get_known_clues(human_id)
response = await holder_agent.investigate(known_clues)
# investigate() 返回的内容已包含前缀
print(response)
# AI角色初始分配线索
for agent in hub.characters:
char_id = agent.character_data.get("id")
if char_id and char_id != human_id:
state = hub.game_state.player_states.get(char_id)
if state and state.is_alive:
ai_distributed = hub._distribute_random_clues(char_id, 1)
if ai_distributed:
print(f"\n{agent.name} 获得了 {len(ai_distributed)} 条线索")
# 显示指认次数
human_state = hub.game_state.player_states.get(human_id)
if human_state:
print(f"\n你的指认次数: {human_state.accusation_points}")
# 显示线索看板
display_clue_board(hub, human_id)
while True:
print("\n输入选项:")
print(" 查看线索 - 查看已获得的线索详情")
print(" 进入讨论 - 结束搜证,进入讨论阶段")
print(" 指认 <角色名字> - 直接指认凶手(整局仅1次)")
user_input = input("> ").strip()
if user_input == "进入讨论":
return
if user_input.startswith("指认"):
parts = user_input.split()
if len(parts) >= 2:
target_name = parts[1]
target_id = hub.archive.get_character_id_by_name(target_name)
if not target_id:
print(f"找不到角色: {target_name}")
continue
if hub.can_accuse(human_id):
correct, msg, game_ended = hub.accuse(human_id, target_id)
print(msg)
if game_ended:
input("\n按回车继续...")
return
else:
print("你已经没有指认次数了!")
continue
return
if user_input == "查看线索":
known = hub._get_known_clues(human_id)
if known:
print("\n【已获得的线索详情】")
for clue in known:
print(f"\n[{clue.type}] {clue.content}")
else:
print("暂无线索")
continue
# 默认进入讨论阶段
return
async def run_discussion_phase(hub: ScriptKillHub, human_id: str):
"""执行讨论阶段"""
print(f"\n{'='*60}")
print("【阶段3:讨论】")
print(f"{'='*60}")
max_rounds = hub.game_state.max_rounds
human_char = hub.archive.get_character(human_id)
for round_num in range(1, max_rounds + 1):
print(f"\n--- 讨论轮次 {round_num}/{max_rounds} ---")
# 显示线索看板
display_clue_board(hub, human_id)
# 人类玩家发言
print("\n请发言(输入你想说的话,或 '跳过'):")
user_speech = input("> ").strip()
if user_speech.lower() not in ["跳过", "pass", "skip"]:
print(f"\n你: {user_speech}")
hub.game_state.discussion_history.append(f"{human_char.name}: {user_speech}")
# 将玩家发言加入所有AI的对话历史
for agent in hub.characters:
if hasattr(agent, 'conversation_history'):
agent.conversation_history.append({
"role": "user",
"speaker": human_char.name,
"message": user_speech
})
# AI角色发言
for agent in hub.characters:
char_id = agent.character_data.get("id")
state = hub.game_state.player_states.get(char_id) if char_id else None
if not state or not state.is_alive:
continue
known = hub._get_known_clues(char_id)
print(f"\n{agent.name} 正在发言...")
response = await agent.discuss(known)
# discuss() 返回的内容已包含前缀
print(response)
hub.game_state.discussion_history.append(f"{agent.name}: {response}")
# 显示选项
print("\n讨论选项:")
print(" 投票 - 进入投票阶段")
print(" 指认 <角色名字> - 直接指认凶手")
print(" 继续 - 进入下一轮讨论")
print(" 返回搜证 - 回到搜证阶段")
user_input = input("> ").strip()
if user_input == "投票":
hub.phase = "voting"
return
if user_input.startswith("指认"):
parts = user_input.split()
if len(parts) >= 2:
target_name = parts[1]
target_id = hub.archive.get_character_id_by_name(target_name)
if not target_id:
print(f"找不到角色: {target_name}")
continue
if hub.can_accuse(human_id):
correct, msg, game_ended = hub.accuse(human_id, target_id)
print(msg)
if game_ended:
input("\n按回车查看真相...")
hub.phase = "reveal"
return
else:
print("你没有指认次数了!")
continue
return
if user_input in ["返回搜证", "搜证", "back"]:
print("\n返回搜证阶段...")
hub.phase = "investigation"
return
print("\n已达到最大讨论轮数,进入投票阶段")
hub.phase = "voting"
async def run_voting_phase(hub: ScriptKillHub, human_id: str):
"""执行投票阶段"""
print(f"\n{'='*60}")
print("【阶段4:投票】")
print(f"{'='*60}")
# 初始化投票轮次计数
if not hasattr(hub.game_state, 'voting_round'):
hub.game_state.voting_round = 0
hub.game_state.voting_round += 1
max_voting_rounds = 1
if hub.game_state.voting_round > max_voting_rounds:
print(f"\n投票次数已用完,无法达成一致,进入真相揭晓")
hub.phase = "reveal"
hub.game_state.game_ended = True
hub.game_state.winner = "killer"
return
print(f"\n投票轮次: {hub.game_state.voting_round}/{max_voting_rounds}")
# 显示存活玩家
print("\n存活玩家:")
for agent in hub.all_agents:
char_id = agent.character_data.get("id") if hasattr(agent, 'character_data') else None
if char_id:
state = hub.game_state.player_states.get(char_id)
if state and state.is_alive:
char = hub.archive.get_character(char_id)
if char:
print(f" - {char.name} ({char.public_identity})")
# 重置投票(如果是新一轮)
if hub.game_state.voting_round > 1:
for state in hub.game_state.player_states.values():
state.vote = None
hub.game_state.votes_record = []
print("\n重新投票...")
# 人类玩家投票
print("\n请投票(输入角色名字):")
print("或者输入 '跳过' 随机投票")
user_vote = input("> ").strip()
# 尝试将名字转换为ID
if user_vote.lower() not in ["跳过", "pass", "skip", "随机"]:
vote_id = hub.archive.get_character_id_by_name(user_vote)
if vote_id:
user_vote = vote_id
else:
print(f"找不到角色: {user_vote},将随机投票")
user_vote = "随机"
if user_vote.lower() in ["跳过", "pass", "skip", "随机"]:
# 随机投票(排除自己)
candidates = []
for agent in hub.all_agents:
char_id = agent.character_data.get("id") if hasattr(agent, 'character_data') else None
if char_id and char_id != human_id:
state = hub.game_state.player_states.get(char_id)
if state and state.is_alive:
candidates.append(char_id)
if candidates:
user_vote = random.choice(candidates)
hub.submit_vote(human_id, user_vote)
print(f"你投票给了: {user_vote}")
# AI角色投票
for agent in hub.characters:
char_id = agent.character_data.get("id")
state = hub.game_state.player_states.get(char_id) if char_id else None
if not state or not state.is_alive:
continue
known = hub._get_known_clues(char_id)
vote_id, reasoning = await agent.vote(known)
# 如果AI没有返回有效ID,随机选择
alive_players = []
for a in hub.all_agents:
cid = a.character_data.get("id") if hasattr(a, 'character_data') else None
if cid:
s = hub.game_state.player_states.get(cid)
if s and s.is_alive and cid != char_id:
alive_players.append(cid)
if vote_id not in alive_players:
if alive_players:
vote_id = random.choice(alive_players)
hub.submit_vote(char_id, vote_id)
voted_char = hub.archive.get_character(vote_id)
voted_name = voted_char.name if voted_char else vote_id
print(f"{agent.name} 投票给了: {voted_name}")
# 统计票数
winner_id, conditions, is_tied, game_ended, winner = hub.check_voting_result()
print(f"\n投票结果: {conditions}")
if game_ended:
if winner == "good":
char = hub.archive.get_character(winner_id)
print(f"\n凶手{char.name if char else winner_id}被投票出局!好人胜利!")
else:
char = hub.archive.get_character(winner_id) if winner_id else None
print(f"\n{char.name if char else winner_id}被投票出局!但这不是凶手,凶手逃脱!")
hub.phase = "reveal"
elif is_tied:
# 平票时回到讨论阶段
print("\n投票平局,回到讨论阶段进行更多讨论...")
hub.phase = "discussion"
hub.game_state.voting_round = 0 # 重置投票轮次
else:
# 票数不足,回到讨论阶段
print(f"\n票数不足,回到讨论阶段进行更多讨论...")
hub.phase = "discussion"
hub.game_state.voting_round = 0 # 重置投票轮次
def run_reveal_phase(hub: ScriptKillHub):
"""执行真相揭晓阶段"""
print(f"\n{'='*60}")
print("【阶段5:真相揭晓】")
print(f"{'='*60}")
result = hub.reveal()
return result
# ==================== 游戏流程 ====================
async def run_game(archive: StoryArchive):
"""运行游戏"""
char_ids = [c.id for c in archive.characters]
human_id = random.choice(char_ids)
human_char = next(c for c in archive.characters if c.id == human_id)
print(f"\n{'='*60}")
print(f"你的身份是:{human_char.name}({human_char.public_identity})")
print(f"{'='*60}\n")
print(f"背景:{human_char.backstory}")
print(f"与受害者关系:{human_char.relationship_with_victim}\n")
model = DashScopeChatModel(
model_name="qwen-max",
api_key=os.getenv("DASHSCOPE_API_KEY")
)
ai_agents = []
user_agent = UserAgent(human_char.name, human_char.public_identity, human_char.id)
for char in archive.characters:
if char.id != human_id:
agent = ScriptCharacterAgent(
name=char.name,
character_data=char.__dict__,
model=model,
archive=archive
)
ai_agents.append(agent)
hub = ScriptKillHub(
characters=ai_agents,
user_agent=user_agent,
archive=archive
)
print("\n" + "="*60)
print("游戏开始!")
print("="*60)
# 游戏主循环
while True:
phase = hub.phase
if phase == "introduction":
await run_introduction_phase(hub, human_id)
hub.next_phase()
elif phase == "investigation":
await run_investigation_phase(hub, human_id)
if hub.phase == "reveal":
run_reveal_phase(hub)
break
elif hub.phase == "investigation":
# 用户输入"进入讨论"或其他默认行为
hub.next_phase()
elif phase == "discussion":
await run_discussion_phase(hub, human_id)
if hub.phase == "reveal":
run_reveal_phase(hub)
break
# 如果 phase 变为 "voting"(用户选择投票),继续循环
# 如果 phase 仍是 "discussion"(达到最大轮数),主循环下次会调用 voting
elif phase == "voting":
await run_voting_phase(hub, human_id)
if hub.phase == "reveal":
run_reveal_phase(hub)
break
# 如果返回后 phase 仍是 voting,说明需要继续投票(平票)
# 不调用 next_phase,继续循环
elif phase == "reveal":
run_reveal_phase(hub)
break
# 如果游戏结束但 phase 不是 reveal,进入 reveal
if hub.game_state.game_ended and hub.phase != "reveal":
hub.phase = "reveal"
run_reveal_phase(hub)
break
# 游戏结束
print("\n" + "="*60)
print("【游戏结束】")
print("="*60)
if hub.game_state.winner == "good":
print("\n🎉 恭喜!你成功指认了凶手,好人胜利!")
elif hub.game_state.winner == "killer":
print("\n😔 很遗憾,凶手逃脱了,下次再接再厉!")
else:
print("\n游戏结束")
# ==================== 主程序 ====================
def new_game_flow():
"""新游戏流程"""
print("\n请输入剧本主题(如:豪华邮轮谋杀、民国上海滩、家族遗产争夺等):")
topic = input("\n主题: ").strip()
if not topic:
print("主题不能为空!")
return None
print(f"\n正在生成剧本,主题:「{topic}」...")
print("(这可能需要一些时间,请耐心等待...\n")
try:
client = create_deepseek_client()
case_data = generate_murder_case(topic, client)
if not case_data:
print("生成剧本失败,请检查 API 配置后重试。")
return None
archive = parse_case_to_models(case_data, topic)
save_story(archive)
print(f"\n剧本生成成功!")
print(f"标题:{archive.title}")
print(f"角色数量:{len(archive.characters)}")
print(f"线索数量:{len(archive.clues)}")
return archive
except Exception as e:
print(f"\n生成剧本出错:{e}")
import traceback
traceback.print_exc()
return None
def load_game_flow():
"""加载游戏流程"""
stories = list_stories()
print_story_list(stories)
if not stories:
return None
print("\n请选择要加载的故事(输入序号):")
try:
choice = int(input("> ").strip())
if 1 <= choice <= len(stories):
story_id = stories[choice - 1]["id"]
return load_story(story_id)
except ValueError:
pass
print("无效选择")
return None
def delete_game_flow():
"""删除游戏流程"""
stories = list_stories()
print_story_list(stories)
if not stories:
return
print("\n请选择要删除的故事(输入序号,输入0返回):")
try:
choice = int(input("> ").strip())
if choice == 0:
return
if 1 <= choice <= len(stories):
story_id = stories[choice - 1]["id"]
confirm = input(f"确认删除 '{stories[choice-1]['title']}'?(y/n): ").strip().lower()
if confirm == 'y':
delete_story(story_id)
except ValueError:
pass
async def main():
"""主函数"""
ensure_stories_dir()
show_banner()
while True:
show_menu()
choice = input("\n请选择: ").strip()
if choice == "1":
# 新游戏
archive = new_game_flow()
if archive:
await run_game(archive)
elif choice == "2":
# 加载游戏
archive = load_game_flow()
if archive:
await run_game(archive)
elif choice == "3":
# 删除存档
delete_game_flow()
elif choice == "4":
# 退出
print("\n再见!")
break
else:
print("无效选择")
if __name__ == "__main__":
asyncio.run(main())