diff --git a/jsinvn.py b/jsinvn.py index 42b3fc2..4b5f355 100644 --- a/jsinvn.py +++ b/jsinvn.py @@ -1,5 +1,15 @@ import streamlit as st import random +import os +import requests +from dotenv import load_dotenv + +# 加载环境变量 +load_dotenv() + +# 获取Deepseek API配置 +DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY") +DEEPSEEK_BASE_URL = os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com") # 页面配置 st.set_page_config( @@ -63,250 +73,45 @@ if "runoff" not in st.session_state: st.session_state.runoff = False if "tied_players" not in st.session_state: st.session_state.tied_players = [] +if "max_rounds" not in st.session_state: + st.session_state.max_rounds = 4 +if "runoff_speeches" not in st.session_state: + st.session_state.runoff_speeches = [] +if "player_runoff_speech" not in st.session_state: + st.session_state.player_runoff_speech = "" +if "player_runoff_speech_saved" not in st.session_state: + st.session_state.player_runoff_speech_saved = False -# 词语描述库 -word_descriptions = { - "苹果": [ - "这是一种常见的东西", - "有特定的颜色", - "圆形的,口感不错", - "可以直接食用,也可以加工", - "富含营养", - "通常生长在高处", - "有外皮和内部结构", - "是一种健康的选择", - "在中国很常见", - "有些品种味道比较特别" - ], - "橘子": [ - "这是一种可以食用的东西", - "有特定的颜色,味道有点特别", - "圆形的,有外皮", - "内部结构是一瓣一瓣的", - "富含营养", - "通常生长在温暖的地方", - "需要剥皮后食用", - "有不同的品种", - "汁水很多", - "在某些季节比较常见" - ], - "电脑": [ - "这是一种电子设备", - "可以用来做很多事情", - "有屏幕和输入设备", - "需要电源才能使用", - "可以连接网络", - "有主机和显示部分", - "可以运行各种程序", - "现在很多家庭都有", - "有不同的类型", - "需要定期维护" - ], - "手机": [ - "这是一种便携式电子设备", - "可以用来沟通和获取信息", - "触摸操作,便于携带", - "需要充电才能使用", - "可以安装各种应用", - "有拍照功能", - "现在几乎人人都有", - "有不同的品牌和型号", - "可以用来娱乐", - "需要特定卡片才能通话" - ], - "篮球": [ - "这是一种球类用品", - "圆形的,有特定材质", - "需要用手操作", - "有特定的颜色", - "在特定的场地上使用", - "有不同的规格", - "可以用来进行比赛", - "需要充气才能使用", - "很多年轻人喜欢", - "有专业的赛事" - ], - "足球": [ - "这是一种球类用品", - "圆形的,有特定的颜色搭配", - "需要用脚操作", - "在特定的场地上使用", - "可以用来进行比赛", - "需要充气才能使用", - "是世界上很受欢迎的活动", - "有不同的规格", - "很多人喜欢观看相关比赛", - "有专业的赛事" - ], - "牛奶": [ - "这是一种饮品", - "白色的,有营养", - "来自动物", - "可以直接饮用", - "富含蛋白质和钙", - "有不同的品牌", - "可以作为早餐的一部分", - "需要特定条件保存", - "有不同的类型", - "很多人每天都会喝" - ], - "豆浆": [ - "这是一种饮品", - "乳白色的,有特殊香味", - "用植物制作", - "可以直接饮用", - "富含植物蛋白", - "有不同的口味", - "可以作为早餐的一部分", - "是传统饮品", - "适合特定人群", - "很多早餐店都有" - ], - "米饭": [ - "这是一种主食", - "白色的,颗粒状", - "用特定食材制作", - "很多人经常吃", - "可以搭配各种食物", - "需要加水烹饪", - "有不同的品种", - "可以有不同的做法", - "吃了会有饱腹感", - "很多地方的人都吃" - ], - "面条": [ - "这是一种主食", - "长条状的,需要烹饪", - "用特定食材制作", - "可以有各种口味", - "有不同的形状", - "需要水煮", - "可以有不同的做法", - "是传统食物", - "烹饪时间不长", - "很多人喜欢吃" - ], - "猫": [ - "这是一种动物", - "毛茸茸的,会发出特定的声音", - "喜欢抓小东西", - "可以作为宠物", - "有不同的品种和颜色", - "喜欢休息", - "比较独立", - "会自己清洁", - "有爪子和尾巴", - "很多人喜欢养" - ], - "狗": [ - "这是一种动物", - "毛茸茸的,会发出特定的声音", - "可以作为宠物", - "很忠诚", - "有不同的品种和大小", - "喜欢活动和玩耍", - "需要定期外出", - "可以训练各种技能", - "有爪子和尾巴", - "被很多人视为朋友" - ], - "夏天": [ - "这是一个季节", - "天气比较热", - "有很多新鲜的东西", - "适合在水上进行活动", - "白天的时间比较长", - "需要穿轻薄的衣服", - "可能会有雷雨天气", - "学生们都有假期", - "适合吃冷的东西", - "阳光比较强烈" - ], - "冬天": [ - "这是一个季节", - "天气比较冷", - "可能会有白色的东西从天上掉下来", - "需要穿厚厚的衣服", - "白天的时间比较短", - "适合在雪地上活动", - "很多节日在这个时候", - "需要注意保暖", - "水面可能会变硬", - "适合喝热的饮料" - ], - "可乐": [ - "这是一种饮料", - "有特定的颜色,有气泡", - "味道甜甜的", - "含有咖啡因", - "可以解渴", - "有不同的品牌", - "可以加冰饮用", - "很多人喜欢喝", - "适合搭配食物", - "开瓶后会有气体冒出" - ], - "雪碧": [ - "这是一种饮料", - "透明的,有气泡", - "味道甜甜的", - "有柠檬味", - "可以解渴", - "有不同的品牌", - "可以加冰饮用", - "很多人喜欢喝", - "适合搭配食物", - "开瓶后会有气体冒出" - ], - "手机": [ - "这是一种便携式电子设备", - "可以用来沟通和获取信息", - "触摸操作,便于携带", - "需要充电才能使用", - "可以安装各种应用", - "有拍照功能", - "现在几乎人人都有", - "有不同的品牌和型号", - "可以用来娱乐", - "需要特定卡片才能通话" - ], - "平板": [ - "这是一种便携式电子设备", - "介于两种电子设备之间", - "触摸操作,比手机大", - "需要充电才能使用", - "可以用来娱乐和工作", - "有拍照功能", - "可以安装各种应用", - "适合观看视频", - "比电脑轻便", - "有不同的品牌和型号" - ], - "老师": [ - "这是一种职业", - "在特定的场所工作", - "教授知识", - "需要准备课程", - "有不同的专业领域", - "需要批改作业", - "对他人有指导作用", - "需要有耐心", - "很多人尊敬", - "可以影响他人的成长" - ], - "学生": [ - "这是一种身份", - "在特定的场所学习", - "需要上课和完成作业", - "有不同的阶段", - "可以学到各种知识", - "需要参加考试", - "有同学和老师", - "可以参加各种活动", - "是未来的希望", - "很多人都很努力" - ] -} +# 调用Deepseek API生成AI发言 +def call_deepseek_api(prompt): + try: + url = f"{DEEPSEEK_BASE_URL}/chat/completions" + headers = { + "Authorization": f"Bearer {DEEPSEEK_API_KEY}", + "Content-Type": "application/json" + } + data = { + "model": "deepseek-chat", + "messages": [ + { + "role": "system", + "content": "你是一个谁是卧底游戏的玩家,需要根据你拿到的词语生成自然的发言,不能直接说出词语本身,发言要符合当前轮数的特点。" + }, + { + "role": "user", + "content": prompt + } + ], + "max_tokens": 100, + "temperature": 0.8 + } + response = requests.post(url, headers=headers, json=data, timeout=10) + response.raise_for_status() + result = response.json() + return result["choices"][0]["message"]["content"].strip() + except Exception as e: + st.error(f"API调用失败: {str(e)}") + return None # 获取AI发言 def get_ai_speech(player_index, round_num, used_speeches): @@ -314,71 +119,89 @@ def get_ai_speech(player_index, round_num, used_speeches): player_word = st.session_state.player_words[player_index] num_players = st.session_state.num_players - max_attempts = 100 + max_attempts = 5 attempt = 0 while attempt < max_attempts: - # 按轮数递进的描述逻辑 - # 每轮选择不同深度的描述 - if player_word in word_descriptions: - all_descriptions = word_descriptions[player_word] - # 根据轮数选择描述数量,轮数越多描述越多 - max_descriptions = min(round_num, len(all_descriptions)) - # 随机选择描述,确保多样性 - if len(all_descriptions) >= max_descriptions: - selected_descriptions = random.sample(all_descriptions, max_descriptions) - else: - selected_descriptions = all_descriptions[:max_descriptions] - else: - # 如果词语不在描述库中,使用通用描述 - base_descriptions = [ - f"这是一个{len(player_word)}字的词语", - "这个词很常见", - "和日常生活相关", - "大家应该都知道" - ] - max_descriptions = min(round_num, len(base_descriptions)) - selected_descriptions = random.sample(base_descriptions, max_descriptions) - - # 构建发言 - description_part = " ".join(selected_descriptions) - - # 第一轮:基础描述 + # 构建API提示词 if round_num == 1: - openings = ["我抽到的词", "我的词", "这个词", "我拿到的词", "这个东西"] - speech = f"{random.choice(openings)},{description_part}" - - # 第二轮:基础描述 + 简单分析 + # 第一轮:基础描述,不要太详细 + prompt = f"你在玩谁是卧底游戏,你拿到的词语是 '{player_word}'。请生成第一轮的发言,只做基础描述,不要直接说出词语,发言要自然简洁。" + if is_spy: + prompt += " 你是卧底,但不要暴露自己的身份。" elif round_num == 2: - analysis_phrases = [ - "这个东西大家应该都接触过", - "日常生活中经常能看到", - "很多地方都能找到", - "比较常见的一种", - "大家应该都不陌生", - "随处可见的一种" - ] - speech = f"{description_part}。{random.choice(analysis_phrases)}" - - # 第三轮及以后:基础描述 + 更多细节 + 怀疑 + # 第二轮:更详细的描述,加一些分析 + prompt = f"你在玩谁是卧底游戏,你拿到的词语是 '{player_word}'。请生成第二轮的发言,在第一轮的基础上增加一些细节分析,不要直接说出词语,发言要自然。" + if is_spy: + prompt += " 你是卧底,但不要暴露自己的身份。" else: - detail_phrases = [ - "仔细想想,它还有一些特点", - "其实它还有其他方面", - "从不同角度看,它有更多特征", - "再补充一点关于它的信息", - "进一步说,它还有", - "另外,它还有一个特点" - ] - suspicion_phrases = [ - f"我发现玩家{random.randint(1, num_players)}的描述好像不太对", - f"玩家{random.randint(1, num_players)}的发言有点可疑", - "对比大家的发言,有些人的描述和我的不太一样", - "根据这些特点,我觉得有人在隐藏什么", - "这些特征应该很明显,不符合的人可能有问题", - "我注意到有些人的描述和这些特征不符" - ] - speech = f"{description_part}。{random.choice(detail_phrases)}。{random.choice(suspicion_phrases)}" + # 第三轮及以后:详细描述 + 合理怀疑 + suspect_player = random.randint(1, num_players) if num_players > 1 else 1 + prompt = f"你在玩谁是卧底游戏,你拿到的词语是 '{player_word}'。请生成第{round_num}轮的发言,包括详细描述和对其他玩家的合理怀疑(比如玩家{suspect_player}),不要直接说出词语,发言要自然。" + if is_spy: + prompt += " 你是卧底,要隐藏自己的身份,同时可以适当引导怀疑其他玩家。" + + # 调用API生成发言 + speech = call_deepseek_api(prompt) + + # 如果API调用失败或返回空,使用传统方式生成 + if not speech: + # 备用方案:使用传统方式生成发言 + if player_word in word_descriptions: + all_descriptions = word_descriptions[player_word] + max_descriptions = min(round_num, len(all_descriptions)) + if len(all_descriptions) >= max_descriptions: + selected_descriptions = random.sample(all_descriptions, max_descriptions) + else: + selected_descriptions = all_descriptions[:max_descriptions] + else: + base_descriptions = [ + f"这是一个{len(player_word)}字的词语", + "这个词很常见", + "和日常生活相关", + "大家应该都知道" + ] + max_descriptions = min(round_num, len(base_descriptions)) + selected_descriptions = random.sample(base_descriptions, max_descriptions) + + description_part = " ".join(selected_descriptions) + + if round_num == 1: + openings = ["我抽到的词", "我的词", "这个词", "我拿到的词", "这个东西"] + speech = f"{random.choice(openings)},{description_part}" + elif round_num == 2: + analysis_phrases = [ + "这个东西大家应该都接触过", + "日常生活中经常能看到", + "很多地方都能找到", + "比较常见的一种", + "大家应该都不陌生", + "随处可见的一种" + ] + speech = f"{description_part}。{random.choice(analysis_phrases)}" + else: + detail_phrases = [ + "仔细想想,它还有一些特点", + "其实它还有其他方面", + "从不同角度看,它有更多特征", + "再补充一点关于它的信息", + "进一步说,它还有", + "另外,它还有一个特点" + ] + suspicion_phrases = [ + f"我发现玩家{random.randint(1, num_players)}的描述好像不太对", + f"玩家{random.randint(1, num_players)}的发言有点可疑", + "对比大家的发言,有些人的描述和我的不太一样", + "根据这些特点,我觉得有人在隐藏什么", + "这些特征应该很明显,不符合的人可能有问题", + "我注意到有些人的描述和这些特征不符" + ] + speech = f"{description_part}。{random.choice(detail_phrases)}。{random.choice(suspicion_phrases)}" + + # 确保发言不直接包含词语本身 + if player_word in speech: + # 如果直接说出了词语,尝试替换或修改 + speech = speech.replace(player_word, "这个词") # 检查发言是否唯一 if speech not in used_speeches: @@ -463,6 +286,56 @@ elif st.session_state.game_state == "playing": for name in tied_player_names: st.write(f"- {name}") + # 生成AI玩家第二轮发言 + current_runoff_speeches = [] + # 检查是否已经为当前第二轮投票生成了发言 + runoff_speeches = st.session_state.runoff_speeches + + if not runoff_speeches: + # 为当前第二轮投票创建一个空的已使用发言集合 + used_speeches = set() + # 为每个AI玩家生成发言 + for i in range(st.session_state.num_players): + if i != player_index and i not in st.session_state.eliminated_players: + # 生成唯一的发言,使用当前轮数+0.5表示第二轮投票 + speech = get_ai_speech(i, st.session_state.current_round + 0.5, used_speeches) + # 将生成的发言添加到已使用集合中 + used_speeches.add(speech) + current_runoff_speeches.append({ + 'player': i + 1, + 'speech': speech, + 'type': 'ai' + }) + + # 保存当前第二轮投票的发言 + st.session_state.runoff_speeches = current_runoff_speeches + else: + # 使用已有的发言 + current_runoff_speeches = runoff_speeches + + # 显示AI玩家第二轮发言 + st.subheader("第二轮发言") + for speech in current_runoff_speeches: + st.write(f"**{st.session_state.player_names[speech['player']-1]}**:{speech['speech']}") + + # 用户第二轮发言输入 + if player_index not in st.session_state.eliminated_players: + st.write(f"\n### 你的第二轮发言({st.session_state.player_names[player_index]})") + st.session_state.player_runoff_speech = st.text_area( + "请输入你的第二轮发言", + value=st.session_state.player_runoff_speech, + height=100, + placeholder="在这里输入你的第二轮发言..." + ) + + # 保存用户第二轮发言 + if st.button("确认第二轮发言"): + if st.session_state.player_runoff_speech.strip(): + st.session_state.player_runoff_speech_saved = True + st.success("第二轮发言已保存!") + else: + st.error("请输入第二轮发言内容!") + # 投票输入(只能在平票玩家中选择) vote_name = st.selectbox( "选择要投票的玩家", @@ -473,9 +346,13 @@ elif st.session_state.game_state == "playing": # 提交第二轮投票 if st.button("提交第二轮投票"): - # 模拟所有玩家的第二轮投票 - st.session_state.votes = {} - st.session_state.votes[vote] = 1 + # 检查用户是否已完成第二轮发言 + if player_index not in st.session_state.eliminated_players and not st.session_state.player_runoff_speech_saved: + st.error("请先完成第二轮发言!") + else: + # 模拟所有玩家的第二轮投票 + st.session_state.votes = {} + st.session_state.votes[vote] = 1 # 模拟AI玩家的第二轮投票 for ai_player in [i for i in range(st.session_state.num_players) if i not in st.session_state.eliminated_players]: @@ -516,16 +393,25 @@ elif st.session_state.game_state == "playing": st.session_state.game_state = "ended" st.success(st.session_state.game_result) else: - # 进入下一轮 - st.write("\n游戏继续,进入下一轮!") - st.session_state.current_round += 1 - st.session_state.votes = {} - st.session_state.player_speech = "" - st.session_state.player_speech_saved = False + # 检查是否超过最大轮数 + if st.session_state.current_round >= st.session_state.max_rounds: + st.session_state.game_result = "游戏结束!已达到最大投票次数(4次)。" + st.session_state.game_state = "ended" + st.success(st.session_state.game_result) + else: + # 进入下一轮 + st.write("\n游戏继续,进入下一轮!") + st.session_state.current_round += 1 + st.session_state.votes = {} + st.session_state.player_speech = "" + st.session_state.player_speech_saved = False - # 重置 runoff 状态 + # 重置 runoff 状态和第二轮发言相关变量 st.session_state.runoff = False st.session_state.tied_players = [] + st.session_state.runoff_speeches = [] + st.session_state.player_runoff_speech = "" + st.session_state.player_runoff_speech_saved = False # 等待用户确认后再刷新 if st.button("进入下一轮"): @@ -714,6 +600,9 @@ elif st.session_state.game_state == "ended": st.session_state.speeches = [] st.session_state.player_speech = "" st.session_state.player_speech_saved = False + st.session_state.runoff_speeches = [] + st.session_state.player_runoff_speech = "" + st.session_state.player_runoff_speech_saved = False st.rerun() # 游戏规则 @@ -724,4 +613,6 @@ with st.sidebar: st.write("3. 玩家通过讨论判断谁是卧底") st.write("4. 每轮结束后进行投票,得票最多的玩家被淘汰") st.write("5. 如果淘汰的是卧底,平民胜利") - st.write("6. 如果最后只剩下卧底,卧底胜利") \ No newline at end of file + st.write("6. 如果最后只剩下卧底,卧底胜利") + st.write("7. 游戏最多进行4轮投票,超过次数则游戏结束") +