ncurses+C++游戏星号偶现生成于障碍物内的问题排查与方案咨询
我用ncurses库和C++开发了一款简易收集类游戏:玩家操控黄色方块在棋盘内移动,尽可能收集“*”,需避开障碍物与边界。初始生成100个星号,每收集一个便随机生成新的星号。
目前遇到的问题:生成星号前会手动检查障碍物坐标,多数情况有效,但偶尔仍会出现星号落在障碍物内的情况。想排查是否存在逻辑漏洞、操作错误,并寻求替代实现方案。
障碍物定义代码
mainwin = initscr(); obscatle=subwin(mainwin,7,2,1,6); obscatle2=subwin(mainwin,10,2,8,20); obscatle3=subwin(mainwin,2,2,17,50); obscatle4=subwin(mainwin,6,2,25,70); obscatle5=subwin(mainwin,12,2,10,90); obscatle6=subwin(mainwin,20,2,8,110); obscatle7=subwin(mainwin,22,2,4,130); obscatle8=subwin(mainwin,17,2,18,150);
星号生成相关函数
// 游戏初始化时生成随机位置的星号 vector<pair<int,int>> generate_asterisk(int numberOfLines, int numberOfColumns) { vector<pair<int,int>> coordinates; srand(time(0)); while(coordinates.size() < 500) { int x = (rand() % (COLS - 3)) + 1; int y = (rand() % (LINES - 5)) +1; // 检查坐标是否在障碍物区域,是则重新生成 while((y >= 1 && y <= 7 && (x >= 5 && x <= 9)) || (y >= 8 && y <= 17 && (x >= 19 && x <= 23)) || (y >= 17 && y <= 18 && (x >= 49 && x <= 53)) || (y >= 25 && y <= 30 && (x >= 69 && x <= 73)) || (y >= 10 && y <= 21 && (x >= 89 && x <= 93)) || (y >= 8 && y <= 27 && (x >= 109 && x <= 114)) || (y >= 4 && y <= 25 && (x >= 129 && x <= 134)) || (y >= 18 && y <= 34 && (x >= 149 && x <= 154))) { x = (rand() % (COLS - 3)) + 1; y = (rand() % (LINES - 5)) + 1; } pair<int,int> coord = make_pair(x,y); if(find(coordinates.begin(), coordinates.end(), coord) == coordinates.end()) { coordinates.push_back(coord); } } return coordinates; } // 收集星号后生成新的星号 void spawnAsterisk(WINDOW* win, int x_max, int y_max, std::vector<std::pair<int, int>>& asterisk_coords) { // 生成新坐标 int new_x = (rand() % (COLS - 3)) + 1; int new_y = (rand() % (LINES - 5)) +1; // 检查是否在障碍物区域,是则重新生成 while((new_y >= 1 && new_y <= 7 && (new_x >= 5 && new_x <= 9)) || (new_y >= 8 && new_y <= 17 && (new_x >= 19 && new_x <= 23)) || (new_y >= 17 && new_y <= 18 && (new_x >= 49 && new_x <= 53)) || (new_y >= 25 && new_y <= 30 && (new_x >= 69 && new_x <= 73)) || (new_y >= 10 && new_y <= 21 && (new_x >= 89 && new_x <= 93)) || (new_y >= 8 && new_y <= 27 && (new_x >= 109 && new_x <= 114)) || (new_y >= 4 && new_y <= 25 && (new_x >= 129 && new_x <= 134)) || (new_y >= 18 && new_y <= 34 && (new_x >= 149 && new_x <= 154))) { new_x = (rand() % (COLS - 3)) + 1; new_y = (rand() % (LINES - 5)) + 1; } // 检查是否和已有星号坐标冲突 bool conflict = false; for (const auto& coord : asterisk_coords) { if (coord.first == new_x && coord.second == new_y) { conflict = true; break; } } // 冲突则重新生成,直到无冲突 while (conflict) { new_x = rand() % x_max; new_y = rand() % y_max; conflict = false; for (const auto& coord : asterisk_coords) { if (coord.first == new_x && coord.second == new_y) { conflict = true; break; } } } asterisk_coords.push_back(std::make_pair(new_x, new_y)); mvwprintw(win, new_y, new_x, "*"); wrefresh(win); }
逻辑漏洞与操作错误
spawnAsterisk函数的二次生成未检查障碍物
这是星号偶尔出现在障碍物里的核心原因:第一次生成坐标时做了障碍物检查,但如果发现和已有星号冲突进入while(conflict)循环后,新生成的new_x和new_y只检查了星号冲突,完全没做障碍物过滤——如果此时生成的坐标刚好落在障碍物区域,就会直接被添加进去。手动坐标范围与实际障碍物不匹配
以第一个障碍物为例:subwin(mainwin,7,2,1,6)的参数是窗口, 行数, 列数, 起始y, 起始x,实际覆盖范围是y=1到7,x=6到7。但你手动检查的范围是y>=1 && y<=7 && x>=5 && x<=9,x的范围比实际障碍物宽了不少。虽然这不会导致星号进障碍物,但属于冗余检查,若其他障碍物的手动范围计算错误(比如起始/结束值偏差),就会出现漏检。rand()初始化重复调用
generate_asterisk里调用了srand(time(0)),如果游戏中多次调用这个函数(比如重启游戏),可能因为时间戳相同导致随机序列重复。建议把srand(time(0))放在程序启动入口处,只调用一次。
替代实现方案
方案1:修复现有代码漏洞
把障碍物检查逻辑封装成独立函数,在所有生成坐标的环节都调用,包括冲突后的二次生成:
// 封装障碍物检查函数,修正坐标范围为实际障碍物覆盖区域 bool isInObstacle(int x, int y) { return (y >= 1 && y <= 7 && x >= 6 && x <= 7) || (y >= 8 && y <= 17 && x >= 20 && x <= 21) || (y >= 17 && y <= 18 && x >= 50 && x <= 51) || (y >= 25 && y <= 30 && x >= 70 && x <= 71) || (y >= 10 && y <= 21 && x >= 90 && x <= 91) || (y >= 8 && y <= 27 && x >= 110 && x <= 111) || (y >= 4 && y <= 25 && x >= 130 && x <= 131) || (y >= 18 && y <= 34 && x >= 150 && x <= 151); } // 修改spawnAsterisk的冲突循环部分 while (conflict || isInObstacle(new_x, new_y)) { new_x = (rand() % (COLS - 3)) + 1; new_y = (rand() % (LINES - 5)) + 1; // 先确保不在障碍物内 while (isInObstacle(new_x, new_y)) { new_x = (rand() % (COLS - 3)) + 1; new_y = (rand() % (LINES - 5)) + 1; } // 再检查星号冲突 conflict = false; for (const auto& coord : asterisk_coords) { if (coord.first == new_x && coord.second == new_y) { conflict = true; break; } } }
方案2:预先生成所有合法坐标,随机选取
- 程序启动时,遍历整个游戏区域,过滤掉障碍物和边界,把合法坐标存入
vector<pair<int,int>> valid_coords。 - 生成星号时直接从
valid_coords中随机选取,选完后移除该坐标避免重复;合法坐标用完时重新初始化。
这种方式完全避免手动检查的错误,逻辑更清晰:
vector<pair<int,int>> valid_coords; // 初始化合法坐标 void initValidCoords() { valid_coords.clear(); for (int y = 1; y <= LINES -5; y++) { for (int x = 1; x <= COLS -3; x++) { if (!isInObstacle(x, y)) { // 使用上面封装的检查函数 valid_coords.emplace_back(x, y); } } } // 打乱顺序保证随机性 random_shuffle(valid_coords.begin(), valid_coords.end()); } // 修改初始化星号生成函数 vector<pair<int,int>> generate_asterisk(int count) { vector<pair<int,int>> coords; if (valid_coords.size() < count) { initValidCoords(); // 合法坐标不足时重新生成 } for (int i = 0; i < count; i++) { coords.push_back(valid_coords.back()); valid_coords.pop_back(); } return coords; } // 修改收集后生成星号的函数 void spawnAsterisk(WINDOW* win, std::vector<std::pair<int, int>>& asterisk_coords) { if (valid_coords.empty()) { initValidCoords(); } auto new_coord = valid_coords.back(); valid_coords.pop_back(); // 极端情况冲突时重新初始化 bool conflict = false; for (const auto& coord : asterisk_coords) { if (coord == new_coord) { conflict = true; break; } } if (conflict) { initValidCoords(); new_coord = valid_coords.back(); valid_coords.pop_back(); } asterisk_coords.push_back(new_coord); mvwprintw(win, new_coord.second, new_coord.first, "*"); wrefresh(win); }
方案3:动态检查障碍物字符
如果障碍物是通过特定字符绘制的(比如'#'),可以用ncurses的mvwinch直接获取坐标处的字符,判断是否为障碍物,无需手动维护坐标范围:
bool isObstacleChar(int x, int y) { chtype c = mvwinch(mainwin, y, x); return (c == '#' || c == ACS_CKBOARD); // 根据实际障碍物字符修改 }
之后生成坐标时调用这个函数替代手动范围检查,障碍物位置或大小修改时无需同步更新检查逻辑,大幅降低出错概率。
内容的提问来源于stack exchange,提问作者Luka Bitsadze

