每日算法 — 使用java实现西蒙记忆:序列生成与模式匹配算法

西蒙记忆(Simon Says)是一款经典的电子记忆游戏。设备依次播放一段由颜色/音调组成的序列,玩家需要按相同顺序重复该序列。随着回合推进,序列长度逐渐增加,考验玩家的短时记忆与模式识别能力。本文用Java实现完整的西蒙记忆游戏,讲解序列生成、模式匹配与难度自适应算法,包含完整代码与复杂度分析。

游戏机制与核心挑战

游戏核心循环:
1. 系统生成一个随机序列(如:红、绿、蓝、黄…)
2. 向玩家展示该序列
3. 玩家输入自己的答案序列
4. 若完全匹配则进入下一回合(序列长度+1);否则游戏结束

从算法视角看,核心挑战包括:
– 序列生成:如何生成既随机又可预测的序列(用于AI演示或关卡设计)
– 模式匹配:高效比对玩家输入与目标序列
– 难度自适应:根据玩家表现动态调整序列长度与展示速度
– 序列分析:检测并避免连续重复模式,保持游戏趣味性

序列生成算法

基础方案采用伪随机数生成器。进阶方案可引入以下约束:
– 禁止连续重复:相邻元素不得相同(避免玩家靠肌肉记忆点击)
– 模式打散:检测并打断明显的周期性模式(如红绿红绿…)
– 长度自适应:根据历史正确率动态调整增量步长

核心代码实现

import java.util.*;

/**
 * 西蒙记忆游戏核心引擎
 */
public class SimonGame {

    public enum Color { RED, GREEN, BLUE, YELLOW }

    private List<Color> sequence;           // 当前目标序列
    private Random random;
    private int currentRound;               // 当前回合
    private int score;                      // 累计得分
    private long baseDisplayMs;             // 基础展示间隔(毫秒)
    private Difficulty difficulty;

    public enum Difficulty { EASY, NORMAL, HARD }

    public SimonGame(Difficulty diff) {
        this.sequence = new ArrayList<>();
        this.random = new Random();
        this.currentRound = 0;
        this.score = 0;
        this.difficulty = diff;
        this.baseDisplayMs = switch(diff) {
            case EASY -> 800;
            case NORMAL -> 500;
            case HARD -> 300;
        };
    }

    /**
     * 生成下一回合的序列(在原有基础上追加新元素)
     * 应用约束:禁止与前一个元素相同
     */
    public void extendSequence() {
        Color last = sequence.isEmpty() ? null : sequence.get(sequence.size() - 1);
        Color next;
        do {
            next = Color.values()[random.nextInt(Color.values().length)];
        } while (next == last);  // 禁止连续重复
        sequence.add(next);
        currentRound++;
    }

    /**
     * 生成指定长度的全新序列(用于新游戏)
     */
    public void generateSequence(int length) {
        sequence.clear();
        for (int i = 0; i < length; i++) {
            extendSequence();
        }
    }

    /**
     * 模式匹配:验证玩家输入序列是否完全匹配目标序列
     * @param playerInput 玩家输入的颜色序列
     * @return 匹配结果,包含首次错误位置(若完全匹配则为-1)
     */
    public MatchResult verifySequence(List<Color> playerInput) {
        if (playerInput.size() != sequence.size()) {
            return new MatchResult(false, Math.min(playerInput.size(), sequence.size()));
        }
        for (int i = 0; i < sequence.size(); i++) {
            if (playerInput.get(i) != sequence.get(i)) {
                return new MatchResult(false, i);
            }
        }
        // 完全匹配,计算得分
        int roundScore = calculateRoundScore();
        score += roundScore;
        return new MatchResult(true, -1);
    }

    /**
     * 计算当前回合得分:基于序列长度与难度系数
     */
    private int calculateRoundScore() {
        int base = sequence.size() * 10;
        int multiplier = switch(difficulty) {
            case EASY -> 1;
            case NORMAL -> 2;
            case HARD -> 3;
        };
        return base * multiplier;
    }

    /**
     * 获取当前展示间隔(随回合增加而略微缩短,增加挑战性)
     */
    public long getDisplayInterval() {
        // 每5回合减少10%,最低不低于基础值的50%
        double factor = Math.max(0.5, 1.0 - (currentRound / 5) * 0.1);
        return (long) (baseDisplayMs * factor);
    }

    /**
     * 检测序列中是否存在过于明显的重复模式(长度>=3)
     * 用于关卡质量检测
     */
    public boolean hasObviousPattern() {
        int n = sequence.size();
        for (int period = 2; period <= n / 2; period++) {
            boolean isPeriodic = true;
            for (int i = period; i < n; i++) {
                if (sequence.get(i) != sequence.get(i - period)) {
                    isPeriodic = false;
                    break;
                }
            }
            if (isPeriodic) return true;
        }
        return false;
    }

    /**
     * 获取最近N个元素的频率统计,用于AI策略或难度分析
     */
    public Map<Color, Integer> getRecentFrequency(int n) {
        Map<Color, Integer> freq = new EnumMap<>(Color.class);
        int start = Math.max(0, sequence.size() - n);
        for (int i = start; i < sequence.size(); i++) {
            freq.merge(sequence.get(i), 1, Integer::sum);
        }
        return freq;
    }

    // Getters
    public List<Color> getSequence() { return new ArrayList<>(sequence); }
    public int getCurrentRound() { return currentRound; }
    public int getScore() { return score; }

    public record MatchResult(boolean success, int firstErrorIndex) {}
}

/**
 * 西蒙记忆AI演示器 - 基于记忆回放的模式复现
 */
public class SimonAI {

    private List<SimonGame.Color> memory;  // AI的"记忆"缓冲区
    private double recallAccuracy;          // 回忆准确率(模拟人类记忆衰减)

    public SimonAI(double accuracy) {
        this.memory = new ArrayList<>();
        this.recallAccuracy = Math.max(0.5, Math.min(1.0, accuracy));
    }

    /**
     * 观看序列并尝试记忆
     */
    public void observe(List<SimonGame.Color> sequence) {
        memory.clear();
        for (SimonGame.Color c : sequence) {
            // 以recallAccuracy概率正确记忆每个元素
            if (Math.random() < recallAccuracy) {
                memory.add(c);
            } else {
                // 记忆错误:随机替换为其他颜色
                SimonGame.Color[] values = SimonGame.Color.values();
                SimonGame.Color wrong = values[new Random().nextInt(values.length)];
                memory.add(wrong);
            }
        }
    }

    /**
     * 回放记忆的序列
     */
    public List<SimonGame.Color> replay() {
        return new ArrayList<>(memory);
    }

    /**
     * 带遗忘曲线的记忆模型:序列越长,后期元素的准确率越低
     */
    public void observeWithForgetting(List<SimonGame.Color> sequence, double decayRate) {
        memory.clear();
        for (int i = 0; i < sequence.size(); i++) {
            double positionAccuracy = recallAccuracy * Math.exp(-decayRate * i);
            if (Math.random() < positionAccuracy) {
                memory.add(sequence.get(i));
            } else {
                SimonGame.Color[] values = SimonGame.Color.values();
                memory.add(values[new Random().nextInt(values.length)]);
            }
        }
    }
}

/**
 * 游戏主循环与控制台演示
 */
public class SimonMain {
    public static void main(String[] args) throws InterruptedException {
        SimonGame game = new SimonGame(SimonGame.Difficulty.NORMAL);
        Scanner scanner = new Scanner(System.in);

        System.out.println("=== 西蒙记忆游戏 ===");
        System.out.println("颜色编码: 0=RED, 1=GREEN, 2=BLUE, 3=YELLOW");

        while (true) {
            game.extendSequence();
            List<SimonGame.Color> seq = game.getSequence();

            // 展示序列
            System.out.print("\n序列: ");
            for (SimonGame.Color c : seq) {
                System.out.print(c + " ");
                Thread.sleep(game.getDisplayInterval());
            }
            System.out.println();

            // 玩家输入
            System.out.print("请按顺序输入颜色编号(空格分隔): ");
            String[] inputs = scanner.nextLine().trim().split("\s+");
            List<SimonGame.Color> playerSeq = new ArrayList<>();
            for (String s : inputs) {
                playerSeq.add(SimonGame.Color.values()[Integer.parseInt(s)]);
            }

            // 验证
            SimonGame.MatchResult result = game.verifySequence(playerSeq);
            if (result.success()) {
                System.out.println("正确! 当前得分: " + game.getScore());
            } else {
                System.out.println("错误! 第 " + (result.firstErrorIndex() + 1) + " 个位置不匹配");
                System.out.println("正确序列: " + seq);
                System.out.println("最终得分: " + game.getScore());
                break;
            }
        }
        scanner.close();
    }
}

模式匹配的复杂度分析

序列验证:逐元素比对,时间复杂度 $O(n)$,空间复杂度 $O(1)$(不计输入存储)。

周期性检测:使用KMP算法思想优化的周期检测可将复杂度从 $O(n^2)$ 降至 $O(n)$。核心观察是:若序列存在周期 $p$,则其前缀函数满足 $\pi[n-1] = n – p$。

频率统计:滑动窗口遍历,时间复杂度 $O(n)$。

难度自适应机制

设计基于玩家历史表现的动态难度调整(DDA)系统:

$$\Delta L = \alpha \cdot R_{current} – \beta \cdot (1 – R_{historical})$$

其中 $R$ 为正确率,$\alpha$、$\beta$ 为调节系数。当 $\Delta L > 0$ 时增加序列长度增速,反之减缓。

总结

西蒙记忆游戏的核心算法围绕序列生成、模式匹配与难度自适应展开。本文通过Java实现了一个完整的游戏引擎,包括禁止连续重复的序列生成策略、线性时间的序列比对算法,以及基于遗忘曲线的AI模拟。该框架可扩展为更复杂的模式识别训练系统或认知能力测试工具。