
import win.ui;
import gdip;
/*DSG{{*/
var winform = win.form(text="泡泡龙游戏 (By Mr_MAO)";right=480;bottom=640;bgcolor=0xFFFFFF;border="dialog frame";max=false)
winform.add(
canvas={cls="plus";left=0;top=0;right=480;bottom=640;db=1;dl=1;dr=1;dt=1;notify=1;z=1}
)
/*}}*/
// --- 游戏常量 ---
var RADIUS = 20; // 泡泡半径
var DIAMETER = RADIUS * 2;
var ROW_HEIGHT = RADIUS * math.sqrt(3); // 六边形行高
var COLS = 11; // 列数 (偶数行11个, 奇数行10个)
var ROWS = 15; // 网格最大行数
var BOARD_WIDTH = COLS * DIAMETER;
// 颜色定义
var colors = {
0xFFFF0000; // 红
0xFF00FF00; // 绿
0xFF0000FF; // 蓝
0xFFFFFF00; // 黄
0xFFFF00FF; // 紫
0xFF00FFFF; // 青
};
// --- 游戏状态 ---
var grid = {}; // 网格二维数组 grid[row][col] = colorIndex
var bullet = null; // 当前飞行的泡泡 {x, y, dx, dy, color}
var nextColor = 0; // 下一个发射的颜色
var currColor = 0; // 当前发射的颜色
var angle = -90; // 发射角度
var isGameOver = false;
// --- 工具函数:获取网格坐标对应的屏幕像素位置 ---
var getPixelPos = function(r, c){
var x = c * DIAMETER + RADIUS;
// 奇数行向右偏移半个身位
if(r % 2 != 0){
x = x + RADIUS;
}
var y = r * ROW_HEIGHT + RADIUS;
return x, y;
}
// --- 工具函数:计算两个圆心的距离平方 ---
var getDistSq = function(x1, y1, x2, y2){
return (x1-x2)*(x1-x2) + (y1-y2)*(y1-y2);
}
// --- 初始化游戏 ---
function initGame(){
grid = {};
isGameOver = false;
// 初始化网格,生成几行初始泡泡
for(r=0; 5){
grid[r] = {};
// 偶数行COLS个,奇数行COLS-1个
var count = (r % 2 == 0) ? COLS : COLS - 1;
for(c=0; count-1){
grid[r][c] = math.random(1, #colors);
}
}
currColor = math.random(1, #colors);
nextColor = math.random(1, #colors);
bullet = null;
winform.canvas.redraw();
}
// --- 查找相连的同色泡泡 (递归/DFS) ---
findCluster = function(r, c, matchColor, foundList, visited){
var key = r + "," + c;
if(visited[key]) return;
// 边界检查
if(!grid[r] || !grid[r][c]) return;
if(grid[r][c] != matchColor) return;
visited[key] = true;
table.push(foundList, {r=r; c=c});
// 六个方向的邻居偏移量 (偶数行和奇数行不同)
var offsets = (r % 2 == 0)
? {{r-1,c-1}, {r-1,c}, {r,c-1}, {r,c+1}, {r+1,c-1}, {r+1,c}} // 偶数行邻居
: {{r-1,c}, {r-1,c+1}, {r,c-1}, {r,c+1}, {r+1,c}, {r+1,c+1}}; // 奇数行邻居
for(i=1; #offsets){
var nr, nc = offsets[i][1], offsets[i][2];
findCluster(nr, nc, matchColor, foundList, visited);
}
}
// --- 查找根节点 (用于悬空检测) ---
var findFloating = function(){
var visited = {};
var queue = {};
// 1. 将第一行的所有泡泡加入队列
if(grid[0]){
for(c, color in grid[0]){
table.push(queue, {r=0; c=c});
visited["0," + c] = true;
}
}
// 2. BFS 遍历所有连接到顶部的泡泡
var head = 1;
while(head <= #queue){
var curr = queue[head];
var r, c = curr.r, curr.c;
head++;
var offsets = (r % 2 == 0)
? {{r-1,c-1}, {r-1,c}, {r,c-1}, {r,c+1}, {r+1,c-1}, {r+1,c}}
: {{r-1,c}, {r-1,c+1}, {r,c-1}, {r,c+1}, {r+1,c}, {r+1,c+1}};
for(i=1; #offsets){
var nr, nc = offsets[i][1], offsets[i][2];
if(grid[nr] && grid[nr][nc]){
var key = nr + "," + nc;
if(!visited[key]){
visited[key] = true;
table.push(queue, {r=nr; c=nc});
}
}
}
}
// 3. 移除未被访问到的泡泡 (即悬空的)
for(r, row in grid){
for(c, color in row){
if(!visited[r + "," + c]){
grid[r][c] = null; // 移除
}
}
}
}
// --- 处理碰撞后的逻辑 ---
function snapBubble(){
// 简单的碰撞处理:找到距离子弹最近的一个空网格位置
var minDist = 999999;
var bestR, bestC = -1, -1;
// 遍历所有可能的网格位置(稍微有些低效但逻辑简单)
for(r=0; ROWS){
var cols = (r % 2 == 0) ? COLS : COLS - 1;
for(c=0; cols-1){
if( (!grid[r] || !grid[r][c]) ){ // 必须是空格
var px, py = getPixelPos(r, c);
var dist = getDistSq(bullet.x, bullet.y, px, py);
if(dist < minDist){
minDist = dist;
bestR, bestC = r, c;
}
}
}
}
// 只有当距离足够近时才吸附 (防止吸附到太远的地方)
if(bestR != -1 && minDist < DIAMETER*DIAMETER){
if(!grid[bestR]) grid[bestR] = {};
grid[bestR][bestC] = bullet.color;
// 1. 检查消除
var matches = {};
findCluster(bestR, bestC, bullet.color, matches, {});
if(#matches >= 3){
// 消除
for(i=1; #matches){
var m = matches[i];
grid[m.r][m.c] = null;
}
// 2. 检查悬空并掉落
findFloating();
}
// 检查游戏结束 (泡泡到底部)
if(bestR >= ROWS - 2) isGameOver = true;
}
bullet = null; // 销毁子弹
currColor = nextColor;
nextColor = math.random(1, #colors);
winform.canvas.redraw();
}
// --- 游戏主循环 (更新逻辑) ---
function update(){
if(isGameOver || !bullet) return;
// 移动子弹
bullet.x = bullet.x + bullet.dx;
bullet.y = bullet.y + bullet.dy;
// 墙壁反弹
if(bullet.x <= RADIUS || bullet.x >= winform.canvas.width - RADIUS){
bullet.dx = -bullet.dx;
bullet.x = bullet.x + bullet.dx; // 推出墙壁
}
// 顶部碰撞
if(bullet.y <= RADIUS){
snapBubble();
return;
}
// 泡泡碰撞检测
var hit = false;
for(r, row in grid){
for(c, col in row){
if(col){
var px, py = getPixelPos(r, c);
// 如果距离小于直径,说明碰撞
if(getDistSq(bullet.x, bullet.y, px, py) < DIAMETER * DIAMETER * 0.8){
// *0.8 是为了让碰撞判定稍微宽松/紧凑一点
hit = true;
break;
}
}
}
if(hit) break;
}
if(hit){
snapBubble();
}
winform.canvas.redraw();
}
// --- 绘图 ---
winform.canvas.onDrawForegroundEnd = function(graphics, rc){
graphics.clear(0xFF333333);
graphics.smoothingMode = 4;
// 绘制网格中的泡泡
for(r=0; ROWS){
if(grid[r]){
for(c, colorIdx in grid[r]){
if(colorIdx){
var x, y = getPixelPos(r, c);
var brush = gdip.solidBrush(colors[colorIdx]);
graphics.fillEllipse(brush, x-RADIUS, y-RADIUS, DIAMETER, DIAMETER);
brush.delete();
// 高光
var brushW = gdip.solidBrush(0x50FFFFFF);
graphics.fillEllipse(brushW, x-RADIUS+5, y-RADIUS+5, 10, 10);
brushW.delete();
}
}
}
}
// 绘制发射台 (底部中间)
var shooterX = rc.width / 2;
var shooterY = rc.height - 50;
// 绘制箭头
var penArrow = gdip.pen(0xFFFFFFFF, 3);
var endX = shooterX + math.cos(angle * math.pi / 180) * 60;
var endY = shooterY + math.sin(angle * math.pi / 180) * 60;
graphics.drawLine(penArrow, shooterX, shooterY, endX, endY);
penArrow.delete();
// 绘制当前待发射泡泡
if(!bullet){
var brushCurr = gdip.solidBrush(colors[currColor]);
graphics.fillEllipse(brushCurr, shooterX-RADIUS, shooterY-RADIUS, DIAMETER, DIAMETER);
brushCurr.delete();
} else {
// 绘制飞行中的泡泡
var brushFly = gdip.solidBrush(colors[bullet.color]);
graphics.fillEllipse(brushFly, bullet.x-RADIUS, bullet.y-RADIUS, DIAMETER, DIAMETER);
brushFly.delete();
}
// 绘制下一个预览泡泡
var brushNext = gdip.solidBrush(colors[nextColor]);
graphics.fillEllipse(brushNext, shooterX + 80, shooterY, RADIUS, RADIUS); // 小一点
brushNext.delete();
var fontFamily = gdip.family("Arial");
var font = fontFamily.createFont(10, 1);
var strformat = gdip.stringformat();
graphics.drawString("NEXT", font, ::RECTF(shooterX + 80, shooterY + 20,1000,1000), strformat, gdip.solidBrush(0xFFFFFFFF));
if(isGameOver){
var fontBig = fontFamily.createFont(30, 1);
graphics.drawString("GAME OVER", fontBig, ::RECTF(100, 300,1000,1000), strformat, gdip.solidBrush(0xFFFF0000));
graphics.drawString("点击重启", font, ::RECTF(180, 350,1000,1000), strformat, gdip.solidBrush(0xFFFFFFFF));
fontBig.delete();
}
font.delete();
}
// --- 鼠标移动 (瞄准) ---
winform.canvas.onMouseMove = function(wParam, lParam){
if(isGameOver) return;
var x, y = win.getMessagePos(lParam);
var shooterX = winform.canvas.width / 2;
var shooterY = winform.canvas.height - 50;
// 计算角度 (atan2)
var dx = x - shooterX;
var dy = y - shooterY;
// 限制角度,防止射向地板
if(dy >= 0) dy = -1;
angle = math.atan2(dy, dx) * 180 / math.pi;
winform.canvas.redraw();
}
// --- 鼠标点击 (发射) ---
winform.canvas.onMouseDown = function(wParam, lParam){
if(isGameOver){
initGame();
return;
}
if(bullet) return; // 已有子弹在飞,不能发射
var shooterX = winform.canvas.width / 2;
var shooterY = winform.canvas.height - 50;
var speed = 15;
bullet = {
x = shooterX;
y = shooterY;
dx = math.cos(angle * math.pi / 180) * speed;
dy = math.sin(angle * math.pi / 180) * speed;
color = currColor;
};
}
// --- 定时器 (60FPS) ---
var tmr = winform.setInterval(
16,
function(){
update();
}
)
// 调整窗口大小以适应网格宽度
var width = COLS * DIAMETER + RADIUS; // 加上左右边距
winform.width = width;
winform.canvas.right = width;
initGame();
winform.enableDpiScaling(false);
winform.show();
win.loopMessage();
最新回复 (3)
-
-
-
今天抽空优化了一下,让这个小游戏也具有可玩性!

import win.ui; import gdip; /*DSG{{*/ var winform = win.form(text="泡泡龙小游戏v2 (By Mr_MAO)";right=480;bottom=640;bgcolor=0x333333;border="dialog frame";max=false) winform.add( canvas={cls="plus";left=0;top=0;right=480;bottom=640;db=1;dl=1;dr=1;dt=1;notify=1;z=1} ) /*}}*/ // --- 游戏配置 --- var RADIUS = 20; var DIAMETER = RADIUS * 2; var ROW_HEIGHT = RADIUS * math.sqrt(3); var COLS = 12; var ROWS = 18; var ADD_ROW_INTERVAL = 10000; var MAX_BUBBLE_ROWS = 15; // 第15行为失败判定线 var INITIAL_ROWS = 3; var GAME_DURATION = 180000; var FADE_SPEED = 12; var colors = { 0xFFFF4444; 0xFF44FF44; 0xFF4444FF; 0xFFFFFF44; 0xFFFF44FF; 0xFF44FFFF }; // --- 预创建绘图资源 --- var gFamily = gdip.family("Arial"); var gFontMain = gFamily.createFont(10, 1); var gFontBig = gFamily.createFont(35, 1); var gStrFormat = gdip.stringformat(); gStrFormat.align = 1/*_GdipStringAlignmentCenter*/; // 居中对齐 // --- 游戏状态 --- var grid = {}; var gridOffsetRows = 0; var bullet = null; var nextColor, currColor = 0, 0; var angle = -90; var isGameOver, isGameWin = false, false; var addBubbleTimer, countdownTimer = null, null; var lastAddTime, timeRemaining = 0, 0; var countdownText = ""; var fallingBubbles = {}; // --- 坐标换算 --- var getPixelPos = function(r, c){ var isShift = (r + gridOffsetRows) % 2 != 0; var x = c * DIAMETER + RADIUS + (isShift ? RADIUS : 0); var y = r * ROW_HEIGHT + RADIUS; return x, y; } var getDistSq = function(x1, y1, x2, y2){ return (x1-x2)*(x1-x2) + (y1-y2)*(y1-y2); } // 泡泡绘制函数 var drawBubble = function(graphics, clrIdx, x, y, alpha = 255){ if(!clrIdx) return; var argb = (alpha << 24) | (colors[clrIdx] & 0xFFFFFF); var brush = gdip.solidBrush(argb); graphics.fillEllipse(brush, x - RADIUS, y - RADIUS, DIAMETER, DIAMETER); brush.delete(); var hBrush = gdip.solidBrush((math.floor(alpha * 0.4) << 24) | 0xFFFFFF); graphics.fillEllipse(hBrush, x - RADIUS + 6, y - RADIUS + 6, RADIUS * 0.6, RADIUS * 0.6); hBrush.delete(); } var getBubbleRowCount = function(){ var maxR = -1; for(r=0; ROWS-1){ if(!grid[r]) continue; for(c=0; COLS) if(grid[r][c]) { maxR = r; break; } } return maxR + 1; } // --- 游戏逻辑:增加行 --- var addNewBubbleRow = function(){ for(r=ROWS-1; 1; -1) grid[r] = grid[r-1]; gridOffsetRows++; grid[0] = {}; var isShift = (0 + gridOffsetRows) % 2 != 0; var count = isShift ? COLS - 1 : COLS; for(c=0; count-1) grid[0][c] = math.random(1, #colors); if(getBubbleRowCount() >= MAX_BUBBLE_ROWS) isGameOver = true; winform.canvas.redraw(); } var getNeighbors = function(r, c){ var isShift = (r + gridOffsetRows) % 2 != 0; return isShift ? {{r-1,c};{r-1,c+1};{r,c-1};{r,c+1};{r+1,c};{r+1,c+1}} : {{r-1,c-1};{r-1,c};{r,c-1};{r,c+1};{r+1,c-1};{r+1,c}}; } var findFloating = function(){ var visited = {}; var queue = {}; if(grid[0]){ for(c=0; COLS) if(grid[0][c]) { table.push(queue, {r=0; c=c}); visited["0,"+c]=true; } } var head = 1; while(head <= #queue){ var curr = queue[head]; head++; var os = getNeighbors(curr.r, curr.c); for(i=1; #os){ var nr, nc = os[i][1], os[i][2]; if(grid[nr] && grid[nr][nc] && !visited[nr+","+nc]){ visited[nr+","+nc] = true; table.push(queue, {r=nr; c=nc}); } } } var fl = {}; for(r=0; ROWS-1){ if(!grid[r]) continue; for(c=0; COLS) if(grid[r][c] && !visited[r+","+c]) table.push(fl, {r=r; c=c}); } return fl; } function initGame(){ if(addBubbleTimer) winform.clearInterval(addBubbleTimer); if(countdownTimer) winform.clearInterval(countdownTimer); grid = {}; for(r=0; ROWS) grid[r] = {}; gridOffsetRows = 0; isGameOver = false; isGameWin = false; fallingBubbles = {}; for(r=0; INITIAL_ROWS-1){ var isShift = (r + gridOffsetRows) % 2 != 0; var count = isShift ? COLS - 1 : COLS; for(c=0; count-1) grid[r][c] = math.random(1, #colors); } currColor = math.random(1, #colors); nextColor = math.random(1, #colors); lastAddTime = time.tick(); timeRemaining = GAME_DURATION; addBubbleTimer = winform.setInterval(ADD_ROW_INTERVAL, function(){ if(!isGameOver) { addNewBubbleRow(); lastAddTime = time.tick(); } }); countdownTimer = winform.setInterval(50, function(){ if(isGameOver) return; timeRemaining -= 50; var remainNext = math.max(0, ADD_ROW_INTERVAL - (time.tick() - lastAddTime)); countdownText = string.format("剩余时间: %d:%02d | 距离塌陷: %d.%d秒", timeRemaining/60000, (timeRemaining%60000)/1000, remainNext/1000, (remainNext%100)/10); if(#fallingBubbles > 0){ var still = {}; for(i=1; #fallingBubbles){ var f = fallingBubbles[i]; f.vy += 0.3; f.y += f.vy; f.alpha -= FADE_SPEED; if(f.alpha > 0) table.push(still, f); } fallingBubbles = still; } if(timeRemaining <= 0) { isGameOver = true; isGameWin = (getBubbleRowCount() == 0); } winform.canvas.redraw(); }); } function snapBullet(){ var minDist = 999999; var bestR, bestC = -1, -1; for(r=0; ROWS-1){ var isShift = (r + gridOffsetRows) % 2 != 0; var cols = isShift ? COLS - 1 : COLS; for(c=0; cols-1){ if(!grid[r][c]){ var px, py = getPixelPos(r, c); var d = getDistSq(bullet.x, bullet.y, px, py); if(d < minDist){ minDist = d; bestR, bestC = r, c; } } } } if(bestR != -1 && minDist < DIAMETER**2){ grid[bestR][bestC] = bullet.color; var matches = {}; var flood; flood = function(r, c, clr, list, vis){ var k = r+","+c; if(vis[k] || !grid[r] || grid[r][c] != clr) return; vis[k] = true; table.push(list, {r=r; c=c}); var os = getNeighbors(r, c); for(i=1;#os) flood(os[i][1], os[i][2], clr, list, vis); } flood(bestR, bestC, bullet.color, matches, {}); if(#matches >= 3){ for(i=1; #matches) grid[matches[i].r][matches[i].c] = null; var fl = findFloating(); for(i=1; #fl){ var f = fl[i]; var px, py = getPixelPos(f.r, f.c); table.push(fallingBubbles, {x=px; y=py; color=grid[f.r][f.c]; alpha=255; vy=2}); grid[f.r][f.c] = null; } } if(getBubbleRowCount() >= MAX_BUBBLE_ROWS) isGameOver = true; if(getBubbleRowCount() == 0) { isGameOver = true; isGameWin = true; } } bullet = null; currColor = nextColor; nextColor = math.random(1, #colors); } // --- 绘图 --- winform.canvas.onDrawForegroundEnd = function(graphics, rc){ graphics.clear(0xFF333333); graphics.smoothingMode = 4; // 绘制网格 for(r=0; ROWS-1){ if(!grid[r]) continue; for(c=0; COLS){ if(grid[r][c]) { var x, y = getPixelPos(r, c); drawBubble(graphics, grid[r][c], x, y); } } } // 绘制警戒线 var dangerY = (MAX_BUBBLE_ROWS - 1) * ROW_HEIGHT + DIAMETER; var penDanger = gdip.pen(0x99FF0000, 1.5); penDanger.dashStyle = 2; graphics.drawLine(penDanger, 0, dangerY, rc.width, dangerY); penDanger.delete(); // 绘制下落泡泡 for(i=1; #fallingBubbles) { var f = fallingBubbles[i]; drawBubble(graphics, f.color, f.x, f.y, f.alpha); } // 绘制发射器 var sx, sy = rc.width/2, rc.height - 50; var pen = gdip.pen(0x88FFFFFF, 2); graphics.drawLine(pen, sx, sy, sx + math.cos(angle*math.pi/180)*60, sy + math.sin(angle*math.pi/180)*60); pen.delete(); if(!bullet) drawBubble(graphics, currColor, sx, sy); else drawBubble(graphics, bullet.color, bullet.x, bullet.y); var nextX, nextY = rc.width - 60, rc.height - 50; drawBubble(graphics, nextColor, nextX, nextY, 200); // 绘制 NEXT var brushTxt = gdip.solidBrush(0xFFAAAAAA); gStrFormat.align = 1; // 居中 graphics.drawString("NEXT", gFontMain, ::RECTF(nextX - 35, nextY + 25, 70, 20), gStrFormat, brushTxt); graphics.drawString(countdownText, gFontMain, ::RECTF(0, 15, rc.width, 30), gStrFormat, brushTxt); brushTxt.delete(); // 游戏结束层 if(isGameOver){ // 半透明黑色背景层 var maskBrush = gdip.solidBrush(0xAA000000); graphics.fillRectangle(maskBrush, 0, rc.height/2 - 100, rc.width, 200); maskBrush.delete(); var title = isGameWin ? "YOU WIN!" : "GAME OVER"; var subTitle = ""; var color = isGameWin ? 0xFF00FF00 : 0xFFFF4444; if(isGameWin){subTitle = "完美消除!你守护了星空。";} else if(getBubbleRowCount() >= MAX_BUBBLE_ROWS){subTitle = "泡泡触底!防线已崩溃。";} else{subTitle = "时间到!恭喜你,坚持到最后:)";} var bigBrush = gdip.solidBrush(color); var subBrush = gdip.solidBrush(0xFFFFFFFF); graphics.drawString(title, gFontBig, ::RECTF(0, rc.height/2 - 70, rc.width, 60), gStrFormat, bigBrush); graphics.drawString(subTitle, gFontMain, ::RECTF(0, rc.height/2 + 10, rc.width, 30), gStrFormat, subBrush); graphics.drawString("点击屏幕重新开始", gFontMain, ::RECTF(0, rc.height/2 + 45, rc.width, 30), gStrFormat, subBrush); bigBrush.delete(); subBrush.delete(); } } // --- 事件 --- winform.canvas.onMouseMove = function(wParam, lParam){ var x, y = win.getMessagePos(lParam); angle = math.atan2(y - (winform.canvas.height-50), x - winform.canvas.width/2) * 180 / math.pi; if(angle > 0) angle = angle > 90 ? -180 : 0; } winform.canvas.onMouseDown = function(){ if(isGameOver) return initGame(); if(bullet) return; bullet = { x=winform.canvas.width/2; y=winform.canvas.height-50; color=currColor; dx=math.cos(angle*math.pi/180)*16; dy=math.sin(angle*math.pi/180)*16 }; } winform.setInterval(16, function(){ if(isGameOver || !bullet) return; bullet.x += bullet.dx; bullet.y += bullet.dy; if(bullet.x < RADIUS || bullet.x > winform.canvas.width-RADIUS) bullet.dx *= -1; if(bullet.y < RADIUS) return snapBullet(); var hit = false; for(r=0; ROWS-1){ if(!grid[r]) continue; for(c=0; COLS){ if(grid[r][c]){ var px, py = getPixelPos(r, c); if(getDistSq(bullet.x, bullet.y, px, py) < DIAMETER**2 * 0.85){ hit=true; break; } } } if(hit) break; } if(hit) snapBullet(); winform.canvas.redraw(); }); initGame(); winform.enableDpiScaling(false); winform.show(); win.loopMessage();