引言
Memory(记忆翻牌、配对游戏)是经典的卡牌益智游戏。玩家每次翻开两张卡牌,若图案相同则消除,不同则翻回,目标是凭记忆力在最短步数内消除所有卡牌对。本文用Java实现Memory游戏的核心逻辑,重点讲解Fisher-Yates洗牌算法的均匀随机性保证,以及游戏状态管理与最优翻牌策略的评估。
游戏设计
Memory的核心数据结构包含:
1. 卡牌池:N 对相同图案的卡牌,共 2N 张。
2. 游戏状态:每张卡牌的状态(背面朝上、正面朝上、已消除)。
3. 回合逻辑:玩家每回合最多翻两张牌,匹配则消除,不匹配则翻回。
核心算法:Fisher-Yates洗牌
保证每张排列等概率出现是Memory公平性的关键。Fisher-Yates算法通过从后向前遍历,将当前位置与 [0, i] 范围内的随机位置交换,确保 n! 种排列每种出现的概率严格为 1/n!。
// Fisher-Yates洗牌(Knuth Shuffle)
for (int i = n - 1; i > 0; i--) {
int j = random.nextInt(i + 1); // [0, i] 均匀随机
swap(array, i, j);
}
对比常见的错误实现(如 random.nextInt(n) 交换),Fisher-Yates避免了排列不均匀的问题。
Java实现
import java.util.*;
/**
* Memory记忆匹配游戏:洗牌算法与状态管理
*/
public class MemoryGame {
private static final int PAIRS = 8; // 8对卡牌
private Card[] board;
private Random random;
private int matchedPairs; // 已匹配对数
private int moves; // 总翻牌次数
private List<Integer> flipped; // 当前翻开的卡牌索引
/**
* 卡牌类:封装图案、状态与位置
*/
static class Card {
int id; // 图案编号(相同id为一对)
int index; // 在棋盘上的位置
State state; // 背面、正面、已消除
enum State { HIDDEN, REVEALED, REMOVED }
Card(int id, int index) {
this.id = id;
this.index = index;
this.state = State.HIDDEN;
}
}
public MemoryGame() {
this.random = new Random();
initBoard();
}
/**
* 初始化棋盘:生成卡牌对并执行Fisher-Yates洗牌
*/
private void initBoard() {
board = new Card[PAIRS * 2];
// 生成配对卡牌
for (int i = 0; i < PAIRS; i++) {
board[i * 2] = new Card(i, i * 2);
board[i * 2 + 1] = new Card(i, i * 2 + 1);
}
// Fisher-Yates洗牌
for (int i = board.length - 1; i > 0; i--) {
int j = random.nextInt(i + 1);
Card temp = board[i];
board[i] = board[j];
board[j] = temp;
// 更新索引
board[i].index = i;
board[j].index = j;
}
this.flipped = new ArrayList<>();
this.matchedPairs = 0;
this.moves = 0;
}
/**
* 翻牌操作:返回操作结果
* @param index 卡牌位置
* @return 0=继续, 1=匹配成功, 2=匹配失败, 3=游戏结束
*/
public int flip(int index) {
Card card = board[index];
if (card.state != Card.State.HIDDEN || flipped.size() >= 2) {
return 0; // 非法操作
}
card.state = Card.State.REVEALED;
flipped.add(index);
moves++;
if (flipped.size() == 2) {
Card first = board[flipped.get(0)];
Card second = board[flipped.get(1)];
if (first.id == second.id) {
// 匹配成功
first.state = Card.State.REMOVED;
second.state = Card.State.REMOVED;
matchedPairs++;
flipped.clear();
return (matchedPairs == PAIRS) ? 3 : 1;
} else {
return 2; // 匹配失败,需要翻回
}
}
return 0;
}
/**
* 翻回未匹配的卡牌(通常在延迟后调用)
*/
public void hideUnmatched() {
for (int idx : flipped) {
if (board[idx].state == Card.State.REVEALED) {
board[idx].state = Card.State.HIDDEN;
}
}
flipped.clear();
}
/**
* 获取当前棋盘状态(用于AI决策)
*/
public Card.State[] getBoardState() {
Card.State[] states = new Card.State[board.length];
for (int i = 0; i < board.length; i++) {
states[i] = board[i].state;
}
return states;
}
/**
* AI策略:基于已知信息的最优翻牌
* 优先级:1) 已知配对 2) 随机探索
*/
public int[] aiMove() {
// 收集已知信息
Map<Integer, List<Integer>> knownIds = new HashMap<>();
List<Integer> hidden = new ArrayList<>();
for (int i = 0; i < board.length; i++) {
if (board[i].state == Card.State.HIDDEN) {
hidden.add(i);
} else if (board[i].state == Card.State.REVEALED) {
knownIds.computeIfAbsent(board[i].id, k -> new ArrayList<>()).add(i);
}
}
// 策略1:若某id已翻开一张,找其配对
for (Map.Entry<Integer, List<Integer>> entry : knownIds.entrySet()) {
if (entry.getValue().size() == 1) {
int targetId = entry.getKey();
for (int idx : hidden) {
if (board[idx].id == targetId) {
return new int[]{entry.getValue().get(0), idx};
}
}
}
}
// 策略2:随机翻开两张未探索卡牌
Collections.shuffle(hidden, random);
return new int[]{hidden.get(0), hidden.get(1)};
}
public boolean isGameOver() {
return matchedPairs == PAIRS;
}
public int getMoves() {
return moves;
}
public void printBoard() {
for (int i = 0; i < board.length; i++) {
if (board[i].state == Card.State.REMOVED) {
System.out.print("[ ] ");
} else if (board[i].state == Card.State.REVEALED) {
System.out.printf("[%2d] ", board[i].id);
} else {
System.out.print("[??] ");
}
if ((i + 1) % 4 == 0) System.out.println();
}
}
public static void main(String[] args) {
MemoryGame game = new MemoryGame();
System.out.println("初始棋盘(洗牌后):");
game.printBoard();
System.out.println("\nAI自动对局:");
while (!game.isGameOver()) {
int[] move = game.aiMove();
int res1 = game.flip(move[0]);
int res2 = game.flip(move[1]);
System.out.printf("翻 %d 和 %d: ", move[0], move[1]);
if (res1 == 1 || res2 == 1) {
System.out.println("匹配!");
} else {
System.out.println("不匹配");
game.hideUnmatched();
}
}
System.out.println("游戏结束,总步数: " + game.getMoves());
}
}
最优策略分析
Memory存在信息完备的最优策略。设当前有 k 张已翻开但未匹配的卡牌(已知信息),AI的期望步数可通过动态规划计算:
– 状态定义:dp[known][unknown] 表示剩余 known 张已知单牌、unknown 对未知牌时的期望翻牌次数。
– 转移方程:优先匹配已知对(若存在),否则计算随机翻牌的期望收益。
对于人类玩家,记忆容量有限,游戏的乐趣在于有限记忆下的概率决策。上述AI策略虽非全局最优,但已能稳定击败随机玩家。
扩展方向
- ** 多玩家对战**:引入回合制与计分系统,匹配成功可连续翻牌。
- ** 难度梯度**:增加卡牌数量、加入干扰图案、限制翻牌时间。
- ** 神经网络记忆模型**:用LSTM模拟人类玩家的有限记忆与遗忘曲线,构建拟人化AI对手。