# aardio 范例: 自绘小游戏 - 圈住小猫

```aardio
import win.ui;
/*DSG{{*/
var winform = win.form(text="自绘小游戏 - 圈住小猫";right=550;bottom=522;border="dialog frame";max=false)
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,backgroundColor,foregroundColor){
    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,txtColor,rcContent,foregroundColor,font){

    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) {
            
        	if(playerWin){
            	// 成功圈住：八音盒,欢快上升音阶
            	midiOut.play("changeInstrument(10), 1_,3_,5_,1'__", "C4", 150);
            	/*
            	midiOut.play(notes,baseKey,tempoMs,startDelay) 在界面线程调用延时器异步启动播放。
            	如果界面线程忙也可以改用 midiOut.playAsync(notes,baseKey,tempoMs) 多线程异步播放。
            	*/
        	}
        	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( !gameOver) {
            //播入木鱼声，清脆短促
        	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();
```