Загрузка данных
"""
Minesweeper Screen Analyzer & Bitboard Mask Generator
=====================================================
Target OS: Windows 7 (64-bit) / Windows 10/11
Python: 3.8+ (64-bit)
RAM Usage: < 35 MB (Strictly no OpenCV, PyTorch, or NumPy)
Dependencies: pip install mss pillow
"""
import sys
import time
import math
import os
import ctypes
import tkinter as tk
from typing import Dict, Tuple, List, Optional
from PIL import Image
import mss
# Virtual Key Codes for Windows API (Native ctypes, 0 RAM overhead)
VK_F7 = 0x76 # F7 Hotkey to select/re-select screen region
VK_ESCAPE = 0x1B # ESC Hotkey to exit live loop
def is_key_pressed(vk_code: int) -> bool:
"""Check if a keyboard key is pressed using native Windows User32 API."""
try:
return bool(ctypes.windll.user32.GetAsyncKeyState(vk_code) & 0x8000)
except Exception:
return False
# =====================================================================
# 1. BITBOARD ENGINE (MinesweeperBitboard)
# =====================================================================
class MinesweeperBitboard:
"""
Class for storing and manipulating Minesweeper board states using bitboards.
Uses standard Python 'int' which provides arbitrary-precision integers,
allowing seamless handling of any board dimension (9x9, 16x16, 16x30, custom).
Bit indexing: index = row * cols + col (0 <= index < rows * cols)
LSB (Bit 0) corresponds to cell (0, 0) top-left.
"""
def __init__(self, rows: int, cols: int):
self.rows: int = rows
self.cols: int = cols
self.total_cells: int = rows * cols
# Bitmasks represented as standard Python integers
self.unopened_mask: int = 0
self.flags_mask: int = 0
self.opened_empty_mask: int = 0 # Opened empty cells (0)
self.numbers_masks: Dict[int, int] = {num: 0 for num in range(1, 9)}
def cell_to_index(self, row: int, col: int) -> int:
"""Convert 2D grid position to 1D bit index."""
if not (0 <= row < self.rows and 0 <= col < self.cols):
raise ValueError(f"Cell ({row}, {col}) out of bounds ({self.rows}x{self.cols})")
return row * self.cols + col
def set_bit(self, mask: int, row: int, col: int) -> int:
"""Set bit at position (row, col) to 1."""
idx = self.cell_to_index(row, col)
return mask | (1 << idx)
def clear_bit(self, mask: int, row: int, col: int) -> int:
"""Clear bit at position (row, col) to 0."""
idx = self.cell_to_index(row, col)
return mask & ~(1 << idx)
def get_bit(self, mask: int, row: int, col: int) -> bool:
"""Check if bit at position (row, col) is 1."""
idx = self.cell_to_index(row, col)
return bool((mask >> idx) & 1)
def print_mask(self, mask: int, title: str = "Bitmask") -> None:
"""Pretty print a single bitmask as an N x M matrix of 1s and 0s."""
print(f"\n--- {title} ({self.rows}x{self.cols}) ---")
# Header column numbers
col_header = " " + "".join([f"{c % 10}" for c in range(self.cols)])
print(col_header)
print(" +" + "-" * self.cols + "+")
for r in range(self.rows):
row_str = f"{r:2d} |"
for c in range(self.cols):
bit = "1" if self.get_bit(mask, r, c) else "0"
row_str += bit
row_str += "|"
print(row_str)
print(" +" + "-" * self.cols + "+")
print(f"Dec: {mask}")
print(f"Hex: 0x{mask:X}")
def print_board_summary(self) -> None:
"""
Display ASCII summary of recognized board state in CMD.
Symbols:
'?' = unopened
'!' = flag
'0' = opened empty
'1'..'8' = opened numbers
'E' = error (unknown state)
"""
print(f"\n================ BOARD STATE SUMMARY ({self.rows}x{self.cols}) ================")
# Build column header with indices (0..cols-1)
if self.cols <= 10:
col_header = " " + " ".join([str(c) for c in range(self.cols)])
else:
tens_row = " " + " ".join([str(c // 10) if c >= 10 else " " for c in range(self.cols)])
units_row = " " + " ".join([str(c % 10) for c in range(self.cols)])
col_header = tens_row + "\n" + units_row
print(col_header)
border_line = " +" + "-" * (self.cols * 2 + 1) + "+"
print(border_line)
for r in range(self.rows):
row_cells = []
for c in range(self.cols):
if self.get_bit(self.flags_mask, r, c):
row_cells.append("!")
elif self.get_bit(self.unopened_mask, r, c):
row_cells.append("?")
elif self.get_bit(self.opened_empty_mask, r, c):
row_cells.append("0")
else:
found_num = None
for num in range(1, 9):
if self.get_bit(self.numbers_masks[num], r, c):
found_num = str(num)
break
if found_num is not None:
row_cells.append(found_num)
else:
row_cells.append("E")
row_str = f"{r:2d} | " + " ".join(row_cells) + " | " + f"{r:2d}"
print(row_str)
print(border_line)
print(col_header)
print("================================================================\n")
def get_fingerprint(self) -> Tuple:
"""Return a hashable tuple representing all bitmasks to quickly detect board changes."""
return (
self.unopened_mask,
self.flags_mask,
self.opened_empty_mask,
tuple(self.numbers_masks[n] for n in range(1, 9))
)
def get_neighbors_mask(self, row: int, col: int) -> int:
"""Return a bitmask containing 1s for all valid 8-adjacent neighbors of cell (row, col)."""
neighbors = 0
for dr in (-1, 0, 1):
for dc in (-1, 0, 1):
if dr == 0 and dc == 0:
continue
nr, nc = row + dr, col + dc
if 0 <= nr < self.rows and 0 <= nc < self.cols:
neighbors = self.set_bit(neighbors, nr, nc)
return neighbors
# =====================================================================
# 2. SCREEN REGION SELECTION OVERLAY (Tkinter)
# =====================================================================
class ScreenSelector:
"""
Lightweight Tkinter semi-transparent fullscreen overlay for dragging
a selection box over the Minesweeper game area.
"""
def __init__(self):
self.selected_region: Optional[Tuple[int, int, int, int]] = None # (left, top, width, height)
self.start_x: int = 0
self.start_y: int = 0
self.curr_x: int = 0
self.curr_y: int = 0
def select_area(self) -> Optional[Tuple[int, int, int, int]]:
root = tk.Tk()
root.attributes("-fullscreen", True)
root.attributes("-topmost", True)
# Set transparency for Windows 7 / 10 / 11
try:
root.attributes("-alpha", 0.35)
except Exception:
pass
root.config(cursor="cross")
canvas = tk.Canvas(root, highlightthickness=0, bg="#000000")
canvas.pack(fill=tk.BOTH, expand=True)
# Instruction text
screen_w = root.winfo_screenwidth()
screen_h = root.winfo_screenheight()
canvas.create_text(
screen_w // 2, 40,
text="Зажмите ЛКМ и выделите область с полем Сапёра. [ESC - Отмена, ENTER - Подтвердить]",
fill="#FFFFFF", font=("Arial", 16, "bold")
)
rect_id = canvas.create_rectangle(0, 0, 0, 0, outline="#00FF00", width=2, dash=(4, 4))
def on_mouse_down(event):
self.start_x, self.start_y = event.x, event.y
canvas.coords(rect_id, self.start_x, self.start_y, self.start_x, self.start_y)
def on_mouse_drag(event):
self.curr_x, self.curr_y = event.x, event.y
canvas.coords(rect_id, self.start_x, self.start_y, self.curr_x, self.curr_y)
def on_mouse_up(event):
self.curr_x, self.curr_y = event.x, event.y
x1, x2 = min(self.start_x, self.curr_x), max(self.start_x, self.curr_x)
y1, y2 = min(self.start_y, self.curr_y), max(self.start_y, self.curr_y)
w, h = x2 - x1, y2 - y1
if w > 10 and h > 10:
self.selected_region = (x1, y1, w, h)
def on_confirm(event=None):
if self.selected_region:
root.destroy()
def on_cancel(event=None):
self.selected_region = None
root.destroy()
canvas.bind("<ButtonPress-1>", on_mouse_down)
canvas.bind("<B1-Motion>", on_mouse_drag)
canvas.bind("<ButtonRelease-1>", on_mouse_up)
root.bind("<Escape>", on_cancel)
root.bind("<Return>", on_confirm)
root.bind("<Double-Button-1>", on_confirm)
root.mainloop()
return self.selected_region
# =====================================================================
# 3. PIXEL COLOR & CELL RECOGNITION ENGINE (Pillow / PIL)
# =====================================================================
class CellAnalyzer:
"""
Lightweight cell classifier analyzing pixel RGB values and contrast bevels
without heavy computer vision libraries. Memory efficient (<5MB overhead).
"""
# Reference RGB colors for standard Minesweeper numbers (1..8)
COLOR_MAP = {
1: (0, 0, 255), # Blue
2: (0, 128, 0), # Green
3: (255, 0, 0), # Red
4: (0, 0, 128), # Dark Blue / Navy
5: (128, 0, 0), # Dark Red / Maroon
6: (0, 128, 128), # Teal / Cyan
7: (0, 0, 0), # Black
8: (128, 128, 128) # Grey
}
@staticmethod
def color_distance(c1: Tuple[int, int, int], c2: Tuple[int, int, int]) -> float:
"""Euclidean distance in RGB color space."""
return math.sqrt(sum((a - b) ** 2 for a, b in zip(c1, c2)))
@classmethod
def classify_cell(cls, cell_img: Image.Image) -> str:
"""
Classify cell image into one of:
'UNOPENED', 'FLAG', 'EMPTY' (0), '1', '2', '3', '4', '5', '6', '7', '8'
"""
w, h = cell_img.size
if w < 4 or h < 4:
return 'UNOPENED'
# Convert image to RGB mode
img = cell_img.convert("RGB")
pixels = img.load()
# 1. Bevel Check for Unopened vs Opened
# Classic Minesweeper unopened tiles have bright top-left border (white)
# and dark bottom-right border (grey/shadow).
top_left_samples = [pixels[x, y] for x in range(1, min(w, 5)) for y in range(1, min(h, 5))]
bot_right_samples = [pixels[x, y] for x in range(max(0, w - 5), w - 1) for y in range(max(0, h - 5), h - 1)]
tl_avg_brightness = sum(sum(p) / 3.0 for p in top_left_samples) / max(len(top_left_samples), 1)
br_avg_brightness = sum(sum(p) / 3.0 for p in bot_right_samples) / max(len(bot_right_samples), 1)
is_beveled = (tl_avg_brightness - br_avg_brightness) > 35.0
# Sample inner 60% region of the cell (avoid borders/grid lines)
margin_x = max(1, int(w * 0.2))
margin_y = max(1, int(h * 0.2))
inner_pixels = []
for x in range(margin_x, w - margin_x):
for y in range(margin_y, h - margin_y):
inner_pixels.append(pixels[x, y])
if not inner_pixels:
return 'UNOPENED' if is_beveled else 'EMPTY'
# 2. Check for Red Flag
# Red flag has high Red component compared to Green & Blue
red_flag_count = sum(
1 for (r, g, b) in inner_pixels
if r > 160 and g < 70 and b < 70
)
if red_flag_count > len(inner_pixels) * 0.05:
return 'FLAG'
# If it has 3D border bevel and no flag -> Unopened tile
if is_beveled:
return 'UNOPENED'
# 3. Process Opened Tile -> Detect Numbers (1..8) or Empty (0)
# Find dominant non-background color in inner region
# Standard opened tile background is light grey ~RGB(192, 192, 192) or ~RGB(229, 229, 229)
color_counts: Dict[int, int] = {num: 0 for num in range(1, 9)}
for r, g, b in inner_pixels:
# Skip grey/white/flat background pixels
if abs(r - g) < 15 and abs(g - b) < 15 and abs(r - b) < 15 and r > 150:
continue
# Compare pixel color with reference number colors
pixel_rgb = (r, g, b)
best_num = None
min_dist = 80.0 # Color distance threshold
for num, ref_rgb in cls.COLOR_MAP.items():
dist = cls.color_distance(pixel_rgb, ref_rgb)
if dist < min_dist:
min_dist = dist
best_num = num
if best_num is not None:
color_counts[best_num] += 1
# Determine if any number color was detected sufficiently
min_pixels_required = max(3, int(len(inner_pixels) * 0.025))
max_detected_num = None
max_count = 0
for num, count in color_counts.items():
if count >= min_pixels_required and count > max_count:
max_count = count
max_detected_num = num
if max_detected_num is not None:
return str(max_detected_num)
return 'EMPTY'
# =====================================================================
# 4. MAIN WORKFLOW & COORDINATOR
# =====================================================================
def analyze_minesweeper_screen(rows: int, cols: int, region: Tuple[int, int, int, int]) -> MinesweeperBitboard:
"""
Captures screen region, divides into rows x cols grid, runs pixel analysis,
and returns populated MinesweeperBitboard.
"""
rx, ry, rw, rh = region
# Capture screen region with MSS
with mss.mss() as sct:
sct_box = {"top": ry, "left": rx, "width": rw, "height": rh}
sct_img = sct.grab(sct_box)
# Convert MSS raw capture to PIL Image (lightweight)
img = Image.frombytes("RGB", sct_img.size, sct_img.bgra, "raw", "BGRX")
board = MinesweeperBitboard(rows, cols)
cell_w = rw / cols
cell_h = rh / rows
for r in range(rows):
for c in range(cols):
x1 = int(c * cell_w)
y1 = int(r * cell_h)
x2 = int((c + 1) * cell_w)
y2 = int((r + 1) * cell_h)
cell_crop = img.crop((x1, y1, x2, y2))
state = CellAnalyzer.classify_cell(cell_crop)
if state == 'UNOPENED':
board.unopened_mask = board.set_bit(board.unopened_mask, r, c)
elif state == 'FLAG':
board.flags_mask = board.set_bit(board.flags_mask, r, c)
elif state == 'EMPTY':
board.opened_empty_mask = board.set_bit(board.opened_empty_mask, r, c)
elif state.isdigit():
num = int(state)
if 1 <= num <= 8:
board.numbers_masks[num] = board.set_bit(board.numbers_masks[num], r, c)
return board
def request_grid_size() -> Tuple[int, int]:
"""Console prompt or default selector for grid dimensions."""
print("\n--- Выберите размерность поля Сапёра ---")
print("1) Новичок (Beginner): 9 x 9")
print("2) Любитель (Intermediate): 16 x 16")
print("3) Эксперт (Expert): 16 x 30")
print("4) Произвольный размер (Custom N x M)")
try:
choice = input("Введите номер варианта [1-4] (по умолчанию 1): ").strip()
except (KeyboardInterrupt, EOFError):
choice = "1"
if choice == "2":
return 16, 16
elif choice == "3":
return 16, 30
elif choice == "4":
try:
r = int(input("Количество строк (N): ").strip())
c = int(input("Количество столбцов (M): ").strip())
return max(1, r), max(1, c)
except Exception:
print("[!] Ошибка ввода. Использовано 9x9.")
return 9, 9
else:
return 9, 9
# =====================================================================
# 5. ENTRY POINT & LIVE MONITORING LOOP (3 FPS)
# =====================================================================
if __name__ == '__main__':
print("==============================================================")
print(" Minesweeper Bitboard Analyzer (MSS + PIL + Tkinter)")
print(" Target: Windows 7 / 10 / 11 | RAM: ~30 MB | Python 3.8+")
print("==============================================================")
print("Горячие клавиши:")
print(" [F7] — Выделить / перевыделить зону Сапёра на экране")
print(" [ESC] — Завершить работу программы\n")
# Step 1: Input dimensions
rows, cols = request_grid_size()
print(f"\n[+] Выбран размер поля: {rows}x{cols}")
# Step 2: Initial Region Selection Overlay
print("\n[+] Запуск оверлея для выбора области экрана...")
print(" Зажмите ЛКМ и выделите прямоугольник игрового поля.")
selector = ScreenSelector()
region = selector.select_area()
if not region:
print("[!] Область не выделена. Ожидание нажатия F7 для выделения или ESC для выхода...")
last_fingerprint = None
update_count = 0
f7_was_pressed = False
print("\n[+] Запуск непрерывного сканирования (3 раза в секунду / ~333 ms)...")
print("[+] Нажмите F7 в любой момент для выбора новой зоны на экране.\n")
try:
while True:
# Check for ESC key press to exit
if is_key_pressed(VK_ESCAPE):
print("\n[!] Получен сигнал ESC. Завершение работы.")
break
# Check for F7 key press to re-select screen region
f7_now = is_key_pressed(VK_F7)
if f7_now and not f7_was_pressed:
print("\n[!] Нажата клавиша [F7]. Открытие оверлея выбора зоны...")
time.sleep(0.2) # Debounce key press
new_region = ScreenSelector().select_area()
if new_region:
region = new_region
last_fingerprint = None # Force UI refresh
print(f"[✓] Новая зона успешно задана: {region}")
else:
print("[!] Выбор новой зоны отменен. Продолжение работы со старой зоной.")
f7_was_pressed = f7_now
# If region is set, capture and analyze at 3 Hz
if region:
board = analyze_minesweeper_screen(rows, cols, region)
current_fingerprint = board.get_fingerprint()
# Update screen only when changes occur or first run
if current_fingerprint != last_fingerprint:
last_fingerprint = current_fingerprint
update_count += 1
# Clear CMD screen on NT / Windows
os.system('cls' if os.name == 'nt' else 'clear')
curr_time_str = time.strftime("%H:%M:%S")
print("==============================================================")
print(f" MINESWEEPER BITBOARD MONITOR [Обновление #{update_count} в {curr_time_str}]")
print(f" Зона экрана: {region} | Поле: {rows}x{cols}")
print(" [F7] - Выделить новую зону | [ESC] - Выход")
print("==============================================================")
# Print board state ASCII summary
board.print_board_summary()
# Print bitmasks
board.print_mask(board.unopened_mask, "UNOPENED_MASK (Закрытые ?)")
board.print_mask(boa