每日算法 — 使用java实现街霸:帧数据状态机与贪心最优连招决策

引言

《街头霸王》是格斗游戏的标杆之作,其深度体现在每一招每一式的帧级精度上。职业选手通过精确掌握各招式的发生帧、持续帧、硬直帧,在电光火石间做出最优决策。本文从算法视角解析格斗游戏的核心机制,用Java实现一个简化版街霸AI:通过有限状态机管理角色的战斗状态,结合帧数据分析构建贪心最优连招决策系统,让AI能够在对战中选择伤害最大化且安全的攻击序列。

核心算法思路

帧数据建模

格斗游戏中的每个招式都由三部分帧数据组成:
– 启动帧(Startup):招式从开始到产生攻击判定前的帧数,此阶段可被对手打断。
– 持续帧(Active):招式具有攻击判定的帧数,命中对手则造成伤害。
– 恢复帧(Recovery):招式结束后的硬直帧数,此阶段容易被对手反击。

一个招式的总硬直风险可用启动帧加恢复帧来衡量;招式的收益则用伤害值与持续帧命中窗口的比值来评估。

有限状态机

角色在对战中处于不同状态:站立、蹲伏、跳跃、攻击中、受击硬直、防御中、倒地等。每个状态下可执行的招式不同,且状态转换受到帧数据约束。使用有限状态机(FSM)清晰管理这些状态及合法转换。

贪心最优连招决策

连招是多个招式的连续命中组合。本文采用贪心策略构建连招:在每一时刻,从当前状态出发,选择所有可执行招式中”伤害/风险比”最高的招式;若该招式命中,则进入下一状态继续选择;若被防御或 miss,则终止连招并转入安全恢复状态。

完整Java实现

import java.util.*;

/**
 * 街霸AI —— 帧数据状态机与贪心最优连招决策
 */
public class StreetFighterAI {

    // 角色状态
    public enum State {
        STANDING, CROUCHING, JUMPING, ATTACKING, HITSTUN, BLOCKING, KNOCKDOWN, RECOVERY
    }

    // 招式定义
    public static class Move {
        String name;
        int startup;      // 启动帧
        int active;       // 持续帧
        int recovery;     // 恢复帧
        int damage;       // 伤害值
        int hitAdvantage; // 命中后有利帧(正数=可继续压制)
        int blockAdvantage; // 被防御后有利帧(负数=容易被反击)
        Set<State> usableFrom; // 可用状态
        State nextState;  // 命中后转移状态

        public Move(String name, int startup, int active, int recovery, int damage,
                    int hitAdvantage, int blockAdvantage, Set<State> usableFrom, State nextState) {
            this.name = name;
            this.startup = startup;
            this.active = active;
            this.recovery = recovery;
            this.damage = damage;
            this.hitAdvantage = hitAdvantage;
            this.blockAdvantage = blockAdvantage;
            this.usableFrom = usableFrom;
            this.nextState = nextState;
        }

        /** 风险 = 启动帧 + 恢复帧,越小越安全 */
        public int risk() {
            return startup + recovery;
        }

        /** 伤害风险比,用于贪心选择 */
        public double efficiency() {
            return (double) damage / Math.max(risk(), 1);
        }
    }

    private State currentState = State.STANDING;
    private int frameAdvantage = 0; // 帧优势(正数=我方可行动,负数=对方先动)

    // 招式库
    private final List<Move> moveList = Arrays.asList(
        new Move("轻拳", 3, 2, 6, 30, 3, -1,
            Set.of(State.STANDING, State.CROUCHING), State.STANDING),
        new Move("重拳", 8, 3, 14, 80, 5, -4,
            Set.of(State.STANDING), State.STANDING),
        new Move("轻脚", 4, 2, 8, 40, 2, -2,
            Set.of(State.STANDING, State.CROUCHING), State.STANDING),
        new Move("重脚", 10, 4, 18, 100, 7, -6,
            Set.of(State.STANDING), State.STANDING),
        new Move("升龙拳", 4, 8, 22, 120, -10, -18,
            Set.of(State.STANDING, State.CROUCHING), State.RECOVERY),
        new Move("波动拳", 13, 2, 28, 90, 2, -7,
            Set.of(State.STANDING, State.CROUCHING), State.RECOVERY),
        new Move("蹲下轻拳", 4, 2, 5, 25, 4, 0,
            Set.of(State.CROUCHING), State.CROUCHING),
        new Move("投技", 5, 3, 16, 130, Integer.MAX_VALUE, Integer.MAX_VALUE,
            Set.of(State.STANDING), State.KNOCKDOWN)
    );

    /**
     * 贪心选择当前最优招式
     * @param opponentState 对手当前状态
     */
    public Move chooseOptimalMove(State opponentState) {
        List<Move> candidates = new ArrayList<>();
        for (Move m : moveList) {
            if (m.usableFrom.contains(currentState)) {
                candidates.add(m);
            }
        }

        if (candidates.isEmpty()) {
            currentState = State.STANDING;
            return chooseOptimalMove(opponentState);
        }

        // 根据对手状态调整优先级
        return candidates.stream()
            .max(Comparator.comparingDouble(m -> scoreMove(m, opponentState)))
            .orElse(candidates.get(0));
    }

    /**
     * 综合评分:考虑伤害效率、帧优势、对手状态
     */
    private double scoreMove(Move m, State opponentState) {
        double score = m.efficiency();

        // 对手处于受击硬直,优先选择高伤害招式
        if (opponentState == State.HITSTUN) {
            score += m.damage * 0.05;
        }

        // 对手倒地,等待或选择安全招式
        if (opponentState == State.KNOCKDOWN) {
            score -= m.risk() * 0.1;
        }

        // 帧优势大时,优先选择高回报招式
        if (frameAdvantage >= 5) {
            score += m.damage * 0.03;
        }

        // 风险惩罚:恢复帧过长容易被确反
        if (m.recovery > 15) {
            score -= 0.5;
        }

        return score;
    }

    /**
     * 模拟执行一招并更新状态
     */
    public void executeMove(Move move, boolean hit, boolean blocked) {
        System.out.printf("执行 [%s]: 启动%dF 持续%dF 恢复%dF 伤害%d%n",
            move.name, move.startup, move.active, move.recovery, move.damage);

        currentState = State.ATTACKING;

        if (hit) {
            frameAdvantage = move.hitAdvantage;
            currentState = move.nextState;
            System.out.println("  -> 命中!获得 " + frameAdvantage + " 帧优势");
        } else if (blocked) {
            frameAdvantage = move.blockAdvantage;
            currentState = State.RECOVERY;
            System.out.println("  -> 被防御!帧优势变为 " + frameAdvantage);
        } else {
            frameAdvantage = -move.recovery;
            currentState = State.RECOVERY;
            System.out.println("  -> 挥空!陷入 " + move.recovery + " 帧硬直");
        }
    }

    /**
     * 贪心构建连招序列:持续选择最优招式直到无法继续
     */
    public List<Move> buildCombo(State initialOpponentState) {
        List<Move> combo = new ArrayList<>();
        State opponentState = initialOpponentState;
        int maxMoves = 5; // 连招上限

        for (int i = 0; i < maxMoves; i++) {
            Move best = chooseOptimalMove(opponentState);

            // 如果帧优势不足或风险过高,终止连招
            if (frameAdvantage < -5 || best.risk() > 25) {
                System.out.println("帧优势不足或风险过高,终止连招。");
                break;
            }

            combo.add(best);

            // 模拟命中(连招中假设连续命中)
            executeMove(best, true, false);
            opponentState = State.HITSTUN;

            // 投技命中后对手倒地,无法继续连招
            if (best.name.equals("投技")) {
                opponentState = State.KNOCKDOWN;
                break;
            }
        }

        return combo;
    }

    public static void main(String[] args) {
        StreetFighterAI ai = new StreetFighterAI();

        System.out.println("=== 街霸AI —— 贪心最优连招决策 ===\n");
        System.out.println("初始状态:站立,对手站立\n");

        List<Move> combo = ai.buildCombo(State.STANDING);

        System.out.println("\n=== 最终连招序列 ===");
        int totalDamage = 0;
        for (int i = 0; i < combo.size(); i++) {
            Move m = combo.get(i);
            totalDamage += m.damage;
            System.out.printf("第%d招: %s (伤害%d)%n", i + 1, m.name, m.damage);
        }
        System.out.println("总伤害: " + totalDamage);

        System.out.println("\n=== 招式库伤害效率排行 ===");
        List<Move> sorted = new ArrayList<>(ai.moveList);
        sorted.sort((a, b) -> Double.compare(b.efficiency(), a.efficiency()));
        for (Move m : sorted) {
            System.out.printf("%-8s: 效率=%.2f (伤害%d/风险%d)%n",
                m.name, m.efficiency(), m.damage, m.risk());
        }
    }
}

复杂度分析

  • 招式选择时间复杂度:$O(M)$,$M$ 为招式库大小,每次贪心选择遍历所有可用招式。
  • 连招构建时间复杂度:$O(K \cdot M)$,$K$ 为连招长度上限。
  • 空间复杂度:$O(M)$,存储招式库与当前状态信息。

扩展方向

  1. 蒙特卡洛树搜索(MCTS):将贪心策略升级为MCTS,通过自我对弈搜索更长的最优连招树。
  2. 对手模型学习:记录对手常用招式与防御习惯,动态调整招式选择策略实现针对性压制。
  3. 帧级模拟器:引入完整帧表与碰撞箱检测,实现像素级精确的招式判定与攻防模拟。

总结

本文以《街头霸王》为背景,将格斗游戏的核心机制抽象为帧数据状态机模型,并通过贪心策略实现了一套高效的连招决策系统。核心洞察在于:格斗游戏的本质是帧数管理,在正确的时间窗口中选择风险收益比最优的招式,是取得胜利的关键。读者可将此状态机框架推广到任何需要精确时机控制的实时决策系统中,如交易算法、工业控制等。