写一个五子棋,主要有三个部分。
1. 界面。
2.下棋功能的实现。
3.人机对战的算法。
1.界面
界面分为两块:菜单,和下棋面板。
菜单要实现基本的功能,开始,撤销,和离开。要实现这些功能就写一个菜单监听器,并且对每一个菜单项setActionCommand,通过判断ActionCommand来分别实现不同的功能。
下棋必须要有格子,这个格子是画了以后就再不会发生改变的,所以把画格子写成一个方法,在重绘时调用就好了。我用的是一个JPanel贴在窗体上,所以就必须在JPanel上得到画布对象,并且实现重绘。
class ChessPanel extends JPanel{
//画格子
public void drawPanel(Graphics g){
//横向十五条线
for (int i = 0; i < Config.Row; i++) {
g.drawLine(Config.X0, Config.Y0 + i * Config.Size, Config.X0
+ (Config.Column - 1) * Config.Size, Config.Size * i
+ Config.Y0);
}
//纵向十五条线
for (int j = 0; j < Config.Column; j++) {
g.drawLine(Config.X0 + j * Config.Size, Config.Y0, Config.X0
+ j * Config.Size, (Config.Row - 1) * Config.Size
+ Config.Y0);
}
}
//重绘棋子
public void drawChess(Graphics g){
for(int i=0;i<chesslist.size();i++){
Qizi qz=chesslist.get(i);
qz.draw(g);
}
}
//重绘窗口
public void paint(Graphics g){
super.paint(g);
drawPanel(g);
drawChess(g);
}
}
这里的config是什么意思呢?就是configuration的意思(配置)。
在所有的代码里,有些东西是会重复使用很多次的。比如列数,行数,棋子大小等等。这些如果直接写(我们把它叫做硬代码),很容易出错,而且很繁琐,一旦要修改就会很麻烦。如果都用一个引用传递的话,全都只需要输入变量名就可以了,实际的值只在Config里面调用,这样需要修改的时候,就只要轻轻改动Config里面的一项或几项就可以了。
2.下棋
和我们自己下棋一样,下棋的步骤为:
找点-->落子
机器也是这样下棋的。我们用一个二维数组来存储棋盘,对棋盘加一个监听器,同时把棋子封装成一个Qizi类,这样对撤销与重绘的实现是有好处的。这个待会再说。
机器没有眼睛不可能一眼看出你下的是哪里,马上给你画上,于是只好很笨很笨的遍历整个二维数组,同时算出哪个位置与你的鼠标落点最接近。
public void mouseReleased(MouseEvent e) {
//鼠标释放时的坐标
int x1=e.getX();
int y1=e.getY();
for(int i=0;i<Config.Row;i++){
for(int j=0;j<Config.Column;j++){
//换算出每一点的坐标,并进行比较
int x=Config.X0+Config.Size*i;
int y=Config.Y0+Config.Size*j;
//误差为格子大小的三分之一
if(Math.abs(x1-x)<Config.Size/3
&&Math.abs(y1-y)<Config.Size/3){
//如果点为空且棋盘可以继续下子
if(UI.chessTable[i][j]==0&&isChange==true){
putChess(i,j,1);//下黑子
}
}
return;
}
}
}
}
注意:1.这里一定要有适当的误差,不然点击的时候很难点准。
2.要把下子写成一个方法,并且可以传参,不论是机器下子还是人下子,都可以调用。这样就会节省很多代码。
下子的方法要包括那些呢?
要可以画一个圆圆的棋子,这个棋子可以指定颜色,同时要把棋子存到重绘的队列里面去。下了棋以后还要判断输赢。
看代码
public void putChess(int r,int c,int flag){
//根据给定的下标算出坐标
int x=Config.X0+r*Config.Size;
int y=Config.Y0+c*Config.Size;
//判定棋子颜色
if(flag==1){
color=Color.BLACK;
}else if(flag==-1){
color=Color.WHITE;
}
//修改棋盘
UI.chessTable[r][c]=flag;
//画棋子
Qizi qz = new Qizi(x,y,color);
qz.draw(g);
//把棋子存入重绘队列
chesslist.add(qz);
//判断输赢
isWin(r,c);
}
判断输赢的方法就是得到下棋的那个点的下标,向四个方向遍历,看是不是有五个子连在一起。只要有一个方向上有五个子连在一起就赢了。
举一个横向的例子
public int checkRow(int r,int c,int[][]chessTable){
int cnt=0;
for(int j=c+1;j<Config.Column;j++){
if(chessTable[r][j]==chessTable[r][c]){
cnt++;
}else
break;
}
for(int j=c;j>=0;j--){
if(chessTable[r][j]==chessTable[r][c]){
cnt++;
}else
break;
}
return cnt;
}
再说说重绘和撤销:
可以看到最上面关于棋盘JPanel的代码,里面有关于重绘的实现,就是把存入队列中的棋子对象一个一个的取出来,重新画上去。
撤销也是一样,首先得到队列的最后一个棋子对象,通过getter和setter方法得到要删除棋子的坐标,这个要写在Qizi的类里面,根据坐标把存有棋盘的二维数组上对应的点清空,然后把队列最后一个棋子对象删除就可以了,注意,删除之后要刷新一下,repaint()
3.人机对战算法
小湖的人机算法很笨,不过对于初学者来说算是最容易理解的方法了。
首先你得有一个机器人类,这个机器人就是与你下棋的对手。机器人有什么功能?就是根据棋盘上的形式选择它自己该下哪个点。
public int[] choosePoint() :这个是找点下子的方法,返回值就是这个点,包括横坐标,纵坐标,以及权值。
int[] point = new int[3];
// 重置权值表
clearValue(Config.Values);
// 给每个点赋权值
for (int i = 0; i < Config.Row; i++) {
for (int j = 0; j < Config.Column; j++) {
if (UI.chessTable[i][j] == 0) {
Config.Values[i][j] = this.setValue(i, j, -1);
}
}
}
// 得到最大权值的点
point = this.getMaxPoint(Config.Values);
return point;
}
把清空权值表和遍历得到最大权值点都写成方法。
public int setValueSimple(int r, int c, int flag,int[][]chesstable):这是给某个点赋基本权值的方法,何谓基本权值?就是目前这个点的棋型能打几分。此时就把几种基本棋型(某一单一方向上)对应的权值一一列举出来。
public int setValue(int r,int c,int flag):这是定最终权值的方法。
由于下棋并不能只看表面,否则就很容易范目光短浅的错误,掉入对手的陷阱里,同时自己的攻势也太直白,对手轻而易举就可以化解,所以除了基本权值之外还要多猜几步,我下这里对方最可能下哪里?对方会不会赢?如果对方会赢那么无论这个权值有多大都不可以下这里了。猜测几步之后,可以用一定的权数进行最终权值的确定。
public int setValue(int r,int c,int flag){
int value=setValueSimple(r,c,flag,UI.chessTable);//定基本权值
clearValue(GuessTable);//清零猜测棋盘
//复制猜测棋盘
for(int i=0;i<Config.Column;i++){
for(int j=0;j<Config.Row;j++){
GuessTable [i][j]=UI.chessTable[i][j];
}
}
point_duifang=guess(r,c,-flag);//假设我下在这里,同时猜对方
if(isWin(r,c)==false){//非我胜点,继续
if(isWin(point_duifang[1],point_duifang[2])==true){
//判断,对方下在这里会赢
value=0;
} else{//否则继续猜下去
point_ziji=guess(point_duifang[1],point_duifang[2],flag);
//对方如果下了,猜自己
value=value*2/3+point_ziji[2]/3;
point_duifang=guess(r,c,-flag);//猜对方
if(isWin(point_duifang[1],point_duifang[2])==true){
//判断,对方下在这里会赢
value=value/4;
}else{//否则继续猜下去
point_ziji=guess(point_duifang[1],point_duifang[2],flag);
//对方如果下了,猜自己
value=value*2/3+point_ziji[2]/3;
point_duifang=guess(r,c,-flag);//猜对方
if(isWin(point_duifang[1],point_duifang[2])==true){
//判断,对方下在这里会赢
value=value/4;
}else{//否则继续猜下去
point_ziji=guess(point_duifang[1],point_duifang[2],flag);
//对方如果下了,猜自己
value=value*2/3+point_ziji[2]/3;
}
}
}
}
return value;
}
这里面猜测了三步,其实可以用迭代的方法,但是由于代码不多我就直接敲上去了。猜测时需要调用猜测的方法,就是传入一个复制了当前棋盘的猜测棋盘二维数组以及要猜测的对象。进行如下步骤
在猜测棋盘上假下子--》遍历找点--》判断权值,存入猜测用权值表(这个表每一次调用都要清零)--》找到权值最大的点,返回。
这种方法很耗时间,如果猜测多于五步就很慢了。
在找点下子的方法中得到最终的权值以后,就可以下子了。之前是在棋盘面板的监听器上手动下子,机器下子也可以添加到那里。只要在人下子的地方多三行代码。
if(isChange==true){
int []point=robot.choosePoint();
putChess(point[1],point[2],-1);
}
isChange指的是棋盘可不可以继续下子。如果有人赢了,isChange就是false ,如果没有人赢isChange就是true。
把机器人类地实例化写在监听器的属性里面,这样就只用实例化一次了,节省了计算机的资源。
其实这个算法很傻,至少连五子棋菜鸟某人都可以做个常胜将军,不过至少让我理解了此类游戏的大致原理。受益颇多。