任务:实现 BOSS 战主交互模块(boss_fight.py)
任务:实现 BOSS 战主交互模块(boss_fight.py)
一、任务背景
在《极妙幻境》中,第 4、7、10 层结束后会触发 BOSS 战。你需要实现一个完整的 BOSS 战交互模块,负责管理玩家与心魔的对战流程。该模块将调用已有的 BOSS AI 决策函数(boss_ai.boss_decide),并使用 boss_dialogues 模块预生成的台词,处理玩家输入、规则执行、状态更新和胜负判定。
此外,本模块需要为游戏主循环提供必要的接口,以便在 BOSS 战中处理系统指令(如 hint、pack、status、help、quit)。其中 hint 指令需要调用 llm_hint.get_llm_hint,因此本模块需要提供足够的牌局信息供主循环构造调用参数。
二、模块位置与依赖
- 文件路径:
src/game_logic/boss_fight.py - 依赖模块:
boss_ai:提供boss_decide、GameState、CardType、Hand(字典手牌)等。boss_dialogues:提供prefetch_boss_dialogues函数(返回台词字典)。typing、enum、random、logging、copy等标准库。
- 可选依赖:
llm_hint不需要直接导入,但本模块需提供状态查询方法供主循环使用。
三、需要实现的内容
3.1 BossFight 类设计
class BossFight:
def __init__(
self,
player_cards: List[Dict], # 玩家手牌列表,每个元素含 'type' (CardType) 和 'name' (str) 等
morality_ratios: Dict[str, float], # 玩家道德比例,如 {'good':0.3, 'neutral':0.2, 'evil':0.5}
boss_hand: Optional[Dict[CardType, int]] = None # 如果为 None,则自动生成
):
"""
初始化一场 BOSS 战。
- 保存玩家手牌副本(深拷贝)
- 如果 boss_hand 为 None,调用 _generate_boss_hand(len(player_cards)) 生成
- 调用 boss_dialogues.prefetch_boss_dialogues(morality_ratios) 预生成台词
- 初始化游戏状态:桌面牌堆(空列表)、先手方(BOSS先手)、上一轮宣称(None)、弃牌堆计数(0)、胜负标志等
- 记录入场台词是否已显示标志
"""
pass
3.2 核心属性
| 属性名 | 类型 | 说明 |
|---|---|---|
player_cards |
List[Dict] | 玩家当前手牌(可变,每张牌为字典) |
boss_hand |
Dict[CardType, int] | BOSS 手牌计数(BOSS视角) |
table_pile |
List[Dict] | 桌面牌堆,每个元素为 {'card': Dict, 'claimed': CardType},card 包含原牌信息(至少 type 和 name) |
discard_pile_count |
int | 弃牌堆总张数(仅用于显示) |
current_turn |
str | 'player' 或 'boss',表示当前该谁行动 |
last_claim |
Optional[CardType] | 上一轮出牌方宣称的类型(用于玩家界面显示) |
game_over |
bool | 是否已结束 |
winner |
Optional[str] | 'player' 或 'boss'(游戏结束时设置) |
dialogues |
Dict | 预生成的台词字典,包含 entrance, taunts, thinking, defeat, victory |
morality_ratios |
Dict | 存储玩家道德比例(用于获取默认台词等) |
entrance_shown |
bool | 标记入场台词是否已显示 |
3.3 公共方法(供主循环调用)
def get_display_state(self) -> Dict:
"""
返回当前牌局状态,用于 UI 显示。
包含:
- player_hand_count: int 玩家手牌总数
- player_hand_by_type: Dict[CardType, int] 玩家手牌各类型数量(可用 _count_player_cards_by_type() 获取)
- boss_hand_count: int BOSS 手牌总数
- table_pile_count: int 桌面牌堆数量
- last_claim: Optional[CardType] 上一轮宣称类型
- current_turn: str 当前轮到谁('player' 或 'boss')
"""
pass
def is_player_turn(self) -> bool:
"""返回当前是否轮到玩家"""
return self.current_turn == 'player'
def player_action(self, action: str, **kwargs) -> Dict:
"""
处理玩家动作。
参数:
action: 'challenge' | 'follow' | 'fold'
kwargs:
- 当 action='follow' 时,需提供 'num_cards' (int) 和 'claim' (CardType 或字符串)
返回字典:
{
'success': bool, # 动作是否合法
'message': str, # 提示信息(如错误原因或成功描述)
'game_over': bool, # 是否战斗结束
'winner': Optional[str], # 如果 game_over 为 True,返回 'player' 或 'boss'
'dialogue': Optional[str] # 如果触发了台词(如劣势思考),返回台词文本(用于显示)
}
"""
# 具体实现见下文
pass
def boss_turn(self) -> Dict:
"""
执行 BOSS 的回合(由主循环在轮到 BOSS 时调用)。
返回字典:
{
'action': dict, # BOSS 执行的动作(同 boss_ai.boss_decide 返回格式)
'message': str, # 描述 BOSS 动作的文本(如“BOSS宣称 2 张善牌”)
'game_over': bool,
'winner': Optional[str],
'dialogue': Optional[str] # 如果触发了台词(如优势嘲讽),返回台词文本
}
"""
pass
def get_entrance_line(self) -> str:
"""获取入场台词(仅首次调用有效)"""
if not self.entrance_shown:
self.entrance_shown = True
return self.dialogues['entrance']
return ""
def get_victory_line(self) -> str:
"""获取胜利宣言(战斗结束时调用)"""
return self.dialogues['victory']
def get_defeat_line(self) -> str:
"""获取失败宣言(战斗结束时调用)"""
return self.dialogues['defeat']
def get_taunt(self) -> str:
"""随机获取一条优势嘲讽语(用于 BOSS 优势时调用)"""
import random
return random.choice(self.dialogues['taunts'])
def get_thinking(self) -> str:
"""随机获取一条劣势思考语(用于玩家有利时调用)"""
import random
return random.choice(self.dialogues['thinking'])
def get_hint_context(self) -> Dict:
"""
返回当前牌局状态,用于构造 LLM 队友提示。
返回字典包含:
- player_hand: Dict[str, int] 玩家手牌各类型数量(格式同 llm_hint 要求的 player_hand 参数)
- boss_hand_count: int BOSS 手牌总数
- table_pile_count: int 桌面牌堆数量
- last_claim: Optional[str] 上一轮对方宣称的类型(字符串形式,如 'good')
- is_player_turn: bool 当前是否轮到玩家
"""
pass
3.4 内部辅助方法
def _generate_boss_hand(self, total_cards: int) -> Dict[CardType, int]:
"""根据玩家手牌总数随机生成 BOSS 手牌分布(确保总牌数相等,万能牌比例适当)"""
pass
def _count_player_cards_by_type(self) -> Dict[CardType, int]:
"""统计玩家手牌各类型数量(用于显示和规则判断)"""
pass
def _remove_player_cards(self, card_type: CardType, count: int) -> Optional[List[Dict]]:
"""
从玩家手牌中移除指定数量的指定类型牌(可用万能牌补足)。
返回被移除的牌列表(包含原牌信息),如果不足则返回 None,不修改手牌。
"""
pass
def _add_player_cards(self, cards: List[Dict]) -> None:
"""将一组牌加回玩家手牌(用于质疑后收回)"""
pass
def _remove_boss_cards(self, card_type: CardType, count: int) -> bool:
"""从 BOSS 手牌中移除指定数量的指定类型牌(BOSS 手牌是字典计数,直接减即可),返回是否成功"""
# 注意:应确保 count 不超过该类型可用牌(包括万能牌)
pass
def _add_boss_cards(self, added_hand: Dict[CardType, int]) -> None:
"""将牌加回 BOSS 手牌(用于质疑后收回)"""
pass
def _apply_challenge(self, challenger: str) -> Dict:
"""
质疑判定核心逻辑。
challenger: 'player' 或 'boss'
返回:{'success': bool, 'message': str, 'new_turn': str},其中 new_turn 为下一轮先手方('player' 或 'boss')
"""
# 1. 从 table_pile 中取出所有牌(从上次质疑后至今)
# 2. 验证每张牌的 actual 与 claimed 是否一致(实际类型为 WILD 时,宣称任何普通类型均视为属实)
# 3. 如果全部属实:
# - 质疑方收回所有牌,并从自己手牌中额外弃 1 张给被质疑方(需实现弃牌逻辑)
# - 下一轮由质疑方先出
# 4. 如果存在虚假:
# - 被质疑方收回所有牌,并从自己手牌中额外弃 1 张给质疑方
# - 下一轮由被质疑方先出
# 5. 更新手牌和桌面牌堆(清空)
# 6. 返回结果
pass
def _check_game_over(self) -> bool:
"""检查游戏是否结束(玩家手牌为0或BOSS手牌为0),并设置 self.game_over 和 self.winner"""
pass
def _should_taunt(self) -> bool:
"""判断是否应该触发优势嘲讽(例如 BOSS 手牌 ≤ 3 且玩家手牌 ≥ 8,或刚成功质疑玩家)"""
# 可根据需要设定概率(如 30%)
pass
def _should_think(self) -> bool:
"""判断是否应该触发劣势思考(例如 BOSS 手牌 ≥ 8 且玩家手牌 ≤ 3,或刚被玩家质疑成功)"""
pass
四、BOSS 战规则说明
请严格遵循以下规则(与之前设计一致):
- 出牌:出牌方扣着出 1-3 张牌,并宣称一个类型(
CardType.GOOD/EVIL/NEUTRAL),可用万能牌冒充。出牌后手牌减少相应数量,牌加入桌面牌堆(记录实际类型和宣称类型)。 - 质疑:回应方可以选择质疑。质疑时翻开桌面从上次质疑后至今的所有牌,验证每次宣称:
- 如果任何宣称虚假,出牌方收回全场牌堆,并额外从手牌中弃 1 张给质疑方。
- 如果全部属实,质疑方收回全场牌堆,并额外从手牌中弃 1 张给出牌方。
- 下一轮由收回牌的一方先出。
- 注意:收回牌时,桌面牌堆的所有牌回到相应方手牌(玩家手牌需还原为带有名称的卡片,BOSS 手牌增加计数)。
- 跟牌:回应方选择跟牌,自己出 1-3 张牌,宣称一个类型,加入桌面牌堆,轮到对方。
- 不跟:回应方选择不跟,将当前桌面牌堆全部移入弃牌堆(永久移除),本轮结束,下一轮由对方先出。
- 万能牌:可充当任何类型,使用后消耗。在质疑时,若实际牌为万能牌,宣称任何类型均视为“属实”(因为万能牌可冒充)。
- 胜利条件:先出完手牌者胜(即手牌数为 0)。
五、玩家动作处理逻辑细节
5.1 player_action('follow', num_cards, claim)
- 检查合法性:
- 当前必须轮到玩家(否则返回
success=False, message="现在不是你的回合")。 num_cards必须是整数且在 1-3 之间,且不超过玩家手牌总数。claim必须是CardType枚举值(可接受字符串,需转换,如'good'→CardType.GOOD)。- 调用
_remove_player_cards(card_type=claim, count=num_cards),如果返回 None,表示手牌不足,返回非法。
- 当前必须轮到玩家(否则返回
- 如果合法:
- 将移除的牌加入桌面牌堆,每张牌记录为
{'card': card, 'claimed': claim}。 - 更新
self.last_claim = claim。 - 切换回合:
self.current_turn = 'boss'。 - 检查游戏结束(调用
_check_game_over()),如果玩家手牌为 0,则游戏结束,玩家胜利。 - 返回成功信息,可能包含
dialogue(如果触发劣势思考,但这里一般不触发,由后续 BOSS 回合判断)。
- 将移除的牌加入桌面牌堆,每张牌记录为
- 返回结果。
5.2 player_action('challenge')
- 合法性检查:当前必须轮到玩家。
- 调用
_apply_challenge(challenger='player')获取结果。 - 根据结果更新状态:
- 更新手牌(
_add_player_cards等) - 清空桌面牌堆
- 设置
self.current_turn = result['new_turn'] - 清空
self.last_claim(质疑后上一轮宣称失去意义,可设为 None)
- 更新手牌(
- 检查游戏结束。
- 返回结果,可能包含
dialogue(例如玩家质疑成功时,可触发 BOSS 劣势思考?但 BOSS 回合会判断,此处不返回)。 - 注意:
_apply_challenge内部需实现弃牌逻辑(额外弃 1 张给对手)。弃牌时,从相应方手牌中移除任意一张牌(可随机或按顺序),加入对方手牌(玩家牌需有名称,可创建默认名称的牌,或从弃牌中选一张)。为简化,可让弃牌直接加入对手手牌,类型按实际,名称可用默认“弃牌”或保留原牌名称。建议保留原牌名称(如果是从玩家手牌移除,则玩家牌有名称,直接转移;BOSS 弃牌时,可生成一张默认名称的牌,类型由随机决定)。但为了简单,我们约定:弃牌时,从手牌中移除一张牌(类型随机或由调用者指定),然后加入对方手牌。玩家牌是列表,可以 pop 任意一张;BOSS 手牌是字典,需要减少一个类型计数,并增加对方相应类型的计数。但对方是玩家时,需要增加一张具体牌到玩家列表。可统一处理:弃牌时,从弃牌方移除一张牌(玩家:pop 一张;BOSS:从计数中减 1 并确定类型),然后生成一张相同类型的默认名称牌加入对方手牌(或如果对方是 BOSS,增加计数)。为简化,可以这样实现:- 玩家弃牌给 BOSS:从
self.player_cards中随机移除一张牌,获取其类型,然后self.boss_hand[type] += 1。 - BOSS 弃牌给玩家:从
self.boss_hand中选一个类型(非零),减少计数,然后生成一张该类型的默认名称牌(如“心魔的馈赠”),加入self.player_cards。 - 注意:弃牌可能发生在质疑成功或失败后,需要根据规则确定谁弃牌。
- 玩家弃牌给 BOSS:从
5.3 player_action('fold')
- 合法性检查:当前必须轮到玩家。
- 将桌面牌堆所有牌移入弃牌堆:
self.discard_pile_count += len(self.table_pile),self.table_pile.clear()。 - 切换回合:
self.current_turn = 'boss'(下一轮由 BOSS 先出)。 - 返回成功。
六、BOSS 回合处理逻辑
boss_turn() 实现:
- 构造
GameState对象(boss_ai.GameState):from .boss_ai import GameState state = GameState() state.boss_hand = self.boss_hand.copy() state.player_hand_count = len(self.player_cards) state.table_pile_count = len(self.table_pile) state.last_claim = self.last_claim # 注意:此时 last_claim 应是上一轮玩家的宣称 state.is_boss_turn = True state.rounds_since_last_challenge = 0 # 可忽略 - 调用
boss_ai.boss_decide(state)获取动作字典。 - 根据动作类型处理:
- play:BOSS 出牌
- 从
self.boss_hand中扣除相应数量的牌:self._remove_boss_cards(claim, num_cards)。 - 为每张牌生成一个虚拟牌对象(用于加入桌面牌堆),可用
{'type': claim, 'name': f"BOSS的{claim}牌"},但实际类型可能是万能牌?注意claim是宣称类型,实际牌可能包含万能牌。我们需要知道实际出的牌类型分布,但 BOSS AI 没有返回具体哪张牌是万能牌。为了简化,我们可以假设 BOSS 出牌时优先使用普通牌,不足时用万能牌补足。因此,_remove_boss_cards应返回实际移除的各类型数量,以便记录桌面牌堆。
建议_remove_boss_cards修改为返回一个列表,包含实际移除的牌的信息(如类型),或者返回一个字典表示各类型实际移除了多少张。但为了桌面牌堆记录,我们需要每张牌的实际类型。我们可以这样实现:- 在
_remove_boss_cards中,先扣除普通牌,再扣除万能牌,并构造一个列表,每个元素为{'type': 实际类型},其中实际类型可能是claim或WILD。 - 然后根据这个列表生成桌面牌堆条目,每张牌的实际类型已知。
- 在
- 将桌面牌堆条目加入
self.table_pile,每个条目{'card': {'type': 实际类型, 'name': f'BOSS的牌'}, 'claimed': claim}。 - 更新
self.last_claim = claim。 - 切换回合:
self.current_turn = 'player'。
- 从
- follow:类似 play,但 BOSS 是在回应玩家的出牌,所以动作后切换回合到玩家。
- challenge:BOSS 质疑
- 调用
self._apply_challenge(challenger='boss'),更新状态。
- 调用
- fold:BOSS 不跟
- 清空桌面到弃牌堆,
self.current_turn = 'player'(下一轮由玩家先出)。
- 清空桌面到弃牌堆,
- play:BOSS 出牌
- 检查游戏结束。
- 判断是否触发优势嘲讽:调用
self._should_taunt(),如果为真,则从self.get_taunt()获取台词,加入返回消息的dialogue字段。 - 返回包含动作信息、游戏结束标志、台词等的字典。
七、关键细节实现建议
- 手牌移除:
_remove_player_cards需要遍历玩家手牌列表,先收集所有指定类型的普通牌,再收集万能牌。如果总数不足,返回 None。否则,从列表中删除这些牌,并返回被删除的牌列表。 - 手牌添加:
_add_player_cards直接将牌列表追加到玩家手牌。 - BOSS 手牌移除:由于 BOSS 手牌是字典,移除时需考虑万能牌。可先减少指定类型的计数,如果不足,则用万能牌补足。返回实际移除的牌的类型列表(或字典)。例如:
removed = [] # 先移除普通牌 ordinary = self.boss_hand.get(card_type, 0) take_ordinary = min(ordinary, count) if take_ordinary > 0: self.boss_hand[card_type] -= take_ordinary removed.extend([card_type] * take_ordinary) count -= take_ordinary # 再移除万能牌 if count > 0: wild = self.boss_hand.get(CardType.WILD, 0) take_wild = min(wild, count) if take_wild < count: return None # 不足 self.boss_hand[CardType.WILD] -= take_wild removed.extend([CardType.WILD] * take_wild) return removed - 桌面牌堆条目:对于玩家出牌,
_remove_player_cards返回的牌列表包含原牌信息(含名称),可以直接使用。对于 BOSS 出牌,我们需要构造虚拟牌,可以用简单字典{'type': 实际类型, 'name': f'BOSS的{实际类型.name}牌'}。 - 弃牌逻辑:在
_apply_challenge中,根据结果确定谁弃牌给谁。弃牌时,从弃牌方移除一张牌,加入对方手牌。玩家手牌用列表,BOSS 手牌用字典,需统一处理。
八、与主循环的交互
主循环在 BOSS 战中的典型流程:
fight = BossFight(player_cards, morality_ratios)
show(fight.get_entrance_line()) # 显示入场台词
while not fight.game_over:
# 显示当前状态
display = fight.get_display_state()
show_state(display)
if fight.is_player_turn():
# 获取玩家输入
cmd = input("> ")
if cmd == 'hint':
# 获取 hint 上下文
context = fight.get_hint_context()
hint = get_llm_hint(
player_hand=context['player_hand'],
boss_hand_count=context['boss_hand_count'],
table_pile_count=context['table_pile_count'],
last_claim=context['last_claim'],
is_player_turn=context['is_player_turn']
)
show(hint)
continue
elif cmd == 'pack':
# 显示玩家手牌详情(可调用已有的背包显示函数)
show_pack(fight.player_cards)
continue
elif cmd == 'status':
# 显示道德比例等
show_status(...)
continue
elif cmd == 'help':
show_help()
continue
elif cmd == 'quit':
# 退出游戏
break
# 解析战斗动作
result = parse_and_call_player_action(fight, cmd)
if result['game_over']:
break
if result.get('dialogue'):
show(result['dialogue'])
else:
# BOSS 回合
result = fight.boss_turn()
show(result['message'])
if result.get('dialogue'):
show(result['dialogue'])
if result['game_over']:
break
# 战斗结束
if fight.winner == 'player':
show(fight.get_victory_line()) # BOSS 的失败宣言?
# 注意:失败宣言是 BOSS 被击败时说的,胜利宣言是 BOSS 击败玩家时说的
# 所以玩家胜利时显示 BOSS 的 defeat_line
show(fight.get_defeat_line())
else:
show(fight.get_victory_line()) # BOSS 的胜利宣言
九、测试要求
请为 BossFight 类编写单元测试文件 tests/test_boss_fight.py,至少覆盖:
- 初始化:正确设置手牌、生成 BOSS 手牌、预生成台词。
- 玩家出牌:
player_action('follow')合法与非法情况(手牌不足、参数错误)。 - 玩家质疑:构造桌面牌堆,测试质疑成功和失败,手牌和先手权更新正确,弃牌逻辑正确。
- 玩家不跟:清空桌面,先手权转移。
- BOSS 回合:模拟 BOSS AI 返回不同动作,验证状态更新。
- 游戏结束:当某方手牌为 0 时,正确设置
game_over和winner。 - 万能牌使用:玩家和 BOSS 出万能牌,质疑时验证逻辑。
- 台词触发:模拟优势/劣势条件,验证
_should_taunt和_should_think逻辑,以及是否返回正确台词。 - 获取 hint 上下文:验证
get_hint_context返回正确信息。
可以使用 mock 模拟 boss_decide 的返回值,以及 boss_dialogues.prefetch_boss_dialogues 的返回值(避免实际 LLM 调用)。测试中可创建简单的 player_cards 列表,例如 [{'type': CardType.GOOD, 'name': '善牌1'}, ...]。
十、提交要求
- 提供完整的
src/game_logic/boss_fight.py文件。 - 提供完整的
tests/test_boss_fight.py单元测试文件。 - 代码注释清晰,关键逻辑有说明。
- 确保所有测试通过。
开始实现吧!如有疑问,随时沟通。

浙公网安备 33010602011771号