对战五子棋——网页版

对战五子棋——网页版目录 一 项目简介 二 用户模块 1 创建用户实体类 2 编写 userMapper 接口文件 3 实现 userMapper xml 文件 4 对用户密码进行加密 5 实现用户登录功能 6 实现用户注册功能 三 实现用户匹配模块 1 展示用户个人信息 2 匹配请求类 3 匹配响应类 4 创建用户管理类 5

大家好,我是讯享网,很高兴认识大家。

目录

一、项目简介

二、用户模块 

1、创建用户实体类

2、编写userMapper接口文件 

3、实现userMapper.xml文件 

4、对用户密码进行加密

5、实现用户登录功能

6、实现用户注册功能 

三、实现用户匹配模块

1、展示用户个人信息 

2、匹配请求类 

3、匹配响应类 

4、创建用户管理类 

5、创建房间类 

6、实现房间管理类 

5、实现匹配器类 

6、实现匹配处理类 

处理连接请求 

处理开始匹配/取消匹配请求

处理连接关闭

处理连接异常 

四、实现五子棋对战模块 

1、前端绘制棋盘和棋子

2、前端初始化 websocket

3、处理落子请求

4、处理落子响应 

5、定义落子请求类

6、定义落子响应类 

7、实现对战功能 

8、实现胜负判定 

9、实现游戏处理类 

处理连接成功

通知玩家就绪 

处理落子请求 

通知另一个玩家获胜

处理玩家下线 以及 连接出错

五、页面展示 


一、项目简介

实现一个网页版的五子棋对战程序,用户可以进行注册,注册完成后登录进入游戏大厅,会显示用户的天梯分数记录以及比赛场次的记录,会根据用户的天梯分数实现匹配机制,实现两个玩家在网页端进行五子棋的对战。

二、用户模块 

1、创建用户实体类

@Data public class User { private int userId; //用户id private String username; //用户名 private String password; //用户密码 private int score; //用户分数 private int totalCount; //用户的比赛场次 private int winCount; //用户的获胜场次 }

讯享网

2、编写userMapper接口文件 

此处主要提供四个方法:

  • selectByName: 根据用户名查找用户信息. 用于实现登录.
  • insert: 新增用户. 用户实现注册.
  • userWin: 用于给获胜玩家修改分数.
  • userLose: 用户给失败玩家修改分数.
讯享网@Mapper public interface UserMapper { // 插入用户. 用于注册功能,返回受影响的行数 int insert(User user); // 根据用户名, 来查询用户的详细信息. 用于登录功能 User selectByName(String username); // 总比赛场数 + 1, 获胜场数 + 1, 天梯分数 + 30 void userWin(int userId); // 总比赛场数 + 1, 获胜场数 不变, 天梯分数 - 30 void userLose(int userId); }

3、实现userMapper.xml文件 

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.gobang.mapper.UserMapper"> <insert id="insert"> insert into user(username,password,score,totalCount,winCount) values(#{username},#{password},1000,0,0); </insert> <update id="userWin"> update user set totalCount = totalCount + 1, winCount = winCount + 1, score = score + 30 where userId = #{userId} </update> <update id="userLose"> update user set totalCount = totalCount + 1, score = score - 30 where userId = #{userId} </update> <select id="selectByName" resultType="com.example.gobang.model.User"> select * from user where username = #{username} </select> </mapper>

4、对用户密码进行加密

为了保证用户登录的安全性,使用MD5加盐的方式对用户的的密码进行加密和解密操作。

讯享网public class PasswordUtil { /*加密操作*/ public static String encryption(String password){ String salt = IdUtil.simpleUUID();//生成随机的32位盐值 String midpwd = SecureUtil.md5(salt+password); return salt+"#"+midpwd;//方便解密 } /*解密:判断密码是否相同,并不能得到解密后的密码*/ public static boolean decrypt(String password,String truePassword){ if(StringUtils.hasLength(password) && StringUtils.hasLength(truePassword)){ if(truePassword.length() == 65 && truePassword.contains("#")){ String[] pwd = truePassword.split("#"); String salt = pwd[0];//得到盐值 String midPassword = pwd[1];//得到盐值+密码使用md5加密后的密码 password = SecureUtil.md5(salt+password); if(password.equals(midPassword)){ return true; } } } return false; } } 

5、实现用户登录功能

实现用户登录功能,首先需要确定好前后端交互的接口:

请求:

POST /login HTTP/1.1 Content-Type: application/x-www-form-urlencoded username=Jake&password=123

响应:

讯享网HTTP/1.1 200 OK Content-Type: application/json { userId: 1, username: 'Jake', score: 1000, totalCount: 10, winCount: 5 } 如果登录失败, 返回的是一个 userId 为 0 的对象 

后端代码实现:

@RequestMapping("/login") public Object login(String username, String password, HttpServletRequest req) { if(!StringUtils.hasLength(username)){ return null; } // 根据username找到匹配的用户, 并且密码也一致, 就认为登录成功 User user = userService.selectByName(username); if (user == null || !PasswordUtil.decrypt(password, user.getPassword())) { // 登录失败; return null; } HttpSession httpSession = req.getSession(true); httpSession.setAttribute("user", user); return user; }

前端代码实现:

讯享网<script>
    function login(){
        //对登录名和密码进行非空校验
        var username = jQuery("#username");
        var password = jQuery("#password");
        //对首部去空格后进行非空校验
        if(jQuery.trim(username.val()) === ""){
            alert("请输入登录名");
            //清除原有数据,将光标定位到输入框起始位置
            username.focus();
            return;
        }
        if(jQuery.trim(password.val()) === ""){
            alert("请输入密码");
            password.focus();
            return;
        }
        jQuery.ajax({
            url:"login",
            type:"GET",
            data:{"username":username.val(),"password":password.val()},
            success:function (user){
                if(user && user.userId > 0){
                    //登录成功,进入游戏大厅页面
                    location.href = "game_hall.html";
                }else{
                    alert("用户名或密码输入错误");
                }
            }

        });
    }
</script>

6、实现用户注册功能 

 对于用户注册功能,同样也需要确定好前后端交互的接口:


讯享网

请求:

POST /register HTTP/1.1 Content-Type: application/x-www-form-urlencoded username=zhangsan&password=123

响应:

讯享网HTTP/1.1 200 OK Content-Type: application/json { userId: 1, username: 'zhangsan', score: 1000, totalCount: 10, winCount: 5 } 如果注册失败(比如用户名重复), 返回的是一个 userId 为 0 的对象.

后端代码实现: 

@RequestMapping("/register") public int register(String username, String password) { if(!StringUtils.hasLength(username) || !StringUtils.hasLength(password)){ return -1; } if(userService.selectByName(username) != null){ return 0; } //将存入数据库中的密码进行加密 password = PasswordUtil.encryption(password); User user = new User(); user.setUsername(username); user.setPassword(password); //将用户存入到数据库 if(userService.insert(user) > 0){ return 1; } return -1; }

前端代码实现:

讯享网<script>
    //进行注册操作
    function reg(){
        var username = jQuery("#username");
        var password = jQuery("#password");
        var password2 = jQuery("#password2");
        //非空校验
        if(jQuery.trim(username.val()) === ""){
            alert("请先输入用户名");
            username.focus();
            return false;
        }

        if(jQuery.trim(password.val()) === ""){
            alert("请先输入密码");
            password.focus();
            return false;
        }
        if(jQuery.trim(password2.val()) === ""){
            alert("请先输入确认密码");
            password2.focus();
            return false;
        }
        if(password.val() !== password2.val()){
            alert("两次密码输入不一致,请重新输入");
            password.focus();
            password2.focus();
            return false;
        }
        jQuery.ajax({
            url:"register",
            type:"POST",
            data:{
                "username":username.val(),
                "password":password.val(),
            },
            success:function(result){
                if(result === 1){
                    alert("注册成功!");
                    location.href = "login.html";
                }else if(result === 0){
                    alert("用户名已存在")
                }else if (result === -1) {
                    alert("注册失败")
                }
            }
        });
    }
</script>

三、实现用户匹配模块

对于用户匹配模块,首先也要确定好前后交互的接口:

请求:

{ message: 'startMatch' / 'stopMatch', } 

响应: (匹配成功后的响应)

讯享网{ ok: true, // 是否成功. 比如用户 id 不存在, 则返回 false reason: '', // 错误原因 message: 'matchSuccess', }

注意:

  • 页面这端拿到匹配响应之后, 就跳转到游戏房间.
  • 如果返回的响应 ok 为 false, 则弹框的方式显示错误原因, 并跳转到登录页面.

1、展示用户个人信息 

用户登录成功之后才会跳转到用户匹配模块 ,在用户匹配模块首先需要展示用户的个人信息。

前端代码实现:

<script> $.ajax({ method: 'get', url: '/userInfo', success: function(data) { let screen = document.querySelector('#screen'); screen.innerHTML = '玩家: ' + data.username + ', 分数: ' + data.score + "<br> 比赛场次: " + data.totalCount + ", 获胜场次: " + data.winCount; } }); </script>

后端代码实现: 

讯享网@RequestMapping("/userInfo") public Object getUserInfo(HttpServletRequest req) { HttpSession httpSession = req.getSession(false); User user = (User) httpSession.getAttribute("user"); // 根据username来查询 User newUser = userService.selectByName(user.getUsername()); return newUser; }

2、匹配请求类 

@Data public class MatchRequest { private String message = ""; //匹配请求信息 }

3、匹配响应类 

讯享网@Data public class MatchResponse { private boolean ok; //是否响应成功 private String reason; //响应失败原因 private String message; //响应信息 }

4、创建用户管理类 

创建用户管理类, 用于管理当前用户的在线状态. 本质上是 哈希表 的结构. key 为用户 id, value 为用户的 WebSocketSession.

借助这个类, 一方面可以判定用户是否是在线, 同时也可以进行方便的获取到 Session 从而给客户端回话.

  • 当玩家建立好 websocket 连接, 则将键值对加入 OnlineUserManager 中.
  • 当玩家断开 websocket 连接, 则将键值对从 OnlineUserManager 中删除.
  • 在玩家连接好的过程中, 随时可以通过 userId 来查询到对应的会话, 以便向客户端返回数据.

由于在游戏大厅以及游戏房间页面都需要用到用户管理,所以使用两个哈希表 来分别存储两部分的会话.

@Component public class OnlineUserManager { // 表示当前用户在游戏大厅在线状态. private ConcurrentHashMap<Integer, WebSocketSession> gameHall = new ConcurrentHashMap<>(); // 表示当前用户在游戏房间的在线状态. private ConcurrentHashMap<Integer, WebSocketSession> gameRoom = new ConcurrentHashMap<>(); public void enterGameHall(int userId, WebSocketSession webSocketSession) { gameHall.put(userId, webSocketSession); } public void exitGameHall(int userId) { gameHall.remove(userId); } public WebSocketSession getFromGameHall(int userId) { return gameHall.get(userId); } public void enterGameRoom(int userId, WebSocketSession webSocketSession) { gameRoom.put(userId, webSocketSession); } public void exitGameRoom(int userId) { gameRoom.remove(userId); } public WebSocketSession getFromGameRoom(int userId) { return gameRoom.get(userId); } }

5、创建房间类 

在匹配成功之后, 需要把对战的两个玩家放到同一个房间对象中.

  • 一个房间要包含一个房间 ID, 使用 UUID 作为房间的唯一身份标识.
  • 房间内要记录对弈的玩家双方信息.
  • 记录先手方的 ID
  • 记录一个 二维数组 , 作为对弈的棋盘.
讯享网public class Room { private String roomId; // 玩家1 private User user1; // 玩家2 private User user2; // 先手方的用户 id private int whiteUserId = 0; // 棋盘, 数字 0 表示未落子位置. 数字 1 表示玩家 1 的落子. 数字 2 表示玩家 2 的落子 private static final int MAX_ROW = 15; private static final int MAX_COL = 15; private int[][] chessBoard = new int[MAX_ROW][MAX_COL]; private ObjectMapper objectMapper = new ObjectMapper(); private OnlineUserManager onlineUserManager; public Room() { // 使用 uuid 作为唯一身份标识 roomId = UUID.randomUUID().toString(); } }

6、实现房间管理类 

Room 对象会存在很多. 每两个对弈的玩家, 都对应一个 Room 对象,所以需要一个管理器对象来管理所有的 Room.

  • 使用一个 Hash 表, 保存所有的房间对象, key 为 roomId, value 为 Room 对象
  • 再使用一个 Hash 表, 保存 userId -> roomId 的映射, 方便根据玩家来查找所在的房间.
  • 提供增, 删, 查方法. (包含基于房间 ID 的查询和基于用户 ID 的查询).
@Component public class RoomManager { // key 为 roomId, value 为一个 Room 对象 private ConcurrentHashMap<String, Room> rooms = new ConcurrentHashMap<>(); private ConcurrentHashMap<Integer, String> userIdToRoomId = new ConcurrentHashMap<>(); public void addRoom(Room room, int userId1, int userId2) { rooms.put(room.getRoomId(), room); userIdToRoomId.put(userId1, room.getRoomId()); userIdToRoomId.put(userId2, room.getRoomId()); } public Room getRoomByRoomId(String roomId) { return rooms.get(roomId); } public Room getRoomByUserId(int userId) { String roomId = userIdToRoomId.get(userId); if (roomId == null) { return null; } return getRoomByRoomId(roomId); } public void removeRoom(String roomId, int userId1, int userId2) { rooms.remove(roomId); userIdToRoomId.remove(userId1); userIdToRoomId.remove(userId2); } }

5、实现匹配器类 

  • 在 Matcher 中创建三个队列 (队列中存储 User 对象), 分别表示不同的段位的玩家. (此处约定 <2000 一档, 2000-3000 一档, >3000 一档)
  • 提供 add 方法, 供 MatchController类来调用, 用来把玩家加入匹配队列.
  • 提供 remove 方法, 供 MatchController类来调用, 用来把玩家移出匹配队列.
  • 同时 Matcher 找那个要记录 OnlineUserManager, 来获取到玩家的 Session

在 Matcher 的构造方法中, 创建一个线程, 使用该线程扫描每个队列, 把每个队列的头两个元素取出来, 匹配到一组中.

讯享网@Component public class Matcher { // 创建三个匹配队列 private Queue<User> normalQueue = new LinkedList<>(); private Queue<User> highQueue = new LinkedList<>(); private Queue<User> veryHighQueue = new LinkedList<>(); @Autowired private OnlineUserManager onlineUserManager; @Autowired private RoomManager roomManager; private ObjectMapper objectMapper = new ObjectMapper(); // 操作匹配队列的方法. // 把玩家放到匹配队列中 public void add(User user) { if (user.getScore() < 2000) { synchronized (normalQueue) { normalQueue.offer(user); normalQueue.notify(); } System.out.println("把玩家 " + user.getUsername() + " 加入到了 normalQueue 中!"); } else if (user.getScore() >= 2000 && user.getScore() < 3000) { synchronized (highQueue) { highQueue.offer(user); highQueue.notify(); } System.out.println("把玩家 " + user.getUsername() + " 加入到了 highQueue 中!"); } else { synchronized (veryHighQueue) { veryHighQueue.offer(user); veryHighQueue.notify(); } System.out.println("把玩家 " + user.getUsername() + " 加入到了 veryHighQueue 中!"); } System.out.println(normalQueue.size()); } // 当玩家点击停止匹配的时候, 就需要把玩家从匹配队列中删除 public void remove(User user) { if (user.getScore() < 2000) { synchronized (normalQueue) { normalQueue.remove(user); } System.out.println("把玩家 " + user.getUsername() + " 移除了 normalQueue!"); } else if (user.getScore() >= 2000 && user.getScore() < 3000) { synchronized (highQueue) { highQueue.remove(user); } System.out.println("把玩家 " + user.getUsername() + " 移除了 highQueue!"); } else { synchronized (veryHighQueue) { veryHighQueue.remove(user); } System.out.println("把玩家 " + user.getUsername() + " 移除了 veryHighQueue!"); } } public Matcher() { // 创建三个线程, 分别针对这三个匹配队列, 进行操作. Thread t1 = new Thread() { @Override public void run() { // 扫描 normalQueue while (true) { handlerMatch(normalQueue); } } }; t1.start(); Thread t2 = new Thread(){ @Override public void run() { while (true) { handlerMatch(highQueue); } } }; t2.start(); Thread t3 = new Thread() { @Override public void run() { while (true) { handlerMatch(veryHighQueue); } } }; t3.start(); } } 

实现 handlerMatch方法:由于 handlerMatch 在单独的线程中调用. 因此要考虑到访问队列的线程安全问题. 需要加上锁,每个队列分别使用队列对象本身作为锁即可,在入口处使用 wait 来等待, 直到队列中达到 2 个元素及其以上, 才唤醒线程消费队列。

private void handlerMatch(Queue<User> matchQueue) { synchronized (matchQueue) { try { // 保证只有一个玩家在队列的时候, 不会被出队列. 从而能支持取消功能. while (matchQueue.size() < 2) { matchQueue.wait(); } // 1. 尝试获取两个元素 User player1 = matchQueue.poll(); User player2 = matchQueue.poll(); System.out.println("匹配出两个玩家: " + player1.getUserId() + ", " + player2.getUserId()); // 2. 检查玩家在线状态(可能在匹配中玩家突然关闭页面) WebSocketSession session1 = onlineUserManager.getSessionFromGameHall(player1.getUserId()); WebSocketSession session2 = onlineUserManager.getSessionFromGameHall(player2.getUserId()); if (session1 == null) { // 如果玩家1 下线, 则把玩家2 放回匹配队列 matchQueue.offer(player2); return; } if (session2 == null) { // 如果玩家2 下线, 则把玩家1 放回匹配队列 matchQueue.offer(player1); return; } if (session1 == session2) { // 如果得到的两个 session 相同, 说明是同一个玩家两次进入匹配队列 // 例如玩家点击开始匹配后, 刷新页面, 重新再点开始匹配 // 此时也把玩家放回匹配队列 matchQueue.offer(player1); return; } // 3. 将这两个玩家加入到游戏房间中. // TODO 一会再写 // 4. 给玩家1 发回响应数据 MatchResponse response1 = new MatchResponse(); response1.setMessage("matchSuccess"); session1.sendMessage(new TextMessage(objectMapper.writeValueAsString(response1))); // 5. 给玩家2 发回响应数据 MatchResponse response2 = new MatchResponse(); response2.setMessage("matchSuccess"); session2.sendMessage(new TextMessage(objectMapper.writeValueAsString(response2))); } catch (InterruptedException | IOException e) { e.printStackTrace(); } } }

6、实现匹配处理类 

 创建匹配处理类, 继承自 TextWebSocketHandler 作为处理 websocket 请求的入口类.

需要用到 ObjectMapper, 后续用来处理 JSON 数据.

讯享网@Component public class MatchAPI extends TextWebSocketHandler { private ObjectMapper objectMapper = new ObjectMapper(); @Autowired private OnlineUserManager onlineUserManager; @Autowired private Matcher matcher; @Component public class MatchAPI extends TextWebSocketHandler { } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { } @Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { } }

处理连接请求 

实现 afterConnectionEstablished 方法.

  • 通过参数中的 session 对象, 拿到之前登录时设置的 User 信息.
  • 使用 onlineUserManager 来管理用户的在线状态.
  • 先判定用户是否是已经在线, 如果在线则直接返回出错 (禁止同一个账号多开).
  • 设置玩家的上线状态.
@Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { // 1. 拿到用户信息. User user = (User) session.getAttributes().get("user"); if (user == null) { // 拿不到用户的登录信息, 说明玩家未登录就进入游戏大厅了. // 则返回错误信息并关闭连接 MatchResponse response = new MatchResponse(); response.setOk(false); response.setReason("玩家尚未登录!"); session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response))); return; } // 2. 检查玩家的上线状态 if (onlineUserManager.getSessionFromGameHall(user.getUserId()) != null || onlineUserManager.getSessionFromGameRoom(user.getUserId()) != null) { MatchResponse response = new MatchResponse(); response.setOk(false); response.setReason("禁止多开游戏大厅页面!"); session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response))); return; } // 3. 设置玩家上线状态 onlineUserManager.enterGameHall(user.getUserId(), session); System.out.println("玩家进入匹配页面: " + user.getUserId()); }

处理开始匹配/取消匹配请求

实现 handleTextMessage

  • 先从会话中拿到当前玩家的信息.
  • 解析客户端发来的请求
  • 判定请求的类型, 如果是 startMatch, 则把用户对象加入到匹配队列. 如果是 stopMatch, 则把用户对象从匹配队列中删除.
  • 此处需要实现一个 匹配器 对象, 来处理匹配的实际逻辑.
讯享网@Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { // 1. 拿到用户信息. User user = (User) session.getAttributes().get("user"); if (user == null) { System.out.println("[onMessage] 玩家尚未登录!"); return; } System.out.println("开始匹配: " + user.getUserId() + " message: " + message.toString()); // 2. 解析读到的数据为 json 对象 MatchRequest request = objectMapper.readValue(message.getPayload(), MatchRequest.class); MatchResponse response = new MatchResponse(); if (request.getMessage().equals("startMatch")) { matcher.add(user); response.setMessage("startMatch"); } else if (request.getMessage().equals("stopMatch")) { matcher.remove(user); response.setMessage("stopMatch"); } else { // 匹配失败 response.setOk(false); response.setReason("非法的匹配请求!"); } session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response))); }

处理连接关闭

实现 afterConnectionClosed

  • 主要的工作就是把玩家从 onlineUserManager 中退出.
  • 退出的时候要注意判定, 当前玩家是否是多开的情况(一个userId, 对应到两个 websocket 连接). 如果一个玩家开启了第二个 websocket 连接, 那么这第二个 websocket 连接不会影响到玩家从 OnlineUserManager 中退出.
  • 如果玩家当前在匹配队列中, 则直接从匹配队列里移除.
@Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { User user = (User) session.getAttributes().get("user"); if (user == null) { System.out.println("[onClose] 玩家尚未登录!"); return; } WebSocketSession existSession = onlineUserManager.getSessionFromGameHall(user.getUserId()); if (existSession != session) { System.out.println("当前的会话不是玩家游戏中的会话, 不做任何处理!"); return; } System.out.println("玩家离开匹配页面: " + user.getUserId()); onlineUserManager.exitGameHall(user.getUserId()); // 如果玩家在匹配中, 则关闭页面时把玩家移出匹配队列 matcher.remove(user); }

处理连接异常 

连接异常与连接关闭的逻辑类似。

讯享网@Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { User user = (User) session.getAttributes().get("user"); if (user == null) { System.out.println("[onError] 玩家尚未登录!"); return; } WebSocketSession existSession = onlineUserManager.getSessionFromGameHall(user.getUserId()); if (existSession != session) { System.out.println("当前的会话不是玩家游戏中的会话, 不做任何处理!"); return; } System.out.println("匹配页面连接出现异常! userId: " + user.getUserId() + ", message: " + exception.getMessage()); onlineUserManager.exitGameHall(user.getUserId()); // 如果玩家在匹配中, 则关闭页面时把玩家移出匹配队列 matcher.remove(user); }

前端代码实现: 

// 1. 和服务器建立连接
let websocket = new WebSocket('ws://127.0.0.1:8080/findMatch');
// 2. 点击开始匹配
let button = document.querySelector('#match-button');
button.onclick = function() {
    if (websocket.readyState == websocket.OPEN) {
        if (button.innerHTML == '开始匹配') {
            console.log('开始匹配!');
            websocket.send(JSON.stringify({
                message: 'startMatch',
            }));
        } else if (button.innerHTML == '匹配中...(点击取消)') {
            console.log('取消匹配!');
            websocket.send(JSON.stringify({
                message: 'stopMatch'
            }));
        }
    } else {
        alert('当前您连接断开! 请重新登录!');
        location.assign('/login.html');
    }
}

// 3. 处理服务器的响应
websocket.onmessage = function(e) {
    let resp = JSON.parse(e.data)
    if (!resp.ok) {
        console.log('游戏大厅中发生错误: ' + resp.reason);
        location.assign('/login.html');
        return;
    }
    if (resp.message == 'startMatch') {
        console.log('进入匹配队列成功!');
        button.innerHTML = '匹配中...(点击取消)';
    } else if (resp.message == 'stopMatch') {
        console.log('离开匹配队列成功!');
        button.innerHTML = '开始匹配';
    } else if (resp.message == 'matchSuccess') {
        console.log('匹配成功! 进入游戏页面!');
        location.assign('/game_room.html');
    } else {
        console.log('非法的 message: ' + resp.message);
    }
}

// 4. 监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function () {
    websocket.close();
}

四、实现五子棋对战模块 

首先约定好前后端交互的接口:

连接:

讯享网ws://127.0.0.1:8080/game

连接响应:当两个玩家都连接好了, 则给双方都返回一个数据表示就绪

{ message: 'gameReady', // 游戏就绪 ok: true, // 是否成功. reason: '', // 错误原因 roomId: 'abcdef', // 房间号. 用来辅助调试. thisUserId: 1, // 玩家自己的 id thatUserId: 2, // 对手的 id whiteUser: 1, // 先手方的 id }

落子请求:

讯享网{ message: 'putChess', userId: 1, row: 0, col: 0 }

落子响应:

{ message: 'putChess', userId: 1, row: 0, col: 0, winner: 0 }

1、前端绘制棋盘和棋子

这部分代码基于 canvas API,首先使用一个二维数组来表示棋盘. 虽然胜负是通过服务器判定的, 但是客户端的棋盘可以避免 "一个位置重复落子" 这样的情况;

oneStep 函数起到的效果是在一个指定的位置上绘制一个棋子. 可以区分出绘制白字还是黑子. 参数是横坐标和纵坐标, 分别对应列和行.

me 变量用来表示当前是否轮到我落子. over 变量用来表示游戏结束.

讯享网gameInfo = { roomId: null, thisUserId: null, thatUserId: null, isWhite: true, } function setScreenText(me) { let screen = document.querySelector('#screen'); if (me) { screen.innerHTML = "轮到你落子了!"; } else { screen.innerHTML = "轮到对方落子了!"; } } //初始化游戏 function initGame() { // 是我下还是对方下. 根据服务器分配的先后手情况决定 let me = gameInfo.isWhite; // 游戏是否结束 let over = false; let chessBoard = []; //初始化chessBord数组(表示棋盘的数组) for (let i = 0; i < 15; i++) { chessBoard[i] = []; for (let j = 0; j < 15; j++) { chessBoard[i][j] = 0; } } let chess = document.querySelector('#chess'); let context = chess.getContext('2d'); context.strokeStyle = "#BFBFBF"; // 背景图片 let logo = new Image(); logo.src = "image/sky.jpeg"; logo.onload = function () { context.drawImage(logo, 0, 0, 450, 450); initChessBoard(); } // 绘制棋盘网格 function initChessBoard() { for (let i = 0; i < 15; i++) { context.moveTo(15 + i * 30, 15); context.lineTo(15 + i * 30, 430); context.stroke(); context.moveTo(15, 15 + i * 30); context.lineTo(435, 15 + i * 30); context.stroke(); } } // 绘制一个棋子, me 为 true function oneStep(i, j, isWhite) { context.beginPath(); context.arc(15 + i * 30, 15 + j * 30, 13, 0, 2 * Math.PI); context.closePath(); var gradient = context.createRadialGradient(15 + i * 30 + 2, 15 + j * 30 - 2, 13, 15 + i * 30 + 2, 15 + j * 30 - 2, 0); if (!isWhite) { gradient.addColorStop(0, "#0A0A0A"); gradient.addColorStop(1, "#"); } else { gradient.addColorStop(0, "#D1D1D1"); gradient.addColorStop(1, "#F9F9F9"); } context.fillStyle = gradient; context.fill(); } } 

2、前端初始化 websocket

在 前端页面中中, 加入 websocket 的连接代码, 实现前后端交互.

  • 在获取到服务器反馈的就绪响应之后, 再初始化棋盘.
  • 创建 websocket 对象, 并注册 onopen/onclose/onerror 函数. 其中在 onerror 中做一个跳转到游戏大厅的逻辑. 当网络异常断开, 则回到大厅.
  • 实现 onmessage 方法. onmessage 先处理游戏就绪响应.
websocket = new WebSocket("ws://127.0.0.1:8080/game");
//连接成功建立的回调方法
websocket.onopen = function (event) {
    console.log("open");
}
//连接关闭的回调方法
websocket.onclose = function () {
    console.log("close");
}
//连接发生错误的回调方法
websocket.onerror = function () {
    console.log("error");
    alert('和服务器连接断开! 返回游戏大厅!')
    location.assign('/game_hall.html')
};
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function () {
    websocket.close();
}

websocket.onmessage = function (event) {
    console.log('handlerGameReady: ' + event.data);

    let response = JSON.parse(event.data);
    if (response.message != 'gameReady') {
        console.log('响应类型错误!');
        return;
    }
    if (!response.ok) {
        alert('连接游戏失败! reason: ' + response.reason);
        location.assign('/game_hall.html')
        return;
    }
    // 初始化游戏信息
    gameInfo.roomId = response.roomId;
    gameInfo.thisUserId = response.thisUserId;
    gameInfo.thatUserId = response.thatUserId;
    gameInfo.isWhite = (response.whiteUserId == gameInfo.thisUserId);
    console.log('[gameReady] ' + JSON.stringify(gameInfo));
    // 初始化棋盘
    initGame();
    // 设置 #screen 的显示
    setScreenText(gameInfo.isWhite);
}

3、处理落子请求

定义 onclick 函数, 在落子操作时加入发送请求的逻辑.

  • 实现 send , 通过 websocket 发送落子请求.
讯享网chess.onclick = function (e) { if (over) { return; } if (!me) { return; } let x = e.offsetX; let y = e.offsetY; // 注意, 横坐标是列, 纵坐标是行 let col = Math.floor(x / 30); let row = Math.floor(y / 30); if (chessBoard[row][col] == 0) { send(row, col); } } function send(row, col) { console.log("send"); let request = { message: "putChess", userId: gameInfo.thisUserId, row: row, col: col, } websocket.send(JSON.stringify(request)); }

4、处理落子响应 

在 initGame 中, 修改 websocket 的 onmessage

  • 在 initGame 之前, 处理的是游戏就绪响应, 在收到游戏响应之后, 就改为接收落子响应了.
  • 在处理落子响应中要处理比赛结果。
websocket.onmessage = function (event) {
    console.log('handlerPutChess: ' + event.data);

    let response = JSON.parse(event.data);
    if (response.message != 'putChess') {
        console.log('响应类型错误!');
        return;
    }

    // 1. 判断 userId 是自己的响应还是对方的响应, 
    //    以此决定当前这个子该画啥颜色的
    if (response.userId == gameInfo.thisUserId) {
        oneStep(response.col, response.row, gameInfo.isWhite);
    } else if (response.userId == gameInfo.thatUserId) {
        oneStep(response.col, response.row, !gameInfo.isWhite);
    } else {
        console.log('[putChess] response userId 错误! response=' + JSON.stringify(response));
        return;
    }
    chessBoard[response.row][response.col] = 1;
    me = !me; // 接下来该下个人落子了. 

    // 2. 判断游戏是否结束
    if (response.winner != 0) {
        // 胜负已分
        if (response.winner == gameInfo.thisUserId) {
            alert("你赢了!");
        } else {
            alert("你输了");
        }
        // 如果游戏结束, 则关闭房间, 回到游戏大厅. 
        location.assign('/game_hall.html')
    }

    // 3. 更新界面显示
    setScreenText(me);
}

5、定义落子请求类

讯享网@Data public class GameReadyResponse { private String message = "gameReady"; private boolean ok = true; private String reason = ""; private String roomId = ""; private int thisUserId = 0; private int thatUserId = 0; private int whiteUserId = 0; }
@Data public class GameRequest { private String message = "putChess"; private int userId; private int row; private int col; }

6、定义落子响应类 

讯享网@Data public class GameResponse { private String message = "putChess"; private int userId; private int row; private int col; private int winner; // 胜利玩家的 userId }

7、实现对战功能 

 在 room 类中定义 putChess 方法.

  • 先把请求解析成请求对象.
  • 根据请求对象中的信息, 往棋盘上落子.
  • 落子完毕之后, 为了方便调试, 可以打印出棋盘的当前状况.
  • 检查游戏是否结束.
  • 构造落子响应, 写回给每个玩家.
  • 写回的时候如果发现某个玩家掉线, 则判定另一方为获胜.
  • 如果游戏胜负已分, 则修改玩家的分数, 并销毁房间.
// 玩家落子 public void putChess(String message) throws IOException { GameRequest req = objectMapper.readValue(message, GameRequest.class); GameResponse response = new GameResponse(); // 1. 进行落子 int chess = req.getUserId() == user1.getUserId() ? 1 : 2; int row = req.getRow(); int col = req.getCol(); if (chessBoard[row][col] != 0) { System.out.println("落子位置有误! " + req); return; } chessBoard[row][col] = chess; printChessBoard(); // 2. 检查游戏结束 // 返回的 winner 为玩家的 userId int winner = checkWinner(chess, row, col); // 3. 把响应写回给玩家 response.setUserId(req.getUserId()); response.setRow(row); response.setCol(col); response.setWinner(winner); WebSocketSession session1 = onlineUserManager.getSessionFromGameRoom(user1.getUserId()); WebSocketSession session2 = onlineUserManager.getSessionFromGameRoom(user2.getUserId()); if (session1 == null) { // 玩家1 掉线, 直接认为玩家2 获胜 response.setWinner(user2.getUserId()); System.out.println("玩家1 掉线!"); } if (session2 == null) { // 玩家2 掉线, 直接认为玩家1 获胜 response.setWinner(user1.getUserId()); System.out.println("玩家2 掉线!"); } String responseJson = objectMapper.writeValueAsString(response); if (session1 != null) { session1.sendMessage(new TextMessage(responseJson)); } if (session2 != null) { session2.sendMessage(new TextMessage(responseJson)); } // 4. 如果玩家胜负已分, 就把 room 从管理器中销毁 if (response.getWinner() != 0) { userMapper.userWin(response.getWinner() == user1.getUserId() ? user1 : user2); userMapper.userLose(response.getWinner() == user1.getUserId() ? user2 : user1); roomManager.removeRoom(roomId, user1.getUserId(), user2.getUserId()); System.out.println("游戏结束, 房间已经销毁! roomId: " + roomId + " 获胜方为: " + response.getWinner()); } }

8、实现胜负判定 

  • 如果游戏分出胜负, 则返回玩家的 id. 如果未分出胜负,则返回 0.
  • 棋盘中值为 1 表示是玩家 1 的落子, 值为 2 表示是玩家 2 的落子.
  • 检查胜负的时候, 以当前落子位置为中心, 检查所有相关的行,列, 对角线即可. 不必遍历整个棋盘.
讯享网// 判定棋盘形式, 找出胜利的玩家. // 如果游戏分出胜负, 则返回玩家的 id. // 如果未分出胜负, 则返回 0 // chess 值为 1 表示玩家1 的落子. 为 2 表示玩家2 的落子 private int checkWinner(int chess, int row, int col) { // 以 row, col 为中心 boolean done = false; // 1. 检查所有的行(循环五次) for (int c = col - 4; c <= col; c++) { if (c < 0 || c >= MAX_COL) { continue; } if (chessBoard[row][c] == chess && chessBoard[row][c + 1] == chess && chessBoard[row][c + 2] == chess && chessBoard[row][c + 3] == chess && chessBoard[row][c + 4] == chess) { done = true; } } // 2. 检查所有的列(循环五次) for (int r = row - 4; r <= row; r++) { if (r < 0 || r >= MAX_ROW) { continue; } if (chessBoard[r][col] == chess && chessBoard[r + 1][col] == chess && chessBoard[r + 2][col] == chess && chessBoard[r + 3][col] == chess && chessBoard[r + 4][col] == chess) { done = true; } } // 3. 检查左对角线 for (int r = row - 4, c = col - 4; r <= row && c <= col; r++, c++) { if (r < 0 || r >= MAX_ROW || c < 0 || c >= MAX_COL) { continue; } if (chessBoard[r][c] == chess && chessBoard[r + 1][c + 1] == chess && chessBoard[r + 2][c + 2] == chess && chessBoard[r + 3][c + 3] == chess && chessBoard[r + 4][c + 4] == chess) { done = true; } } // 4. 检查右对角线 for (int r = row - 4, c = col + 4; r <= row && c >= col; r++, c--) { if (r < 0 || r >= MAX_ROW || c < 0 || c >= MAX_COL) { continue; } if (chessBoard[r][c] == chess && chessBoard[r + 1][c - 1] == chess && chessBoard[r + 2][c - 2] == chess && chessBoard[r + 3][c - 3] == chess && chessBoard[r + 4][c - 4] == chess) { done = true; } } if (!done) { return 0; } return chess == 1 ? user1.getUserId() : user2.getUserId(); }

9、实现游戏处理类 

创建 gameController类 , 处理 websocket 请求.

@Component public class GameAPI extends TextWebSocketHandler { private ObjectMapper objectMapper = new ObjectMapper(); @Autowired private RoomManager roomManager; // 这个是管理 game 页面的会话 @Autowired private OnlineUserManager onlineUserManager; @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { } @Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { } }

处理连接成功

实现 GameController类的 afterConnectionEstablished 方法.

  • 首先需要检测用户的登录状态. 从 Session 中拿到当前用户信息.
  • 然后要判定当前玩家是否是在房间中.
  • 接下来进行多开判定.如果玩家已经在游戏中, 则不能再次连接.
  • 把两个玩家放到对应的房间对象中. 当两个玩家都建立了连接, 房间就放满了.这个时候通知两个玩家双方都准备就绪.
  • 如果有第三个玩家尝试也想加入房间, 则给出一个提示, 房间已经满了.
讯享网@Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { GameReadyResponse resp = new GameReadyResponse(); User user = (User) session.getAttributes().get("user"); if (user == null) { resp.setOk(false); resp.setReason("用户尚未登录!"); session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp))); return; } Room room = roomManager.getRoomByUserId(user.getUserId()); if (room == null) { resp.setOk(false); resp.setReason("用户并未匹配成功! 不能开始游戏!"); session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp))); return; } System.out.println("连接游戏! roomId=" + room.getRoomId() + ", userId=" + user.getUserId()); // 先判定用户是不是已经在游戏中了. if (onlineUserManager.getSessionFromGameHall(user.getUserId()) != null || onlineUserManager.getSessionFromGameRoom(user.getUserId()) != null) { resp.setOk(false); resp.setReason("禁止多开游戏页面!"); session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp))); return; } // 更新会话 onlineUserManager.enterGameRoom(user.getUserId(), session); // 同一个房间的两个玩家, 同时连接时要考虑线程安全问题. synchronized (room) { if (room.getUser1() == null) { room.setUser1(user); // 设置 userId1 为先手方 room.setWhiteUserId(user.getUserId()); System.out.println("userId=" + user.getUserId() + " 玩家1准备就绪!"); return; } if (room.getUser2() == null) { room.setUser2(user); System.out.println("userId=" + user.getUserId() + " 玩家2准备就绪!"); // 通知玩家1 就绪 noticeGameReady(room, room.getUser1().getUserId(), room.getUser2().getUserId()); // 通知玩家2 就绪 noticeGameReady(room, room.getUser2().getUserId(), room.getUser1().getUserId()); return; } } // 房间已经满了! resp.setOk(false); String log = "roomId=" + room.getRoomId() + " 已经满了! 连接游戏失败!"; resp.setReason(log); session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp))); System.out.println(log); }

通知玩家就绪 

private void noticeGameReady(Room room, int thisUserId, int thatUserId) throws IOException { GameReadyResponse resp = new GameReadyResponse(); resp.setRoomId(room.getRoomId()); resp.setThisUserId(thisUserId); resp.setThatUserId(thatUserId); resp.setWhiteUserId(room.getWhiteUserId()); WebSocketSession session1 = onlineUserManager.getSessionFromGameRoom(thisUserId); session1.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp))); }

处理落子请求 

讯享网@Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { User user = (User) session.getAttributes().get("user"); if (user == null) { return; } Room room = roomManager.getRoomByUserId(user.getUserId()); room.putChess(message.getPayload()); }

通知另一个玩家获胜

// 通知另外一个玩家直接获胜! private void noticeThatUserWin(User user) throws IOException { Room room = roomManager.getRoomByUserId(user.getUserId()); if (room == null) { System.out.println("房间已经释放, 无需通知!"); return; } User thatUser = (user == room.getUser1() ? room.getUser2() : room.getUser1()); WebSocketSession session = onlineUserManager.getSessionFromGameRoom(thatUser.getUserId()); if (session == null) { System.out.println(thatUser.getUserId() + " 该玩家已经下线, 无需通知!"); return; } GameResponse resp = new GameResponse(); resp.setUserId(thatUser.getUserId()); resp.setWinner(thatUser.getUserId()); session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp))); }

处理玩家下线 以及 连接出错

讯享网@Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { User user = (User) session.getAttributes().get("user"); if (user == null) { return; } WebSocketSession existSession = onlineUserManager.getSessionFromGameRoom(user.getUserId()); if (existSession != session) { System.out.println("当前的会话不是玩家游戏中的会话, 不做任何处理!"); return; } System.out.println("连接出错! userId=" + user.getUserId()); onlineUserManager.exitGameRoom(user.getUserId()); noticeThatUserWin(user); } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { User user = (User) session.getAttributes().get("user"); if (user == null) { return; } WebSocketSession existSession = onlineUserManager.getSessionFromGameRoom(user.getUserId()); if (existSession != session) { System.out.println("当前的会话不是玩家游戏中的会话, 不做任何处理!"); return; } System.out.println("用户退出! userId=" + user.getUserId()); onlineUserManager.exitGameRoom(user.getUserId()); noticeThatUserWin(user); }

五、页面展示 

小讯
上一篇 2025-01-29 11:17
下一篇 2025-02-09 22:26

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/52507.html