消消乐(Match-3)是三消类益智游戏的代表,玩家通过交换相邻元素使三个或以上相同元素连成一线以消除得分。本文用Java实现消消乐核心算法,重点讲解BFS连通检测用于识别可消除区域,以及级联反应模拟用于计算连锁消除的得分。
游戏机制与问题建模
消消乐的核心算法挑战包括:
- 匹配检测:找出棋盘上所有三个或以上相同元素的连通区域
- 消除与下落:消除匹配元素后,上方元素下落填补空缺
- 级联反应:下落后可能形成新的匹配,需要递归检测
- 可移动判定:判断当前棋盘是否存在至少一个合法交换
BFS连通检测算法
BFS(广度优先搜索)是检测连通区域的最优选择。我们以每个未访问的格子为起点,向上下左右四个方向搜索相同类型的元素,标记整个连通区域。
import java.util.*;
/**
* 消消乐 - BFS连通检测与消除算法
*/
public class Match3Solver {
private static final int ROWS = 8;
private static final int COLS = 8;
private static final int[] DR = {-1, 1, 0, 0}; // 上下左右
private static final int[] DC = {0, 0, -1, 1};
private int[][] board; // 棋盘,0表示空,1-6表示不同元素类型
public Match3Solver(int[][] board) {
this.board = new int[ROWS][COLS];
for (int i = 0; i < ROWS; i++) {
System.arraycopy(board[i], 0, this.board[i], 0, COLS);
}
}
/**
* BFS连通检测:找出所有大小>=3的连通区域
* 返回所有需要消除的坐标集合
*/
public List<List<int[]>> findMatches() {
boolean[][] visited = new boolean[ROWS][COLS];
List<List<int[]>> allMatches = new ArrayList<>();
for (int r = 0; r < ROWS; r++) {
for (int c = 0; c < COLS; c++) {
if (board[r][c] == 0 || visited[r][c]) continue;
// BFS搜索连通区域
List<int[]> group = new ArrayList<>();
Queue<int[]> queue = new LinkedList<>();
queue.offer(new int[]{r, c});
visited[r][c] = true;
int type = board[r][c];
while (!queue.isEmpty()) {
int[] curr = queue.poll();
group.add(curr);
// 向四个方向扩展
for (int d = 0; d < 4; d++) {
int nr = curr[0] + DR[d];
int nc = curr[1] + DC[d];
if (nr < 0 || nr >= ROWS || nc < 0 || nc >= COLS) continue;
if (visited[nr][nc] || board[nr][nc] != type) continue;
visited[nr][nc] = true;
queue.offer(new int[]{nr, nc});
}
}
// 只保留大小>=3的连通区域
if (group.size() >= 3) {
allMatches.add(group);
}
}
}
return allMatches;
}
/**
* 线性扫描版本:更高效地检测横向和纵向的连续三个及以上
* 时间复杂度 O(ROWS * COLS),无需BFS的队列开销
*/
public boolean[][] findMatchesLinear() {
boolean[][] toRemove = new boolean[ROWS][COLS];
// 横向扫描
for (int r = 0; r < ROWS; r++) {
int count = 1;
for (int c = 1; c < COLS; c++) {
if (board[r][c] != 0 && board[r][c] == board[r][c-1]) {
count++;
} else {
if (count >= 3) {
for (int k = c - count; k < c; k++) {
toRemove[r][k] = true;
}
}
count = 1;
}
}
if (count >= 3) {
for (int k = COLS - count; k < COLS; k++) {
toRemove[r][k] = true;
}
}
}
// 纵向扫描
for (int c = 0; c < COLS; c++) {
int count = 1;
for (int r = 1; r < ROWS; r++) {
if (board[r][c] != 0 && board[r][c] == board[r-1][c]) {
count++;
} else {
if (count >= 3) {
for (int k = r - count; k < r; k++) {
toRemove[k][c] = true;
}
}
count = 1;
}
}
if (count >= 3) {
for (int k = ROWS - count; k < ROWS; k++) {
toRemove[k][c] = true;
}
}
}
return toRemove;
}
/**
* 执行消除并让上方元素下落
* 返回本次消除获得的分数
*/
public int eliminateAndDrop() {
boolean[][] toRemove = findMatchesLinear();
int removed = 0;
// 统计消除数量
for (int r = 0; r < ROWS; r++) {
for (int c = 0; c < COLS; c++) {
if (toRemove[r][c]) {
board[r][c] = 0;
removed++;
}
}
}
// 重力下落:每列从下往上填充
for (int c = 0; c < COLS; c++) {
int writeRow = ROWS - 1;
for (int r = ROWS - 1; r >= 0; r--) {
if (board[r][c] != 0) {
board[writeRow][c] = board[r][c];
if (writeRow != r) {
board[r][c] = 0;
}
writeRow--;
}
}
// 顶部空缺用随机新元素填充
Random rand = new Random();
for (int r = writeRow; r >= 0; r--) {
board[r][c] = rand.nextInt(6) + 1;
}
}
// 分数 = 消除数量的平方(鼓励大规模消除)
return removed * removed;
}
/**
* 级联反应模拟:递归消除直到没有新匹配
* 返回总分和每次消除的详细信息
*/
public CascadeResult simulateCascade() {
int totalScore = 0;
int cascadeLevel = 0;
List<Integer> scores = new ArrayList<>();
while (true) {
boolean[][] matches = findMatchesLinear();
int matchCount = 0;
for (boolean[] row : matches) {
for (boolean b : row) if (b) matchCount++;
}
if (matchCount == 0) break;
cascadeLevel++;
int score = eliminateAndDrop();
// 级联加成:第n次级联分数乘以n
score *= cascadeLevel;
totalScore += score;
scores.add(score);
}
return new CascadeResult(totalScore, cascadeLevel, scores);
}
static class CascadeResult {
int totalScore;
int maxLevel;
List<Integer> levelScores;
CascadeResult(int total, int level, List<Integer> scores) {
this.totalScore = total; this.maxLevel = level; this.levelScores = scores;
}
}
}
合法交换检测
游戏需要确保棋盘始终存在至少一个合法交换,否则需要重新生成棋盘。我们使用贪心策略预计算所有可能的交换:
/**
* 消消乐 - 合法移动检测与AI提示
*/
public class Match3MoveFinder {
private static final int ROWS = 8;
private static final int COLS = 8;
private static final int[] DR = {-1, 1, 0, 0};
private static final int[] DC = {0, 0, -1, 1};
/**
* 检查交换(r1,c1)和(r2,c2)是否能产生匹配
* 只检查与交换位置相关的区域,避免全棋盘扫描
*/
public boolean isValidSwap(int[][] board, int r1, int c1, int r2, int c2) {
// 执行临时交换
int temp = board[r1][c1];
board[r1][c1] = board[r2][c2];
board[r2][c2] = temp;
boolean valid = checkMatchAt(board, r1, c1) || checkMatchAt(board, r2, c2);
// 还原交换
temp = board[r1][c1];
board[r1][c1] = board[r2][c2];
board[r2][c2] = temp;
return valid;
}
/**
* 检查指定位置是否属于某个长度>=3的连续序列
*/
private boolean checkMatchAt(int[][] board, int r, int c) {
int type = board[r][c];
if (type == 0) return false;
// 检查横向连续
int left = 0, right = 0;
for (int k = c - 1; k >= 0 && board[r][k] == type; k--) left++;
for (int k = c + 1; k < COLS && board[r][k] == type; k++) right++;
if (left + right + 1 >= 3) return true;
// 检查纵向连续
int up = 0, down = 0;
for (int k = r - 1; k >= 0 && board[k][c] == type; k--) up++;
for (int k = r + 1; k < ROWS && board[k][c] == type; k++) down++;
if (up + down + 1 >= 3) return true;
return false;
}
/**
* 贪心查找最优移动:返回能引发最大级联得分的交换
* 时间复杂度 O(ROWS * COLS * 4 * cascade_cost)
*/
public int[] findBestMove(int[][] board) {
int bestScore = -1;
int[] bestMove = null;
for (int r = 0; r < ROWS; r++) {
for (int c = 0; c < COLS; c++) {
// 尝试四个方向的交换
for (int d = 0; d < 4; d++) {
int nr = r + DR[d];
int nc = c + DC[d];
if (nr < 0 || nr >= ROWS || nc < 0 || nc >= COLS) continue;
if (!isValidSwap(board, r, c, nr, nc)) continue;
// 模拟执行交换并计算级联得分
int[][] copy = deepCopy(board);
int temp = copy[r][c];
copy[r][c] = copy[nr][nc];
copy[nr][nc] = temp;
Match3Solver solver = new Match3Solver(copy);
Match3Solver.CascadeResult result = solver.simulateCascade();
if (result.totalScore > bestScore) {
bestScore = result.totalScore;
bestMove = new int[]{r, c, nr, nc, result.totalScore};
}
}
}
}
return bestMove;
}
private int[][] deepCopy(int[][] board) {
int[][] copy = new int[ROWS][COLS];
for (int i = 0; i < ROWS; i++) {
System.arraycopy(board[i], 0, copy[i], 0, COLS);
}
return copy;
}
}
完整可运行项目
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.util.List;
/**
* 消消乐完整游戏实现
*/
public class Match3Game extends JPanel implements MouseListener, ActionListener {
private static final int ROWS = 8;
private static final int COLS = 8;
private static final int CELL_SIZE = 60;
private static final int TYPES = 6;
private int[][] board = new int[ROWS][COLS];
private int selectedRow = -1, selectedCol = -1;
private int score = 0;
private int moves = 0;
private Timer animTimer;
private boolean isAnimating = false;
private Random rand = new Random();
// 颜色映射
private Color[] colors = {
Color.RED, Color.BLUE, Color.GREEN,
Color.YELLOW, Color.ORANGE, Color.MAGENTA
};
public Match3Game() {
setPreferredSize(new Dimension(COLS * CELL_SIZE, ROWS * CELL_SIZE + 50));
setBackground(Color.DARK_GRAY);
addMouseListener(this);
// 初始化棋盘,确保没有初始匹配
do {
generateBoard();
} while (hasMatches() || !hasValidMoves());
animTimer = new Timer(50, this);
}
private void generateBoard() {
for (int r = 0; r < ROWS; r++) {
for (int c = 0; c < COLS; c++) {
board[r][c] = rand.nextInt(TYPES) + 1;
}
}
}
/**
* 贪心策略:检查是否存在匹配
*/
private boolean hasMatches() {
// 横向
for (int r = 0; r < ROWS; r++) {
int count = 1;
for (int c = 1; c < COLS; c++) {
if (board[r][c] == board[r][c-1]) count++;
else {
if (count >= 3) return true;
count = 1;
}
}
if (count >= 3) return true;
}
// 纵向
for (int c = 0; c < COLS; c++) {
int count = 1;
for (int r = 1; r < ROWS; r++) {
if (board[r][c] == board[r-1][c]) count++;
else {
if (count >= 3) return true;
count = 1;
}
}
if (count >= 3) return true;
}
return false;
}
/**
* 检查是否存在合法移动
*/
private boolean hasValidMoves() {
int[][] dirs = {{0,1},{1,0}};
for (int r = 0; r < ROWS; r++) {
for (int c = 0; c < COLS; c++) {
for (int[] d : dirs) {
int nr = r + d[0], nc = c + d[1];
if (nr >= ROWS || nc >= COLS) continue;
if (isValidSwap(r, c, nr, nc)) return true;
}
}
}
return false;
}
private boolean isValidSwap(int r1, int c1, int r2, int c2) {
int temp = board[r1][c1];
board[r1][c1] = board[r2][c2];
board[r2][c2] = temp;
boolean valid = checkMatchAt(r1, c1) || checkMatchAt(r2, c2);
board[r2][c2] = board[r1][c1];
board[r1][c1] = temp;
return valid;
}
private boolean checkMatchAt(int r, int c) {
int type = board[r][c];
int left = 0, right = 0, up = 0, down = 0;
for (int k = c-1; k >= 0 && board[r][k] == type; k--) left++;
for (int k = c+1; k < COLS && board[r][k] == type; k++) right++;
for (int k = r-1; k >= 0 && board[k][c] == type; k--) up++;
for (int k = r+1; k < ROWS && board[k][c] == type; k++) down++;
return left + right + 1 >= 3 || up + down + 1 >= 3;
}
@Override
public void mousePressed(MouseEvent e) {
if (isAnimating) return;
int c = e.getX() / CELL_SIZE;
int r = e.getY() / CELL_SIZE;
if (r >= ROWS || c >= COLS) return;
if (selectedRow == -1) {
selectedRow = r;
selectedCol = c;
} else {
// 检查是否相邻
if (Math.abs(r - selectedRow) + Math.abs(c - selectedCol) == 1) {
if (isValidSwap(selectedRow, selectedCol, r, c)) {
// 执行交换
int temp = board[selectedRow][selectedCol];
board[selectedRow][selectedCol] = board[r][c];
board[r][c] = temp;
moves++;
// 触发级联消除
processMatches();
}
}
selectedRow = -1;
selectedCol = -1;
}
repaint();
}
/**
* 处理匹配消除与级联反应
*/
private void processMatches() {
isAnimating = true;
int cascade = 0;
while (true) {
boolean[][] toRemove = findMatches();
int count = 0;
for (boolean[] row : toRemove) {
for (boolean b : row) if (b) count++;
}
if (count == 0) break;
cascade++;
// 消除
for (int r = 0; r < ROWS; r++) {
for (int c = 0; c < COLS; c++) {
if (toRemove[r][c]) board[r][c] = 0;
}
}
// 下落与填充
for (int c = 0; c < COLS; c++) {
int write = ROWS - 1;
for (int r = ROWS - 1; r >= 0; r--) {
if (board[r][c] != 0) {
board[write--][c] = board[r][c];
}
}
for (int r = write; r >= 0; r--) {
board[r][c] = rand.nextInt(TYPES) + 1;
}
}
score += count * count * cascade;
repaint();
// 模拟动画延迟
try { Thread.sleep(200); } catch (InterruptedException e) {}
}
isAnimating = false;
if (!hasValidMoves()) {
JOptionPane.showMessageDialog(this, "Game Over! Score: " + score);
}
}
private boolean[][] findMatches() {
boolean[][] remove = new boolean[ROWS][COLS];
// 横向
for (int r = 0; r < ROWS; r++) {
int count = 1;
for (int c = 1; c < COLS; c++) {
if (board[r][c] != 0 && board[r][c] == board[r][c-1]) count++;
else {
if (count >= 3) {
for (int k = c - count; k < c; k++) remove[r][k] = true;
}
count = 1;
}
}
if (count >= 3) {
for (int k = COLS - count; k < COLS; k++) remove[r][k] = true;
}
}
// 纵向
for (int c = 0; c < COLS; c++) {
int count = 1;
for (int r = 1; r < ROWS; r++) {
if (board[r][c] != 0 && board[r][c] == board[r-1][c]) count++;
else {
if (count >= 3) {
for (int k = r - count; k < r; k++) remove[k][c] = true;
}
count = 1;
}
}
if (count >= 3) {
for (int k = ROWS - count; k < ROWS; k++) remove[k][c] = true;
}
}
return remove;
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
for (int r = 0; r < ROWS; r++) {
for (int c = 0; c < COLS; c++) {
int x = c * CELL_SIZE;
int y = r * CELL_SIZE;
if (board[r][c] == 0) {
g.setColor(Color.DARK_GRAY);
g.fillRect(x, y, CELL_SIZE, CELL_SIZE);
} else {
g.setColor(colors[board[r][c] - 1]);
g.fillRoundRect(x + 2, y + 2, CELL_SIZE - 4, CELL_SIZE - 4, 10, 10);
}
g.setColor(Color.GRAY);
g.drawRect(x, y, CELL_SIZE, CELL_SIZE);
}
}
// 高亮选中
if (selectedRow != -1) {
g.setColor(Color.WHITE);
g.drawRect(selectedCol * CELL_SIZE + 1, selectedRow * CELL_SIZE + 1,
CELL_SIZE - 2, CELL_SIZE - 2);
}
// UI信息
g.setColor(Color.WHITE);
g.setFont(new Font("Arial", Font.BOLD, 16));
g.drawString("Score: " + score + " Moves: " + moves, 10, ROWS * CELL_SIZE + 30);
}
@Override public void actionPerformed(ActionEvent e) { repaint(); }
@Override public void mouseClicked(MouseEvent e) {}
@Override public void mouseReleased(MouseEvent e) {}
@Override public void mouseEntered(MouseEvent e) {}
@Override public void mouseExited(MouseEvent e) {}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
JFrame frame = new JFrame("消消乐 - BFS连通检测");
Match3Game game = new Match3Game();
frame.add(game);
frame.pack();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
});
}
}
算法复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| BFS连通检测 | O(ROWS × COLS) | O(ROWS × COLS) | 完整连通区域检测 |
| 线性扫描检测 | O(ROWS × COLS) | O(ROWS × COLS) | 仅检测横纵连续,更高效 |
| 消除与下落 | O(ROWS × COLS) | O(1) | 逐列重力下落 |
| 级联模拟 | O(K × ROWS × COLS) | O(ROWS × COLS) | K为级联次数 |
| 最优移动搜索 | O(ROWS × COLS × 4 × K × ROWS × COLS) | O(ROWS × COLS) | 枚举所有交换并模拟 |
BFS适合检测不规则连通区域(如L形、T形匹配),线性扫描适合标准的三消规则(仅横纵直线)。实际游戏中可根据规则灵活选择。
扩展方向
- 特殊元素生成:消除4个或5个时生成炸弹、彩虹等特殊元素,扩展匹配规则
- 关卡目标系统:引入收集指定元素、消除障碍等多样化目标
- A*搜索最优解:对于步数限制的关卡,用A*搜索最少步数完成方案