Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -275,52 +275,7 @@ private void swapAndCheck(int x1, int y1, int x2, int y2) {
}

private List<int[]> findAllMatches() {
List<int[]> matches = new ArrayList<>();
boolean[][] checked = new boolean[GRID_SIZE][GRID_SIZE];

// 检查水平匹配
for (int y = 0; y < GRID_SIZE; y++) {
for (int x = 0; x < GRID_SIZE - 2; x++) {
if (!checked[x][y] && gameGrid[x][y] == gameGrid[x + 1][y] &&
gameGrid[x][y] == gameGrid[x + 2][y]) {

int endX = x + 2;
while (endX + 1 < GRID_SIZE && gameGrid[x][y] == gameGrid[endX + 1][y]) {
endX++;
}

for (int i = x; i <= endX; i++) {
if (!checked[i][y]) {
matches.add(new int[]{i, y});
checked[i][y] = true;
}
}
}
}
}

// 检查垂直匹配
for (int x = 0; x < GRID_SIZE; x++) {
for (int y = 0; y < GRID_SIZE - 2; y++) {
if (!checked[x][y] && gameGrid[x][y] == gameGrid[x][y + 1] &&
gameGrid[x][y] == gameGrid[x][y + 2]) {

int endY = y + 2;
while (endY + 1 < GRID_SIZE && gameGrid[x][y] == gameGrid[x][endY + 1]) {
endY++;
}

for (int i = y; i <= endY; i++) {
if (!checked[x][i]) {
matches.add(new int[]{x, i});
checked[x][i] = true;
}
}
}
}
}

return matches;
return Match3Matches.findAll(gameGrid);
}

private void processMatches(List<int[]> matches) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.wzz.game_console.client.screens.games;

import java.util.ArrayList;
import java.util.List;

/**
* 消消乐三连判定。抽成无 Minecraft 依赖的纯函数,避免 T/L 交叉漏消。
*/
final class Match3Matches {
private Match3Matches() {}

static List<int[]> findAll(int[][] grid) {
int n = grid.length;
List<int[]> matches = new ArrayList<>();
boolean[][] checked = new boolean[n][n];

for (int y = 0; y < n; y++) {
for (int x = 0; x < n - 2; x++) {
if (!checked[x][y] && grid[x][y] == grid[x + 1][y]
&& grid[x][y] == grid[x + 2][y] && grid[x][y] >= 0) {
int endX = x + 2;
while (endX + 1 < n && grid[x][y] == grid[endX + 1][y]) {
endX++;
}
for (int i = x; i <= endX; i++) {
if (!checked[i][y]) {
matches.add(new int[]{i, y});
checked[i][y] = true;
}
}
}
}
}

// 竖向扫描不得用 checked 门禁起点:T/L 形竖臂常从已计入的横三连格子出发。
for (int x = 0; x < n; x++) {
for (int y = 0; y < n - 2; y++) {
if (grid[x][y] == grid[x][y + 1] && grid[x][y] == grid[x][y + 2] && grid[x][y] >= 0) {
int endY = y + 2;
while (endY + 1 < n && grid[x][y] == grid[x][endY + 1]) {
endY++;
}
for (int i = y; i <= endY; i++) {
if (!checked[x][i]) {
matches.add(new int[]{x, i});
checked[x][i] = true;
}
}
}
}
}
return matches;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,27 @@ private void generateLevel(int levelNum) {
// 生成没有箱子的死局。收尾 boxCount<=0 时不再接受该结果,改为整关
// 重新生成(最多 10 次,仍失败保留最后一次)。首次尝试种子与原版
// 一致,正常关卡生成的地图不变。
for (int attempt = 0; attempt < 10; attempt++) {
for (int attempt = 0; attempt < 24; attempt++) {
if (generateLevelOnce(levelNum, attempt)) return;
}
forcePlayableLevel(Math.min(5 + (levelNum - 1) * 2 / 3, 14));
}

/** 24 次启发式生成都失败时,写入一箱一目标的可玩关,禁止零箱局落到玩家手里。 */
private void forcePlayableLevel(int gridSize) {
gridSize = Math.max(5, gridSize);
char[][] grid = new char[gridSize][gridSize];
for (int y = 0; y < gridSize; y++)
for (int x = 0; x < gridSize; x++)
grid[y][x] = (x == 0 || x == gridSize - 1 || y == 0 || y == gridSize - 1) ? '#' : ' ';
grid[1][2] = '$';
grid[1][3] = '.';
grid[2][1] = '@';
playerX = 1;
playerY = 2;
levelWidth = gridSize;
levelHeight = gridSize;
level = grid;
}

/** 生成一关并写入 level 字段;返回 false 表示本次生成出零箱局,需要重试 */
Expand Down Expand Up @@ -79,11 +97,13 @@ private boolean generateLevelOnce(int levelNum, int attempt) {

// 初始状态:箱子全部在目标点上(已解决状态 '+')
int placed = 0;
boolean[][] originalPlus = new boolean[gridSize][gridSize];
for (int p = 0; p < 500 && placed < boxCount; p++) {
int bx = 1 + rand.nextInt(gridSize - 2);
int by = 1 + rand.nextInt(gridSize - 2);
if (grid[by][bx] == ' ') {
grid[by][bx] = '+';
originalPlus[by][bx] = true;
placed++;
}
}
Expand All @@ -93,15 +113,22 @@ private boolean generateLevelOnce(int levelNum, int attempt) {
boxCount = 1;
}

// 放置玩家在左上角空地
playerX = 1;
playerY = 1;
if (grid[1][1] != ' ') {
// 放置玩家:只落在空地,禁止覆盖箱子/目标(否则少箱且可能开局即通)
playerX = -1;
playerY = -1;
if (grid[1][1] == ' ') {
playerX = 1;
playerY = 1;
} else {
outer:
for (int y = 1; y < gridSize - 1; y++)
for (int x = 1; x < gridSize - 1; x++)
if (grid[y][x] == ' ') { playerX = x; playerY = y; break outer; }
}
if (playerX < 0) {
level = grid;
return false;
}
grid[playerY][playerX] = '@';

// 打乱阶段:随机移动玩家来推动箱子离开目标点
Expand Down Expand Up @@ -130,17 +157,17 @@ private boolean generateLevelOnce(int levelNum, int attempt) {
if (bx <= 0 || bx >= gridSize - 1 || by <= 0 || by >= gridSize - 1) continue;
if (grid[by][bx] == '#' || grid[by][bx] == '+' || grid[by][bx] == '$') continue;

// 死角校验:箱子被推到非目标点的角落(两个正交相邻方向均为墙/边界)后永远推不动,
// 本轮打乱作废,由 generateLevel 以 attempt+1 重来(沿用 attempt 上限模式)
// 死角:跳过这次推动,不要整关作废。小地图上几乎每次随机推都可能擦到墙角,
// 整关 return false 会把 24 次机会耗尽,落到零箱局。
if (grid[by][bx] != '.' && isDeadCorner(grid, gridSize, bx, by)) {
level = grid;
return false;
continue;
}

char oldPos = grid[playerY][playerX];
char boxDest = grid[by][bx];
grid[playerY][playerX] = (oldPos == '*') ? '.' : ' ';
grid[ny][nx] = wasOnTarget ? '*' : '@';
grid[by][bx] = '$';
grid[by][bx] = (boxDest == '.') ? '+' : '$';
playerX = nx;
playerY = ny;
if (wasOnTarget) pushes++;
Expand All @@ -159,21 +186,32 @@ private boolean generateLevelOnce(int levelNum, int attempt) {
}
}

// 最终安全处理:如果仍有 '+' 未被推动,转为 '.'(移除未打乱的箱子)
// 确保不会出现开局即胜利的情况
// ★ 同步把 boxCount 调成实际成功推动数,避免"玩家推完原 boxCount 但还有多余目标点"导致无法通关
// 从未推动过的原始 '+':箱子和目标一起撤掉。打乱过程中压上别人目标点的 '+' 保留。
for (int y = 1; y < gridSize - 1; y++) {
for (int x = 1; x < gridSize - 1; x++) {
if (grid[y][x] == '+') {
grid[y][x] = '.';
boxCount--;
if (originalPlus[y][x] && grid[y][x] == '+') {
grid[y][x] = ' ';
}
}
}

// 按盘面重数:箱子($/+) 必须等于目标(./ * /+),且至少有一个未进目标的箱子,
// 否则开局即通或永远通不了。
int boxes = 0, targets = 0, offTarget = 0, players = 0;
for (int y = 0; y < gridSize; y++) {
for (int x = 0; x < gridSize; x++) {
switch (grid[y][x]) {
case '$' -> { boxes++; offTarget++; }
case '.' -> targets++;
case '+' -> { boxes++; targets++; }
case '*' -> { targets++; players++; }
case '@' -> players++;
default -> {}
}
}
}
level = grid;
// boxCount<=0('+' 全转 '.' 且无箱可推)时返回 false,由 generateLevel 重试
return boxCount > 0;
return boxes > 0 && boxes == targets && offTarget > 0 && players == 1;
}

/** 死角判定:箱子四周存在一组正交相邻方向(上/下/左/右)均为墙或边界(调用前需确认箱子不在目标点上) */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,10 @@ private void clearLines() {
board[0] = new int[GW]; colorBoard[0] = new int[GW];
lines++; score += 100 * level;
if (lines % 10 == 0) level++;
if (Minecraft.getInstance().player != null) Minecraft.getInstance().player.playSound(SoundEvents.EXPERIENCE_BOTTLE_THROW, 1.0F, 1.0F);
y--; // 下移后原 y+1 行进入 y 行,需重新检测(连续满行)
// 音效放在 y-- 之后,且先缓存 getInstance:player 为空时不得中断连消
Minecraft mc = Minecraft.getInstance();
if (mc != null && mc.player != null) mc.player.playSound(SoundEvents.EXPERIENCE_BOTTLE_THROW, 1.0F, 1.0F);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,33 @@ void puzzleRestartClearsFrozenCompletionTime() {
assertEquals(2, timer.elapsedSeconds(32_100));
}

@Test
void match3FindsVerticalArmOfTShape() {
int[][] grid = new int[8][8];
for (int x = 0; x < 8; x++) {
for (int y = 0; y < 8; y++) {
grid[x][y] = (x + 3 * y) % 7;
}
}
// T:横杠 (0,0)(1,0)(2,0),竖臂从横杠中点向下 (1,1)(1,2)
grid[0][0] = grid[1][0] = grid[2][0] = 9;
grid[1][1] = grid[1][2] = 9;
// L:横杠 (5,5)(6,5)(7,5),竖臂 (7,6)(7,7)
grid[5][5] = grid[6][5] = grid[7][5] = 8;
grid[7][6] = grid[7][7] = 8;

boolean[][] hit = new boolean[8][8];
for (int[] cell : Match3Matches.findAll(grid)) {
hit[cell[0]][cell[1]] = true;
}
assertTrue(hit[0][0] && hit[1][0] && hit[2][0], "T 横杠");
assertTrue(hit[1][1] && hit[1][2], "T 竖臂不得因交叉点已计入而漏消");
assertTrue(hit[5][5] && hit[6][5] && hit[7][5], "L 横杠");
assertTrue(hit[7][6] && hit[7][7], "L 竖臂不得漏消");
assertFalse(hit[0][1], "T 旁无关格");
assertFalse(hit[6][6], "L 旁无关格");
}

@Test
void moleHitboxRejectsUndergroundAndHiddenPixels() {
assertFalse(MoleHitbox.containsVisiblePart(10, 20, 48, 32,
Expand Down
Loading