Java第八课——实现五子棋
上节课讲了数组,这节课利用数组完成五子棋,当然想完成其他的棋也可以借这节课的内容当作参考。
首先在完成作品之前,先明确思路。需要一个界面,监听器,以及可能用到的其他类。在窗体上需要画出棋盘和棋子。监听器中要MouseListener和ActionListener(按钮可以用于悔棋)。其他类可以是判断输赢的类,或是用于将鼠标点下的位置转换成棋盘坐标的方法
第一步:窗体和监听器的雏形
1、因为窗体上需要画出棋盘和棋子,所以在创造窗体时可以继承(extends)父类,更方便作图。另外在设置窗体和添加组件时可以直接用this表示当前窗体
2、创建一个二维数组用于记录棋子位置和种类(下面的代码中我创建的是18×18的棋盘,可自行调整)
public class Frame extends JFrame{
int [ ] [ ] type=new int [18][18];
public void show(){
this.setSize(1000,1000);
this.setVisible(true);
this.setLocationRelativeTo(null);//使窗体位置居中
}
public static void main(String[] args){
Frame f=new Frame();
f.show();
}
监听器,添加完监听器类之后记得在show()中添加监听器:this.addMouseListener(l);
public class Listener implements MouseListener{
public void mousePressed(MouseEvent e){ }
...//其他的方法用不到就省略了
}
那么要把棋盘和棋子画出来才算是完成雏形,因为继承了父类,那么要重写paint方法用于画棋盘和棋子
public class Frame extends JFrame{
int [ ] [ ] type=new int [18][18];
public void show(){ ... }
public void paint(Graphics g){
super.paint(g);//这条语句的作用是能在原有图像上继续画图
//画棋盘
for (int i=0;i<18;i++){//这里棋盘格子大小可以自己调整,我定的是50
g.drawLine(75+i*50, 75, 75+i*50, 925);//竖线
g.drawLine(75, 75+i*50, 925, 75+i*50);//横线
}
}
public static void main(String[] args){ ... }
}
接下来画棋子,那么我们规定1表示黑棋,2表示白棋。因为type[ ] [ ]在初始化时全部元素都为0,那么当type=0时表示为空,没有落子;type=1时表示落了黑子;type=2时表示落了白子。
public void paint(Graphics g){
super.paint(g);//这条语句的作用是能在原有图像上继续画图
//画棋盘
for (int i=0;i<18;i++){//这里棋盘格子大小可以自己调整,我定的是50
g.drawLine(75+i*50, 75, 75+i*50, 925);//竖线
g.drawLine(75, 75+i*50, 925, 75+i*50);//横线
}
for(int i=0;i<18;i++){//遍历数组
for(int j=0;j<18;j++){
if(t==1){//黑棋
g.setColor(Color.BLACK);
g.fillOval(75+i*50-10, 75+j*50-10, 20, 20);
}else if(t==2){//白棋
g.setColor(Color.WHITE);
g.fillOval(75+i*50-10, 75+j*50-10, 20, 20);
}
}
}
}
(不建议直接抄代码,要自己设计过棋盘大小和棋子大小才会对后面的操作得心应手)
到这里,棋子算是可以画出来了,只是还需要在监听器里获取坐标,那么需要x,y来存储坐标
public class Listener implements MouseListener{
int x,y;
public void mousePressed(MouseEvent e){
x=e.getX();
y=e.getY();
}
}
雏形到这里就算完成了,下一步便是把棋子画在棋盘上,就要创建一个新的类,将x,y转换成棋盘上的坐标,并实现黑白子交替落子
第二步:Tool类的添加并实现落子
Tool类的作用便是将监听器中按下时获取的绝对坐标放进棋盘,先创建一个Tool类,在这个类中需要x,y来接收从监听器中传过来的按下时的x,y坐标。另外还需要numx,numy将转化后的坐标传回监听器
public class Tool{
int x,y;
int numx,numy;
public void location(){
}
}
那么问题来了
1、如何转化坐标,比如按下时的坐标是125,怎么判断应该在哪个点位上落子
2、有时按下的位置可能偏离棋盘上的点,那么就要一个区间来判断落子
3、如果没有按在棋盘上,按在了棋盘外,那么该如何判断,比如按下的点位置是(999,999)距棋盘比较远,超出落子区间,这时又怎么做?
那么一条一条解决。首先可以同时解决第1、2个问题
(这里提供一个思路,我以我自己的棋盘为例)
格子的x坐标区间:75-125,125-175,175-225,225,275,325
每个点落子x区间:50-100,100-150,150-200,200,250,300
也就是说按下时x坐标是125时,就应该在第二个点落子;按下时x坐标时270时,应该在第五个点落子。
了解大致落子位置之后开始推理,首先棋盘最左端的落子区间距离窗体最左端距离为50(意思是只要x坐标大于50就进入可以落子的区间了),那么先把鼠标点下时的坐标减去50,再除以50取商,如果有余数则加一,如果没有则不用加。但因为在编程中以0为第一个点的话,就可以不用管余数了(那么50<=x<950都在区间内了)
第三个问题就只需要限制x,y的范围就好,在if中实现
public void location(){
if(x>=50&&x<=950&&y>=50&&y<=950){
numx=(x-50)/50;
numy=(y-50)/50;
}
}
解决了落子区间就要实现点击鼠标落子了,mousePressed中原有的基础上继续,这时要把获取的坐标传给Tool类,那么就要实例化对象,并且也需要numx,numy来接收Tool传回来的值
public class Listener implements MouseListener{
int x,y;
int numx,numy;
public void mousePressed(MouseEvent e){
x=e.getX();
y=e.getY();
Tool tool=new Tool();
tool.location();
numx=tool.numx;
numy=tool.numy;
}
}
接下来就又有几个问题
1、如何判断应该落白子还是黑子
2、如果点击位置本来就已经落子了,会出现什么冲突?
3、如果已经有一方胜利了,就应该不能再落子了,那么该如何知道或者在何时判断胜利?
第一个问题很好解决,建立一个int count用于计数,当count为偶数时落黑子,反之为白子。
第二个问题,可以在落子之前先判断type[numx][numy]是否为0来确定能不能落子,那么冲突便是当不能落子时,count不能增加,否则会出现同一颜色连续落子。
第三个问题可以在加入一个int win,当win不为0时便不能再落子了。而判断输赢的位置应是每一步落子完毕之后判断,至于判断方法后面会讲(win为1时黑子胜,2为白子胜)
public class Listener implements MouseListener{
int x,y;
int numx,numy;
int count;
int win;
public void mousePressed(MouseEvent e){
x=e.getX();
y=e.getY();
Tool tool=new Tool();
tool.location();
numx=tool.numx;
numy=tool.numy;
if(win==0){
if(type[numx][numy]==0){
if(count%2==0){//黑子
type[numx][numy]=1;
}else{
type[numx][numy]=2;
}
count++;
}
}
}
}
接下来就又有一个小问题,那就是点击棋盘并没有落子,但最小化窗体后再打开便会出现落子,因为窗体的paint方法只在出现窗体时调用了一次,后面点击之后并没有调用paint方法,最小化之后打开相当于打开窗体,便会调用一次paint。那么如何解决?用repaint方法,每次点击都会重新画一次,但repaint方法是窗体frame的方法,那么便需要传进来
public class Listener implements MouseListener{
int x,y;
int numx,numy;
int count;
int win;
JFrame JFr;//创建一个空壳用于接收
public void mousePressed(MouseEvent e){ ... }
}
在窗体的show方法中
public void show(){
...
Listener l=new Listener();
this.addMouseListener(l);
l.JFr=this;
}
于是在count++之前(后)调用repaint方法
public void mousePressed(MouseEvent e){
...
if(win==0){
if(type[numx][numy]==0){
...
JFr.repaint();
count++;
}
}
}
第三步:判断输赢
可以另外创造一个类,也可以在Listener类中创建一个新的方法
这里我以创建新方法为例
public void towin(){ }
那么我们知道,要想判断输赢是必须要遍历数组,把所有子的位置找出来来判断的,那么就要获取type数组
public class Listener implements MouseListener{
...
Frame JFr;//创建一个空壳用于接收
int type[][]=new int[18][18];
public void mousePressed(MouseEvent e){ ... }
public void towin(){ }
在窗体的show方法中
public void show(){
...
Listener l=new Listener();
this.addMouseListener(l);
l.JFr=this;
l.type=type;
}
然后就可以开始写如何判断输赢了,首先用双重循环遍历数组
public void towin(){
for(int i=0;i<18;i++){
for(int j=0;j<18;j++){
}
}
}
赢有四种连起来的方法
1.横着5个 2.竖着5个 3.向左倾斜5个 4.向右倾斜5个
要注意的是不要越界:
1、横着时:j<=13才能保证type[i][j],type[i][j+1],type[i][j+2],type[i][j+3],type[i][j+4]中的type[i][j+4]不会越界
2、竖着时,同理i<=13使得type[i+4][j]不越界
3、左斜时:i<=13&&j<=13使得type[i+4][j+4]不越界
4、右斜时:i<=13&&j>=4使得type[i+4][j-4]不越界
那么找到注意的点之后便简单了,胜利可以用弹窗 JOptionPane.showConfirmDialog(null, “胜利”, “结束”, JOptionPane.CLOSED_OPTION) 做提醒
public void towin(){
for(int i=0;i<18;i++){
for(int j=0;j<18;j++){
if(j<=13){
if(type[i][j]==1&&type[i][j+1]==1&&type[i][j+2]==1&&type[i][j+3]==1&&type[i][j+4]==1){
win=1;
}else if(j<=13&&type[i][j]==2&&type[i][j+1]==2&&type[i][j+2]==2&&type[i][j+3]==2&&type[i][j+4]==2){
win=2;
}
}
if(i<=13){
if(type[i][j]==1&&type[i+1][j]==1&&type[i+2][j]==1&&type[i+3][j]==1&&type[i+4][j]==1){
win=1;
}else if(i<=13&&type[i][j]==2&&type[i+1][j]==2&&type[i+2][j]==2&&type[i+3][j]==2&&type[i+4][j]==2){
win=2;
}
}
if(i<=13&&j<=13){
if(type[i][j]==1&&type[i+1][j+1]==1&&type[i+2][j+2]==1&&type[i+3][j+3]==1&&type[i+4][j+4]==1){
win=1;
}else if(i<=13&&j<=13&&type[i][j]==2&&type[i+1][j+1]==2&&type[i+2][j+2]==2&&type[i+3][j+3]==2&&type[i+4][j+4]==2){
win=2;
}
}
if(i<=13&&j>=4){
if(type[i][j]==1&&type[i+1][j-1]==1&&type[i+2][j-2]==1&&type[i+3][j-3]==1&&type[i+4][j-4]==1){
win=1;
}else if(i<=13&&j>=4&&type[i][j]==2&&type[i+1][j-1]==2&&type[i+2][j-2]==2&&type[i+3][j-3]==2&&type[i+4][j-4]==2){
win=2;
}
}
}
}
if(win==1){
JOptionPane.showConfirmDialog(null, "黑子胜", "结束", JOptionPane.CLOSED_OPTION);
}else if(win==2){
JOptionPane.showConfirmDialog(null, "白子胜", "结束", JOptionPane.CLOSED_OPTION);
}
}
那么调用towin方法的时候便是落子之后
public void mousePressed(MouseEvent e){
...
if(win==0){
if(type[numx][numy]==0){
...
JFr.repaint();
towin()
count++;
}
}
}
那么判断输赢就完成了,最后便是悔棋了
第四步:悔棋
同判断输赢一样,可以另写一个类,也可以在Listener类下面创造新方法
这里我以创建新方法为例
public void retract(){ }
那么在写之前要知道如何实现悔棋
1、上一步是谁走的?哪个位置?
2、当悔到不能再悔了如何处理?
1、要知道是谁走的就看count的值即可,因为每按一次count+1,所以要知道上一步谁走的就要先减一,记录位置可以用数组,创建两个新的一维数组,一个记录行位置,一个记录列位置。当悔棋时,先让count减一,再获取落子位置使type变为0,最后让记录位置的两个数组在当前位置清零
2、当count>0时可以悔棋,反之则弹出提示框
先创建两个一维数组:
public class Listener implements MouseListener{
int x,y;
int count=0;//控制黑白棋落子,偶数为黑棋
int type[][]=new int[18][18];
int row[]=new int[350];//在悔棋中记录行位置
int col[]=new int[350];//在悔棋中记录列位置
int numx,numy;
JFrame JFr;
int win;
然后再mousePressed函数中把落子位置放进去
public void mousePressed(MouseEvent e){
x=e.getX();
y=e.getY();
Tool tool=new Tool();
tool.location();
numx=tool.numx;
numy=tool.numy;
row[count]=numx;
col[count]=numy;
if ...
}
最后便是再retract函数里实现
public void retract(){
if(count>0){
count--;
type[row[count]][col[count]]=0;
row[count]=0;//记得把原位置清零
col[count]=0;
}else{
JOptionPane.showConfirmDialog(null, "不能再悔了", "提示", JOptionPane.CLOSED_OPTION);
}
JFr.repaint();//改变了棋子布局,要调用repaint
}
当然写完之后要记得添加按钮,以及给按钮添加动作监听器
public class Listener implements ActionListener,MouseListener{ ... }
show方法里添加按钮
public void show(){
JButton btn=new JButton("悔棋");
this.add(btn);
btn.addActionListener(l);
...
}
再在监听其中补充actionPerformed方法
public void actionPerformed(ActionEvent e){
String action = e.getActionCommand();
if ("悔棋".equals(action)) {
retract();
}
}
那么到这里五子棋就全部完成了,五子棋是棋类中最好实现的棋类,大家可以以这个为模板去实现其他棋类