• JavaFx-----五子棋(单机版和对战版)


    一.项目介绍

    使用 JavaFx + MySql + MyBatis 实现单机和网络版五子棋对战.

    二.功能介绍

    1. 登录

      -- 使用MyBatis和JDBC连接数据库, 实现登录功能

      -- 使用I/O流,实现本地文件记住密码功能

     2.注册

      -- 使用MyBatis和JDBC连接数据库, 实现注册功能

      -- 注册完密码后,返回登录界面,自动填充注册的用户名和密码

     

     3.网络模式选择

     4.单机版

      -- 对战

      -- 新局

      -- 悔棋

      -- 保存棋谱

      -- 打开棋谱

     5.网络版

      -- 显示在线用户

      -- 连接对战

     

    三. 项目源码

    package com.wn.main;
    
    import java.net.InetAddress;
    import java.net.UnknownHostException;
    
    import com.wn.pojo.Global;
    import com.wn.ui.LoginStage;
    
    import javafx.application.Application;
    import javafx.stage.Stage;
    
    public class MainApplication extends Application {
    
    	public static void main(String[] args) {
    		launch(args);
    	}
    	@Override
    	public void start(Stage primaryStage) throws Exception {
    		//获取当前用户的本机IP地址
    		InetAddress inetAddress = null;
    		try {
    			inetAddress = InetAddress.getLocalHost();
    		} catch (UnknownHostException e) {
    			e.printStackTrace();
    		}
    		//将本机ip存储到全局变量中
    		Global.myIp = inetAddress.getHostAddress();
    		new LoginStage().show();		
    	}
    }
    
    package com.wn.ui;
    
    import com.wn.CommonUtils.DAOUtils;
    import com.wn.CommonUtils.DataHanding;
    import com.wn.dao.UserDAO;
    import com.wn.dao.UserStatusDAO;
    import com.wn.pojo.Global;
    import com.wn.pojo.User;
    import com.wn.pojo.UserStatus;
    
    import javafx.event.ActionEvent;
    import javafx.event.EventHandler;
    import javafx.scene.Scene;
    import javafx.scene.control.Alert;
    import javafx.scene.control.Alert.AlertType;
    import javafx.scene.control.Button;
    import javafx.scene.control.CheckBox;
    import javafx.scene.control.Label;
    import javafx.scene.control.PasswordField;
    import javafx.scene.control.TextField;
    import javafx.scene.image.Image;
    import javafx.scene.layout.Background;
    import javafx.scene.layout.BackgroundFill;
    import javafx.scene.layout.Pane;
    import javafx.scene.paint.Color;
    import javafx.scene.text.Font;
    import javafx.scene.text.Text;
    import javafx.stage.Stage;
    
    public class LoginStage extends Stage implements EventHandler<ActionEvent> {
    	private Button loginBtn;
    	private Button registerBtn;
    	private TextField account;
    	private PasswordField password;
    	private CheckBox checkBox;
    
    	public LoginStage() {
    		setTitle("五子棋-登录");
    		Pane pane = new Pane();
    		Scene scene = new Scene(pane, 400, 250);
    		setScene(scene);
    		getIcons().add(new Image("Imgs/icon-2.jpg"));
    		pane.setBackground(new Background(new BackgroundFill(Color.GAINSBORO, null, null)));
    		// 设置窗口不可变
    		setResizable(false);
    
    		// 标题:用户登录
    		Label title = new Label("用户登录");
    		title.setFont(new Font("KaiTi", 30));
    		title.setLayoutX(150);
    		title.setLayoutY(30);
    		// 账户标签
    		Label accountLabel = new Label("账户 : ");
    		accountLabel.setFont(new Font("KaiTi", 20));
    		accountLabel.setLayoutX(80);
    		accountLabel.setLayoutY(90);
    		// 账户输入框
    		account = new TextField();
    		account.setLayoutX(150);
    		account.setLayoutY(90);
    
    		// 密码标签
    		Label passwordLabel = new Label("密码 : ");
    		passwordLabel.setFont(new Font("KaiTi", 20));
    		passwordLabel.setLayoutX(80);
    		passwordLabel.setLayoutY(140);
    		// 密码输入框
    		password = new PasswordField();
    		password.setLayoutX(150);
    		password.setLayoutY(140);
    		// 记住密码
    		checkBox = new CheckBox();
    		checkBox.setLayoutX(80);
    		checkBox.setLayoutY(180);
    		Text text = new Text(100, 195,"记住密码");
    		text.setFont(new Font("KaiTi",15));
    		// 登录按钮
    		loginBtn = new Button("登	录");
    		loginBtn.setLayoutX(80);
    		loginBtn.setLayoutY(210);
    		loginBtn.setDefaultButton(true);
    		loginBtn.setOnAction(this);
    		// 注册按钮
    		registerBtn = new Button("注	册");
    		registerBtn.setLayoutX(250);
    		registerBtn.setLayoutY(210);
    		registerBtn.setOnAction(this);
    		// 把元素添加到面板
    		pane.getChildren().addAll(title, accountLabel, account, passwordLabel, 
    				password, loginBtn, registerBtn,checkBox,text);
    		
    		// 如果用户刚注册,则在登录界面读取用户注册的账户和密码
    		String content = null;
    		if (Global.account != null && Global.password != null) {
    			this.account.setText(Global.account);
    			this.password.setText(Global.password);
    			//读取用户上一次在本机登录的账户和密码
    		}else if ((content=DataHanding.readLastAccount())!=null) {
    			String[] strings = content.split(",");
    			if (strings[strings.length-1].equals(Global.myIp)) {
    				account.setText(strings[0]);
    				password.setText(strings[1]);
    			}
    		}
    	}
    	
    	
    	
    	public void login() {
    		String accountText = account.getText();
    		String passwordText = password.getText();
    		Global.account = accountText;
    		//用户名或密码不能为空
    		if (accountText.equals("")||passwordText.equals("")) {
    			Alert alert = new Alert(AlertType.INFORMATION);
    			alert.setContentText("用户名或密码不能为空");
    			alert.show();
    			return;
    		}
    		//获取用户当前登录状态,如果已登录,不能重复登录
    		UserStatusDAO statusDAO = DAOUtils.getMapper(UserStatusDAO.class);
    		UserStatus status = statusDAO.selectInfoByAccount(accountText);
    		if (status.getStatus() == 1) {
    			Alert alert = new Alert(AlertType.INFORMATION);
    			alert.setContentText("当前用户已在线,不能重复登录");
    			alert.show();
    			return;
    		}
    		// 获取UserDAO的实现类
    		UserDAO userDAO = DAOUtils.getMapper(UserDAO.class);
    		// 根据用户输入的account获得User对象
    		User user = userDAO.getByAccount(accountText);
    		if (user != null && user.getPassword().equals(passwordText)) {
    			//登录成功,判断用户是否选择记住密码
    			if (checkBox.isSelected()) {
    				//用户选择记住密码,调用记住密码方法
    				DataHanding.rememberLastAccount(accountText, passwordText,Global.myIp);
    			}
    			//更新用户状态为在线状态
    			statusDAO.updateStatusByAccount(accountText,Global.myIp, 1);
    			// 已设置自动提交事务
    			// 打开游戏模式选择界面
    			new ModeChooseStage().show();
    			this.close();
    		} else {
    			Alert alert = new Alert(AlertType.INFORMATION);
    			alert.setContentText("用户名或密码错误 ,登录失败");
    			alert.showAndWait();
    		}
    	}
    	
    	
    
    	@Override
    	public void handle(ActionEvent event) {
    		//获取用户点击的按钮
    		Button btn = (Button) event.getSource();
    		String text = btn.getText();
    		switch (text) {
    		case "登	录":
    			login();
    			break;
    		case "注	册":
    			new RegisterStage().show();
    			this.close();
    			break;
    		}
    	}
    }
    

      

    package com.wn.ui;
    
    import com.wn.CommonUtils.DAOUtils;
    import com.wn.CommonUtils.DataHanding;
    import com.wn.dao.UserDAO;
    import com.wn.dao.UserStatusDAO;
    import com.wn.pojo.Global;
    import com.wn.pojo.User;
    import com.wn.pojo.UserStatus;
    
    import javafx.event.ActionEvent;
    import javafx.event.EventHandler;
    import javafx.scene.Scene;
    import javafx.scene.control.Alert;
    import javafx.scene.control.Alert.AlertType;
    import javafx.scene.control.Button;
    import javafx.scene.control.CheckBox;
    import javafx.scene.control.Label;
    import javafx.scene.control.PasswordField;
    import javafx.scene.control.TextField;
    import javafx.scene.image.Image;
    import javafx.scene.layout.Background;
    import javafx.scene.layout.BackgroundFill;
    import javafx.scene.layout.Pane;
    import javafx.scene.paint.Color;
    import javafx.scene.text.Font;
    import javafx.scene.text.Text;
    import javafx.stage.Stage;
    
    public class LoginStage extends Stage implements EventHandler<ActionEvent> {
    	private Button loginBtn;
    	private Button registerBtn;
    	private TextField account;
    	private PasswordField password;
    	private CheckBox checkBox;
    
    	public LoginStage() {
    		setTitle("五子棋-登录");
    		Pane pane = new Pane();
    		Scene scene = new Scene(pane, 400, 250);
    		setScene(scene);
    		getIcons().add(new Image("Imgs/icon-2.jpg"));
    		pane.setBackground(new Background(new BackgroundFill(Color.GAINSBORO, null, null)));
    		// 设置窗口不可变
    		setResizable(false);
    
    		// 标题:用户登录
    		Label title = new Label("用户登录");
    		title.setFont(new Font("KaiTi", 30));
    		title.setLayoutX(150);
    		title.setLayoutY(30);
    		// 账户标签
    		Label accountLabel = new Label("账户 : ");
    		accountLabel.setFont(new Font("KaiTi", 20));
    		accountLabel.setLayoutX(80);
    		accountLabel.setLayoutY(90);
    		// 账户输入框
    		account = new TextField();
    		account.setLayoutX(150);
    		account.setLayoutY(90);
    
    		// 密码标签
    		Label passwordLabel = new Label("密码 : ");
    		passwordLabel.setFont(new Font("KaiTi", 20));
    		passwordLabel.setLayoutX(80);
    		passwordLabel.setLayoutY(140);
    		// 密码输入框
    		password = new PasswordField();
    		password.setLayoutX(150);
    		password.setLayoutY(140);
    		// 记住密码
    		checkBox = new CheckBox();
    		checkBox.setLayoutX(80);
    		checkBox.setLayoutY(180);
    		Text text = new Text(100, 195,"记住密码");
    		text.setFont(new Font("KaiTi",15));
    		// 登录按钮
    		loginBtn = new Button("登	录");
    		loginBtn.setLayoutX(80);
    		loginBtn.setLayoutY(210);
    		loginBtn.setDefaultButton(true);
    		loginBtn.setOnAction(this);
    		// 注册按钮
    		registerBtn = new Button("注	册");
    		registerBtn.setLayoutX(250);
    		registerBtn.setLayoutY(210);
    		registerBtn.setOnAction(this);
    		// 把元素添加到面板
    		pane.getChildren().addAll(title, accountLabel, account, passwordLabel, 
    				password, loginBtn, registerBtn,checkBox,text);
    		
    		// 如果用户刚注册,则在登录界面读取用户注册的账户和密码
    		String content = null;
    		if (Global.account != null && Global.password != null) {
    			this.account.setText(Global.account);
    			this.password.setText(Global.password);
    			//读取用户上一次在本机登录的账户和密码
    		}else if ((content=DataHanding.readLastAccount())!=null) {
    			String[] strings = content.split(",");
    			if (strings[strings.length-1].equals(Global.myIp)) {
    				account.setText(strings[0]);
    				password.setText(strings[1]);
    			}
    		}
    	}
    	
    	
    	
    	public void login() {
    		String accountText = account.getText();
    		String passwordText = password.getText();
    		Global.account = accountText;
    		//用户名或密码不能为空
    		if (accountText.equals("")||passwordText.equals("")) {
    			Alert alert = new Alert(AlertType.INFORMATION);
    			alert.setContentText("用户名或密码不能为空");
    			alert.show();
    			return;
    		}
    		//获取用户当前登录状态,如果已登录,不能重复登录
    		UserStatusDAO statusDAO = DAOUtils.getMapper(UserStatusDAO.class);
    		UserStatus status = statusDAO.selectInfoByAccount(accountText);
    		if (status.getStatus() == 1) {
    			Alert alert = new Alert(AlertType.INFORMATION);
    			alert.setContentText("当前用户已在线,不能重复登录");
    			alert.show();
    			return;
    		}
    		// 获取UserDAO的实现类
    		UserDAO userDAO = DAOUtils.getMapper(UserDAO.class);
    		// 根据用户输入的account获得User对象
    		User user = userDAO.getByAccount(accountText);
    		if (user != null && user.getPassword().equals(passwordText)) {
    			//登录成功,判断用户是否选择记住密码
    			if (checkBox.isSelected()) {
    				//用户选择记住密码,调用记住密码方法
    				DataHanding.rememberLastAccount(accountText, passwordText,Global.myIp);
    			}
    			//更新用户状态为在线状态
    			statusDAO.updateStatusByAccount(accountText,Global.myIp, 1);
    			// 已设置自动提交事务
    			// 打开游戏模式选择界面
    			new ModeChooseStage().show();
    			this.close();
    		} else {
    			Alert alert = new Alert(AlertType.INFORMATION);
    			alert.setContentText("用户名或密码错误 ,登录失败");
    			alert.showAndWait();
    		}
    	}
    	
    	
    
    	@Override
    	public void handle(ActionEvent event) {
    		//获取用户点击的按钮
    		Button btn = (Button) event.getSource();
    		String text = btn.getText();
    		switch (text) {
    		case "登	录":
    			login();
    			break;
    		case "注	册":
    			new RegisterStage().show();
    			this.close();
    			break;
    		}
    	}
    }
    

      

    package com.wn.ui;
    
    import com.wn.CommonUtils.DAOUtils;
    import com.wn.dao.UserStatusDAO;
    import com.wn.pojo.ChessMode;
    import com.wn.pojo.Global;
    
    import javafx.event.ActionEvent;
    import javafx.event.EventHandler;
    import javafx.scene.Scene;
    import javafx.scene.control.Button;
    import javafx.scene.control.Label;
    import javafx.scene.image.Image;
    import javafx.scene.image.ImageView;
    import javafx.scene.layout.Background;
    import javafx.scene.layout.BackgroundFill;
    import javafx.scene.layout.Pane;
    import javafx.scene.paint.Color;
    import javafx.scene.text.Font;
    import javafx.stage.Stage;
    import javafx.stage.WindowEvent;
    
    public class ModeChooseStage extends Stage implements EventHandler<ActionEvent> {
    	Pane pane;
    	public ModeChooseStage() {
    		setTitle("五林大会");
    		pane = new Pane();
    		Scene scene = new Scene(pane,460,260);
    		setScene(scene);
    		getIcons().add(new Image("Imgs/icon-2.jpg"));
    		Image image = new Image("Imgs/Gobang.jpg",480,280,false,true,true);
    		ImageView imageView = new ImageView(image); 
    		pane.getChildren().add(imageView);
    		pane.setBackground(new Background(
    				new BackgroundFill(Color.DARKOLIVEGREEN, null, null)));
    		// 设置窗口不可变
    		setResizable(false);
    		
    		setOnCloseRequest(new EventHandler<WindowEvent>() {
    
    			@Override
    			public void handle(WindowEvent event) {
    				UserStatusDAO statusDAO = DAOUtils.getMapper(UserStatusDAO.class);
    				statusDAO.setStatusByAccount(Global.account, 0);
    			}
    		});
    		
    		Label title = new Label("选择游戏模式");
    		title.setFont(new Font("KaiTi", 30));
    		title.setLayoutX(155);
    		title.setLayoutY(80);
    		
    		//添加单机版和网络部按钮
    		Button buttonOnline = new Button("网络版");
    		buttonOnline.setFont(new Font("KaiTi",20));
    		buttonOnline.setPrefSize(100, 60);
    		buttonOnline.setLayoutX(280);
    		buttonOnline.setLayoutY(140);
    		buttonOnline.setOnAction(this);
    		
    		Button buttonSingle = new Button("单机版");
    		buttonSingle.setFont(new Font("KaiTi",20));
    		buttonSingle.setPrefSize(100, 60);
    		buttonSingle.setLayoutX(80);
    		buttonSingle.setLayoutY(140);
    		buttonSingle.setOnAction(this);
    		//将按钮添加进面板
    		pane.getChildren().addAll(title,buttonOnline,buttonSingle);
    		
    	}
    	
    	
    	/**
    	 * 	在线模式
    	 * @author Dracarys
    	 */
    	public void playOnineVersion() {
    		new OnlineConfigStage().show();
    		this.close();
    		Global.mode = ChessMode.NETWORK;
    	}
    	
    	/**
    	 *	单机版按钮及单机版游戏事件 
    	 * @author Dracarys
    	 * @param pane
    	 */
    	public void playSingleVersion() {
    		try {
    			new GameStage().show();
    			Global.mode = ChessMode.SINGLE;
    			this.close();
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}	
    		
    	}
    
    	@Override
    	public void handle(ActionEvent event) {
    		Button btn = (Button)event.getSource();
    		switch (btn.getText()) {
    		case "单机版":
    			playSingleVersion();
    			break;
    
    		case "网络版":
    			playOnineVersion();
    			break;
    		}
    		
    	}
    
    }
    

      

    package com.wn.ui;
    
    import java.util.List;
    
    import com.wn.CommonUtils.DAOUtils;
    import com.wn.CommonUtils.NetUtils;
    import com.wn.CommonUtils.ReceiveThread;
    import com.wn.dao.UserStatusDAO;
    import com.wn.pojo.ConnectionMessage;
    import com.wn.pojo.Global;
    import com.wn.pojo.UserStatus;
    
    import javafx.event.ActionEvent;
    import javafx.event.EventHandler;
    import javafx.scene.Scene;
    import javafx.scene.control.Alert;
    import javafx.scene.control.Alert.AlertType;
    import javafx.scene.control.Button;
    import javafx.scene.control.Label;
    import javafx.scene.control.TextField;
    import javafx.scene.image.Image;
    import javafx.scene.layout.Background;
    import javafx.scene.layout.BackgroundFill;
    import javafx.scene.layout.Pane;
    import javafx.scene.paint.Color;
    import javafx.scene.text.Font;
    import javafx.stage.Stage;
    import javafx.stage.WindowEvent;
    
    public class OnlineConfigStage extends Stage implements EventHandler<ActionEvent> {
    	private Pane pane;          //舞台面板
    	private TextField myPort;	//本机端口
    	private TextField conIp;	//连接ip
    	private TextField conPort;	//连接端口
    	private List<UserStatus> list;	//在线用户列表
    	
    	public OnlineConfigStage(){
    		setTitle("网络连接设置");
    		pane = new Pane();
    		Scene scene = new Scene(pane,600,400);
    		setScene(scene);
    		getIcons().add(new Image("Imgs/icon-2.jpg"));
    		pane.setBackground(new Background(new BackgroundFill(Color.GAINSBORO, null, null)));
    		// 设置窗口不可变
    		setResizable(false);
    		addElement();
    		getOnlineUser();
    		
    		//当用户退出程序时,将用户的状态设置为下线
    		setOnCloseRequest(new EventHandler<WindowEvent>() {
    
    			@Override
    			public void handle(WindowEvent event) {
    				UserStatusDAO statusDAO = DAOUtils.getMapper(UserStatusDAO.class);
    				statusDAO.setStatusByAccount(Global.account, 0);
    			}
    		});
    		
    	}
    	
    	private void addTipsInfo() {
    		//在线玩家标题提示
    		Label title = new Label("当前在线玩家");
    		title.setFont(new Font("KaiTi", 30));
    		title.setLayoutX(350);
    		title.setLayoutY(30);
    		pane.getChildren().add(title);
    		//在线用户信息行
    		String[] tipsInfo = {"用户名","连接IP","连接PORT"};
    		for (int i = 0; i < tipsInfo.length; i++) {
    			Label Lable = new Label(tipsInfo[i]);
    			Lable.setFont(new Font("KaiTi", 20));
    			Lable.setLayoutX(280+100*i);
    			Lable.setLayoutY(80);
    			pane.getChildren().add(Lable);
    		}
    	}
    	
    	private void addElement() {
    		//网络连接设置标题提示
    		Label title = new Label("网络连接设置");
    		title.setFont(new Font("KaiTi", 30));
    		title.setLayoutX(50);
    		title.setLayoutY(30);
    		//连接地址文本提示
    		Label myPortLable = new Label("本机端口");
    		myPortLable.setFont(new Font("KaiTi", 20));
    		myPortLable.setLayoutX(60);
    		myPortLable.setLayoutY(90);
    		//添加本机端口输入框
    		myPort = new TextField();
    		myPort.setLayoutX(60);
    		myPort.setLayoutY(120);
    		myPort.setText("6666"); //测试port
    		//连接端口文本提示
    		Label conIPLable = new Label("连接IP");
    		conIPLable.setFont(new Font("KaiTi", 20));
    		conIPLable.setLayoutX(60);
    		conIPLable.setLayoutY(160);
    		//添加连接IP输入框
    		conIp = new TextField();
    		conIp.setLayoutX(60);
    		conIp.setLayoutY(190);
    		conIp.setText("192.172.4.8"); //测试ip
    		//连接端口文本提示
    		Label conPortLable = new Label("连接端口");
    		conPortLable.setFont(new Font("KaiTi", 20));
    		conPortLable.setLayoutX(60);
    		conPortLable.setLayoutY(230);
    		//添加连接PORT输入框
    		conPort = new TextField();
    		conPort.setLayoutX(60);
    		conPort.setLayoutY(260);
    		conPort.setText("6666"); //测试port
    		
    		//添加确定和取消按钮
    		Button conBtn = new Button("连	接");
    		conBtn.setPrefSize(150, 30);
    		conBtn.setLayoutX(60);
    		conBtn.setLayoutY(320);
    		conBtn.setOnAction(this);
    		
    		Button canBtn = new Button("取	消");
    		canBtn.setPrefSize(150, 25);
    		canBtn.setLayoutX(60);
    		canBtn.setLayoutY(360);
    		canBtn.setOnAction(this);
    		
    		pane.getChildren().addAll(title,myPortLable,myPort,conIPLable,conIp,conPortLable,conPort,conBtn,canBtn);
    	}
    	
    		
    	
    	private void getOnlineUser() {
    		//获取UserStatusDAO实现类
    		UserStatusDAO statusDAO = DAOUtils.getMapper(UserStatusDAO.class);
    		//获取在线用户列表
    		list = statusDAO.selectInfoByStatus(1);
    		if (list.size()<=1) {
    			return;
    		}
    		addTipsInfo();
    		int i = 0;
    		for (UserStatus userStatus : list) {
    			if (!userStatus.getAccount().equals(Global.account)) {
    				//添加用户名信息
    				Label accountLable = new Label(userStatus.getAccount());
    				accountLable.setFont(new Font("KaiTi", 20));
    				accountLable.setLayoutX(290);
    				accountLable.setLayoutY(120+60*i);
    				
    				//添加ip信息
    				Label ipLable = new Label(userStatus.getIp());
    				ipLable.setFont(new Font("KaiTi", 20));
    				ipLable.setLayoutX(360);
    				ipLable.setLayoutY(120+60*i);
    				
    				//添加port信息
    				Label portLable = new Label(userStatus.getPort()+"");
    				portLable.setFont(new Font("KaiTi", 20));
    				portLable.setLayoutX(500);
    				portLable.setLayoutY(120+60*i);
    				pane.getChildren().addAll(accountLable,ipLable,portLable);
    				i++;
    			}
    		}
    	}
    	
    	public void connect(){
    		//当网络连接信息未填写时,弹窗提醒
    		if (myPort.getText().equals("") || conIp.getText().equals("") || conPort.getText().equals("")) {
    			Alert alert = new Alert(AlertType.INFORMATION,"网络连接信息不能为空");
    			alert.initOwner(this);
    			alert.showAndWait();
    			return;
    		}
    		//如果没有在线用户时,无法正常连接
    		if (list.size()<=1) {
    			Alert alert = new Alert(AlertType.INFORMATION,"当前无在线用户,无法进行连接");
    			alert.initOwner(this);
    			alert.showAndWait();
    			return;
    		}
    		
    		//尝试将用户输入的端口信息转为int型
    		try {
    			Global.myPort = Integer.parseInt(myPort.getText());
    			Global.oppoIp = conIp.getText();
    			Global.oppoPort = Integer.parseInt(conPort.getText());
    		} catch (NumberFormatException e1) {
    			e1.printStackTrace();
    			return;
    		}
    		try {
    			//启动线程,接收客户端连接
    			ReceiveThread receiveThread = new ReceiveThread();
    			Thread thread = new Thread(receiveThread);
    			thread.start();
    			new GameStage().show();
    			//将用户输入的本机端口保存到数据库
    			UserStatusDAO statusDAO = DAOUtils.getMapper(UserStatusDAO.class);
    			statusDAO.updatePortByAccount(Global.account, Global.myPort);
    			//向服务端发送连接请求信息,连接信息中包含客户端的用户
    			NetUtils.sendMessage(new ConnectionMessage(Global.account));
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    		this.close();
    	}
    
    	@Override
    	public void handle(ActionEvent event) {
    		Button btn = (Button)event.getSource();
    		switch (btn.getText()) {
    		case "连	接":
    			connect();
    			break;
    		case "取	消":
    			new ModeChooseStage().show();
    			this.close();
    			break;
    		}
    	}
    }
    

      

    package com.wn.ui;
    
    import java.io.BufferedReader;
    import java.io.BufferedWriter;
    import java.io.File;
    import java.io.FileReader;
    import java.io.FileWriter;
    import java.util.ArrayList;
    import java.util.List;
    
    import com.wn.CommonUtils.DAOUtils;
    import com.wn.CommonUtils.NetUtils;
    import com.wn.dao.UserStatusDAO;
    import com.wn.pojo.ButtonMessage;
    import com.wn.pojo.ChessMessage;
    import com.wn.pojo.ChessMode;
    import com.wn.pojo.ConnectionMessage;
    import com.wn.pojo.Global;
    import com.wn.pojo.Message;
    import com.wn.pojo.MessageTimes;
    
    import javafx.event.ActionEvent;
    import javafx.event.EventHandler;
    import javafx.scene.Scene;
    import javafx.scene.control.Alert;
    import javafx.scene.control.Alert.AlertType;
    import javafx.scene.control.Button;
    import javafx.scene.control.ButtonType;
    import javafx.scene.image.Image;
    import javafx.scene.image.ImageView;
    import javafx.scene.layout.Pane;
    import javafx.scene.paint.Color;
    import javafx.scene.shape.Circle;
    import javafx.scene.shape.Line;
    import javafx.stage.FileChooser;
    import javafx.stage.Stage;
    import javafx.stage.WindowEvent;
    
    public class GameStage extends Stage implements EventHandler<ActionEvent> {
    	
    	private List<Circle> chessList = new ArrayList<Circle>(); 
    	private boolean isBlack = true; //判断棋子是否是黑色
    	private boolean gameOver = false; //判断游戏是否结束 true游戏结束,false代表可以下棋
    	private int topMargin = 50;//上间距
    	private int leftMargin = 350;//上间距
    	private int gap = 50; //内间距
    	private int chessWidth = 1400; //棋盘宽
    	private int chessHeight = 830; //棋盘高
    	private int size = 15; //棋盘线条数
    	private int buttonWidth = 80; //按钮宽
    	private int buttonLength = 40; //按钮高
    	private int cicreRadius = 18; //棋子半径
    	private int i = 0; //打开棋谱记录当前是第几手棋
    	private boolean canPlay = false;  //定义网络模式下,当前是否能下棋
    	
    	Pane pane;
    		
    	public GameStage() throws InterruptedException {
    		//设置窗口不可改变
    //		setResizable(false);
    		//创建面板对象
    		pane = new Pane();
    		//创建场景对象
    		Scene scene = new Scene(pane,chessWidth,chessHeight);
    		//将场景放进舞台
    		setScene(scene);
    		getIcons().add(new Image("Imgs/icon-2.jpg"));
    		Image image = new Image("Imgs/background.jpg",1400,830,false,true,true);
    		ImageView imageView = new ImageView(image); 
    		pane.getChildren().add(imageView);
    		setTitle("五子棋");
    		drawLine();
    		addButton();
    		Global.gameStage = this;
    		playChess();
    		
    		//当用户退出程序时,将用户的状态设置为下线
    		setOnCloseRequest(new EventHandler<WindowEvent>() {
    
    			@Override
    			public void handle(WindowEvent event) {
    				exitGame();
    			}
    		});
    	}
    	
    	/**
    	 * 	根据接收到的Message对象,更新棋子信息
    	 * @author Dracarys
    	 */
    	public void updateUI(Message message) {
    		if (message instanceof ConnectionMessage) {
    			ConnectionMessage conMessage = (ConnectionMessage)message;
    			if (conMessage.getClientUser()!=null) {
    				//有客户端连接时,弹窗提醒
    				Alert alert = new Alert(AlertType.CONFIRMATION,conMessage.getClientUser()+"已进入战斗!!");
    				alert.initOwner(this);
    				alert.show();
    			}
    		//接收到的消息为棋子信息消息
    		}else if (message instanceof ChessMessage) {
    			ChessMessage chessMessage = (ChessMessage)message;
    			double x = chessMessage.getX();
    			double y = chessMessage.getY();
    			double pieceX = x * gap + leftMargin;
    			double pieceY = y * gap + topMargin;
    			dropPiece(pieceX, pieceY);
    			//接收到消息之后将标签改完false
    			canPlay = false;
    			return;
    		//接收到的消息为按钮信息消息
    		}else if (message instanceof ButtonMessage) {
    			ButtonMessage btnMessage = (ButtonMessage)message;
    			String btnName = btnMessage.getBtnName();
    			switch (btnName) {
    			case "新	局":
    				//接收到点击新局按钮的第一次消息
    				if (btnMessage.getTimes().equals(MessageTimes.FIRST)) {
    					//弹窗提醒,让接收方选择是否同意开启新局
    					Alert alert = new Alert(AlertType.CONFIRMATION,"对方想开启新局");
    					alert.initOwner(this);
    					alert.showAndWait();
    					//如果用户选择同意开启新局,则开启新局
    					if (alert.getResult() == ButtonType.OK) {
    						startGame();
    					//然后用户不同意开启新局,将按钮消息的同意情况更改为false
    					}else {
    						//不同意,更改isAgree为false
    						btnMessage.setAgree(false);
    					}
    					//向新局发起方发送消息
    					btnMessage.setTimes(MessageTimes.SECOND);
    					NetUtils.sendMessage(btnMessage);
    				}else if (btnMessage.getTimes().equals(MessageTimes.SECOND)) {
    					Alert alert = new Alert(AlertType.INFORMATION);
    					//对方不同意开启新局
    					if (!btnMessage.isAgree()) {
    						alert.setContentText("对方不同意开启新局");
    						alert.initOwner(this);
    						alert.showAndWait();
    					}else {
    						alert.setContentText("对方同意开启新局");
    						alert.initOwner(this);
    						alert.showAndWait();
    						startGame();
    					}
    				}
    				break;
    			case "悔	棋":
    				//第一次接收到悔棋消息时,让接收方选择是否悔棋
    				if (btnMessage.getTimes().equals(MessageTimes.FIRST)) {
    					//弹窗提醒,通过同意,就开启新局
    					Alert alert = new Alert(AlertType.CONFIRMATION,"对方想悔棋");
    					alert.showAndWait();
    					//如果用户选择同意悔棋,则直接悔棋,并发送消息
    					if (alert.getResult() == ButtonType.OK) {
    						regretChess();
    						canPlay = false;
    					//然后用户不同意悔棋,将按钮消息的同意情况更改为false
    					}else {
    						//不同意,更改isAgree为false
    						btnMessage.setAgree(false);
    					}
    					//向悔棋发起方发送消息
    					btnMessage.setTimes(MessageTimes.SECOND);
    					NetUtils.sendMessage(btnMessage);
    				}else if (btnMessage.getTimes().equals(MessageTimes.SECOND)) {
    					Alert alert = new Alert(AlertType.INFORMATION);
    					//对方不同意悔棋
    					if (!btnMessage.isAgree()) {
    						alert.setContentText("对方不同意悔棋");
    						alert.show();
    					}else {
    						regretChess();
    						canPlay = false;
    						Global.regretTimes = 1;
    					}
    				}
    				break;
    			}
    		}
    	}
    	
    	
    	/**
    	 *	保存棋谱 
    	 * @author Dracarys
    	 * @param pane
    	 */
    	public void saveChessRecord() {
    		if (!gameOver) {
    			return;
    		}
    		//创建打开文件弹窗对象
    		FileChooser fileChooser = new FileChooser();
    		//设置弹窗标题
    		fileChooser.setTitle("保存棋谱");
    		//设置默认目录
    		fileChooser.setInitialDirectory(new File(System.getProperty("user.home")));
    		//设置文件后缀
    		fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("csv", "*.csv"));
    		//打开文件保存对话框
    		File file = fileChooser.showSaveDialog(this);
    		//点击取消,则文件为空,停止保存
    		if (file == null) {
    			return;
    		}
    		try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
    			for (Circle chess : chessList) {
    				bw.write(chess.getCenterX()+","+chess.getCenterY()+","+chess.getFill());
    				bw.newLine();
    			}
    			Alert alert = new Alert(AlertType.INFORMATION);
    			alert.setContentText("保存棋谱成功");
    			alert.show();
    		} catch (Exception e) {
    		}
    	}
    	/**
    	 *	打开棋谱 
    	 * @author Dracarys
    	 * @param pane
    	 */
    	public void openChessScore() {
    		//打开棋谱仅在单机版有效,网络版点击弹框提醒
    		if (Global.mode.equals(ChessMode.NETWORK)) {
    			Alert alert = new Alert(AlertType.INFORMATION,"该功能仅在单机版有效");
    			alert.showAndWait();
    			return;
    		}
    		//创建打开文件弹窗对象
    		FileChooser fc = new FileChooser();
    		//设置对象标题
    		fc.setTitle("打开棋谱");
    		//设置打开的初始路径
    		fc.setInitialDirectory(new File(System.getProperty("user.home")));
    		//设置打开的文件默认后缀
    		fc.getExtensionFilters().add(new FileChooser.ExtensionFilter("csv", "*.csv"));
    		//将选择的文件赋值给file对象
    		File file = fc.showOpenDialog(this);
    		//如果文件为空则不继续打开
    		if (file == null) {
    			return;
    		}
    		//打开棋谱时情况棋子集合和棋盘上的棋子
    		chessList.clear();
    		pane.getChildren().removeIf(c-> c instanceof Circle);
    		i = 0;
    		//创建缓冲流,读取棋谱文件
    		try (BufferedReader br = new BufferedReader(new FileReader(file)) ) {
    			String content;
    			while ((content = br.readLine())!=null) {
    				String[] list = content.split(",");
    				//根据文件棋子信息创建棋子
    				Circle chess = new Circle(Double.parseDouble(list[0]), Double.parseDouble(list[1]), cicreRadius,Color.web(list[2]));
    				//将棋子添加进集合
    				chessList.add(chess);
    			}
    		} catch (Exception e) {
    		}
    		//产生三个按钮控制棋谱读取
    		String[] list = {"<",">","x"};
    		for (int i = 0; i < list.length; i++) {
    			Button btn = new Button(list[i]);
    			btn.setPrefSize(30, 30);
    			btn.setLayoutX(1060);
    			btn.setLayoutY(300+60*i);
    			btn.setOnAction(this);
    			pane.getChildren().add(btn);
    		}
    	}
    	
    	/**
    	 *	退出游戏 
    	 * @author Dracarys
    	 * @param pane
    	 */
    	public void exitGame() {
    		Alert alert = new Alert(AlertType.CONFIRMATION);
    		alert.setTitle("退出游戏确认");
    		alert.setHeaderText("点击确认按钮将退出游戏程序");
    		alert.setContentText("您确认退出吗?");
    		alert.showAndWait();
    		if (alert.getResult() == ButtonType.OK) {
    			if (Global.mode.equals(ChessMode.SINGLE)) {
    				System.exit(0);
    			}else {
    				UserStatusDAO statusDAO = DAOUtils.getMapper(UserStatusDAO.class);
    				statusDAO.setStatusByAccount(Global.account, 0);
    				System.exit(0);
    			}
    		}
    	}
    	/**
    	 *	悔棋
    	 * @author Dracarys
    	 */
    	public void regretChess() {
    		if (chessList.size()>=1&&gameOver == false) {
    			chessList.remove(chessList.size()-1);
    			pane.getChildren().remove(pane.getChildren().size()-1);
    			isBlack = !isBlack;
    		}		
    	}
    	
    	/**
    	 *	开始游戏事件
    	 * @author Dracarys
    	 * @param pane
    	 */
    	public void startGame() {
    //		if (!gameOver) {
    //			Alert alert = new Alert(AlertType.INFORMATION,"游戏还未结束不能开始新局");
    //			alert.showAndWait();
    //			return;
    //		}
    		gameOver = false;
    		isBlack = true;
    		chessList.clear();
    		pane.getChildren().removeIf(c -> c instanceof Circle);	
    	}
    	
    	
    	/**
    	 * 	添加棋盘线
    	 * @author Dracarys
    	 * @param pane
    	 */
    	public void drawLine() {
    		//棋盘700*700,横竖14条线,每条先间隔50
    		//第一条横线起点(350,50),y起点(1050,50);第一条竖线x起点(350,50),y起点(350,650)
    		//第二条横线x起点(350,100),y起点(1050,100);第二条竖线x起点(1050,50),y起点(1050,650)
    		for (int i = 0; i < size; i++) {
    			//横线
    			Line line1 = new Line(leftMargin, topMargin+i*gap, leftMargin+50*14, topMargin+i*gap);
    			//竖线
    			Line line2 = new Line(leftMargin+i*gap, topMargin, leftMargin+i*gap, topMargin+50*14);
    			pane.getChildren().add(line1);
    			pane.getChildren().add(line2);
    		}
    		
    	}
    	//添加按钮
    	public void addButton() {
    		String[] btnList = {"新	局","保存棋谱","打开棋谱","悔	棋","退	出"};
    		for (int i = 0; i < btnList.length; i++) {
    			Button button = new Button(btnList[i]);
    			button.setPrefSize(buttonWidth, buttonLength);
    			button.setLayoutX(leftMargin+75*i+buttonWidth*i);
    			button.setLayoutY(size*topMargin+20);
    			button.setOnAction(this);
    			pane.getChildren().add(button);
    		}
    	}
    	
    	/**
    	 * 	在鼠标点击的位置落子
    	 * @author Dracarys
    	 * @param pane
    	 */
    	public void playChess() {
    		pane.setOnMouseClicked(e -> {
    			//网络版:判断用户是否已发送棋子消息,如果已发送
    			if (canPlay) {
    				return;
    			}
    			if (gameOver) {
    				return;
    			}
    			double x = e.getX(); //获取用户鼠标点击的坐标x值
    			double y = e.getY(); //获取用户鼠标点击的坐标y值
    			//左上顶点坐标(50,50),棋子半径为18,边界为坐标-棋子半径
    			//右下顶点坐标(750,750),棋子半径为18,边界为坐标+棋子半径
    			if (x < leftMargin - cicreRadius || x > chessWidth - leftMargin + cicreRadius) {
    				return;
    			}
    			if (y < topMargin - cicreRadius || y > 800 - topMargin + cicreRadius) {
    					 //50-18 = 32                         
    				return;
    			}
    			//将鼠标的坐标x和y都换算成(1,1)->(14,14)范围
    			double xIndex = Math.round(((x - leftMargin) / gap));
    			double yIndex = Math.round(((y - topMargin) / gap));
    			//判断当前鼠标点的位置是否存在棋子
    			double pieceX = xIndex * gap + leftMargin;
    			double pieceY = yIndex * gap + topMargin;
    			if (hasPiece(pieceX, pieceY)) {
    				return;
    			}
    			//落棋子
    			dropPiece(pieceX,pieceY);
    			//创建棋子消息对象
    			if (Global.mode.equals(ChessMode.NETWORK)) {
    				ChessMessage message = new ChessMessage(xIndex,yIndex);
    				//发送棋子消息对象
    				NetUtils.sendMessage(message);
    				canPlay = true;
    			}
    		});	
    	}
    	
    	public void dropPiece(double x,double y) {
    		Circle circle = new Circle();
    		//设置棋子落子的坐标
    		circle.setCenterX(x);
    		circle.setCenterY(y);
    		
    		circle.setRadius(cicreRadius);
    		if (isBlack) {
    			circle.setFill(Color.BLACK);
    		} else {
    			circle.setFill(Color.WHITE);
    		}
    		pane.getChildren().add(circle);
    		isBlack = !isBlack;
    		Circle chess = new Circle(x, y,cicreRadius, isBlack ? Color.BLACK : Color.WHITE);
    		chessList.add(chess);
    		//判断游戏输赢
    		if (isWin(chess)) {
    			Alert alert = new Alert(AlertType.INFORMATION);
    			alert.setTitle("游戏结束");
    			alert.setHeaderText("游戏结果");
    			alert.setContentText(isBlack ? "白棋胜" : "黑棋胜");
    			alert.show();
    			gameOver = true;
    		}
    	}
    	
    	 /** 	判断当前坐标位置是否有棋子
    	 * @author Dracarys
    	 * @param x
    	 * @param y
    	 * @return
    	 */
    	private boolean hasPiece(double x,double y) {
    		//遍历数组
    		for (int i = 0; i < chessList.size(); i++) {
    			Circle c = chessList.get(i);
    			if (c.getCenterX() == x && c.getCenterY() == y) {
    				return true;
    			}
    		}
    		return false;
    	}
    	
    	/**
    	 * 	判断是否胜利
    	 * @author Dracarys
    	 * @param chess
    	 * @return
    	 */
    	private boolean isWin(Circle chess) {
    		int count = 1;
    		//判断x轴向左边判断另外4个位置棋子是否同色
    		for (int i = 1; i < 5; i++) {
    			Circle chessLeft = new Circle(chess.getCenterX()-i*gap, 
    					chess.getCenterY(),cicreRadius,chess.getFill());
    			if (contain(chessLeft)) {
    				count++;
    			}else {
    				break;
    			}
    		}
    		//判断x轴向右边判断另外4个位子棋子是否同色
    		for (int i = 1; i < 5; i++) {
    			Circle chessRight = new Circle(chess.getCenterX()+i*gap, 
    					chess.getCenterY(),cicreRadius,chess.getFill());
    			if (contain(chessRight)) {
    				count++;
    			}else {
    				break;
    			}
    		}
    		if (count>=5) {
    			return true;
    		}
    		count = 1;
    		//判读y轴向上4个位置的棋子是否同色
    		for (int i = 1; i < 5; i++) {
    			Circle chessTop = new Circle(chess.getCenterX(),
    					chess.getCenterY()-i*gap,cicreRadius,chess.getFill());
    			if (contain(chessTop)) {
    				count++;
    			}else {
    				break;
    			}
    		}
    		//判读y轴向下4个位置的棋子是否同色
    		for (int i = 1; i < 5; i++) {
    			Circle chessDown = new Circle(chess.getCenterX(), 
    					chess.getCenterY()+i*gap,cicreRadius,chess.getFill());
    			if (contain(chessDown)) {
    				count++;
    			}else {
    				break;
    			}
    		}
    		if (count>=5) {
    			return true;
    		}
    		count = 1;
    		//判读左斜线边4个棋子是否同色
    		for (int i = 1; i < 5; i++) {
    			Circle chessTopLeft = new Circle(chess.getCenterX()-i*gap, 
    					chess.getCenterY()-i*gap,cicreRadius,chess.getFill());
    			if (contain(chessTopLeft)) {
    				count++;
    			}else {
    				break;
    			}
    		}
    		//判读左斜线边4个棋子是否同色
    		for (int i = 1; i < 5; i++) {
    			Circle chessTopRight = new Circle(chess.getCenterX()+i*gap, 
    					chess.getCenterY()+i*gap,cicreRadius,chess.getFill());
    			if (contain(chessTopRight)) {
    				count++;
    			}else {
    				break;
    			}
    		}
    		if (count>=5) {
    			return true;
    		}
    		count = 1;
    		//判读右斜线边4个棋子是否同色
    		for (int i = 1; i < 5; i++) {
    			Circle chessDownLeft = new Circle(chess.getCenterX()+i*gap, 
    					chess.getCenterY()-i*gap,cicreRadius,chess.getFill());
    			if (contain(chessDownLeft)) {
    				count++;
    			}else {
    				break;
    			}
    		}
    		//判读右斜线边4个棋子是否同色
    		for (int i = 1; i < 5; i++) {
    			Circle chessDownRight = new Circle(chess.getCenterX()-i*gap, 
    					chess.getCenterY()+i*gap,cicreRadius,chess.getFill());
    			if (contain(chessDownRight)) {
    				count++;
    			}else {
    				break;
    			}
    		}
    		if (count>=5) {
    			return true;
    		}
    		return false;
    	}
    	/**
    	 * 	判断棋子集合中是否包含当前棋子
    	 * @author Dracarys
    	 * @param chess
    	 * @return
    	 */
    	public boolean contain(Circle chess) {
    		for (Circle circle : chessList) {
    			if (circle.getCenterX() == chess.getCenterX() && circle.getCenterY()==chess.getCenterY()&&circle.getFill().equals(chess.getFill())) {
    				return true;
    			}		
    		}
    	return false;		
    	}
    	/**
    	 * 	重写用户点击按钮的方法
    	 */
    	@Override
    	public void handle(ActionEvent event) {
    		Button source = (Button) event.getSource();
    		String text = source.getText();
    		switch (text) {
    		case "新	局":
    			if (Global.mode.equals(ChessMode.SINGLE)) {
    				startGame();
    			}else {
    				ButtonMessage btnMessage = new ButtonMessage(text,true,MessageTimes.FIRST);
    				NetUtils.sendMessage(btnMessage);
    			}
    			break;
    		case "保存棋谱":
    			saveChessRecord();
    			break;
    		case "打开棋谱":
    			openChessScore();
    			break;
    		case "悔	棋":
    			if (gameOver) {
    				return;
    			}
    			if (chessList.isEmpty()) {
    				return;
    			}
    			if (Global.regretTimes>=1) {
    				Alert alert = new Alert(AlertType.INFORMATION,"已经悔悔过棋, 不能再次悔棋");
    				alert.initOwner(this);
    				alert.show();
    				return;
    			}
    			
    			if (!canPlay) {
    				Alert alert = new Alert(AlertType.INFORMATION,"不能悔棋");
    				alert.initOwner(this);
    				alert.show();
    				return;
    			}
    			if (Global.mode.equals(ChessMode.SINGLE)) {
    				regretChess();			
    			}else if (Global.mode.equals(ChessMode.NETWORK)) {
    				System.out.println(Global.regretTimes);
    				//网络模式下,一方选择悔棋,发送悔棋消息给另一方,按钮为名称为悔棋,发送方同意悔棋,消息次数为第一次
    				NetUtils.sendMessage(new ButtonMessage("悔	棋", true, MessageTimes.FIRST));
    			}
    			break;
    		case "退	出":
    			exitGame();
    			break;
    		case "<":
    			openChessScoreLast();
    			break;
    		case ">":
    			openChessScoreNext();
    			break;
    		case "x":
    			openChessScoreAll();
    			break;
    		}
    		
    	}
    	/**
    	 * 	打开棋谱,点击x,显示所有棋子
    	 * @author Dracarys
    	 */
    	private void openChessScoreAll() {
    		for (int j = i; j < chessList.size(); j++) {
    			pane.getChildren().add(chessList.get(j));
    		}
    		pane.getChildren().remove(36);
    		pane.getChildren().remove(36);
    		pane.getChildren().remove(36);
    	}
    	/**
    	 * 	打开棋谱,点击上一步按钮
    	 * @author Dracarys
    	 */
    	private void openChessScoreLast() {
    		if (i == 0) {
    			return;
    		}
    		pane.getChildren().remove(pane.getChildren().size()-1);
    		i--;
    		
    	}
    	/**
    	 * 	打开棋谱,点击下一步按钮
    	 * @author Dracarys
    	 */
    	private void openChessScoreNext() {
    		if (i == chessList.size()) {
    			pane.getChildren().remove(36);
    			pane.getChildren().remove(36);
    			pane.getChildren().remove(36);
    			return;
    		}
    		Circle chess = chessList.get(i);
    		pane.getChildren().add(chess);
    		i++;
    	}
    	
    }
    

      

      

  • 相关阅读:
    .NET开发工具
    二维图形的比例变换
    Java对十六进制文件读取
    ??Get Personal SPWeb Object(Mysite Object)获取MOSS个人站点的SPWeb对象
    ?使用SPSiteDataQuery和SPQuery查询不到数据
    moss和exchange 2007的sso
    [解决办法]正在尝试使用已关闭或释放并且不再有效的 SPWeb 对象
    MOSS的用户配置文件及其管理
    Moss要是有两个域用户在用的话.及界面修改 moss2010界面改成2007
    WebForm_PostBackOptions 未定义
  • 原文地址:https://www.cnblogs.com/japhi/p/15083468.html
Copyright © 2020-2023  润新知