--// CONFIG local SIZE = 16 local TOTAL_MINES = 40 -- chỉnh đúng số mìn của board local folder = workspace:WaitForChild("Lobby"):WaitForChild("MineSweeper") local Players = game:GetService("Players") local player = Players.LocalPlayer --// CHARACTER REFRESH local char, root, humanoid local function refreshCharacter() char = player.Character or player.CharacterAdded:Wait() root = char:WaitForChild("HumanoidRootPart") humanoid = char:WaitForChild("Humanoid") end refreshCharacter() player.CharacterAdded:Connect(function() task.wait(0.5) refreshCharacter() print("🔁 Character refreshed") end) --// STATE local grid = {} local tiles = {} local tilePos = {} local flagged = {} local revealed = {} local solving = false local lastRebuild = 0 local gridVersion = 0 local stuckCount = 0 local STUCK_LIMIT = 3 local gameStarted = false local roundEnding = false --// WATCHDOG local lastAction = tick() local lastRevealedCount = 0 local noProgressCount = 0 local solvingStartedAt = 0 local completeWaitStarted = nil --// EXACT SOLVER CONFIG local MAX_FRONTIER_ENUM = 22 -- tăng 24 nếu máy khỏe, giảm 20 nếu bị lag --// TELEPORT + JUMP to guarantee TouchInterest fires local function touch(part) if not part or not part.Parent then return false end if not root or not root.Parent or not humanoid or not humanoid.Parent then refreshCharacter() end if not root or not humanoid then return false end root.CFrame = part.CFrame + Vector3.new(0, 3, 0) humanoid:ChangeState(Enum.HumanoidStateType.Jumping) lastAction = tick() task.wait(0.04) return true end --// SAFE TILE ACCESS local function getTile(x, y) local row = grid and grid[y] return row and row[x] end local function getNeighbors(x, y) local result = {} for dy = -1, 1 do for dx = -1, 1 do if not (dx == 0 and dy == 0) then local nx, ny = x + dx, y + dy local tile = getTile(nx, ny) if tile then table.insert(result, { nx, ny, tile }) end end end end return result end local function getNumber(tile) if not tile or tile.Name ~= "Revealed" then return nil end local gui = tile:FindFirstChildOfClass("SurfaceGui") local label = gui and gui:FindFirstChild("TextLabel") return label and tonumber(label.Text) end --// STATE COUNTER local function countStates() local rev, cov = 0, 0 if not tiles then return 0, 0 end for _, t in ipairs(tiles) do if t and t.Parent then if t.Name == "Revealed" then rev += 1 elseif t.Name == "Covered" then cov += 1 end end end return rev, cov end --// OVERLAY GUI local overlayCache = {} local function clearOverlays() for _, gui in pairs(overlayCache) do if gui and gui.Parent then gui:Destroy() end end overlayCache = {} end local function setOverlay(tile, text, color) if not tile or not tile.Parent then return end local gui = overlayCache[tile] if not gui then gui = Instance.new("SurfaceGui") gui.Name = "SolverGui" gui.AlwaysOnTop = true gui.Face = Enum.NormalId.Top local label = Instance.new("TextLabel") label.Size = UDim2.new(1, 0, 1, 0) label.BackgroundTransparency = 1 label.TextScaled = true label.Font = Enum.Font.SourceSansBold label.Parent = gui gui.Parent = tile overlayCache[tile] = gui end local label = gui:FindFirstChildOfClass("TextLabel") if label then label.Text = text or "" label.TextColor3 = color or Color3.new(1, 1, 1) end end --// BUILD GRID local function rebuild() gridVersion += 1 local myVersion = gridVersion clearOverlays() tiles = {} grid = {} tilePos = {} flagged = {} revealed = {} repeat task.wait(0.05) until #folder:GetChildren() >= SIZE * SIZE or myVersion ~= gridVersion if myVersion ~= gridVersion then return end for _, v in ipairs(folder:GetChildren()) do if v:IsA("BasePart") then table.insert(tiles, v) end end if #tiles < SIZE * SIZE then warn("⚠️ Not enough tiles:", #tiles) return end table.sort(tiles, function(a, b) if math.abs(a.Position.Z - b.Position.Z) < 1 then return a.Position.X < b.Position.X end return a.Position.Z < b.Position.Z end) local i = 1 for y = 1, SIZE do grid[y] = {} for x = 1, SIZE do local tile = tiles[i] grid[y][x] = tile tilePos[tile] = { x, y } i += 1 end end lastRebuild = tick() lastAction = tick() lastRevealedCount = 0 noProgressCount = 0 stuckCount = 0 completeWaitStarted = nil gameStarted = false print("✅ Rebuilt v" .. gridVersion .. " (" .. #tiles .. " tiles)") end --// OPENING MOVE -- Chỉ mở 1 ô giữa bàn để giảm rủi ro so với mở 4 góc. local function doOpeningMoves() local cx = math.floor(SIZE / 2) local cy = math.floor(SIZE / 2) local startTile = getTile(cx, cy) if startTile and startTile.Parent and startTile.Name == "Covered" then touch(startTile) task.wait(0.15) end gameStarted = true lastAction = tick() end --// WATCH FOR BOARD RESETS local rebuildDebounce = false folder.ChildAdded:Connect(function() if not gameStarted then return end if rebuildDebounce then return end rebuildDebounce = true task.delay(0.3, function() rebuildDebounce = false local rev, _ = countStates() if rev <= 1 then rebuild() end end) end) --// BOARD COMPLETION local function isBoardComplete() if not tiles then return false end for _, t in ipairs(tiles) do if t and t.Parent and t.Name == "Covered" and not flagged[t] then return false end end return true end --// CASCADE RISK local function bordersNumberedTile(tile) local pos = tilePos[tile] if not pos then return false end for _, n in ipairs(getNeighbors(pos[1], pos[2])) do local num = getNumber(n[3]) if num and num > 0 then return true end end return false end --// BUILD CONSTRAINTS local function buildConstraints() local constraints = {} for y = 1, SIZE do for x = 1, SIZE do local tile = getTile(x, y) local num = getNumber(tile) if not num then continue end local neighbors = getNeighbors(x, y) local covered = {} local flagCount = 0 for _, n in ipairs(neighbors) do local t = n[3] if flagged[t] then flagCount += 1 elseif t and t.Parent and t.Name == "Covered" then table.insert(covered, t) end end local remainingMines = num - flagCount if #covered > 0 and remainingMines >= 0 then table.insert(constraints, { tiles = covered, mines = remainingMines, x = x, y = y, }) end end end return constraints end --// SUBSET CONSTRAINT SOLVING local function isSubset(small, large) for _, t in ipairs(small) do if not table.find(large, t) then return false end end return true end local function solveConstraints(constraints) local madeProgress = false for i = 1, #constraints do local A = constraints[i] for j = 1, #constraints do if i == j then continue end local B = constraints[j] if #B.tiles < #A.tiles and isSubset(B.tiles, A.tiles) then local diff = {} for _, t in ipairs(A.tiles) do if not table.find(B.tiles, t) then table.insert(diff, t) end end local mineDiff = A.mines - B.mines if mineDiff == 0 and #diff > 0 then table.sort(diff, function(a, b) local an = bordersNumberedTile(a) and 0 or 1 local bn = bordersNumberedTile(b) and 0 or 1 return an < bn end) for _, t in ipairs(diff) do if t and t.Parent and not flagged[t] and t.Name == "Covered" then touch(t) madeProgress = true task.wait(0.04) end end elseif mineDiff == #diff and #diff > 0 then for _, t in ipairs(diff) do if t and t.Parent and not flagged[t] then flagged[t] = true setOverlay(t, "M", Color3.fromRGB(255, 80, 80)) madeProgress = true lastAction = tick() end end end end end end return madeProgress end --// COMBINATION local function comb(n, k) if k < 0 or k > n then return 0 end if k == 0 or k == n then return 1 end if k > n - k then k = n - k end local result = 1 for i = 1, k do result = result * (n - k + i) / i end return result end --// FALLBACK HEURISTIC PROBABILITY local function calculateHeuristicProbabilities(constraints) local probs = {} local counts = {} local flaggedCount = 0 for _, v in pairs(flagged) do if v then flaggedCount += 1 end end local allCovered = {} for _, t in ipairs(tiles) do if t and t.Parent and t.Name == "Covered" and not flagged[t] then table.insert(allCovered, t) end end local minesLeft = math.max(TOTAL_MINES - flaggedCount, 0) local globalRisk = #allCovered > 0 and math.clamp(minesLeft / #allCovered, 0, 1) or 0.5 for _, c in ipairs(constraints) do if #c.tiles > 0 and c.mines >= 0 then local risk = math.clamp(c.mines / #c.tiles, 0, 1) for _, t in ipairs(c.tiles) do if t and t.Parent and t.Name == "Covered" and not flagged[t] then probs[t] = (probs[t] or 0) + risk counts[t] = (counts[t] or 0) + 1 end end end end local final = {} for _, t in ipairs(allCovered) do if counts[t] then local constraintRisk = probs[t] / counts[t] final[t] = constraintRisk * 0.85 + globalRisk * 0.15 else final[t] = globalRisk end end return final end --// EXACT FRONTIER PROBABILITY SOLVER local function calculateProbabilities(constraints) local flaggedCount = 0 for _, v in pairs(flagged) do if v then flaggedCount += 1 end end local allCovered = {} for _, t in ipairs(tiles) do if t and t.Parent and t.Name == "Covered" and not flagged[t] then table.insert(allCovered, t) end end local minesLeftTotal = math.max(TOTAL_MINES - flaggedCount, 0) local frontierIndex = {} local frontier = {} for _, c in ipairs(constraints) do for _, t in ipairs(c.tiles) do if t and t.Parent and t.Name == "Covered" and not flagged[t] then if not frontierIndex[t] then table.insert(frontier, t) frontierIndex[t] = #frontier end end end end if #frontier == 0 or #frontier > MAX_FRONTIER_ENUM then return calculateHeuristicProbabilities(constraints) end local indexedConstraints = {} local constraintsByTileIndex = {} for ci, c in ipairs(constraints) do local idxs = {} for _, t in ipairs(c.tiles) do local idx = frontierIndex[t] if idx then table.insert(idxs, idx) constraintsByTileIndex[idx] = constraintsByTileIndex[idx] or {} table.insert(constraintsByTileIndex[idx], ci) end end if #idxs > 0 then indexedConstraints[ci] = { idxs = idxs, mines = c.mines, mineCount = 0, unknownCount = #idxs, } end end local frontierSet = {} for _, t in ipairs(frontier) do frontierSet[t] = true end local outsideCoveredCount = 0 for _, t in ipairs(allCovered) do if not frontierSet[t] then outsideCoveredCount += 1 end end local assignment = {} local mineSums = {} local totalWeight = 0 local validBoards = 0 for i = 1, #frontier do mineSums[i] = 0 end local function checkConstraintsFor(idx) local related = constraintsByTileIndex[idx] if not related then return true end for _, ci in ipairs(related) do local c = indexedConstraints[ci] if c then if c.mineCount > c.mines then return false end if c.mineCount + c.unknownCount < c.mines then return false end end end return true end local function checkFinal() for _, c in pairs(indexedConstraints) do if c.mineCount ~= c.mines then return false end end return true end local function applyValue(idx, value) assignment[idx] = value local related = constraintsByTileIndex[idx] if related then for _, ci in ipairs(related) do local c = indexedConstraints[ci] if c then c.unknownCount -= 1 if value == 1 then c.mineCount += 1 end end end end end local function undoValue(idx, value) local related = constraintsByTileIndex[idx] if related then for _, ci in ipairs(related) do local c = indexedConstraints[ci] if c then c.unknownCount += 1 if value == 1 then c.mineCount -= 1 end end end end assignment[idx] = nil end local function backtrack(idx, frontierMineCount) if idx > #frontier then if not checkFinal() then return end local outsideMines = minesLeftTotal - frontierMineCount local weight = comb(outsideCoveredCount, outsideMines) if weight <= 0 then return end validBoards += 1 totalWeight += weight for i = 1, #frontier do if assignment[i] == 1 then mineSums[i] += weight end end return end -- Try safe applyValue(idx, 0) if checkConstraintsFor(idx) then backtrack(idx + 1, frontierMineCount) end undoValue(idx, 0) -- Try mine applyValue(idx, 1) if checkConstraintsFor(idx) then backtrack(idx + 1, frontierMineCount + 1) end undoValue(idx, 1) end backtrack(1, 0) if totalWeight <= 0 then return calculateHeuristicProbabilities(constraints) end local final = {} for i, t in ipairs(frontier) do final[t] = math.clamp(mineSums[i] / totalWeight, 0, 1) end local expectedFrontierMines = 0 for i = 1, #frontier do expectedFrontierMines += mineSums[i] / totalWeight end local outsideRisk = 0 if outsideCoveredCount > 0 then outsideRisk = math.clamp((minesLeftTotal - expectedFrontierMines) / outsideCoveredCount, 0, 1) end for _, t in ipairs(allCovered) do if not final[t] then final[t] = outsideRisk end end return final end --// BEST MOVE USING EXACT PROBABILITY local function makeBestMove(constraints) local probs = calculateProbabilities(constraints) local safeTiles = {} local mineTiles = {} local bestTile = nil local bestRisk = math.huge local bestScore = -math.huge for tile, risk in pairs(probs) do if tile and tile.Parent and tile.Name == "Covered" and not flagged[tile] then if risk >= 0.999999 then table.insert(mineTiles, tile) elseif risk <= 0.000001 then table.insert(safeTiles, tile) else local infoScore = 0 local pos = tilePos[tile] if pos then for _, n in ipairs(getNeighbors(pos[1], pos[2])) do local num = getNumber(n[3]) if num and num > 0 then infoScore += 1 end end end local score = -risk * 100 + infoScore if risk < bestRisk or (math.abs(risk - bestRisk) < 0.000001 and score > bestScore) then bestRisk = risk bestScore = score bestTile = tile end end end end -- Flag sure mines if #mineTiles > 0 then for _, t in ipairs(mineTiles) do if t and t.Parent and not flagged[t] then flagged[t] = true setOverlay(t, "M", Color3.fromRGB(255, 80, 80)) lastAction = tick() end end return true end -- Reveal sure safe tiles if #safeTiles > 0 then table.sort(safeTiles, function(a, b) local an = bordersNumberedTile(a) and 0 or 1 local bn = bordersNumberedTile(b) and 0 or 1 return an < bn end) for _, t in ipairs(safeTiles) do if t and t.Parent and t.Name == "Covered" and not flagged[t] then setOverlay(t, "0%", Color3.fromRGB(80, 255, 80)) touch(t) task.wait(0.04) end end return true end -- Best guess if bestTile then setOverlay( bestTile, string.format("%.1f%%", bestRisk * 100), Color3.fromRGB(255, 200, 0) ) touch(bestTile) return true end -- Fallback for _, t in ipairs(tiles) do if t and t.Parent and t.Name == "Covered" and not flagged[t] then touch(t) return true end end return false end --// RESET DETECTION BY TILE COLORS local FOREST_GREEN = BrickColor.new("Forest green") local SEA_GREEN = BrickColor.new("Sea green") local function isBoardReset() if not tiles then return false end local greenCount = 0 local total = 0 for _, t in ipairs(tiles) do if t and t.Parent then total += 1 local bc = t.BrickColor if bc == FOREST_GREEN or bc == SEA_GREEN then greenCount += 1 end end end if total == 0 then return false end return greenCount >= total * 0.9 end --// ROUND END DETECTION local function onRoundEnd(reason) if roundEnding then return end roundEnding = true solving = false print("🔔 Round ended (" .. reason .. "), waiting...") if not root or not root.Parent then refreshCharacter() end local posAtEnd = root and root.Position or Vector3.new(0, 0, 0) local startWait = tick() repeat task.wait(0.1) if not root or not root.Parent then refreshCharacter() end until not root or (root.Position - posAtEnd).Magnitude > 20 or tick() - startWait > 8 print("🔄 Starting next round...") task.wait(3) roundEnding = false rebuild() task.wait(0.3) doOpeningMoves() end local soundFolder = workspace:WaitForChild("Lobby"):WaitForChild("MinesweeperCenter") local badgeSound = soundFolder:FindFirstChild("badge") local rocketSound = soundFolder:FindFirstChild("Rocket") if badgeSound then badgeSound.Played:Connect(function() onRoundEnd("win") end) end if rocketSound then rocketSound.Played:Connect(function() onRoundEnd("death") end) end --// START rebuild() --// MAIN SOLVE LOOP task.spawn(function() task.wait(0.5) doOpeningMoves() while true do local ok, err = pcall(function() if not grid or not tiles then return end if tick() - lastRebuild < 0.15 then return end if solving then if tick() - solvingStartedAt > 6 then warn("⚠️ Solver timeout, unlocking solving state") solving = false else return end end if roundEnding then return end if not root or not root.Parent or not humanoid or not humanoid.Parent then refreshCharacter() end --// HARD WATCHDOG if gameStarted and tick() - lastAction > 12 then print("⚠️ Watchdog: no action for 12s, clearing flags and making a move...") solving = false flagged = {} stuckCount = STUCK_LIMIT completeWaitStarted = nil local constraints = buildConstraints() if not makeBestMove(constraints) then rebuild() task.wait(0.3) doOpeningMoves() end lastAction = tick() return end --// PROGRESS WATCHDOG local revNow, covNow = countStates() if revNow > lastRevealedCount then lastRevealedCount = revNow noProgressCount = 0 else noProgressCount += 1 end if gameStarted and noProgressCount > 180 then print("⚠️ No reveal progress, forcing probability move...") noProgressCount = 0 flagged = {} stuckCount = STUCK_LIMIT completeWaitStarted = nil makeBestMove(buildConstraints()) lastAction = tick() return end --// Detect board reset in-place if gameStarted and isBoardReset() then print("♻️ Board reset detected, rebuilding...") rebuild() task.wait(0.3) doOpeningMoves() return end --// Detect empty board reset if gameStarted then local rev, cov = countStates() if rev == 0 and cov >= SIZE * SIZE - 5 then print("♻️ Empty board detected, rebuilding...") rebuild() task.wait(0.3) doOpeningMoves() return end end --// Prevent fake complete lock if isBoardComplete() then if not completeWaitStarted then completeWaitStarted = tick() print("⏳ Board looks complete, waiting for round result...") end if tick() - completeWaitStarted > 5 then print("⚠️ Complete timeout. Clearing internal flags and forcing a move...") flagged = {} completeWaitStarted = nil stuckCount = STUCK_LIMIT local constraints = buildConstraints() makeBestMove(constraints) lastAction = tick() end task.wait(0.3) return else completeWaitStarted = nil end solving = true solvingStartedAt = tick() local didSomething = false local myVersion = gridVersion --// PASS 1: Basic logic for y = 1, SIZE do if myVersion ~= gridVersion then solving = false return end for x = 1, SIZE do local tile = getTile(x, y) local number = getNumber(tile) if not number then continue end local neighbors = getNeighbors(x, y) local covered = {} local nFlagged = 0 for _, n in ipairs(neighbors) do local t = n[3] if flagged[t] then nFlagged += 1 elseif t and t.Parent and t.Name == "Covered" then table.insert(covered, t) end end -- All remaining covered neighbors are mines if (#covered + nFlagged) == number and #covered > 0 then for _, c in ipairs(covered) do if c and c.Parent and not flagged[c] then flagged[c] = true setOverlay(c, "M", Color3.fromRGB(255, 80, 80)) didSomething = true lastAction = tick() end end end -- All mines accounted for, reveal safe covered neighbors if nFlagged == number and #covered > 0 then table.sort(covered, function(a, b) local an = bordersNumberedTile(a) and 0 or 1 local bn = bordersNumberedTile(b) and 0 or 1 return an < bn end) for _, c in ipairs(covered) do if c and c.Parent and c.Name == "Covered" and not flagged[c] then touch(c) didSomething = true task.wait(0.04) end end end end end --// PASS 2: Subset constraints if myVersion == gridVersion then local constraints = buildConstraints() if solveConstraints(constraints) then didSomething = true end --// PASS 3: Exact probability if stuck if not didSomething then stuckCount += 1 if stuckCount >= STUCK_LIMIT then stuckCount = 0 makeBestMove(buildConstraints()) end else stuckCount = 0 end end solving = false end) if not ok then warn("💥 Solver error:", err) solving = false end task.wait(0.05) end end)