五子棋可能大家都玩过或者听说过,规则非常简单:双方分别使用黑白两色的棋子,下在棋盘(15*15)直线与横线的交叉点上,先形成5子连线者获胜。
最近,我用Java写了一个五子棋小游戏,现在和大家分享一下。
首先我创建了四个类,分别为:
ChessUI,创建窗口,负责游戏的界面;
ChessPosition,包含两个参数,分别为棋子的x,y坐标;
ChessBoardListener,监听器类,当监听到鼠标的动作时,调用ChessBoard中的方法以做出响应。
ChessBoard,负责管理棋子,包括下棋,悔棋,重新开始,判断输赢等。
首先来实现游戏的界面:
public class ChessUI extends JPanel{
Graphics p;
BufferedImage bgImage;//棋盘的背景图片
AlphaComposite ac; //用以设置透明度
ChessBoard qz = new ChessBoard(this);
public void initUI() {
JFrame frame = new JFrame();
frame.setSize(534,614);//设置窗口大小
frame.setLocationRelativeTo(null);//设置窗口在屏幕居中
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//关闭窗口即结束程序
frame.setTitle("五子棋");//设置
//创建鼠标监听器对象
ChessBoardListener cblisten = new ChessBoardListener(qz);
//创建南边功能面板并添加到窗体的南面
JPanel southPanel = new JPanel();
southPanel.setPreferredSize(new Dimension(45,45));
frame.add(southPanel,BorderLayout.SOUTH);
// //创建西边功能面板并添加到窗体的西边
// JPanel westPanel = new JPanel();
// westPanel.setPreferredSize(new Dimension(45,500));
// frame.add(westPanel,BorderLayout.WEST);
//创建中间绘图区域面板并添加窗体的中间
frame.add(this,BorderLayout.CENTER);
this.setBackground(Color.white);
this.addMouseListener(cblisten);//添加鼠标监听器
//设置一个字符数组来存储图形按钮上的文字
String[] text = {"重新开始","悔棋","认输","人人对战","人机对战"};
for(int i=0; i<text.length;i++) {
//创建按钮
JButton btn = new JButton(text[i]);
btn.setPreferredSize(new Dimension(90,35));
//添加按钮到南面板
southPanel.add(btn);
btn.addActionListener(cblisten);//给按钮加上动作监听器
}
try {
bgImage = ImageIO.read(new File("C:/Users/background.png"));//给棋盘添加背景图片
} catch (IOException e) {
e.printStackTrace();
}
// ac = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.1f);//设置透明度
frame.setVisible(true);//设置窗体可见
//获取画布
Graphics2D p = (Graphics2D) this.getGraphics();
p.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
// ((Graphics2D) p).setComposite(ac);//设置透明度
System.out.println(p);//
cblisten.setGraphics(p);
qz.init();
}
public void paint(Graphics g) {//重绘,每次缩小放大窗体时棋盘和棋子能够再次被画出来
super.paint(g);
g.drawImage(bgImage, 0, 0, 512, 512, this);
if(qz!=null) {
qz.drawChessBorad(g);
qz.drawChess(g);
}
}
public static void main (String[] args) {
ChessUI pl = new ChessUI();
pl.initUI();
}
}
值得注意的是,getGraphics这一步要在setVisible后,要不然会报出空指针异常。
效果如下图所示:
接下来分人人对战,人机对战两部分来讲述。
人人对战的算法相对来说简单一点,所以先来说说人人对战。
分为下棋和判断输赢两部分:
先说下棋:
鼠标在棋盘上点击一下,就在棋盘上画一颗棋子,这里需要对鼠标监听器获取的x,y坐标处理一下,使鼠标点击后棋子画在离获取的坐标最近的格子的正中间,因为我们做不到每一次点击都点在精准的棋盘线交叉点上。
为了使黑白子轮流下棋,我用了一个标志位flag来标志,当flag为1时下的棋为黑色,flag为2时下的棋为白色。一开始初始化flag为1,规定了黑子先下。下完一颗棋子后就反转标志位,就可实现黑白子轮流下棋。
创建一个大小为15*15的二维数组,用来保存棋盘上棋子的值,初始化为0后,下了一颗黑棋就把数组对应的位置的值置为1,下白棋则置为2,同时每下一颗棋子就将其放入ArrayList中,方便后续的悔棋。
public void humanAndhuman(Graphics g, int rowh, int rowl){
if(this.allchess[rowh][rowl]==0){
if(this.flag==1) {
g.setColor(Color.black);
g.fillOval((rowh+1)*gap-radius, (rowl+1)*gap-radius, 2*radius, 2*radius);
this.list.add(new ChessPosition(rowh,rowl));
this.allchess[rowh][rowl] = 1;
this.flag = 2;
}
else if(this.flag==2){
g.setColor(Color.white);
g.fillOval((rowh+1)*gap-radius, (rowl+1)*gap-radius, 2*radius, 2*radius);
this.list.add(new ChessPosition(rowh,rowl));
this.allchess[rowh][rowl] = 2;
this.flag = 1;
}
}
checkWin(rowh,rowl);
}
判断输赢:
每下完一步棋,就要判断一下是否分出胜负。判断胜负的规则非常简单,就是看棋盘上有没有相同颜色的棋子连成5颗或以上,先连成5颗的一方胜利。
所以判断输赢的算法这样来写:
从当前棋子的位置开始,分为四个方向:横向、竖向、左斜、右斜,每个方向都要判断是否有5颗或以上相同颜色的棋子连成一线。举个例子,定义一个变量count来计数,初始化为1(即当前棋子自身),检查横向时,先向左开始检查,遇到相同颜色的棋子时count自加一,直到遇到颜色不同的棋子或者到了棋盘边界,接着从当前棋子位置的右边开始检查,遇到相同颜色的棋子时count自加一,直到遇到颜色不同的棋子或者到了棋盘边界,最后判断count是否大于等于5,若是,获取当前棋子的颜色,若是黑色则弹出“黑方胜利,白方失败”这样的弹窗。
另外,设置一个标志位gameover,初始化为1,意思是允许下棋,当分出胜负后,设置gameover为0,就不能再下棋或者悔棋了。
public void checkWin(int rowh, int rowl){
boolean winflag = false;
int count = 1;
int color = this.allchess[rowh][rowl];
count = checkCount(rowh,rowl,1,0,color);//检查横向
if(count>=5){
winflag = true;
}else{
count = checkCount(rowh,rowl,0,1,color);//检查纵向
if(count>=5){
winflag = true;
}else{
count = checkCount(rowh,rowl,1,1,color);//检查左下右上
if(count>=5){
winflag = true;
}else{
count = checkCount(rowh,rowl,1,-1,color);//检查左上右下
if(count>=5){
winflag = true;
}
}
}
}
if(winflag == true){
if(color == 1){//黑子胜
JOptionPane.showMessageDialog(null, "游戏结束,黑方获胜!");
}else if(color == 2){
JOptionPane.showMessageDialog(null, "游戏结束,白方获胜!");
}
this.gameover = 0;
}
}
public int checkCount(int rowh, int rowl, int xChange, int yChange ,int color){
int count = 1;
int tempX = xChange;
int tempY = yChange;
while((rowh+xChange>=0)&&(rowh+xChange<=14)&&(rowl+yChange>=0)&&(rowl+yChange<=14)&&(color==this.allchess[rowh+xChange][rowl+yChange])){
count++;
if(xChange != 0){
xChange++;
}
if(yChange != 0){
if(yChange>0){
yChange++;
}else{
yChange--;
}
}
}
xChange = tempX;
yChange = tempY;
while((rowh-xChange>=0)&&(rowh-xChange<=14)&&(rowl-yChange>=0)&&(rowl-yChange<=14)&&(color==this.allchess[rowh-xChange][rowl-yChange])){
count++;
if(xChange != 0){
xChange++;
}
if(yChange != 0){
if(yChange>0){
yChange++;
}else{
yChange--;
}
}
}
return count;
}
人机对战:
我设置了人先下棋(黑子),然后到机器下棋。
人机中,人下棋与上述人人对战时一样,在此不再赘述。
难点在于机器下棋,当人下完一步后,机器应该下在哪里呢?
我用了权值法去解决这个问题。
我们把每一种行棋的情况用枚举法尽可能地列举出来:
1、若当前位置四周为空,这个位置落子的意义不大,相应的给这个位置的权值就要低很多;
2、若该位置的四周出线一条连线,此线上的棋子越多,说明该位置越重要,该位置的权值就越高;
3、当一条线的一端被堵住,称为眠连;当连线的两端没有被堵住,称为活连。眠连意味着受到对方的牵制,所以活连价值比眠连高。
根据以上规则,把棋局的状态用类似“0110”、“01111”这样的字符串描述出来,每一种状态一一对应一个权值,用哈希表存储。
对于每一个空格,从四个方向来考察其得分,最后把四个得分相加起来,得到总的分数,取得分最高的空格为机器落子点。
值得注意的是,攻的同时要注意守,当对方出现活三连或者眠四连的情况时,要去截住对方。对对方来说很重要的位置也就意味着对自己来说也是很重要的位置,所以列举情况时,要把对方的情况也考虑进来。
哈希表部分内容如下:
map.put("01012",65);
map.put("02021",60);
map.put("01102",80);
map.put("02201",76);
map.put("01120",80);
map.put("02210",76);
map.put("00112",65);
map.put("00221",60);
这个权值需要根据实际情况逐渐调整,打分的合理性决定了AI的智能性,越合理AI表现就会越好。
//人机对战下棋
public void human(Graphics g, int rowh, int rowl){
int scoremax = 0;
int x = 0,y = 0;
if(this.allchess[rowh][rowl]==0){
g.setColor(Color.black);
g.fillOval((rowh+1)*gap-radius, (rowl+1)*gap-radius, 2*radius, 2*radius);
this.list.add(new ChessPosition(rowh,rowl));
this.allchess[rowh][rowl] = 1;
this.flag = 2;
}
checkWin(rowh,rowl);
}
public void machine(Graphics g){
int weightmax=0;
for(int i=0;i<15;i++) {
for(int j=0;j<15;j++) {
if(weightmax<this.chessvalue[i][j]) {
weightmax=this.chessvalue[i][j];
x2=i;
y2=j;
}
}
}
g.setColor(Color.white);
g.fillOval((x2+1)*gap-radius, (y2+1)*gap-radius, 2*radius, 2*radius);
this.list.add(new ChessPosition(x2,y2));
this.allchess[x2][y2] = 2;
this.flag = 1;
for(i=0;i<15;i++){
for(j=0;j<15;j++){
this.chessvalue[i][j]=0;
}
}
checkWin(x2,y2);
}
public void countvalue(){
for(int i=0;i<15;i++) {
for(int j=0;j<15;j++) {
//首先判断当前位置是否为空
if(this.allchess[i][j]==0) {
//往左延伸
String ConnectType="0";
int jmin=Math.max(0, j-4);
for(int positionj=j-1;positionj>=jmin;positionj--) {
//依次加上前面的棋子
ConnectType=ConnectType+this.allchess[i][positionj];
}
//从数组中取出相应的权值,加到权值数组的当前位置中
Integer valueleft = map.get(ConnectType);
if(valueleft!=null) this.chessvalue[i][j]+=valueleft;
//往右延伸
ConnectType="0";
int jmax=Math.min(14, j+4);
for(int positionj=j+1;positionj<=jmax;positionj++) {
//依次加上前面的棋子
ConnectType=ConnectType+this.allchess[i][positionj];
}
//从数组中取出相应的权值,加到权值数组的当前位置中
Integer valueright = map.get(ConnectType);
if(valueright!=null) this.chessvalue[i][j]+=valueright;
//联合判断,判断行
this.chessvalue[i][j]+=unionvalue(valueleft,valueright);
//往上延伸
ConnectType="0";
int imin=Math.max(0, i-4);
for(int positioni=i-1;positioni>=imin;positioni--) {
//依次加上前面的棋子
ConnectType=ConnectType+this.allchess[positioni][j];
}
//从数组中取出相应的权值,加到权值数组的当前位置中
Integer valueup=map.get(ConnectType);
if(valueup!=null) this.chessvalue[i][j]+=valueup;
//往下延伸
ConnectType="0";
int imax=Math.min(14, i+4);
for(int positioni=i+1;positioni<=imax;positioni++) {
//依次加上前面的棋子
ConnectType=ConnectType+this.allchess[positioni][j];
}
//从数组中取出相应的权值,加到权值数组的当前位置中
Integer valuedown=map.get(ConnectType);
if(valuedown!=null) this.chessvalue[i][j]+=valuedown;
//联合判断,判断列
this.chessvalue[i][j]+=unionvalue(valueup,valuedown);
//往左上方延伸,i,j,都减去相同的数
ConnectType="0";
for(int position=-1;position>=-4;position--) {
if((i+position>=0)&&(i+position<=14)&&(j+position>=0)&&(j+position<=14))
ConnectType=ConnectType+this.allchess[i+position][j+position];
}
//从数组中取出相应的权值,加到权值数组的当前位置
Integer valueLeftUp=map.get(ConnectType);
if(valueLeftUp!=null) this.chessvalue[i][j]+=valueLeftUp;
//往右下方延伸,i,j,都加上相同的数
ConnectType="0";
for(int position=1;position<=4;position++) {
if((i+position>=0)&&(i+position<=14)&&(j+position>=0)&&(j+position<=14))
ConnectType=ConnectType+this.allchess[i+position][j+position];
}
//从数组中取出相应的权值,加到权值数组的当前位置
Integer valueRightDown=map.get(ConnectType);
if(valueRightDown!=null) this.chessvalue[i][j]+=valueRightDown;
//联合判断,判断行
this.chessvalue[i][j]+=unionvalue(valueLeftUp,valueRightDown);
//往左下方延伸,i加,j减
ConnectType="0";
for(int position=1;position<=4;position++) {
if((i+position>=0)&&(i+position<=14)&&(j-position>=0)&&(j-position<=14))
ConnectType=ConnectType+this.allchess[i+position][j-position];
}
//从数组中取出相应的权值,加到权值数组的当前位置
Integer valueLeftDown=map.get(ConnectType);
if(valueLeftDown!=null) this.chessvalue[i][j]+=valueLeftDown;
//往右上方延伸,i减,j加
ConnectType="0";
for(int position=1;position<=4;position++) {
if((i-position>=0)&&(i-position<=14)&&(j+position>=0)&&(j+position<=14))
ConnectType=ConnectType+this.allchess[i-position][j+position];
}
//从数组中取出相应的权值,加到权值数组的当前位置
Integer valueRightUp=map.get(ConnectType);
if(valueRightUp!=null) this.chessvalue[i][j]+=valueRightUp;
//联合判断,判断行
this.chessvalue[i][j]+=unionvalue(valueLeftDown,valueRightUp);
}
}
}
//取出最大的权值
}
public Integer unionvalue(Integer a,Integer b){
if((a==null)||(b==null)) return 0;
//一一:101/202
else if((a>=22)&&(a<=25)&&(b>=22)&&(b<=25)) return 60;
//一二、二一:1011/2022
else if(((a>=22)&&(a<=25)&&(b>=76)&&(b<=80))||((a>=76)&&(a<=80)&&(b>=22)&&(b<=25))) return 800;
//一三、三一、二二:10111/20222
else if(((a>=10)&&(a<=25)&&(b>=1050)&&(b<=1100))||((a>=1050)&&(a<=1100)&&(b>=10)&&(b<=25))||((a>=76)&&(a<=80)&&(b>=76)&&(b<=80)))
return 3000;
//眠三连和眠一连。一三、三一
else if(((a>=22)&&(a<=25)&&(b>=140)&&(b<=150))||((a>=140)&&(a<=150)&&(b>=22)&&(b<=25))) return 3000;
//二三、三二:110111
else if(((a>=76)&&(a<=80)&&(b>=1050)&&(b<=1100))||((a>=1050)&&(a<=1100)&&(b>=76)&&(b<=80))) return 3000;
else return 0;
}
监听器类的代码如下:
import java.awt.Graphics;
import java.awt.Color;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JOptionPane;
import com.sun.glass.ui.Robot;
public class ChessBoardListener implements MouseListener,ActionListener {
public ChessBoard chess;
Graphics p;
int x1,y1;
int rowh,rowl;
int color;
int num = 15;
int gap = 32;
int radius = 12;
public ChessBoardListener(ChessBoard chess){
this.chess = chess;
}
public void setGraphics(Graphics g) {
this.p = g;
}
public void actionPerformed(ActionEvent e) {
if(e.getActionCommand().equals("重新开始")){
chess.restart();
}
else if(e.getActionCommand().equals("悔棋")){
if(chess.gameover==0) JOptionPane.showMessageDialog(null, "游戏已结束,不能悔棋");
chess.reback();
}
else if(e.getActionCommand().equals("人人对战")){
chess.humanormachine = 1;
}
else if(e.getActionCommand().equals("人机对战")){
chess.humanormachine = 2;
}
else if(e.getActionCommand().equals("认输")){
chess.surrender();
}
}
public void mouseClicked(MouseEvent e) {
x1 = e.getX();
y1 = e.getY();
rowh = ((x1-gap+num)/gap)%gap;
rowl = ((y1-gap+num)/gap)%gap;
if((x1<495&&x1>0&&y1<495&&y1>0) && (chess.gameover == 1)){
if(chess.humanormachine != 0){
if(chess.humanormachine == 1){
chess.humanAndhuman(p, rowh, rowl);
}
else if(chess.humanormachine == 2){
chess.human(p, rowh, rowl);
chess.countvalue();
chess.machine(p);
}
}
}
}
public void mousePressed(MouseEvent e) {}
public void mouseReleased(MouseEvent e) {}
public void mouseEntered(MouseEvent e) {}
public void mouseExited(MouseEvent e) {}
}
悔棋功能:
之前每下一步棋就把棋子存进ArrayList中,当鼠标监听器监听到鼠标点击悔棋按钮时,获取ArrayList中最上面那个节点之后删除它,此元素的数据类型我们定义为棋子的x,y坐标,所以取出来的就是最新下的棋子的x,y坐标,然后把二维数组中对应的位置置为0。这时候还要获取当前ArrayList中最上面的棋子的颜色,若获得的flag为2(白色),则要把flag赋值为1(黑色),因为白色的棋子下完之后就到黑色的棋子下了。最后调用repaint方法后刷新棋盘。
public ArrayList<ChessPosition> list=new ArrayList<ChessPosition>();
public void reback(){
if(list.size()>0&&this.gameover==1){
ChessPosition cp = new ChessPosition();
cp = list.remove(list.size()-1);
this.allchess[cp.i][cp.j]=0;
if(flag==1) flag=2;
if(flag==2) flag=1;
pl.repaint();
}
}
重新开始功能:
就是把二维数组重新全部置为0,再调用repaint方法刷新棋盘。然后把flag置为1,因为我始终使黑棋先下。使gameover标志位为1,表示可以下棋。
public void restart(){
if(this.list.size()>0){
for(i=0;i<15;i++) {
for(j=0;j<15;j++) {
this.allchess[i][j]=0;
}
}
flag = 1;
this.gameover = 1;
pl.repaint();
}
}
认输功能:
监听器监听到鼠标点击认输按钮时,使gameover标志位为0,终止游戏,弹出胜负弹窗。
public void surrender(){
if(this.flag==2){
JOptionPane.showMessageDialog(null, "白方投降,黑方获胜!");
}
else if(this.flag==1){
JOptionPane.showMessageDialog(null, "黑方投降,白方获胜!");
}
this.gameover = 0;
}
实现效果:
以上就是我做的五子棋的大概内容,写得不好的地方还请大家指教。(声明一下,人机中部分算法借鉴了网上的一些大佬,在此致谢。)