#一个使用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();
八、界面
