Aardio 实现圈住小猫游戏
一、算法分析
1. 网格表示:
- 游戏使用一个9x9的二维数组(grid)来表示六边形网格。每个格子有三种状态:0(空)、1(障碍)、2(猫)。
2. 六边形网格坐标:
- 使用轴向坐标(axial coordinates),但这里用偏移坐标(offset coordinates)来表示。奇偶行的邻居计算不同。
基于六边形几何特性:
宽度=√3×半径,高度=2×半径
3. 动态布局计算:
- 根据控件大小动态计算每个六边形的大小和位置,确保网格始终居中并适应控件。
4. 小猫移动算法:
- 使用BFS(广度优先搜索)寻找最短逃跑路径。如果找不到路径,则玩家获胜;如果小猫移动到边界,则小猫获胜。
5. 点击检测:
- 通过计算鼠标点击位置与每个六边形中心的距离,判断点击了哪个六边形。
二、游戏AI算法
-
小猫每次移动都重新计算最优逃跑路径
-
如果无路可逃,玩家获胜
-
如果到达边界,小猫获胜
-
游戏状态管理
使用二维数组grid[y][x]存储游戏状态
状态值:0=空,1=障碍,2=小猫
三、代码结构
-
全局变量:包括游戏配置(gridSize)、布局参数(layout)、游戏状态(grid, catX, catY, gameOver, playerWin)等。
-
布局计算函数:calculateLayout根据控件大小计算六边形半径和偏移。
-
坐标转换函数:getHexCenter将网格坐标转换为像素坐标。
-
邻居获取函数:getNeighbors根据奇偶行返回邻居坐标。
-
边界判断:isOnEdge判断格子是否在边界。
-
BFS寻路:findEscapePath使用BFS寻找最短逃跑路径。
-
游戏逻辑:初始化游戏(initGame)、小猫移动(moveCat)、点击处理(onMouseUp)等。
-
绘制函数:onDrawContent绘制整个游戏界面,包括六边形网格和小猫。
-
事件处理:按钮点击重新开始游戏,鼠标点击放置障碍。
四、广度优先搜索
-
宽度优先搜索算法(Breadth First Search,简称BFS),又称广度优先搜索,是计算机科学中应用于图结构遍历的基础算法,属于盲目搜寻法类别。该算法通过队列实现层序遍历,能够系统地发现图中所有节点并计算最短路径,其核心思想被应用于Dijkstra最短路径算法和Prim最小生成树算法。
-
BFS从源节点出发逐层扩展,使用颜色标记(白、灰、黑)区分未访问、待访问和已访问节点。算法通过维护先进先出队列确保优先处理当前层节点,时间复杂度为O(V+E),其中V为顶点数、E为边数。该特性使其在迷宫最短路径求解等场景中具有应用价值,
-
在实现层面,BFS通过邻接表存储图结构,借助开放-闭合表管理节点状态,其伪代码包含初始化、队列操作和节点访问三个主要阶段,确保每个节点仅被访问一次并记录遍历路径。
-
BFS,其英文全称是Breadth First Search。 BFS并不使用经验法则算法。从算法的观点,所有因为展开节点而得到的子节点都会被加进一个先进先出的队列中。一般的实验里,其邻居节点尚未被检验过的节点会被放置在一个被称为 open 的容器中(例如队列或是链表),而被检验过的节点则被放置在被称为 closed 的容器中。(open-closed表)
五、程序
import win.ui;
/*DSG{{*/
var winform = win.form(text="圈住小猫";right=550;bottom=522)
winform.add(
btnRestart={cls="button";text='\uD83D\uDD04 重新开始';left=205;top=474;right=355;bottom=504;db=1;dl=1;dr=1;z=2};
gameBox={cls="plus";left=0;top=0;right=551;bottom=456;db=1;dl=1;dr=1;dt=1;notify=1;z=1}
)
/*}}*/
// 游戏配置
var gridSize = 9;
// 动态布局参数(每次绘制/点击时根据控件大小重新计算)
var layout = {};
// 计算布局参数(根据控件实际像素大小)
var calculateLayout = function(rcWidth, rcHeight) {
// 网格总宽度 ≈ (gridSize + 0.5) * hexR * 1.732
// 网格总高度 ≈ (gridSize * 0.75 + 0.25) * hexR * 2
var maxRadiusByWidth = rcWidth / ((gridSize + 1) * 1.732);
var maxRadiusByHeight = rcHeight / ((gridSize * 0.75 + 1) * 2);
var hexR = math.min(maxRadiusByWidth, maxRadiusByHeight);
// 计算居中偏移
var totalWidth = (gridSize + 0.5) * hexR * 1.732;
var totalHeight = (gridSize * 0.75 + 0.25) * hexR * 2;
layout.hexRadius = hexR;
layout.offsetX = (rcWidth - totalWidth) / 2 + hexR * 1.732 / 2;
layout.offsetY = (rcHeight - totalHeight) / 2 + hexR;
}
// 游戏状态
var grid = {};
var catX, catY;
var gameOver, playerWin = false, false;
// 获取六边形中心坐标(使用动态布局参数)
var getHexCenter = function(gx, gy) {
var hexR = layout.hexRadius;
var w = hexR * 1.732;
var h = hexR * 2;
return layout.offsetX + (gx-1) * w + (gy % 2 == 0 ? w/2 : 0),
layout.offsetY + (gy-1) * h * 0.75;
}
// 获取六边形邻居(关键:奇偶行偏移不同)
var getNeighbors = function(gx, gy) {
var odd = (gy % 2 == 1);
return {
{x=gx-1, y=gy}, {x=gx+1, y=gy},
{x=odd?gx-1:gx, y=gy-1}, {x=odd?gx:gx+1, y=gy-1},
{x=odd?gx-1:gx, y=gy+1}, {x=odd?gx:gx+1, y=gy+1}
};
}
// 是否在边界(逃跑出口)
var isOnEdge = function(gx, gy) {
return gx <= 1 || gx >= gridSize || gy <= 1 || gy >= gridSize;
}
// 是否有效格子
var isValid = function(gx, gy) {
return gx >= 1 && gx <= gridSize && gy >= 1 && gy <= gridSize;
}
// BFS 寻找最短逃跑路径
var findEscapePath = function(sx, sy) {
var visited = {};
var queue = {{x=sx, y=sy}};
var prev = {};
visited[sy*100+sx] = true;
while(#queue > 0) {
var cur = table.shift(queue);
if(isOnEdge(cur.x, cur.y)) {
var path = {cur};
while(prev[cur.y*100+cur.x]) {
cur = prev[cur.y*100+cur.x];
// table.insert(path, cur, 1/*插入位置*/);
table.unshift(path,cur);
}
return path;
}
// (getNeighbors(cur.x, cur.y)) 要加括号以避免被识别为创建迭代器的函数调用语句
for(_, n in (getNeighbors(cur.x, cur.y))) {
var key = n.y*100+n.x;
if(isValid(n.x, n.y) && !visited[key] && grid[n.y][n.x] != 1) {
visited[key] = true;
prev[key] = cur;
table.push(queue, n);
}
}
}
return null;
}
// 猫移动
var moveCat = function() {
var path = findEscapePath(catX, catY);
if(!path) {
gameOver, playerWin = true, true;
return;
}
if(#path >= 2) {
grid[catY][catX] = 0;
catX, catY = path[2].x, path[2].y;
if(isOnEdge(catX, catY)) {
gameOver, playerWin = true, false;
} else {
grid[catY][catX] = 2;
}
}
}
// 初始化游戏
var initGame = function() {
grid = {};
for(y=1; gridSize) {
grid[y] = {};
for(x=1; gridSize) grid[y][x] = 0;
}
var center = math.floor(gridSize/2) + 1;
for(i=1; 8) {
var rx, ry = math.random(2, gridSize-1), math.random(2, gridSize-1);
if(!(rx == center && ry == center)) grid[ry][rx] = 1;
}
catX, catY = center, center;
grid[catY][catX] = 2;
gameOver, playerWin = false, false;
}
// 点击检测(使用动态布局参数)
var hitTest = function(mx, my) {
var hexR = layout.hexRadius;
for(gy=1; gridSize) {
for(gx=1; gridSize) {
var cx, cy = getHexCenter(gx, gy);
if( ((mx-cx)**2 + (my-cy)**2) < (hexR*0.85)**2 ) return gx, gy;
}
}
}
initGame();
// 绘制背景
winform.gameBox.onDrawBackground = function(graphics, rc) {
var brush = gdip.solidBrush(0xFFf5f6fa);
graphics.fillRectangle(brush, rc);
brush.delete();
}
import sys.midiOut;
var midiOut = sys.midiOut();// 用于合成 MIDI 音效,如果没有音频设备返回 null
// 绘制内容
winform.gameBox.onDrawContent = function(graphics, rc) {
graphics.smoothingMode = 4;
// 关键:根据控件实际像素大小计算布局
var rcWidth = rc.right - rc.left;
var rcHeight = rc.bottom - rc.top;
calculateLayout(rcWidth, rcHeight);
var hexR = layout.hexRadius;
// 绘制六边形网格
for(gy=1; gridSize) {
for(gx=1; gridSize) {
var cx, cy = getHexCenter(gx, gy);
// 六边形顶点
var pts = {};
for(i=0; 5) {
var angle = math.pi/6 + i * math.pi/3;
pts[i+1] = ::POINTF(cx + hexR*math.cos(angle), cy + hexR*math.sin(angle));
}
// 填充颜色
var state = grid[gy][gx];
var fillColor = state==1 ? 0xFF636e72 : (state==2 ? 0xFFffeaa7 : 0xFF00b894);
var brush = gdip.solidBrush(fillColor);
graphics.fillPolygon(brush, pts);
brush.delete();
var pen = gdip.pen(0xFFdfe6e9, 2);
graphics.drawPolygon(pen, pts);
pen.delete();
// 绘制小猫 🐱
if(state == 2) {
var s = hexR * 0.6;
// 脸
var faceBrush = gdip.solidBrush(0xFFfeca57);
graphics.fillEllipse(faceBrush, cx-s, cy-s*0.7, s*2, s*1.6);
faceBrush.delete();
// 耳朵
var earBrush = gdip.solidBrush(0xFFff9f43);
var ear1 = {::POINTF(cx-s*0.8,cy-s*0.3), ::POINTF(cx-s*0.5,cy-s*1.1), ::POINTF(cx-s*0.1,cy-s*0.4)};
var ear2 = {::POINTF(cx+s*0.8,cy-s*0.3), ::POINTF(cx+s*0.5,cy-s*1.1), ::POINTF(cx+s*0.1,cy-s*0.4)};
graphics.fillPolygon(earBrush, ear1);
graphics.fillPolygon(earBrush, ear2);
earBrush.delete();
// 眼睛
var eyeBrush = gdip.solidBrush(0xFF2d3436);
graphics.fillEllipse(eyeBrush, cx-s*0.45, cy-s*0.2, s*0.25, s*0.35);
graphics.fillEllipse(eyeBrush, cx+s*0.2, cy-s*0.2, s*0.25, s*0.35);
eyeBrush.delete();
// 鼻子
var noseBrush = gdip.solidBrush(0xFFe17055);
graphics.fillEllipse(noseBrush, cx-s*0.1, cy+s*0.15, s*0.2, s*0.15);
noseBrush.delete();
}
}
}
// 游戏结束提示
if(gameOver) {
var maskBrush = gdip.solidBrush(playerWin ? 0x9000b894 : 0x90e74c3c);
graphics.fillRectangle(maskBrush, ::RECT(0, rcHeight*0.4, rcWidth, rcHeight*0.6));
maskBrush.delete();
var fontSize = math.max(12, hexR * 0.8);
var font = gdip.font("Tahoma",fontSize);
var format = gdip.stringformat();
format.lineAlign = 1/*_StringAlignmentCenter*/;
format.align = 1/*_StringAlignmentCenter*/;
var msg = playerWin ? "✨ 太棒了!小猫被圈住了!" : " 小猫逃跑了... ⚠";
var msgBrush = gdip.solidBrush(0xFFFFFFFF);
graphics.drawString(msg, font, ::RECTF(0, rcHeight*0.4, rcWidth, rcHeight*0.2), format, msgBrush);
msgBrush.delete(); format.delete(); font.delete();
// 先显示,再播放声音
if(midiOut) winform.setTimeout(
function(){
if(playerWin){
// 成功圈住:八音盒,欢快上升音阶
midiOut.play("changeInstrument(10), 1_,3_,5_,1'__", "C4", 150);
}
else{
// 小猫逃跑:定音鼓,低沉下降音阶
midiOut.play("changeInstrument(47), '7_,'6_,'5__", "C3", 200);
}
}
);
}
}
// 鼠标点击
winform.gameBox.onMouseUp = function(wParam, lParam) {
if(gameOver) return;
// 获取点击位置(控件客户区像素坐标)
var x, y = win.getMessagePos();
x, y = win.toClient(owner.hwnd, x, y);
// 先更新布局参数(确保与绘制时一致)
var rc = owner.getClientRect();
calculateLayout(rc.right, rc.bottom);
var gx, gy = hitTest(x, y);
if(gx && gy && grid[gy][gx] == 0) {
grid[gy][gx] = 1;
moveCat();
owner.redraw();
//播入木鱼声,清脆短促
if(midiOut) midiOut.play("changeInstrument(115), 5_", "C5", 100);
}
}
winform.btnRestart.oncommand = function() {
initGame();
winform.gameBox.redraw();
// 开始游戏音效
if(midiOut) midiOut.play("changeInstrument(10), 1_,2_,3_,4_,5_", "C4", 80);
winform.gameBox.setFocus();
}
winform.show();
win.loopMessage();
六、界面
