一、画棋盘
可以理解为简单的字符画技巧:第一行、中间与最后一行不同;对于每一行,第一列,中间与最后一列不同。
画星位是有一点技巧的,重点理解这个公式的含义: ((i-4)%6==0 && (j-4)%6==0)
#include<iostream>
using namespace std;
int main(){cout<<"●○"<< endl;// 第1行 cout << "┏ ";for (int i=2; i<=18; i++){cout << "┯ ";}cout << "┓ "<< endl;// 中间17行 for (int i=1; i<=19; i++){cout << "┠ ";for (int j=2; j<=18; j++){if ((i-4)%6==0 && (j-4)%6==0){cout<<"╋ ";} else {cout<<"┼ ";} }cout << "┨ ";cout << endl; } // 第19行 cout << "┗ ";for (int i=2; i<=18; i++){cout << "┷ ";}cout << "┛ "<< endl;return 0;// cout<<"╝╗╔╚║═╧╢╟╤┼╋";// ┯┷┠┨┗┏┓┛┼╋
}
二、落子
为了让效果好一些,首先加入一些颜色。另外,为了方便使用,增加了鼠标操作的支持。这两部分代码的功能在其他文章有论述,这里略去。到目前为止,仍然是在建立基本的框架。
一开始,我们使用左键落子为白色,右键落子为黑色的简单策略。
#include<iostream>
#include "mousetool.cpp"
#include "tools.cpp"
using namespace std;
void drawBoard(){// 第1行 cout << "┏ ";for (int i=2; i<=18; i++){cout << "┯ ";}cout << "┓ "<< endl;// 中间17行 for (int i=2; i<=18; i++){cout << "┠ ";for (int j=2; j<=18; j++){if ((i-4)%6==0 && (j-4)%6==0){cout<<"╋ ";} else {cout<<"┼ ";} }cout << "┨ ";cout << endl; } // 第19行 cout << "┗ ";for (int i=2; i<=18; i++){cout << "┷ ";}cout << "┛ "<< endl;
}
void click(int a, int x, int y){x=x/2*2;if (y>=19 || x>=38){return;}gotoxy(x,y);if (a==1){setColor(15,6);cout << "●";} else {setColor(0,6);cout << "●";}
}
int main(){initMouse();hideCursor();addEvent(1, click);addEvent(2, click);setColor(1,6);drawBoard();listenMouse();//cout<<"●○"<< endl;return 0;// cout<<"╝╗╔╚║═╧╢╟╤┼╋";// ┯┷┠┨┗┏┓┛┼╋
}
三、单键落子
两键落子使用非常不方便,我们希望换成单键落子。因为围棋是一人一手,记录当前落子方的颜色,即可实现单键落子。算法的思路是状态设置。
代码简单,略(可直接下载)
四、增加提子功能
由于采用了单键落子,另外一个键就空下来。现在左键落子,我们事先右键提子功能。
会下围棋的同学都知道,提子一次可以提起多个子,显然提子时一键提起全部相连的棋子是比较方便合理的操作。
从结构上,这里开始使用二维数组存储落子信息。从算法的角度来说,重点是简单的递归。只展示关键部分逻辑。
void pullStone(int a, int x, int y){if (x<0 || y<0 || x>18 || y>18){return;}//cout << a << " " << x << " " << y << endl;if (arr[x][y]==a){arr[x][y]=0;remove(x,y);pullStone(a, x-1, y);pullStone(a, x+1, y);pullStone(a, x, y-1);pullStone(a, x, y+1);}
}
五、自动提子
手工提子和实物状态是相似的,但计算机上理应更方便一些,我们尝试实现自动提子功能。自动提子的关键在于判断一团相连棋子是否有气。
当每次落子后,查看它周围四个方向的棋子,是否存在没有气的情况,如果有,则提起。
这里的算法重点是深度优先搜索。函数noQi,判断某个位置的棋子是否“没有气”。深搜的一个标志性操作是修改状态与恢复状态。
bool noQi(int a, int x, int y){if (x<0 || y<0 || x>18 || y>18){return true;} //cout << a << " " << x << " " << y << endl;if (arr[x][y]==0){return false;} if (arr[x][y]==3-a){return true;} if (arr[x][y]==a){arr[x][y]=-a;bool ret= true;for (int i=-1; i<=1; i++){for (int j=-1; j<=1; j++){if (i*j==0){int x1= x+i;int y1= y+j;ret= ret && noQi(a, x1, y1);}}} arr[x][y]=a;return ret;} return true;
}
六、无气禁入
有了前面的基础,现在可以实现围棋的一个落子规则,落子的位置不能没有气。这个规则就否决了《天龙八部》中虚竹自撞一气直接送掉整条大龙的办法,围棋并不允许这样落子。
当然,落子于无气之处,但是可以直接提子,这就是允许的。 所以我们首先判断是否能提子,能够提子则无条件允许落子,否则检测这枚落子是否“无气”,来决定是否允许落子。
核心逻辑如下:
// 首先预落子arr[x][y]=gSide;// 检查周围是否存在无气的对方棋子,如果有,则提起 int c= 3-gSide, cnt=0;cnt+=pull(c, x-1, y);cnt+=pull(c, x+1, y);cnt+=pull(c, x, y-1);cnt+=pull(c, x, y+1);if (cnt>0){// 有提子的情况下,无条件可以落子,继续操作 } else {// 没有提子,则本子落下不能是无气状态 if (noQi(gSide, x, y)){// 无气,则取消操作 arr[x][y]=0;return; }}
七、最后一步,实现打劫的判断
前面的逻辑已经比较完善,除了打劫。打劫的时候,虽然满足落子的时候可以提子,但不能立即提起对方刚刚提过来的子。但是打二还一的情况又是允许的。那么打劫规则应当怎样实现呢?
算法思路是这样的:首先如果刚刚对方的落子导致了提子,并且提子是一个子,则记录这个落子的位置。那么这个时候,本方的落子如果仍然只提一个子,而且提的子恰好是这个子,则不允许。
在前面的基础上,算法逻辑修改如下:
// 首先预落子arr[x][y]=gSide;// 检查周围是否存在无气的对方棋子,如果有,则提起 int c= 3-gSide, cnt=0;cnt+=pull(c, x-1, y);cnt+=pull(c, x+1, y);cnt+=pull(c, x, y-1);cnt+=pull(c, x, y+1);if (cnt>1){// 有提多子的情况下,无条件可以落子,继续操作 } else if (cnt==1){// 恰好提一子,先看所提子是否是刚刚落下的 if (arr[prex][prey]==0){// 提刚刚落下的子,首先还原状态,再看本子是否无气 arr[prex][prey]=c;if (noQi(gSide, x, y)){// 如果无气,说明是打劫状态,则不能提,还原场景 putStone(c, prex, prey);arr[x][y]=0;return;} else {// 有气状态,则允许,再次还原 arr[prex][prey]=0; }} } else {// 没有提子,则本子落下不能是无气状态 if (noQi(gSide, x, y)){// 无气,则取消操作 arr[x][y]=0;return; }}
八、未实现的功能
功能的实现是没有穷尽的,我们这个小软件实现了完整的单机热座对弈功能。但没有记录棋谱的功能,更没有读谱的功能。最令人发指的是,竟然没有收官后自动数子的功能。有兴趣的同学可以自行尝试,这不是一个非常简单的功能。