SWE / RE · Coding

怪兽对战引擎:回合次序、补位和可验证日志

A先攻击,只有存活的B反击,日志应能重放出最终结果。

题目重点:A先攻击,只有存活的B反击,日志应能重放出最终结果。

题意与假设

这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。

  • 两队有序,每个怪兽初始HP和攻击力为正。A先打B;B仍活才反击。死亡者立即记日志,替补在下一轮行动,不在本轮追加攻击。
  • 属性克制用正有理数倍率,训练代码不取整;如需整数伤害必须固定floor/ceil及最低伤害。技能按实际伤害最大选,并列保留输入顺序。
  • 本合同禁止临死反击,所以正HP双队不会同归于尽。仅双方初始空队返回draw;如要求同轮互杀,应明确同时结算或临死反击并改引擎,不能同时宣称只存活者反击。

解题路线

  1. 把怪兽数据、当前队员选择、伤害函数和日志事件分开。示例用dataclass与独立fight/best_attack函数,生产接口可包装Team/BattleResult,重点是规则统一。
  2. 每击记攻击方、目标、damage、before、after;HP≤0记death并前移队列位置,若有下一员记replacement。引擎根据这一击是否存活决定反击,替补提示不等于替补立即行动。
  3. 复制输入怪兽避免一次fight改变下一次测试起点;Fraction消除0.1倍率累积的舍入歧义。固定随机种子仅适用于另有随机命中率的合同,本题无需引入随机。
  4. 属性扩展先集中到damage函数,技能策略只选动作,不自己改HP或推进队列。日志最好结构化而非只有字符串,展示层可格式化,验证器可逐事件核对HP和事件顺序。

手动走一遍

A HP5/atk4,B HP3/atk9:第一击B HP3→−1,B死亡,A获胜;不应出现B反击。B还有第二只时先记replacement,下一轮仍由A攻击新B。不要让被击败一方的新怪兽抢到本轮反击权。对water,fire技能4伤害×1/2=2,water技能2伤害×1=2,并列选择先列出的fire技能。

Python 参考实现

下面的代码按上文假设实现,适合本地运行并追加自己的测试用例。

代码区可横向滚动;使用键盘时,先聚焦代码区再按方向键查看。

from dataclasses import dataclass
from fractions import Fraction
@dataclass
class Monster:
    name:str
    health:Fraction
    attack:Fraction
    kind:str='normal'

def fight(team_a,team_b,multipliers=None):
    # A attacks first; only a surviving B counterattacks. Inputs are not mutated.
    teams=[[Monster(m.name,Fraction(m.health),Fraction(m.attack),m.kind) for m in team] for team in (team_a,team_b)]
    if any(m.health<=0 or m.attack<=0 for team in teams for m in team):raise ValueError("positive health/attack")
    multipliers={} if multipliers is None else dict(multipliers)
    if any(Fraction(x)<=0 for x in multipliers.values()):raise ValueError("positive damage multiplier")
    index=[0,0];logs=[]
    def attack(source,target):
        a=teams[source][index[source]];b=teams[target][index[target]]
        damage=a.attack*Fraction(multipliers.get((a.kind,b.kind),1));before=b.health;b.health-=damage
        logs.append(('attack',source,a.name,target,b.name,damage,before,b.health))
        if b.health<=0:
            logs.append(('death',target,b.name));index[target]+=1
            if index[target]<len(teams[target]):logs.append(('replacement',target,teams[target][index[target]].name))
            return False
        return True
    while all(index[i]<len(teams[i]) for i in (0,1)):
        survived=attack(0,1)
        if survived:attack(1,0)
    alive=[index[i]<len(teams[i]) for i in (0,1)]
    winner='A' if alive[0] else 'B' if alive[1] else 'draw';logs.append(('result',winner));return winner,logs

def best_attack(attacks,defender,multipliers=None):
    multipliers=multipliers or {}
    if not attacks:raise ValueError("at least one skill")
    # Skills: (name,positive_base_damage,kind); ties preserve input order.
    return max(attacks,key=lambda skill:Fraction(skill[1])*Fraction(multipliers.get((skill[2],defender),1)))

复杂度

K次实际攻击、M总怪兽,时间O(K+M)、状态O(M)、完整日志O(K+M)。K依HP/最低伤害而变,不能只说O(M);Fraction精确运算还与分子分母位长有关。

易错点与追问

容易失分的边界

  • 允许死亡B反击却仍按survived决定反击,会让规则与平局示例冲突。
  • 只返回胜者无法检查补位及倍率阶段错误。
  • 零伤害或无限回血扩展会破坏必终止前提,需要轮数上限/循环判定。

继续深挖

  • 允许同时攻击时如何保存回合开头两人的伤害并一起结算?
  • 技能有冷却、能量和目标选择时,策略接口与状态推进怎样拆开?
✓已复制微信号!