此文转载自:https://blog.csdn.net/Krumitz/article/details/110236045
目录
3.5.1 rules.AlreadyHadStone 判断已有子
0.前言
我小时候学过一段时间的围棋,可惜脑子不好使,是个臭棋篓子,到现在也有十多年的时间没有下过棋了,但是近几年围棋AI的出现,又让我重新关注了围棋
围棋真的很有意思,千变万化,有人简明的围空,有人进行复杂的战斗,在高手的对局里,一手棋都会对全局产生十分大的影响
最近上班摸鱼,闲来无事,打开了IDEA,想着写一个围棋程序
选择使用Java的原因完全是因为这台电脑的Visual Studio打开太卡了,其实感觉可能用C++或者C#写会更好一点
网上关于围棋基础规则的文章不是很多,基本都是讲述围棋AI的实现,因此在完成了这个简单的围棋小游戏之后,特此记录一下
1.概述
这里先说明本文实现的内容,本文实现了绘制棋盘、黑白两棋交替落子、标记手数、提子、高亮最后一手棋的功能
交替落子只实现了在棋盘内、不能落在有子的交叉点的判断
没有实现对打劫、自杀的判断(其实自杀的判断好像加一两行if判断就好了,但是临近周五下班,赶紧写了一篇记录出来,有兴趣的读者可以自己实现)
提子的算法是参考
的算法实现
他讲提子描述为一个迷宫问题,将相领的相同颜色的棋子视为通路,将不同颜色的棋子视为“墙”,用深度优先搜索算法,只要能搜索出出口,便是有气
这应该是本文中最难实现的一个部分了,其他部分都很简单
不过我的代码中,有些代码在打完之后让我觉得自己很愚蠢,在一开始没有计划好,后来为了不把“屎山”推倒重来,还引入了一些三维数组来实现功能
如果想看其他内容的话,可以去搜索其他的文章了,没必要看这篇入门级的文章
2.结构
我的项目结构如下,其中有些英文是网上查来的,可能不够标准
draw中实现绘图类,跟绘图有关的实现在该包中
BackGround类是一个Frame
ChessPad类是一个Frame里的Panel,绘制了棋盘
HighLight类实现了高亮最后一手的功能
Place类实现了绘制落子、提子
TeNum类实现了绘制手数
main中Main只简单创建BackGround
player中的Player类存储对局棋手的信息,只有两个简单的属性,棋子的颜色和是否轮到他下
stone的Stone类只有一个简单的属性,就是棋子的颜色----黑、白、无
rules中实现了一些简单落子规则的判断
AlreadyHadStone判断该落子点是否已有子
InBoard判断落子点是否在棋盘内
Ko本来想实现打劫的判断,但是还没有实现
Liberty判断是否有气
Take通过Liberty判断是否有气,进而判断是否可以提子
详细的描述在代码注释中,这里不过多赘述
3.代码实现
3.1 main
3.1.1 main.Main
package com.krumitz.main;
import com.krumitz.draw.BackGround;
import com.krumitz.player.Player;
public class Main {
public static void main(String args[]){
//调用创建棋盘
new BackGround();
}
}
3.2 stone
3.2.1 stone.Stone 棋子类
package com.krumitz.stone;
/**
* 棋子类
*/
public class Stone {
public enum StoneColor
{
BLACK,WHITE,NONE
}
private StoneColor stoneColor;
public Stone()
{
this.stoneColor = StoneColor.NONE;
}
public void setStoneColor(StoneColor stoneColor)
{
this.stoneColor = stoneColor;
}
public StoneColor getStoneColor()
{
return this.stoneColor;
}
}
3.3 player
3.3.1 player.Player 棋手类
package com.krumitz.player;
import com.krumitz.stone.Stone;
public class Player {
private Stone stone;
private boolean isMoving;
public Player()
{
stone = new Stone();
stone.setStoneColor(Stone.StoneColor.NONE);
this.isMoving = false;
}
public Stone getStone() { return this.stone; }
public void setStone(Stone.StoneColor stoneColor){ this.stone.setStoneColor(stoneColor); }
public boolean getIsMoving()
{
return this.isMoving;
}
public void setIsMoving(boolean isMoving)
{
this.isMoving = isMoving;
}
}
3.4 draw
3.4.1 draw.BackGround 背景类
package com.krumitz.draw;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
public class BackGround extends Frame {
ChessPad chessPad;
public BackGround()
{
chessPad = new ChessPad();
this.add(chessPad);
this.setSize(600,600);
this.setVisible(true);
this.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
}
}
3.4.2 draw.ChessPad 棋盘类
package com.krumitz.draw;
import com.krumitz.player.Player;
import com.krumitz.rules.AlreadyHadStone;
import com.krumitz.rules.InBoard;
import com.krumitz.rules.Take;
import com.krumitz.stone.Stone;
import java.awt.*;
import java.awt.event.*;
public class ChessPad extends Panel implements MouseListener, ActionListener {
/**
* 声明Player类存储棋手下棋顺序
* 声明落子绘图类用于绘制棋子
* 声明teNum类用于绘制手数
* 声明highLight高亮最后一手
* 声明19*19 move数组,存储已落子的信息
* 声明teNum记录手数
* 声明move_teNum 记录每一个坐标的棋子是第几手棋
* 声明上一手的坐标last_coordinate_x,last_coordinate_y
*/
Player BLACK_PLAYER ;
Player WHITE_PLAYER ;
Place BLACK_STONE;
Place WHITE_STONE;
TeNum class_teNum;
HighLight highLight;
Stone.StoneColor move[][] ;
int teNum;
int move_teNum[][][];
int last_coordinate_x,last_coordinate_y;
/**
*构造棋盘大小、背景、鼠标监听器
*/
ChessPad()
{
// 初始化执黑棋手
BLACK_PLAYER = new Player();
BLACK_PLAYER.setIsMoving(true);
BLACK_PLAYER.setStone(Stone.StoneColor.BLACK);
// 初始化执白棋手
WHITE_PLAYER = new Player();
WHITE_PLAYER.setIsMoving(false);
WHITE_PLAYER.setStone(Stone.StoneColor.WHITE);
// 初始化手数类
class_teNum = new TeNum();
// 初始化高亮最后一手类
highLight = new HighLight();
// 初始化黑棋、白棋
BLACK_STONE = new Place(this);
WHITE_STONE = new Place(this);
// 初始化棋谱数组、手数数组
move = new Stone.StoneColor[19][19];
move_teNum = new int[19][19][1];
for (int i = 0; i < 19; i++)
{
for (int j = 0; j < 19; j++)
{
move[i][j] = Stone.StoneColor.NONE;
move_teNum[i][j][0] = -1;
}
}
//初始化手数、最后一手的坐标
teNum = 1;
last_coordinate_x = 0;
last_coordinate_y = 0;
this.add(BLACK_STONE);
this.add(WHITE_STONE);
this.add(class_teNum);
this.setSize(600,600);
this.setLayout(null);
this.setBackground(Color.ORANGE);
this.addMouseListener(this);
}
/**
* 画棋盘的线和点
* @param g
*/
public void paint(Graphics g)
{
for (int i = 45; i <= 495; i += 25)
{
g.drawLine(i, 45, i, 495);
}
for (int i = 45; i <= 495; i += 25)
{
g.drawLine(45, i, 495, i);
}
//D16
g.fillOval(116,116,8,8);
//Q4
g.fillOval(416,416,8,8);
//D4
g.fillOval(116,416,8,8);
//Q16
g.fillOval(416,116,8,8);
//D10
g.fillOval(116,266,8,8);
//K16
g.fillOval(266,116,8,8);
//Q10
g.fillOval(416,266,8,8);
//K4
g.fillOval(266,416,8,8);
//天元
g.fillOval(266,266,8,8);
}
/**
* 按下鼠标,调用落子类绘图方法
* @param mouseEvent
*/
@Override
public void mouseClicked(MouseEvent mouseEvent)
{
if((mouseEvent.getModifiers() == InputEvent.BUTTON1_MASK))
{
// 这里减数是棋子的宽度、高度的一半 -- 10
int x = (int)mouseEvent.getX()-10;
int y = (int)mouseEvent.getY()-10;
// 这里先求余、相减、再除
// 求余数和除数是棋盘每路之间的宽度 -- 25
// 得到的是棋盘坐标
// -1 为了跟数组对应
int coordinate_x = (x-(x%25))/25-1;
int coordinate_y = (y-(y%25))/25-1;
// 这里用棋盘坐标乘以棋盘每路之间的宽度 -- 25
// 再加上棋子的宽度、高度的一半 -- 10
// 得到的是落子类绘图方法需要的坐标
int place_x = (coordinate_x+1)*25 + 10;
int place_y = (coordinate_y+1)*25 + 10;
// 判断是否在棋盘内
if(InBoard.ifInBoard(coordinate_x,coordinate_y))
{
if(!AlreadyHadStone.ifAlreadyHadStone(move,coordinate_x,coordinate_y))
{
// 黑棋
if(this.BLACK_PLAYER.getIsMoving())
{
// 落子、绘图
Place.placeStone(this.BLACK_PLAYER,place_x, place_y, this.getGraphics());
// 绘制手数
class_teNum.drawTeNum(place_x,place_y,teNum,this.BLACK_PLAYER.getStone().getStoneColor(),this.getGraphics());
// 设置有子
move[coordinate_x][coordinate_y] = this.BLACK_PLAYER.getStone().getStoneColor();
}
// 白棋
if(this.WHITE_PLAYER.getIsMoving())
{
// 落子、绘图
Place.placeStone(this.WHITE_PLAYER,place_x, place_y, this.getGraphics());
// 绘制手数
class_teNum.drawTeNum(place_x,place_y,teNum,this.WHITE_PLAYER.getStone().getStoneColor(),this.getGraphics());
// 设置有子
move[coordinate_x][coordinate_y] = this.WHITE_PLAYER.getStone().getStoneColor();
}
// 手数加1
move_teNum[coordinate_x][coordinate_y][0] = teNum;
teNum ++;
// 如果可以提子
if(Take.takeStones(move,coordinate_x,coordinate_y))
{
takeStones(this.getGraphics());
System.out.println("提子");
}
else
{
System.out.println("落子");
}
// 高亮最后一手,并将倒数第二手的高亮去除
highLight.highLightLastStone(coordinate_x,coordinate_y,last_coordinate_x,last_coordinate_y,move,teNum-1,this.getGraphics());
last_coordinate_x = coordinate_x;
last_coordinate_y = coordinate_y;
// 两级反转.表明包
BLACK_PLAYER.setIsMoving(!(BLACK_PLAYER.getIsMoving()));
WHITE_PLAYER.setIsMoving(!(WHITE_PLAYER.getIsMoving()));
}
else
{
System.out.println("已有子");
}
}
else
{
System.out.println("棋盘外");
}
}
if((mouseEvent.getModifiers() == InputEvent.BUTTON3_MASK))
{
System.out.println("右键");
}
}
@Override
public void actionPerformed(ActionEvent actionEvent) {
}
@Override
public void mousePressed(MouseEvent mouseEvent) {
}
@Override
public void mouseReleased(MouseEvent mouseEvent) {
}
@Override
public void mouseEntered(MouseEvent mouseEvent) {
}
@Override
public void mouseExited(MouseEvent mouseEvent) {
}
// 提子
public void takeStones(Graphics graphics)
{
int coordinate_x,coordinate_y,remove_x,remove_y;
// 获得提子数量
int length[][] = Take.getLength();
// 获得提子坐标
int takeStones[][][] = Take.getTakeStones();
for(int i=0;i<4;i++)
{
// 如果记录的数量不为0,有子可提
if(length[i][0] != 0)
{
for(int j=0;j<length[i][0];j++)
{
// 获得要提的子的坐标
coordinate_x = takeStones[i][j][0];
coordinate_y = takeStones[i][j][1];
// 将坐标转换为绘图坐标
remove_x = (coordinate_x+1)*25 + 10;
remove_y = (coordinate_y+1)*25 + 10;
// 去除棋谱上该子
move[coordinate_x][coordinate_y] = Stone.StoneColor.NONE;
// 提子
Place.takeStone(remove_x,remove_y,graphics);
}
}
}
// 重绘
removeAll();
paint(graphics);
// 重绘仍在棋盘上的棋子
for (int i = 0; i < 19; i++)
{
for (int j = 0; j < 19; j++)
{
if (move[i][j] == Stone.StoneColor.BLACK)
{
Place.placeStone(this.BLACK_PLAYER,((i+1)*25 + 10),((j+1)*25 + 10),this.getGraphics());
class_teNum.drawTeNum(((i+1)*25 + 10),((j+1)*25 + 10),move_teNum[i][j][0],move[i][j],this.getGraphics());
}
if (move[i][j] == Stone.StoneColor.WHITE)
{
Place.placeStone(this.WHITE_PLAYER,((i+1)*25 + 10),((j+1)*25 + 10),this.getGraphics());
class_teNum.drawTeNum(((i+1)*25 + 10),((j+1)*25 + 10),move_teNum[i][j][0],move[i][j],this.getGraphics());
}
if(move_teNum[i][j][0] == teNum)
{
highLight.highLightLastStone(i,j,0,0,move,0,this.getGraphics());
}
}
}
}
}
3.4.3 draw.Place 落子类
package com.krumitz.draw;
import com.krumitz.player.Player;
import com.krumitz.stone.Stone;
import java.awt.*;
public class Place extends Panel {
ChessPad chessPad;
public Place(ChessPad chessPad)
{
setSize(20,20);
this.chessPad = chessPad;
}
// 落子
public static void placeStone(Player player,int x,int y,Graphics graphics)
{
if(player.getStone().getStoneColor() == Stone.StoneColor.BLACK)
{
graphics.setColor(Color.BLACK);
graphics.fillOval(x,y,20,20);
}
if(player.getStone().getStoneColor() == Stone.StoneColor.WHITE)
{
graphics.setColor(Color.WHITE);
graphics.fillOval(x,y,20,20);
}
}
// 提子
public static void takeStone(int x,int y,Graphics graphics)
{
graphics.clearRect(x,y,20,20);
}
}
3.4.4 draw.TeNum 手数类
绘制手数,让手数的文字居中是根据
0如何在Java中居中显示Graphics.drawString()?
实现的
package com.krumitz.draw;
import com.krumitz.stone.Stone;
import java.awt.*;
public class TeNum extends Panel {
public static void drawTeNum(int place_x, int place_y, int teNum, Stone.StoneColor color, Graphics graphics)
{
if(color == Stone.StoneColor.BLACK)
{
graphics.setColor(Color.WHITE);
}
if(color == Stone.StoneColor.WHITE)
{
graphics.setColor(Color.BLACK);
}
Font font = graphics.getFont();
FontMetrics metrics = graphics.getFontMetrics(font);
// Determine the X coordinate for the text
int teNum_x = place_x + (20 - metrics.stringWidth(String.valueOf(teNum))) / 2;
// Determine the Y coordinate for the text (note we add the ascent, as in java 2d 0 is top of the screen)
int teNum_y = place_y + ((20 - metrics.getHeight()) / 2) + metrics.getAscent();
// Set the font
graphics.setFont(font);
// Draw the String
graphics.drawString(String.valueOf(teNum),teNum_x,teNum_y);
}
}
3.4.5 draw.HighLight 高亮类
这个方法实现的时候十分的偷懒,去除倒数第二颗棋子的高亮的时候,直接用棋盘底色覆盖了
package com.krumitz.draw;
import com.krumitz.stone.Stone;
import java.awt.*;
/**
* 高亮最后一手
*/
public class HighLight{
private static BasicStroke strokeLine = new BasicStroke(1.5f);
// 给最后一手棋子加一圈红色边框
public static void highLightLastStone(int coordinate_x, int coordinate_y,
int last_coordinate_x, int last_coordinate_y,
Stone.StoneColor move[][],int teNum, Graphics graphics)
{
graphics.setColor(Color.RED);
int draw_x = (coordinate_x+1)*25 + 10;
int draw_y = (coordinate_y+1)*25 + 10;
//
Graphics2D g = (Graphics2D) graphics;
g.setStroke(strokeLine);
g.drawOval(draw_x,draw_y,20,20);
// 如果手数大于1,把倒数第二手的红色边框去除
if(teNum > 1)
{
removeLastButOneLight(last_coordinate_x,last_coordinate_y,move,g);
}
}
// 直接偷懒,用棋盘底色在原来的那一圈上面再画一圈
public static void removeLastButOneLight(int last_coordinate_x, int last_coordinate_y, Stone.StoneColor move[][], Graphics g)
{
int draw_x = (last_coordinate_x + 1) * 25 + 10;
int draw_y = (last_coordinate_y + 1) * 25 + 10;
if (move[last_coordinate_x][last_coordinate_y] == Stone.StoneColor.BLACK) {
g.setColor(Color.BLACK);
}
if (move[last_coordinate_x][last_coordinate_y] == Stone.StoneColor.WHITE) {
g.setColor(Color.WHITE);
}
g.drawOval(draw_x, draw_y, 20, 20);
g.setColor(Color.ORANGE);
g.drawOval(draw_x,draw_y,20,20);
}
}
3.5 rules
3.5.1 rules.AlreadyHadStone 判断已有子
package com.krumitz.rules;
import com.krumitz.stone.Stone;
/**
* 判断落子点是否已有子
*/
public class AlreadyHadStone {
public static boolean ifAlreadyHadStone(Stone.StoneColor move[][],int coordinate_x,int coordinate_y)
{
if(move[coordinate_x][coordinate_y] == Stone.StoneColor.NONE)
{
return false;
}
return true;
}
}
3.5.2 rules.Inboard 判断棋盘内
package com.krumitz.rules;
/**
* 判断是否在棋盘内
*/
public class InBoard {
public static boolean ifInBoard(int coordinate_x, int coordinate_y)
{
if((coordinate_x>=0 && coordinate_x<=18) && (coordinate_y>=0 && coordinate_y<=18))
{
return true;
}
else
{
return false;
}
}
}
3.5.3 rules.Liberty 判断有气
package com.krumitz.rules;
import com.krumitz.stone.Stone;
/**
* 气
*/
public class Liberty {
// 声明记录数组
private static int[][] visited = new int[19][19];
// 声明上下左右四个方向
private static int[][] directions = {{0,1},{1,0},{-1,0},{0,-1}};
// 声明记录提子的坐标的二维数组
private static int[][] liberty_takeStones = new int[19][2];
// 声明记录二维数组的长度
private static int liberty_length;
// 记录数组初始化函数
private static void setUpVisited()
{
for (int i = 0; i < 19; i++)
{
for (int j = 0; j < 19; j++)
{
visited[i][j] = 0;
}
}
}
private static void setUpTakeStones()
{
for (int i = 0; i < 19; i++)
{
for (int j = 0; j < 2; j++)
{
liberty_takeStones[i][j] = 0;
}
}
}
private static boolean DFS(Stone.StoneColor move[][], int coordinate_x, int coordinate_y)
{
int direction_x,direction_y;
// 设置已访问标志1
visited[coordinate_x][coordinate_y] = 1;
// 将当前子的坐标存入提子数组,数组长度+1
liberty_takeStones[liberty_length][0] = coordinate_x;
liberty_takeStones[liberty_length][1] = coordinate_y;
liberty_length++;
// 遍历上下左右四个方向
for(int i = 0;i < 4;i++)
{
direction_x = coordinate_x + directions[i][0];
direction_y = coordinate_y + directions[i][1];
// 判断是否在棋盘内
if(!(InBoard.ifInBoard(direction_x,direction_y)))
{
// 不在棋盘内就遍历下一个点
continue;
}
// 如果在棋盘内,且没访问过
else if(visited[direction_x][direction_y] == 0)
{
// 如果该位置无子,则有气,返回true
if(move[direction_x][direction_y] == Stone.StoneColor.NONE)
{
// 这些输出是在debug的时候用的,可以删掉
System.out.println("有气: "+direction_x+" "+direction_y);
return true;
}
// 如果该位置有子,且子的颜色不同,就遍历下一个点
if(move[direction_x][direction_y] != move[coordinate_x][coordinate_y])
{
System.out.println("不同色: "+direction_x+" "+direction_y);
continue;
}
// 如果该位置有子,且颜色相同,递归遍历该子
if(move[direction_x][direction_y] == move[coordinate_x][coordinate_y])
{
System.out.println("同色: "+direction_x+" "+direction_y);
//如果下一个子返回true
if(DFS(move,direction_x,direction_y))
{
return true;
}
}
}
}
// 如果遍历完都没气,返回false
return false;
}
// 判断是否有气函数
public static boolean hasLiberty(Stone.StoneColor move[][], int coordinate_x, int coordinate_y)
{
// 初始化遍历记录访问数组
setUpVisited();
setUpTakeStones();
// 重置记录长度
liberty_length = 0;
System.out.println("hasLiberty开始: "+coordinate_x+" "+coordinate_y);
if(DFS(move,coordinate_x,coordinate_y))
{
System.out.println("hasLiberty结束,返回true");
return true;
}
else
{
System.out.println("hasLiberty结束,返回false");
return false;
}
}
public static int[][] getTakeStones()
{
return liberty_takeStones;
}
public static int getLength()
{
return liberty_length;
}
}
3.5.4 rules.Take 判断提子
package com.krumitz.rules;
import com.krumitz.stone.Stone;
/**
* 提子
*/
public class Take {
// 声明上下左右四个方向
private static int[][] directions = {{0,1},{1,0},{-1,0},{0,-1}};
// 记录上下左右四颗子的hasLiberty返回的长度
private static int[][] take_length=new int[4][1];
// 记录上下左右四颗子的hasLiberty返回的提子数组,这里感觉提的子不会很多,因此长度只有19
// 4表示4个方向
// 19表示第N个要提的子
// 最后两位表示第N个要提的子的x、y坐标
private static int[][][] take_takeStones=new int[4][19][2];
// 初始化length
private static void setUpLength()
{
for(int i=0;i<4;i++)
{
take_length[i][0] = 0;
}
}
// 初始化takeStones
private static void setUpTakeStones()
{
for (int i = 0; i < 4; i++)
{
for (int j = 0; j < 19; j++)
{
for (int k = 0; k < 2; k++)
{
take_takeStones[i][j][k] = 0;
}
}
}
}
// 提子函数
public static boolean takeStones(Stone.StoneColor move[][],int coordinate_x,int coordinate_y)
{
// flag为1则有子可提
int flag = 0;
// 初始化记录数组
setUpLength();
setUpTakeStones();
int direction_x,direction_y;
// 获得当前局面最后一手棋的颜色
Stone.StoneColor color = move[coordinate_x][coordinate_y];
// 判断该手棋上下左右四个方向的相领棋子
for(int i=0;i<4;i++)
{
direction_x = coordinate_x + directions[i][0];
direction_y = coordinate_y + directions[i][1];
// 如果不在棋盘内,继续下一个循环
if(!(InBoard.ifInBoard(direction_x,direction_y)))
{
continue;
}
// 如果该方向上的有棋
// 且棋子颜色与当前局面最后一手棋颜色不同
else if(move[direction_x][direction_y] != color && move[direction_x][direction_y] != Stone.StoneColor.NONE)
{
// 如果该棋子所在的块有气,继续下一个循环
if(Liberty.hasLiberty(move,direction_x,direction_y))
{
continue;
}
// 如果没气,flag为1
else
{
flag = 1;
// 记录第i个方向上的提子的数量
take_length[i][0] = Liberty.getLength();
// 记录第i个方向上的提子的坐标
int temp[][] = Liberty.getTakeStones();
for(int j=0;j<19;j++)
{
for(int k=0;k<2;k++)
{
take_takeStones[i][j][k] = temp[j][k];
}
}
}
}
}
// flag不为0,可提子,返回true
if(flag!=0)
{
return true;
}
else
{
return false;
}
}
public static int[][][] getTakeStones()
{
return take_takeStones;
}
public static int[][] getLength()
{
return take_length;
}
}
4. 运行结果 & 小结
本次实现的功能还是很简单的,赶在周五下班之前完成了这个程序并且记录了下来
后续仍有继续改进已有功能和增加新的功能的意愿,如果有空实现了的话,将会继续更新