From 479957ae8bcda095dea47e0b7056d781f7afd793 Mon Sep 17 00:00:00 2001 From: Barbatosishere <584146143@qq.com> Date: Sat, 19 Sep 2026 13:10:17 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=B6=88=E6=B6=88=E4=B9=90T/L=E5=9E=8B?= =?UTF-8?q?=E6=BC=8F=E6=B6=88+=E6=8E=A8=E7=AE=B1=E5=AD=90=E9=9B=B6?= =?UTF-8?q?=E7=AE=B1=E5=B1=80+=E4=BF=84=E7=BD=97=E6=96=AF=E6=96=B9?= =?UTF-8?q?=E5=9D=97=E8=BF=9E=E6=B6=88=E4=B8=AD=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Match3: findAllMatches 抽为纯函数 Match3Matches,修复 T/L 交叉型 因 checked 去重导致竖臂/短边漏消;补 T/L 回归测试 - Sokoban: 生成重试 10→24 次并新增 forcePlayableLevel 保底关,杜绝 零箱局;玩家落点不再覆盖箱子/目标;擦墙角的推动跳过而非整关作废; 收尾按盘面重数箱/目标并校验可通性 - Tetris: 消行音效移到 y-- 之后并缓存 getInstance,player 为空不再 中断连续消行 --- .../screens/games/Match3GameScreen.java | 47 +----------- .../client/screens/games/Match3Matches.java | 54 ++++++++++++++ .../client/screens/games/SokobanScreen.java | 74 ++++++++++++++----- .../screens/games/TetrisGameScreen.java | 4 +- .../screens/games/MiniGameRegressionTest.java | 27 +++++++ 5 files changed, 141 insertions(+), 65 deletions(-) create mode 100644 src/main/java/com/wzz/game_console/client/screens/games/Match3Matches.java diff --git a/src/main/java/com/wzz/game_console/client/screens/games/Match3GameScreen.java b/src/main/java/com/wzz/game_console/client/screens/games/Match3GameScreen.java index a48ca8c..7c63c9b 100644 --- a/src/main/java/com/wzz/game_console/client/screens/games/Match3GameScreen.java +++ b/src/main/java/com/wzz/game_console/client/screens/games/Match3GameScreen.java @@ -275,52 +275,7 @@ private void swapAndCheck(int x1, int y1, int x2, int y2) { } private List findAllMatches() { - List 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 matches) { diff --git a/src/main/java/com/wzz/game_console/client/screens/games/Match3Matches.java b/src/main/java/com/wzz/game_console/client/screens/games/Match3Matches.java new file mode 100644 index 0000000..abe25a4 --- /dev/null +++ b/src/main/java/com/wzz/game_console/client/screens/games/Match3Matches.java @@ -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 findAll(int[][] grid) { + int n = grid.length; + List 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; + } +} diff --git a/src/main/java/com/wzz/game_console/client/screens/games/SokobanScreen.java b/src/main/java/com/wzz/game_console/client/screens/games/SokobanScreen.java index 9bf59c2..6cd4bd3 100644 --- a/src/main/java/com/wzz/game_console/client/screens/games/SokobanScreen.java +++ b/src/main/java/com/wzz/game_console/client/screens/games/SokobanScreen.java @@ -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 表示本次生成出零箱局,需要重试 */ @@ -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++; } } @@ -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] = '@'; // 打乱阶段:随机移动玩家来推动箱子离开目标点 @@ -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++; @@ -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; } /** 死角判定:箱子四周存在一组正交相邻方向(上/下/左/右)均为墙或边界(调用前需确认箱子不在目标点上) */ diff --git a/src/main/java/com/wzz/game_console/client/screens/games/TetrisGameScreen.java b/src/main/java/com/wzz/game_console/client/screens/games/TetrisGameScreen.java index 450887d..f00e2ad 100644 --- a/src/main/java/com/wzz/game_console/client/screens/games/TetrisGameScreen.java +++ b/src/main/java/com/wzz/game_console/client/screens/games/TetrisGameScreen.java @@ -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); } } } diff --git a/src/test/java/com/wzz/game_console/client/screens/games/MiniGameRegressionTest.java b/src/test/java/com/wzz/game_console/client/screens/games/MiniGameRegressionTest.java index d421a3d..cbcd4b1 100644 --- a/src/test/java/com/wzz/game_console/client/screens/games/MiniGameRegressionTest.java +++ b/src/test/java/com/wzz/game_console/client/screens/games/MiniGameRegressionTest.java @@ -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,