专门学习java已经有一个月了,舍友说:“对女生来说,学技术是条不归路啊!”做画图板时不这么觉得,但做五子棋时就感受深刻了,不是因为难学,是因为坎坷,很容易放弃……尤其是在做P2C(人机对战)的时候,纠结于算法好久,单单是用最笨的权值法,我就在分类、赋值的时候,从不同的角度考虑了各种方法。之前写了一个版本,我称之为智商为负。后来,考虑了很久,这样的赋值方法很欠缺考虑,以后P2C完成时的总结时会,共享给大家见笑见笑。总之,P2C的算法,我还没有纠结出来,所以,先对P2P对战总结一下。
五子棋的开发,之前自己在做,基本实现了可以相互交换放棋子,和判断输赢。但代码结构十分的混乱,排错也比较困难,需要重头梳理一遍思路。龙哥后来给我们梳理了一下各个类之间的关系,令我茅塞顿开啊。原来,每一个项目的不能着急开始,都需要整体的规划。
混乱版的就不拿出来献丑了,就总结一下整理后的版本。
一、面板的制作。
用三块面板将窗体分为控制区、游戏区、菜单栏。
代码如下:
1、面板类
2、控制面板
public FCChesscontrlfield(FCChessplayfield playfield,JFrame fccp){ this.setSize( 150, 600);//(150,600); this.setBackground(Color.GRAY); this.setLayout(null); //creat some button to playcontrl javax.swing.JButton undo=new javax.swing.JButton("悔棋"); undo.setBounds(650, 200, 60, 25); this.add(undo); undo.setActionCommand("undo"); javax.swing.JButton giveup=new javax.swing.JButton("认输"); giveup.setBounds(650, 300, 60, 25); this.add(giveup); giveup.setActionCommand("giveup"); javax.swing.JButton rebegin=new javax.swing.JButton("重玩"); rebegin.setBounds(650, 400, 60, 25); this.add(rebegin); rebegin.setActionCommand("rebegin"); //创建一个鼠标监听器,监听控制面板 FCChesscontrlfieldListener rbml=new FCChesscontrlfieldListener(playfield,fccp,p); giveup.addMouseListener(rbml); undo.addMouseListener(rbml); rebegin.addMouseListener(rbml); } }
3、游戏面板
public class FCChessplayfield extends JPanel implements FCCconfig{ public FCChessplayfield(){ this.setBounds(0, 25, 600, 600); this.setBackground(Color.LIGHT_GRAY); this.setLayout(null);} // 屏幕重绘 public void paint(Graphics g) { super.paint(g); System.out.println("jpanel重绘了"); // 在屏幕重绘时重绘棋盘网格 drawchess(g); // 在屏幕重绘时,重绘棋子 redrawp(g); } // 绘制棋盘 public void drawchess(Graphics g) { System.out.println("网格重绘了"); g.setColor(Color.black); for (int i = 0; i < 11; i++) { g.drawLine(FCCconfig.x0, FCCconfig.gsize * (i + 1), FCCconfig.xe, FCCconfig.gsize * (i + 1)); } for (int j = 0; j < 11; j++) { g.drawLine(FCCconfig.gsize * (j + 1), FCCconfig.y0, FCCconfig.gsize * (j + 1), FCCconfig.ye); } } // 遍历二维数组,为棋盘所有点标记为0 public void remark() { System.out.println("标记恢复为0了"); for (int i = 0; i < FCCconfig.row; i++) { for (int j = 0; j < FCCconfig.rank; j++) { CCmark[i][j] = 0; } } } // 棋子的重绘 public void redrawp(Graphics g) { System.out.println("棋子重绘了!"); for (int i = 0; i < FCCconfig.row; i++) { for (int j = 0; j < FCCconfig.rank; j++) { System.out.print(CCmark[i][j] + "\t"); if (CCmark[i][j] != 0) { if (CCmark[i][j] == 1) { g.setColor(Color.BLACK); } else if (CCmark[i][j] == -1) { g.setColor(Color.BLUE); } g.fillOval((i + 1) * FCCconfig.gsize - FCCconfig.CCPsize / 2, (j + 1) * FCCconfig.gsize - FCCconfig.CCPsize / 2, 40, 40); } } System.out.println(); } } }
4、菜单面板
public FCChessmenubar(){ this.setBounds(0, 0, 750, 25); //为菜单栏加布局 java.awt.FlowLayout menubarfl=new java.awt.FlowLayout(0); this.setLayout(menubarfl); // add menu to menubar javax.swing.JMenu File = new javax.swing.JMenu("File"); File.setPreferredSize(new Dimension(100, 25)); this.add(File); // add menuitem to menu-File javax.swing.JMenuItem open = new javax.swing.JMenuItem("open"); open.setSize(100, 25); javax.swing.JMenuItem save = new javax.swing.JMenuItem("save"); save.setSize(100, 25); javax.swing.JMenuItem exit = new javax.swing.JMenuItem("exit"); exit.setSize(100, 25); File.add(open); File.add(save); File.add(exit); //System.out.println("FILE按钮添加了"); javax.swing.JMenu about = new javax.swing.JMenu("about"); about.setPreferredSize(new Dimension(100, 25)); this.add(about); //add menuitem to menu-about javax.swing.JMenuItem aboutgame = new javax.swing.JMenuItem("aboutgame"); aboutgame.setSize(100, 25); javax.swing.JMenuItem abouteditor= new javax.swing.JMenuItem("abouteditor"); abouteditor.setSize(100, 25); about.add(aboutgame); about.add(abouteditor); //创建动作监听器,监听按钮aboutgame ActionListener alis = new ActionListener(){ public void actionPerformed(ActionEvent e){ javax.swing.JFrame aboutgame=new javax.swing.JFrame("about game"); aboutgame.setSize(300, 300); aboutgame.setBackground(Color.blue); javax.swing.JLabel jlab1=new javax.swing.JLabel("made by Ailse"); jlab1.setBounds(25, 25, 200,200 ); aboutgame.add(jlab1); aboutgame.setVisible(true); } }; aboutgame.addActionListener(alis); } }
效果图:
二、监听
监听时相应的,分为三个部分,和三块面板对应。如上图示。
但是游戏面板的监听却放在了窗体类里了。
// 创建并鼠标监听器,通过鼠标监听棋盘面板 FCChessplayfieldListener pillms = new FCChessplayfieldListener(g, fccp,CCmark); playfield.addMouseListener(pillms);
这是因为,鼠标监听器需要画布,但画布的定义要在窗体可见后,而游戏面板却要在窗体定义时就要定义。所以只能放在窗体类中定义监听器。同样的问题出现在了窗体重绘时,这时调用了超父类的paint()方法-----super.paint();
1、控制面板监听
public void mousePressed(MouseEvent e) { JButton btn = (JButton)e.getSource(); String command=btn.getActionCommand(); if(command=="rebegin"){ cp.remark(); cp.repaint(); } if(command=="giveup"){ javax.swing.JOptionPane.showMessageDialog(jf2, "你太厉害了,甘拜下风啊!"); } if(command=="undo"){ javax.swing.JOptionPane.showMessageDialog(jf2, "呵呵呵!下错位置了,我要悔棋!"); int x=0, y=0; if(list.size()-1<1){ javax.swing.JOptionPane.showMessageDialog(jf2, "请按重玩按钮!!"); // System.out.println(x+","+y+","+CCmark[x][y]); //System.out.println("棋子数目:"+list.size()); // System.out.println("重新标记棋盘了"); }else{ p=list.get(list.size()-1); x=p.getx(); y=p.gety(); CCmark[x][y]=0; list.delect(list.size()-1); p=list.get(list.size()-1); x=p.getx(); y=p.gety(); CCmark[x][y]=0; list.delect(list.size()-1); } cp.repaint(); } }
对于重玩的实现,还是比较费力的。要想悔棋,就要让我们的计算机记得下棋的顺序和下的棋子的颜色。我用了一个棋子类将每放的一颗棋子的坐标和棋子颜色记录,然后用队列(大小可变数组)顺序存放这些棋子类的棋子对象。
public class pill { private int x; private int y; private int color; //标记 public pill(int x, int y, int color){ this.color=color; this.x=x; this.y=y; } public int getx(){ return x; } public int gety(){ return y; }
2、游戏面板监听
public void mouseReleased(MouseEvent e) { // 定义一个变量用于控制放置黑棋和白棋交替放置 // count为奇数时,黑棋下;count为偶数时,蓝棋下 x1 = e.getX(); y1 = e.getY(); // 通过count,判断黑棋行还是白棋行 if (count % 2 == 1) { g.setColor(Color.BLACK); t = 1; } else if (count % 2 == 0) { g.setColor(Color.blue); t = -1; } // 判断点击位置是否在棋盘上 if ((x1 > 50 && x1 < 550) && (y1 > 50 && y1 < 550)) { // 判断点击位置,并在离的最近的交叉点放置棋子 // FCCconfig.gsize:格宽;FCCconfig.CCPsize/2:棋半径;FCCconfig.gsize/2:格中间的位置 // x1,y1为鼠标按下处点的坐标 // x2,y2为离按下处最近的交叉点的坐标 if ((x1 % FCCconfig.gsize) < FCCconfig.gsize / 2) { x2 = x1 - (x1 % FCCconfig.gsize) - FCCconfig.CCPsize / 2; } else if ((x1 % FCCconfig.gsize) >= FCCconfig.gsize / 2) { x2 = x1 + (FCCconfig.gsize - (x1 % FCCconfig.gsize)) - FCCconfig.CCPsize / 2; } if ((y1 % FCCconfig.gsize) < FCCconfig.gsize / 2) { y2 = y1 - (y1 % FCCconfig.gsize) - FCCconfig.CCPsize / 2; } else if ((y1 % FCCconfig.gsize) >= FCCconfig.gsize / 2) { y2 = y1 + (FCCconfig.gsize - (y1 % FCCconfig.gsize)) - FCCconfig.CCPsize / 2; } i = x2 / FCCconfig.gsize; j = y2 / FCCconfig.gsize; if (CCmark[i][j] == 0) { // 绘制棋子 g.fillOval(x2, y2, FCCconfig.CCPsize, FCCconfig.CCPsize); count++; CCmark[i][j] = t; pill pill = new pill(i,j,t); list.add(pill);
棋子是通过在对应点画圆实现的。但是画圆时需要的参数是(x1,y1,height,wide),且x1,y1是圆外切矩形的左上顶点。而鼠标监听只能获得按下的点,且我们不是总能恰好按到那个点。于是我们就要得到棋盘上纵横线的交点。我选取了比较笨的方法,就是用小学数学加减乘除法,将点击时获取的点换算到我们需要的离其最近的交叉点。
3、菜单栏监听
操作较少,暂不赘述。
三、判断输赢
判断输赢放在了游戏面板监听器中。
// 判断是否获胜 successcheck check=new successcheck( i, j, CCmark); if (check.checkrow() >= 5 || check.checkrank() >=5 || check.checkl() >= 5 || check.checkr() >= 5) { if(CCmark[i][j]==1){ javax.swing.JOptionPane.showMessageDialog(jf, "黑方获胜了!!"); }else if(CCmark[i][j]==-1){ javax.swing.JOptionPane.showMessageDialog(jf, "蓝方获胜了!!"); }
而判断方法则单另写了一个类,方便超找排错。而且分为了横、竖、左斜和右斜四个方法。以方便调用。
/** * 用于检验行 * @return */ public int checkrow() { int pcount = 1;// 定义一个棋子个数计数器 for (int i = x + 1; i <= CCmark.length; i++) {// 向右 if (i < 11 && CCmark[i][y] == CCmark[x][y]) { pcount++; } else break; } for (int i = x - 1; i >= 0; i--) {// 向左 if (i < 11 && CCmark[i][y] == CCmark[x][y]) { pcount++; } else break; } return pcount; } /** * 用于检验列 */ public int checkrank() { int pcount = 1;// 定义一个棋子个数计数器 for (int i = y + 1; i <= CCmark.length; i++) {// 向下 if (i < 11 && CCmark[x][i] == CCmark[x][y]) { pcount++; } else break; } for (int i = y - 1; i >= 0; i--) {// 向上 if (i < 11 && CCmark[x][i] == CCmark[x][y]) { pcount++; } else break; } return pcount; } /** * 用于检验左斜线方向 */ public int checkl() { int pcount = 1;// 定义一个棋子个数计数器 for (int i = x - 1, j = y - 1; i >= 0 && j >= 0; i--, j--) {// 向左上 if (CCmark[i][j] == CCmark[x][y]) { pcount++; } else { break; } } // System.out.println(pcount); for (int i = x + 1, j = y + 1; i < 11 && i < CCmark.length && j < 11 && j < CCmark.length; i++, j++) {// 向右下 if (CCmark[i][j] == CCmark[x][y]) { pcount++; } else { break; } } // System.out.println(pcount); return pcount; } /** * 用于检验右斜线方向 */ public int checkr() { int pcount = 1;// 定义一个棋子个数计数器 for (int i = x + 1, j = y - 1; i < 11 && i < CCmark.length && j >= 0; i++, j--) {// 向右上 if (CCmark[i][j] == CCmark[x][y]) { pcount++; } else { break; } } // System.out.println(pcount); for (int i = x - 1, j = y + 1; i >= 0 && j < 11 && j < CCmark.length; i--, j++) {// 向左下 if (CCmark[i][j] == CCmark[x][y]) { pcount++; } else { break; } } // System.out.println(pcount); return pcount; }
想要判断输赢,就要计算机可以记得棋盘上棋子的位置和颜色。所以,我选择了用二维数组存储这些信息,刚好下标可以存交叉点坐标,而数组的值也可以存棋子颜色,个人认为利用率比较高,比起用队列存,应该要方便些。
四、配置类。
经常玩游戏,知道可以设置许多的配置,原来,这就算配置类的图形化。这样在人性化和可移植性都比较好,所以也学来:
public interface FCCconfig { public final static int row = 11;// 棋盘的行 public final static int rank = 11;// 棋盘的列 public final static int CCPsize = 40;// 棋子直径 public final static int gsize = 50;// 格间距 public final static int x0 = 50;// 棋盘左边缘x坐标 public final static int y0 = 50;// 棋盘上边缘y坐标 public final static int ye = 550;// 棋盘下边缘y坐标 public final static int xe = 550;// 棋盘右边缘x坐标 // 定义一个二维数组用来标记下过棋的位置 public final static int[][] CCmark = new int[FCCconfig.row][FCCconfig.rank]; //定义一个队列用来记录棋子的情况 ListImp<pill> list=new ListImp<pill>();
定义为接口确实调用也方便,修改也方便。
我的收获:
1、果然,好的框架结构,会为后面的开发提供莫大的方便。
2、要充分理解各个类之间的结构,这样可以避免混乱和代码的重复。
3、学会利用接口,和很有实际意义的Config类。
4、数据结构这门课真是大有用处啊!好的结构不仅可以省下不少代码,还可以提高运行效率。
5、罗马不是一天建成的。浮躁是我的弊病,只有一步一步的走才能爬上山,不要期望自己可以瞬间漂移到山顶,这只会是神话。空想主义者并不会比实干家过的好多少!