A26:扫雷游戏

#一个使用aardio编写的经典扫雷游戏实现

一、整体架构与设计模式

1、界面框架:

  • 基于aardio的win.ui窗口,使用plus控件作为游戏画布,通过onDrawContent自定义绘制,实现高效的双缓冲绘图。

2、事件驱动:

  • 监听鼠标左/右键按下事件,响应式更新状态并触发重绘。

3、资源管理:

  • 利用fonts.fontAwesome引入FontAwesome字体图标,美化按钮与标记。

4、状态分离:

将地图数据(map)与界面状态(state)分离,符合MVC思想,便于逻辑与渲染解耦。

二、数据结构

1、地图

  • map[r][c]
    // 数字矩阵:0~8 表示周围雷数,-1 表示地雷
  • state[r][c]
    // 状态矩阵:0-未翻开,1-已翻开,2-插旗
  • 隐式坐标映射:
    单元格索引从1开始,与屏幕像素坐标通过cellSize换算

三、核心算法

1、随机布雷算法

  • 等概率随机:
    使用math.random生成1~9的整数,直接放置地雷。
  • 冲突检测:
    若该位置已为雷则重试,直至布完所有雷。时间复杂度不固定,但9×9棋盘下10个雷通常很快。

2、数字计算算法

  • 双重嵌套遍历:

    对每个非雷格,遍历其3×3邻域,统计地雷数。

  • 边界检查:

    通过nr >=1 && nr <= rows等条件防止越界。

    ###3、递归翻开空白格(DFS)

  • 深度优先搜索:

    当翻开数字0(即周围无雷)时,递归翻开所有相邻格子,模拟扫雷的"自动展开"行为。

  • 终止条件:

    边界检查+状态过滤,避免无限递归。

  • 触雷处理:

    一旦点到地雷立即标记gameOver,并强制显示所有地雷位置。

    ###4、 胜利判定

  • 计数法:

    统计已翻开非雷格数量,与目标数量(总格-雷数)相等即胜利。

  • 时机:

    每次左键翻开后调用checkWin(),若胜利则显示奖杯并弹窗。

    ##四、事件处理与交互逻辑

    ###左键单击(onMouseDown)

  • 解析lParam获取客户区像素坐标,转换为行列索引。

  • 若state[r][c]==0则调用reveal(r,c)。

  • 检查胜利,重绘画板。

右键单击(onRightMouseDown)

  • 同样转换坐标。

  • 状态切换:未翻开 → 插旗;已插旗 → 取消旗。

  • 更新剩余雷数显示,重绘。

关键技术:

  • 坐标转换:

    math.ceil(x / cellSize) 直接映射到单元格。

  • 状态机简洁:

    三个状态通过0/1/2整数控制,无需复杂判断。

五、图形绘制技术

单元格绘制流程

  • 背景:根据状态(未翻开/插旗)填充灰色渐变效果,已翻开为浅灰色。

  • 边框:使用gdip.pen绘制1像素边框,模拟立体按钮效果。

  • 图标与数字:

    插旗:\uF024 (FontAwesome 旗子) 红色。

    地雷:\uF1E2 (FontAwesome 地雷) 黑色。

    数字:根据数字1~8设置不同颜色(经典扫雷配色),居中绘制。

六、潜在问题

  • 递归深度风险:

    9×9棋盘最大展开约81格,递归深度有限,无栈溢出风险。但若棋盘极大时需改为显式栈迭代。

  • 胜利弹窗重复:

    每次翻开后都调用checkWin(),若胜利时多个操作同时触发可能导致多次弹窗。可增加胜利标志位防止重复。

  • 第一次点击保护:

    未实现"第一次点击不会是雷"的经典优化,可增加首次点击时移动地雷的逻辑。

  • 剩余雷数显示:

    右键插旗时递减,但若插旗位置并非真正地雷,仍会减少计数,这与传统扫雷一致(仅计数标记数)。

七、程序

复制代码
//编写一个扫雷程序
import win.ui;
import fonts.fontAwesome;

var winform = win.form(text="aardio 扫雷";right=305;bottom=360;bgcolor=0xFFFFFF;border="dialog";max=false)
winform.add(
    plus={cls="plus";left=10;top=60;right=290;bottom=340;db=1;dl=1;dr=1;dt=1;notify=1;z=1};
    btnRestart={cls="plus";text='\uF01E';left=130;top=10;right=170;bottom=50;color=32768;font=LOGFONT(h=-24;name='FontAwesome');notify=1;z=2};
    static={cls="static";text="剩余雷数: 10";left=180;top=22;right=280;bottom=42;transparent=1;z=3}
)

var rows, cols, minesCount = 9, 9, 10;
var cellSize = 30;
var map, state, minesLeft = {}, {}, minesCount;
var gameOver = false;

// 初始化游戏
var initGame = function(){
    map = {};
    state = {};
    minesLeft = minesCount;
    gameOver = false;
    winform.static.text = "剩余雷数: " + minesLeft;
    winform.btnRestart.text = '\uF118'; // 微笑脸

    // 初始化地图和状态
    for(r=1;rows){
        map[r] = {};
        state[r] = {};
        for(c=1;cols){
            map[r][c] = 0;   // 0-8 数字, -1 雷
            state[r][c] = 0; // 0 未点击, 1 已翻开, 2 插旗
        }
    }

    // 随机布雷
    var m = 0;
    while(m < minesCount){
        var r = math.random(1, rows);
        var c = math.random(1, cols);
        if(map[r][c] != -1){
            map[r][c] = -1;
            m++;
        }
    }

    // 计算周围雷数
    for(r=1;rows){
        for(c=1;cols){
            if(map[r][c] == -1) continue;
            var count = 0;
            for(dr=-1;1){
                for(dc=-1;1){
                    var nr = r + dr;
                    var nc = c + dc;
                    if(nr >= 1 && nr <= rows && nc >= 1 && nc <= cols && map[nr][nc] == -1){
                        count++;
                    }
                }
            }  
            map[r][c] = count;
        }
    }
}

// 递归翻开空白格
var reveal;
reveal = function(r, c){
    if(r < 1 || r > rows || c < 1 || c > cols || state[r][c] != 0) return;
    state[r][c] = 1;
    if(map[r][c] == -1){
        gameOver = true;
        winform.btnRestart.text = '\uF119'; // 哭脸
        for(i=1;rows){
            for(j=1;cols){
                if(map[i][j] == -1) state[i][j] = 1;
            }
        }
        return;
    }
    if(map[r][c] == 0){
        for(dr=-1;1){
            for(dc=-1;1){
                reveal(r + dr, c + dc);
            }
        } 
    }
}

// 检查胜利
var checkWin = function(){
    var opened = 0;
    for(r=1;rows){
        for(c=1;cols){
            if(state[r][c] == 1) opened++;
        }
    }
    if(opened == rows * cols - minesCount){
        gameOver = true;
        winform.btnRestart.text = '\uF091'; // 奖杯
        winform.msgbox("恭喜,你赢了!");
    }
}

// 绘制界面
winform.plus.onDrawContent = function(graphics, rc){
    graphics.smoothingMode = 4;
    for(r=1;rows){
        for(c=1;cols){
            var x = (c-1) * cellSize;
            var y = (r-1) * cellSize;
            var rect = ::RECTF(x, y, cellSize, cellSize);
            var s = state[r][c];

            if(s == 0 || s == 2){
                var brush = gdip.solidBrush(0xFFCCCCCC);
                graphics.fillRectangle(brush, rect);
                brush.delete();

                var pen = gdip.pen(0xFFAAAAAA, 1);
                graphics.drawRectangle(pen, rect);
                pen.delete();

                if(s == 2){
                    var font = gdip.font("FontAwesome", 14);
                    var brushRed = gdip.solidBrush(0xFFFF0000);
                    graphics.drawString('\uF024', font, rect, gdip.stringformat(), brushRed);
                    brushRed.delete();
                    font.delete();
                }
            } else {
                var brush = gdip.solidBrush(0xFFEEEEEE);
                graphics.fillRectangle(brush, rect);
                brush.delete();

                var pen = gdip.pen(0xFFDDDDDD, 1);
                graphics.drawRectangle(pen, rect);
                pen.delete();

                var val = map[r][c];
                if(val == -1){
                    var font = gdip.font("FontAwesome", 14);
                    var brushBlack = gdip.solidBrush(0xFF000000);
                    graphics.drawString('\uF1E2', font, rect, gdip.stringformat(), brushBlack);
                    brushBlack.delete();
                    font.delete();
                } else if(val > 0){
                    var colors = {0xFF0000FF, 0xFF008000, 0xFFFF0000, 0xFF000080, 0xFF800000, 0xFF008080, 0xFF000000, 0xFF808080};
                    var font = gdip.font("Arial", 14, 1);
                    var brushNum = gdip.solidBrush(colors[val] || 0xFF000000);
                    var format = gdip.stringformat();
                    format.align = 1; 
                    format.lineAlign = 1;
                    graphics.drawString(tostring(val), font, rect, format, brushNum);
                    brushNum.delete();
                    font.delete();
                    format.delete();
                }
            }
        }
    }
}

// 鼠标事件
winform.plus.onMouseDown = function(wParam, lParam){
    if(gameOver) return;
    // 直接从 lParam 获取客户区坐标(低16位x,高16位y)
    var x = lParam & 0xFFFF;
    var y = lParam >> 16;
    var c = math.ceil(x / cellSize);
    var r = math.ceil(y / cellSize);

    if(state[r][c] == 0) reveal(r, c);
    checkWin();

    winform.plus.redraw();
}

winform.plus.onRightMouseDown = function(wParam, lParam){
    if(gameOver) return;
    // 直接从 lParam 获取客户区坐标(低16位x,高16位y)
    var x = lParam & 0xFFFF;
    var y = lParam >> 16;
    var c = math.ceil(x / cellSize);
    var r = math.ceil(y / cellSize);
	
  //   winform.static.text = "右键: " +  "x " + tostring(x)+":y,"+ tostring(y)+" c"+ tostring(c)+" r"+ tostring(r); 
        if(state[r][c] == 0){
            state[r][c] = 2;
            minesLeft--;
        } else if(state[r][c] == 2){
            state[r][c] = 0;
            minesLeft++;
        }
	winform.static.text = "剩余雷数: " + minesLeft; 
    winform.plus.redraw();
	}

// 重启按钮
winform.btnRestart.oncommand = function(id, event){
    initGame();
    winform.plus.redraw();
}

winform.btnRestart.skin({
    color = {
        hover = 0xFFFF0000;
        active = 0xFF00FF00;
    }
})

initGame();
winform.show();
win.loopMessage();

八、界面

相关推荐
山岚的运维笔记2 小时前
SQL Server笔记 -- 第50章 存储过程
数据库·笔记·sql·microsoft·oracle·sqlserver
寒秋花开曾相惜3 小时前
(学习笔记)2.1 信息存储(2.1.1 十六进制表示法)
笔记·学习
神明不懂浪漫3 小时前
【第十三章】操作符详解,预处理指令详解
c语言·开发语言·经验分享·笔记
此刻觐神4 小时前
Windows学习笔记-18(MFC项目-制作快捷方式管理工具)
windows·笔记·学习·mfc
henry1010104 小时前
HTML5小游戏 - 数字消除 · 合并2048
前端·游戏·html·html5
FakeOccupational4 小时前
【电路笔记 元器件】存储设备:RAM 静态随机存取存储器(SRAM)芯片+异步 SRAM 的特性+异步 SRAM读写测试(HDL)
笔记·fpga开发
Alice_whj5 小时前
AI云原生笔记
人工智能·笔记·云原生
Lyan-X5 小时前
鲁鹏教授《计算机视觉与深度学习》课程笔记与思考 ——13. 生成模型 VAE:从无监督学习到显式密度估计的建模与实现
人工智能·笔记·深度学习·计算机视觉
马猴烧酒.5 小时前
【面试八股|Mysql篇】Mysql常见面试题详解笔记
笔记·mysql·面试