任务:实现 BOSS 战主交互模块(boss_fight.py)

任务:实现 BOSS 战主交互模块(boss_fight.py)

一、任务背景

在《极妙幻境》中,第 4、7、10 层结束后会触发 BOSS 战。你需要实现一个完整的 BOSS 战交互模块,负责管理玩家与心魔的对战流程。该模块将调用已有的 BOSS AI 决策函数(boss_ai.boss_decide),并使用 boss_dialogues 模块预生成的台词,处理玩家输入、规则执行、状态更新和胜负判定。

此外,本模块需要为游戏主循环提供必要的接口,以便在 BOSS 战中处理系统指令(如 hintpackstatushelpquit)。其中 hint 指令需要调用 llm_hint.get_llm_hint,因此本模块需要提供足够的牌局信息供主循环构造调用参数。

二、模块位置与依赖

  • 文件路径src/game_logic/boss_fight.py
  • 依赖模块
    • boss_ai:提供 boss_decideGameStateCardTypeHand(字典手牌)等。
    • boss_dialogues:提供 prefetch_boss_dialogues 函数(返回台词字典)。
    • typingenumrandomloggingcopy 等标准库。
  • 可选依赖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)

  1. 检查合法性:
    • 当前必须轮到玩家(否则返回 success=False, message="现在不是你的回合")。
    • num_cards 必须是整数且在 1-3 之间,且不超过玩家手牌总数。
    • claim 必须是 CardType 枚举值(可接受字符串,需转换,如 'good'CardType.GOOD)。
    • 调用 _remove_player_cards(card_type=claim, count=num_cards),如果返回 None,表示手牌不足,返回非法。
  2. 如果合法:
    • 将移除的牌加入桌面牌堆,每张牌记录为 {'card': card, 'claimed': claim}
    • 更新 self.last_claim = claim
    • 切换回合:self.current_turn = 'boss'
    • 检查游戏结束(调用 _check_game_over()),如果玩家手牌为 0,则游戏结束,玩家胜利。
    • 返回成功信息,可能包含 dialogue(如果触发劣势思考,但这里一般不触发,由后续 BOSS 回合判断)。
  3. 返回结果。

5.2 player_action('challenge')

  1. 合法性检查:当前必须轮到玩家。
  2. 调用 _apply_challenge(challenger='player') 获取结果。
  3. 根据结果更新状态:
    • 更新手牌(_add_player_cards 等)
    • 清空桌面牌堆
    • 设置 self.current_turn = result['new_turn']
    • 清空 self.last_claim(质疑后上一轮宣称失去意义,可设为 None)
  4. 检查游戏结束。
  5. 返回结果,可能包含 dialogue(例如玩家质疑成功时,可触发 BOSS 劣势思考?但 BOSS 回合会判断,此处不返回)。
  6. 注意:_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
    • 注意:弃牌可能发生在质疑成功或失败后,需要根据规则确定谁弃牌。

5.3 player_action('fold')

  1. 合法性检查:当前必须轮到玩家。
  2. 将桌面牌堆所有牌移入弃牌堆:self.discard_pile_count += len(self.table_pile)self.table_pile.clear()
  3. 切换回合:self.current_turn = 'boss'(下一轮由 BOSS 先出)。
  4. 返回成功。

六、BOSS 回合处理逻辑

boss_turn() 实现:

  1. 构造 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  # 可忽略
    
  2. 调用 boss_ai.boss_decide(state) 获取动作字典。
  3. 根据动作类型处理:
    • 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': 实际类型},其中实际类型可能是 claimWILD
        • 然后根据这个列表生成桌面牌堆条目,每张牌的实际类型已知。
      • 将桌面牌堆条目加入 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'(下一轮由玩家先出)。
  4. 检查游戏结束。
  5. 判断是否触发优势嘲讽:调用 self._should_taunt(),如果为真,则从 self.get_taunt() 获取台词,加入返回消息的 dialogue 字段。
  6. 返回包含动作信息、游戏结束标志、台词等的字典。

七、关键细节实现建议

  • 手牌移除_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,至少覆盖:

  1. 初始化:正确设置手牌、生成 BOSS 手牌、预生成台词。
  2. 玩家出牌player_action('follow') 合法与非法情况(手牌不足、参数错误)。
  3. 玩家质疑:构造桌面牌堆,测试质疑成功和失败,手牌和先手权更新正确,弃牌逻辑正确。
  4. 玩家不跟:清空桌面,先手权转移。
  5. BOSS 回合:模拟 BOSS AI 返回不同动作,验证状态更新。
  6. 游戏结束:当某方手牌为 0 时,正确设置 game_overwinner
  7. 万能牌使用:玩家和 BOSS 出万能牌,质疑时验证逻辑。
  8. 台词触发:模拟优势/劣势条件,验证 _should_taunt_should_think 逻辑,以及是否返回正确台词。
  9. 获取 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 单元测试文件。
  • 代码注释清晰,关键逻辑有说明。
  • 确保所有测试通过。

开始实现吧!如有疑问,随时沟通。

posted @ 2026-03-17 00:09  神秘园欢迎您  阅读(15)  评论(0)    收藏  举报