【九转宫格】相关
背景
九转宫格是我的同学因为考试复习太无聊发明出来的,一局十分钟左右的,尚未被找到规律的,每一局大同小异手感却大相径庭的自认为很有趣味的小游戏。于是我将其改编为电脑程序,并打算添加bot、online等功能。
概括
本文章包括四部分,分别为资源查找,开发过程,bot算法所提供的接口,背景
对于想要开发游戏的同学,可以积攒一些API的用法,移步开发过程,对于想要写bot的犇犇,可以直接移步bot算法所提供的接口
游戏规则详见游戏!
游戏规则详见游戏!
游戏规则详见游戏!
资源查找
游戏下载https://www.luogu.com.cn/problem/U284285
源码https://www.luogu.com.cn/paste/nx4bol1c
团队https://www.luogu.com.cn/team/56396
开发过程
1.rgb配色
首先,我费尽心思地找到了实现rgb配色的函数,并添加了游戏中使用的颜色
/*=============================RGB调色====================================*/
//https://blog.csdn.net/wangxun20081008/article/details/115046817
//https://www.bilibili.com/read/cv18885696(为主)
HANDLE hOutput=GetStdHandle(STD_OUTPUT_HANDLE);
COLORREF colors[16]={RGB(233,212,166),RGB(0,0,0),RGB(84,76,65),RGB(150,105,44),RGB(255,0,0),RGB(0,0,255),RGB(158,70,33),RGB(58,100,133),RGB(64,0,64),RGB(255,0,0)};
void RgbInit(){
CONSOLE_SCREEN_BUFFER_INFOEX csbi = {sizeof(CONSOLE_SCREEN_BUFFER_INFOEX) };
GetConsoleScreenBufferInfoEx(hOutput, &csbi);
memcpy(csbi.ColorTable, colors, sizeof(colors)); //把颜色配置拷进去
SetConsoleScreenBufferInfoEx(hOutput, &csbi); //应用设置
}
void RgbSet(int front,int back=0){//设置RGB,此游戏的默认背景色已经设定为colors[0]
SetConsoleTextAttribute(hOutput,(back<<4)|front);
}
2.模板搭建
游戏使用鼠标操作,所以有一套鼠标系统
void GPT(POINT &pt) {//返回相对窗口的坐标,还不算框框
//pt.x是横,.y是竖
GetCursorPos(&pt);
ScreenToClient(FindWindow("ConsoleWindowClass", NULL), &pt);
}
int ConPosx,ConPosy;//字体大小,分别是长,高
RECT Rect;//存储窗口坐标
HWND hwnd=GetForegroundWindow();
void GetCon(){
HANDLE hOutput;
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_FONT_INFO consoleCurrentFont;
GetCurrentConsoleFont(hOutput, FALSE, &consoleCurrentFont);
//printf("通过GetCurrentConsoleFont()函数获取到的控制台当前字体长度是%d, 高度是%d.\n",consoleCurrentFont.dwFontSize.X,consoleCurrentFont.dwFontSize.Y);
ConPosx=consoleCurrentFont.dwFontSize.X;
ConPosy=consoleCurrentFont.dwFontSize.Y;
/*GT(9,10);
printf("%d %d",ConPosx,ConPosy);*/
}
POINT PT;//全局变量
void PtToNum(int& x,int& y){//返回x行,y列
//将鼠标坐标转换成控制台坐标
GetCon();
GPT(PT);
x=PT.x/ConPosx;//两格作为一格
y=PT.y/ConPosy;
swap(x,y);//要置换过来
}
class MESSAGE{//鼠标类
public:
int x,y;//储存第一的字符的控制台坐标
int length,height;//储存被判区域的长度和宽度“都是多少字符”
bool Is_Touching;//(1是0否)被鼠标触碰
POINT pt;//存储鼠标
short Left,Right,Top,Bottom;//存储字符串的可点击范围
MESSAGE(int x=0,int y=0,int length=0,int height=0){
this->x=x;
this->y=y;
this->length=length;
this->height=height;
}
void Army(short x,short y,short length,short height){
this->x=x;
this->y=y;
this->length=length;
this->height=height;
}
bool If_Touching(){
Left=y*ConPosx;
Right=Left+length*ConPosx;
Top=x*ConPosy;
Bottom=Top+height*ConPosy;
GPT(pt);
return pt.x>=Left&&pt.x<Right&&pt.y>=Top&&pt.y<Bottom;
}
short Be_Clicked(){
if(If_Touching()){
if(GetAsyncKeyState(1))//左键
return 1;
else if(GetAsyncKeyState(2))//右键
return 2;
else if(GetAsyncKeyState(4))//中键
return 3;
else//没按键
return 0;
}
return 0;
}
};
MESSAGE cur;
MESSAGE类提供对控制台的某一块范围进行触碰探测和点击探测的功能,例如,对(3,2)开始,向右向下分别延伸4和5个坐标的区间,探测方式如下
cur.Army(3 2,4,5);
if(cur.If_Touching())
...
//或者
if(cur.Be_Clicked()==1)//1左键2右键3中键
...
游戏准备了一套控制台属性初始化的代码,解决设计者们的燃眉之急
RgbInit();//rgb配色初始化
system("mode con cols=90 lines=32");//屏幕大小设置
HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);
DWORD mode;
GetConsoleMode(hStdin, &mode);
mode &= ~ENABLE_QUICK_EDIT_MODE; //移除快速编辑模式
mode &= ~ENABLE_INSERT_MODE; //移除插入模式
mode &= ~ENABLE_MOUSE_INPUT;
SetConsoleMode(hStdin, mode);
srand((unsigned)time(0));//随机数设置
GetCon();//字体设置
GetWindowRect(hwnd,&Rect);//字体设置
HideCursor();//光标初始化
其中RgbInit会影响控制台属性,所以需要最先执行,并只能执行一次
void RgbInit(){
CONSOLE_SCREEN_BUFFER_INFOEX csbi = {sizeof(CONSOLE_SCREEN_BUFFER_INFOEX) };
GetConsoleScreenBufferInfoEx(hOutput, &csbi);
memcpy(csbi.ColorTable, colors, sizeof(colors)); //把颜色配置拷进去
SetConsoleScreenBufferInfoEx(hOutput, &csbi); //应用设置
}
各类开发游戏的思想请移步源码
bot算法所提供的接口
高潮部分来了,我想征集一份关于游戏的bot
1.Transform();
游戏为了设计方便,使用了两种不同的坐标模式,我起名为矩阵坐标和井坐标,两种坐标的转换是有非常美的对称的。具体探究过程可以借鉴这篇讨论
void Transform(int i,int j,int& trani,int& tranj){//变换坐标,超级美的对称公式
//更绝妙的是,对一组(x,y)运算两次该变换,会得到原数对
trani=((i-1)/3)*3+((j-1)/3)+1;//Map下标从1开始,有着上述良好的性质
tranj=((i-1)%3)*3+((j-1)%3)+1;
}
这种运算两次坐标不变的性质确实是开发中的意外之喜。
少了个定义: 对于矩阵坐标(x,y),起名为 (矩阵坐标)横坐标和 (矩阵坐标)纵坐标
对于矩阵坐标(a,b),起名为 (井坐标)第一序数和 (井坐标)第二序数
2.CheckFull(int trani);
我提供了检测落子后下一个棋子应下的格子是否满格的函数
bool CheckFull(int trani){//检测满格,返回0
if(trani>9||trani<1)//悔棋的特判
return 0;
int x,y;
for(int j=1;j<=9;++j){
Transform(trani,j,x,y);
if(Map[x][y]==-1)//ok
return 1;
}
return 0;//没格子,任意下
}
提供trani,即当前落子的井坐标第一序数,给出是否满格
restrict=CheckFull(trani)?tranj:0;
这条代码通过这个函数设置了下一个落子的范围(井坐标第一序数),这在悔棋部分也有应用。详见游戏规则和源码
CheckWin(int trani,int now);
检测是否产生胜局,暴力枚举法
int Check[3][3];
bool CheckWin(int trani,int now){//检查胜利(1出现胜利,0未出现)
//由于是井字棋的检测方式,我们可以枚举八种结果
//首先将转换下来的坐标存储方便使用
int x,y;
for(int j=1;j<=9;++j){
Transform(trani,j,x,y);
Check[(j-1)/3][(j-1)%3]=Map[x][y];
}
return (Check[0][0]==Check[0][1]&&Check[0][1]==Check[0][2]&&Check[0][2]==now)||
(Check[1][0]==Check[1][1]&&Check[1][1]==Check[1][2]&&Check[1][2]==now)||
(Check[2][0]==Check[2][1]&&Check[2][1]==Check[2][2]&&Check[2][2]==now)||
(Check[0][0]==Check[1][0]&&Check[1][0]==Check[2][0]&&Check[2][0]==now)||
(Check[0][1]==Check[1][1]&&Check[1][1]==Check[2][1]&&Check[2][1]==now)||
(Check[0][2]==Check[1][2]&&Check[1][2]==Check[2][2]&&Check[2][2]==now)||
(Check[0][0]==Check[1][1]&&Check[1][1]==Check[2][2]&&Check[2][2]==now)||
(Check[0][2]==Check[1][1]&&Check[1][1]==Check[2][0]&&Check[2][0]==now);
}
这个没啥好说的
现在,征求一份bot,满足对于当前局面可以提供较优(或者最优)解,时间限制在5s内,有志愿者可以考虑提供源码或加入团队(见资源查找)
最后,我认为我关注的增加数可以反映这个项目在洛谷的影响力,所以这篇blog如果帮助了你,不要忘记斟酌之后的关注,谢谢大家!