每日算法 — 使用java实现钓鱼游戏:概率分布建模与最优收杆时机动态决策

引言

钓鱼游戏是休闲游戏中经久不衰的题材:鱼漂在水中浮动,当鱼咬钩时玩家需要在合适的时机收杆。真实的钓鱼过程充满随机性——不同鱼类有不同的咬钩概率分布、挣扎时长和逃跑速度。本文将用 Java 完整实现钓鱼游戏的核心 AI 系统,重点讲解泊松过程与指数分布建模咬钩事件以及动态决策下的最优收杆时机计算。

鱼类行为概率模型

咬钩事件:泊松过程

在现实钓鱼中,鱼咬钩是一个随机事件。假设鱼在任意时刻咬钩的概率均等,且不同时间段的咬钩事件相互独立,则单位时间内的咬钩次数服从泊松分布:

P(k次咬钩) = (λt)ᵏ × e^(-λt) / k!

其中 λ 是咬钩率(每小时平均咬钩次数),相邻两次咬钩的等待时间服从指数分布:

P(T ≤ t) = 1 - e^(-λt)

import java.util.*;

/**
 * 鱼类定义与概率模型
 */
public class FishType {
    public final String name;           // 鱼种名称
    public final double biteRate;       // 咬钩率 λ(次/分钟)
    public final double struggleMean;   // 挣扎时长均值(秒)
    public final double escapeRate;     // 每秒逃跑概率
    public final int baseScore;         // 基础得分
    public final double weightMean;     // 平均重量(kg)
    public final double weightStd;      // 重量标准差

    public FishType(String name, double biteRate, double struggleMean,
                    double escapeRate, int baseScore, double weightMean, double weightStd) {
        this.name = name;
        this.biteRate = biteRate;
        this.struggleMean = struggleMean;
        this.escapeRate = escapeRate;
        this.baseScore = baseScore;
        this.weightMean = weightMean;
        this.weightStd = weightStd;
    }

    /**
     * 根据指数分布生成下次咬钩等待时间(秒)
     */
    public double nextBiteWait(Random rand) {
        // 逆变换采样:T = -ln(U) / λ
        return -Math.log(1 - rand.nextDouble()) / (biteRate / 60.0);
    }

    /**
     * 生成鱼的重量(正态分布)
     */
    public double generateWeight(Random rand) {
        return weightMean + weightStd * rand.nextGaussian();
    }

    // 预定义常见鱼种
    public static final FishType CARP = new FishType("鲤鱼", 0.8, 15, 0.03, 50, 2.5, 0.8);
    public static final FishType BASS = new FishType("鲈鱼", 1.2, 10, 0.05, 80, 1.8, 0.5);
    public static final FishType CATFISH = new FishType("鲶鱼", 0.4, 25, 0.02, 150, 5.0, 1.5);
    public static final FishType SALMON = new FishType("鲑鱼", 0.6, 20, 0.04, 120, 3.2, 1.0);
}

钓鱼状态机

钓鱼过程可建模为有限状态机(FSM),包含四个状态:

  1. 等待(WAITING):鱼漂静止,等待鱼咬钩
  2. 咬钩(BITING):鱼已咬钩,浮漂抖动,玩家需判断收杆时机
  3. 收杆(REELING):正在收线,鱼在挣扎
  4. 结果(RESULT):成功捕获或鱼逃脱
/**
 * 钓鱼游戏核心状态机
 */
public class FishingGame {

    enum State { WAITING, BITING, REELING, RESULT }

    private State state;
    private double timer;           // 当前状态已持续时间
    private FishType currentFish;   // 当前咬钩的鱼种
    private double fishWeight;      // 鱼的重量
    private double biteProgress;    // 咬钩进度(0-1)
    private Random rand;

    // 游戏统计
    private int totalScore;
    private int caughtCount;
    private int escapedCount;
    private List<FishType> inventory;

    public FishingGame() {
        this.state = State.WAITING;
        this.timer = 0;
        this.rand = new Random();
        this.inventory = new ArrayList<>();
        this.totalScore = 0;
    }

    /**
     * 状态更新(每帧调用,dt为秒)
     */
    public void update(double dt, boolean playerReel) {
        timer += dt;

        switch (state) {
            case WAITING -> {
                // 随机选择一种鱼作为潜在咬钩对象
                if (currentFish == null) {
                    FishType[] types = {FishType.CARP, FishType.BASS, 
                                         FishType.CATFISH, FishType.SALMON};
                    currentFish = types[rand.nextInt(types.length)];
                    timer = 0; // 重置等待计时
                }

                // 检查是否咬钩
                double waitTime = currentFish.nextBiteWait(rand);
                if (timer >= waitTime) {
                    state = State.BITING;
                    timer = 0;
                    biteProgress = 0;
                    fishWeight = currentFish.generateWeight(rand);
                    System.out.printf("%n浮漂动了!一条 %.1fkg 的%s似乎咬钩了...%n", 
                        fishWeight, currentFish.name);
                }
            }

            case BITING -> {
                // 咬钩窗口期:鱼在试探,进度逐渐增加
                biteProgress += dt / currentFish.struggleMean;

                // 如果玩家长时间不拉杆,鱼会逃跑
                if (timer > currentFish.struggleMean * 1.5) {
                    System.out.println("鱼脱钩逃走了...");
                    escapedCount++;
                    resetToWaiting();
                    return;
                }

                // 玩家收杆
                if (playerReel) {
                    if (biteProgress >= 0.3 && biteProgress <= 0.9) {
                        // 在最佳窗口期收杆,成功率最高
                        state = State.REELING;
                        timer = 0;
                        System.out.println("起杆!开始收线!");
                    } else if (biteProgress < 0.3) {
                        System.out.println("太早了!鱼还没咬稳...");
                        escapedCount++;
                        resetToWaiting();
                    } else {
                        System.out.println("太晚了!鱼已经察觉...");
                        escapedCount++;
                        resetToWaiting();
                    }
                }
            }

            case REELING -> {
                // 收线过程中鱼可能逃跑
                double escapeProb = 1 - Math.exp(-currentFish.escapeRate * dt);
                if (rand.nextDouble() < escapeProb) {
                    System.out.println("鱼挣脱了!好大的力气!");
                    escapedCount++;
                    resetToWaiting();
                    return;
                }

                // 收线时长与鱼重成正比
                double reelTime = fishWeight * 2.0;
                if (timer >= reelTime) {
                    state = State.RESULT;
                    timer = 0;
                    caughtCount++;
                    int score = (int)(currentFish.baseScore * fishWeight);
                    totalScore += score;
                    inventory.add(currentFish);
                    System.out.printf("成功捕获 %.1fkg %s!获得 %d 分!%n", 
                        fishWeight, currentFish.name, score);
                }
            }

            case RESULT -> {
                // 显示结果1秒后自动回到等待
                if (timer > 1.0) {
                    resetToWaiting();
                }
            }
        }
    }

    private void resetToWaiting() {
        state = State.WAITING;
        timer = 0;
        currentFish = null;
    }

    public State getState() { return state; }
    public double getBiteProgress() { return biteProgress; }
    public int getTotalScore() { return totalScore; }
    public int getCaughtCount() { return caughtCount; }
    public int getEscapedCount() { return escapedCount; }
}

最优收杆时机:动态决策算法

问题建模

玩家在咬钩阶段需要决定何时收杆。过早(biteProgress < 0.3)鱼未咬稳必逃跑;过晚(biteProgress > 0.9)鱼已察觉必逃跑;在窗口期 [0.3, 0.9] 内收杆,成功概率与进度正相关。

这是一个最优停止问题(Optimal Stopping Problem),类似于”秘书问题”。我们需要在观察过程中动态评估当前状态的价值,决定是否”停止”(收杆)或”继续观察”。

/**
 * 钓鱼AI:动态决策最优收杆时机
 */
public class FishingAI {

    /**
     * 动态策略:根据当前咬钩进度和鱼的特性,计算期望收益并决策
     * @param progress 当前咬钩进度 [0, 1]
     * @param fish 鱼种信息
     * @param remainingTime 剩余可等待时间
     * @return true表示应该立即收杆
     */
    public static boolean shouldReel(double progress, FishType fish, double remainingTime) {
        // 当前立即收杆的期望收益
        double immediateReward = expectedReward(progress, fish);

        // 继续等待的期望收益(考虑时间价值和逃跑风险)
        double futureReward = expectedFutureReward(progress, fish, remainingTime);

        // 贪心决策:当立即收杆的期望收益不低于继续等待时,收杆
        return immediateReward >= futureReward;
    }

    /**
     * 计算在当前进度立即收杆的期望收益
     */
    private static double expectedReward(double progress, FishType fish) {
        if (progress < 0.3) return -10;  // 太早,必定失败
        if (progress > 0.9) return -10;  // 太晚,必定失败

        // 窗口期内,成功率随进度增加(模拟鱼咬得更稳)
        double successRate = 0.5 + 0.5 * ((progress - 0.3) / 0.6);
        double expectedScore = fish.baseScore * fish.weightMean * successRate;

        return expectedScore;
    }

    /**
     * 估算继续等待的期望收益
     * 考虑:时间推移可能获得更高成功率,但也增加了逃跑风险
     */
    private static double expectedFutureReward(double progress, FishType fish, 
                                                double remainingTime) {
        if (remainingTime <= 0) return -100; // 没时间了
        if (progress >= 0.9) return -100;    // 已经超过窗口期

        // 简化模型:假设再等待 dt 时间后的期望收益
        double dt = Math.min(remainingTime, 0.1);
        double nextProgress = progress + dt / fish.struggleMean;

        // 鱼在dt时间内逃跑的概率(指数分布)
        double escapeProb = 1 - Math.exp(-0.1 * dt); // 基础逃跑率

        // 继续等待的期望 = (1-逃跑概率) * 下一状态期望 + 逃跑概率 * 0
        return (1 - escapeProb) * expectedReward(nextProgress, fish);
    }

    /**
     * 保守策略:在最佳窗口期中点收杆
     */
    public static boolean conservativeReel(double progress) {
        return progress >= 0.55 && progress <= 0.75;
    }

    /**
     * 激进策略:尽早收杆,追求速度
     */
    public static boolean aggressiveReel(double progress) {
        return progress >= 0.35 && progress <= 0.5;
    }

    /**
     * 完整AI决策(综合策略)
     */
    public static boolean aiDecide(FishingGame game, FishType fish) {
        State state = game.getState();
        if (state != State.BITING) return false;

        double progress = game.getBiteProgress();
        double remaining = fish.struggleMean * 1.5 - game.timer;

        return shouldReel(progress, fish, remaining);
    }
}

模拟对战系统

/**
 * 钓鱼游戏AI对战模拟器
 * 对比三种策略的表现
 */
public class FishingSimulator {

    interface ReelStrategy {
        boolean shouldReel(FishingGame game, FishType fish);
    }

    public static void main(String[] args) {
        // 定义三种策略
        ReelStrategy dynamic = (g, f) -> FishingAI.shouldReel(
            g.getBiteProgress(), f, f.struggleMean * 1.5);
        ReelStrategy conservative = (g, f) -> 
            FishingAI.conservativeReel(g.getBiteProgress());
        ReelStrategy aggressive = (g, f) -> 
            FishingAI.aggressiveReel(g.getBiteProgress());

        System.out.println("=== 钓鱼AI策略对比模拟 ===\n");

        simulateStrategy("动态决策策略", dynamic, 100);
        simulateStrategy("保守中点策略", conservative, 100);
        simulateStrategy("激进速攻策略", aggressive, 100);
    }

    private static void simulateStrategy(String name, ReelStrategy strategy, int rounds) {
        FishingGame game = new FishingGame();
        Random rand = new Random(42); // 固定种子保证公平

        int frame = 0;
        while (game.getCaughtCount() + game.getEscapedCount() < rounds && frame < 100000) {
            frame++;
            double dt = 0.1; // 100ms一帧

            boolean reel = false;
            if (game.getState() == State.BITING && game.currentFish != null) {
                reel = strategy.shouldReel(game, game.currentFish);
            }

            game.update(dt, reel);
        }

        int total = game.getCaughtCount() + game.getEscapedCount();
        double rate = total > 0 ? (double) game.getCaughtCount() / total * 100 : 0;

        System.out.printf("%s:%n", name);
        System.out.printf("  捕获: %d, 逃脱: %d, 成功率: %.1f%%, 总分: %d%n%n",
            game.getCaughtCount(), game.getEscapedCount(), rate, game.getTotalScore());
    }
}

复杂度分析

模块 时间复杂度 说明
指数分布采样 O(1) 逆变换法直接计算
状态机更新 O(1) 固定分支判断
AI决策计算 O(1) 常数次算术运算
模拟运行 O(frames) 与模拟帧数线性相关

总结

钓鱼游戏的 AI 实现展示了随机过程建模与动态决策的完美结合:泊松过程与指数分布精确刻画了咬钩事件的随机性,有限状态机清晰管理了钓鱼的完整流程,而最优停止策略则在不确定性中做出收益最大化的决策。这类”观察-决策-执行”的框架在股票交易、资源竞拍等时序决策场景中有着广泛的应用。