Spring项目-在线五子棋

网页版在线五子棋

1. 项目介绍

实现一个网页版在线对战五子棋

支持以下功能:

  • 用户模块:用户注册、用户登录、用户天梯积分记录、用户比赛场数记录
  • 匹配模块:根据玩家天梯积分进行匹配
  • 对战模块:实现1v1的实时对战功能

核心技术:

  • Spring/SpringBoot/SpringMVC
  • Websocket
  • MyBatis
  • MySQL
  • HTML/CSS/JS/AJAX

2. 项目演示

请添加图片描述

3. 前置知识

3.1 WebSocket

如果你了解过Http协议,那么应该知道Http协议是无状态、无连接、单向的应用层协议。它采用了请求-响应模式,由客户端发送一个请求,由服务端返回一个响应。它有一个弊端就是服务端无法主动向客户端发起消息。这样就导致客户端想要获取服务端连续的状态变化很困难,大多是web程序将通过频繁的异步JavaScript和XML(AJAX)请求实现长轮询。轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。

举个例子,我们在餐馆点餐后,有两种选择:
①时不时跑到前台询问老板我的菜做好没,老板说没有,我溜达一圈后又来问菜做好没……循环直到我的菜做好了,我端着菜找个位置坐下用餐
②我直接找个位置坐下,等菜做好后,老板端着菜过来递给我然后用餐
第一种做法(轮询)就是使用客户端(我)一直向服务器(老板)发送请求,检查数据是否发生了变化(菜做好没)。
第二种做法(websocket)就是服务器(老板)直接向客户端(我)发送消息(菜做好了)

为了建立一个 WebSocket 连接,客户端浏览器首先要向服务器发起一个 HTTP 请求,这个请求和通常的 HTTP 请求不同,包含了一些附加头信息,通过这个附加头信息完成握手过程.

请求头

在这里插入图片描述

返回头

在这里插入图片描述

3.2 代码示例

Spring中内置了websocket,我们可以直接使用。

3.2.1 服务器代码

创建TestAPI类:

这个类用来处理websocket请求,并返回响应。

每个方法中都带有一个 session 对象, 这个 session 和 Servlet 的 session 并不相同, 而是 WebSocket 内部搞的另外一组 Session.

通过这个 Session 可以给客户端返回数据, 或者主动断开连接.

@Component
/**
 * 这是一个测试类
 * 继承自TextWebSocketHandler的类是一个webSocket消息处理类
 */
public class TestAPI extends TextWebSocketHandler {
    @Override
    //用户建立连接后触发的方法
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        System.out.println("连接成功");
    }

    @Override
    //收到文本消息后触发的方法
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        System.out.println("收到消息 : " + message.getPayload());
        session.sendMessage(new TextMessage("我收到了你的消息" + message.getPayload()));
    }

    @Override
    //触发异常后触发的方法
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        System.out.println("连接异常");
    }

    @Override
    //关闭连接后触发的方法
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        System.out.println("关闭连接");
    }
}

创建WebSocketConfig类:

@Configuration
@EnableWebSocket//这个注释可以让Spring知道这是一个WebSocket配置类
public class WebSocketConfig implements WebSocketConfigurer {

    @Autowired
    private TestAPI testAPI;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        //这个方法可以将一个消息处理器和一个路由关联上,访问这个路由后将使用testAPI的方法进行消息处理
        registry.addHandler(testAPI,"/test");
    }
}
3.2.2 客户端代码

创建test.html

<body>
    <input type="text" id = "message">
    <input type="button" id = "submit" value="提交">

    <script>

        /* 创建一个websocket实例 */
        let url = "ws://127.0.0.1:8080/test"
        let websocket = new WebSocket(url)

        /* 给实例挂载一些回调函数 */
        websocket.onopen = function() {
            console.log("建立连接");
        }

        websocket.onmessage = function(e) {
            console.log("收到消息" + e.date);
        }

        websocket.onerror = function() {
            console.log("连接异常");
        }

        websocket.onclose = function() {
            console.log("连接关闭");
        }

        let input = document.querySelector('#message');
        let button = document.querySelector('#submit')
        button.onclick = function() {
            console.log("发送消息" + input.value);
            websocket.send(input.value);
        }

    </script>
</body>

启动服务器,观察效果:
在这里插入图片描述
在这里插入图片描述

这样服务器和客户端就实现了交互~

4. 需求分析和概要设计

整个项目分成以下三个模块

  • 用户模块
  • 匹配模块
  • 对战模块

4.1 用户模块

该模块主要用于用户登录、注册、记录一些用户比赛信息。

用MySQL存储数据。

客户端提供登录注册页面。

服务器基于Spring + MyBatis来实现增删查改。

4.2 匹配模块

用户登录成功,进入游戏大厅,大厅里显示玩家的比赛信息。

同时显示一个匹配按钮,当玩家按下开始匹配,将玩家加入匹配队列,同时开始匹配变为匹配中……(点击停止)停止匹配后从队列中将玩家移除。

如果匹配成功,将进入游戏房间。

通过websocket实现通讯“开始匹配”、“停止匹配”、“匹配成功”。

4.3 对战模块

玩家匹配成功,则进入游戏房间界面

每两个玩家在同一个游戏房间

在游戏房间中显示棋盘,玩家点解棋盘实现落子功能

当五子连珠时,显示你赢了/你输了

页面加载时和服务器建立 websocket 连接. 双方通过 websocket 来传输 “准备就绪”, “落子位置”, “胜负” 这样的信息.

  • 准备就绪: 两个玩家均连上游戏房间的 websocket 时, 则认为双方准备就绪.
  • 落子位置: 有一方玩家落子时, 会通过 websocket 给服务器发送落子的用户信息和落子位置, 同时服务器再将这样的信息返回给房间内的双方客户端. 然后客户端根据服务器的响应来绘制棋子位置.
  • 胜负: 服务器判定这一局游戏的胜负关系. 如果某一方玩家落子, 产生了五子连珠, 则判定胜负并返回胜负信息. 或者如果某一方玩家掉线(比如关闭页面), 也会判定对方获胜.

5. 项目创建

使用idea创建一个SpringBoot项目

引入SpringBoot / Spring MVC / MyBatis /lombok依赖

6. 实现用户模块

6.1 编写数据库代码

6.1.1数据库设计
create database if not exists java_gobang;

use java_gobang;

drop table if exists user;
create table user (
    userId int primary key auto_increment,
    username varchar(50) unique ,
    password varchar(50),
    score int,   -- 天梯积分
    totalCount int,  -- 比赛总场数
    winCount int  -- 获胜场数
);

insert into user values(null,'zhangsan','123',1000,0,0);
insert into user values(null,'lisi','123',1000,0,0);
insert into user values(null,'wangwu','123',1000,0,0);
6.1.2 配置MyBatis

编写application.yml

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/java_gobang?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
  mapper-locations: classpath:mapper/**Mapper.xml
6.1.3 创建实体类
@Data
public class User {
    private int userId;
    private String username;
    private String password;
    private int score;
    private int totalCount;
    private int winCount;
}
6.1.4 创建UserMapper

此类主要提供4个方法:

  • selectByName : 根据用户名查找用户信息,实现登录
  • insert :根据信息新增用户,用于注册
  • userWin :给获胜者修改游戏分数
  • userLose:给失败者修改游戏分数
@Mapper
public interface UserMapper {

    int insert(User user);

    User selectByName(String name);

    void userWin(int userId);

    void userLose(int userId);
}
6.1.5 实现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.java_gobang.model.UserMapper">
    <insert id="insert">
        insert into user values(null, #{username}, #{password}, 1000, 0, 0);
    </insert>

    <select id="selectByName" resultType="com.example.java_gobang.model.User">
        select * from user where username = #{username};
    </select>

    <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>
</mapper>

6.2 约定前后端交互

在这里插入图片描述

6.3 服务器开发

创建controller.UserController

实现三个方法:

  • login :实现用户登录逻辑
  • register : 实现用户注册逻辑
  • userInfo:实现登录成功后查找用户分数逻辑
@RestController
//这个类用来实现三个方法
//①注册  ②登录   ③获取用户信息
public class UserController {

    @Autowired
    private UserMapper userMapper;

    @PostMapping("/login")
    public Object login(String username, String password, HttpServletRequest req){
        User user = userMapper.selectByName(username);
        if(user == null || !user.getPassword().equals(password)){
            return new User();
        }
        System.out.println("登录" + username);
        HttpSession session = req.getSession(true);
        session.setAttribute("user",user);
        return user;
    }

    @PostMapping("/register")
    public Object register(String username,String password){
        User user = null;
        try {
            user = new User();
            user.setUsername(username);
            user.setPassword(password);
            System.out.println("register" + username);
            int ret = userMapper.insert(user);
            System.out.println("受影响的行数" + ret);
            //可能会触发一个主键重复的异常
        }catch (org.springframework.dao.DuplicateKeyException e){
            user = new User();
            //System.out.println("用户名重复");
        }
        return user;
    }


    @GetMapping("/userInfo")
    public Object getUserInfo(HttpServletRequest req){
        try{
            HttpSession session = req.getSession(false);
            User user = (User) session.getAttribute("user");
            //保证用户的分数是数据库中最新的数据
            User newUser = userMapper.selectByName(user.getUsername());
            return newUser;
        }catch (NullPointerException e){
            return new User();
        }
    }
}

6.4 客户端开发

6.4.1 登录页面

创建login.html

<!-- 导航栏 -->
    <div class="nav">
      <span>五子棋对战</span>
    </div>

    <div class="login-container">
      <div class="login-dialog">
        <!-- 标题 -->
        <h2>登录</h2>

        <div class="row">
          <span>用户名</span>
          <input type="text" id = "username">
        </div>
  
        <div class="row">
          <span>密码</span>
          <input type="password" id="password">
        </div>
  
        <div class="row-button">
          <button id="submit">提交</button>
        </div>

        <div class="register">
          <a href="register.html">注册</a>
        </div>
      </div>
    </div>

创建css.common.css

html,body {
    height: 100%;
    background-image: url(../img/背景.jpg);
    background-size: cover;
    background-repeat: no-repeat;
    background-position: center;
}

.nav{
    width: 100%;
    height: 50px;
    display: flex;
    background-color: rgba(51, 51, 51,0.4);
    color: white;
    padding-left: 20px;
    align-items: center;
}

.container{
    height: calc(100% - 50px);
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
}

创建css.login.css

.login-container{
    height: calc(100% - 50px);
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
}

.login-dialog{
    width: 400px;
    height: 320px;
    background-color: rgba(255,255,255,0.8);
    border-radius: 10px;
}

.login-dialog h2{
    text-align: center;
    padding: 20px 0;
}

.login-dialog .row{
    width: 100%;
    height: 50px;
    align-items: center;
    justify-content: center;
    display: flex;
}

.login-dialog span{
    width: 100px;
    display: block;
    /* 字体加粗 */
    font-weight: 700;
}

.row #username,#password{
    outline: none;
    border: none;
    width: 200px;
    height: 40px;
    font-size: 20px;
    text-indent: 10px;
    border-radius: 10px;
}

.login-dialog .row-button{
    margin-top: 10px;
}

.row-button #submit{
    width: 300px;
    border: none;
    height: 50px;
    color: white;
    background-color: rgb(0, 128, 0);
    font-size: 20px;
    border-radius: 10px;
    margin-left: 50px;
}

.register a{
    align-items: center;
    margin-left: 50px;
    text-decoration: none;
}

#submit:active{
    background-color: #666;
}

login.html中编写js代码,实现交互

<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>

    <script>
      let submit = document.querySelector('#submit');
      submit.onclick = function(){
        let username = document.querySelector('#username').value;
        let password = document.querySelector('#password').value;
        $.ajax({
          method:"post",
          url:"/login",
          data:{
            username : username,
            password : password
          },
          success: function(data){
            console.log(JSON.stringify(data));
            if(data && data.userId > 0){
              alert("登录成功");
              location.assign('game_hall.html');
            }else{
              alert("登录失败! 用户名或密码错误!")
            }
          }
        })
      }
    </script>

在这里插入图片描述

6.4.2 注册页面

创建register.html

<!-- 导航栏 -->
    <div class="nav">
        <span>五子棋对战</span>
      </div>
  
      <div class="register-container">
        <div class="register-dialog">
          <!-- 标题 -->
          <h2>注册</h2>

          <div class="row">
            <span>用户名</span>
            <input type="text" id = "username">
          </div>
    
          <div class="row">
            <span>密码</span>
            <input type="password" id="password">
          </div>
    
          <div class="row-button">
            <button id="submit">提交</button>
          </div>
        </div>
      </div>

css部分可以使用css.common.css部分

register.html中编写js代码实现交互

<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>

      <script>
          let submit = document.querySelector('#submit');
          submit.onclick = function(){
              let username = document.querySelector('#username').value;
              let password = document.querySelector('#password').value;
              $.ajax({
                  method:"post",
                  url:"/register",
                  data:{
                      username: username,
                      password: password
                  },
                  success: function(data){
                      console.log(JSON.stringify(data));
                    if(data && data.username){
                        alert("注册成功");
                        location.assign('login.html');
                    }else{
                        alert("注册失败")
                    }
                  }
              })
          }
      </script>

在这里插入图片描述

7. 实现匹配模块

7.1 约定前后端交互接口

在这里插入图片描述

7.2 客户端开发

7.2.1 实现页面基本属性

创建 game_hall.html

screen用于显示玩家分数

button作为匹配按钮

<div class="nav">
        五子棋对战
    </div>

    <div class="container">
        <!-- 这个用来存放用户的比赛信息 -->
        <div>
            <div id="screen"></div>
            <button id="match-button">开始匹配</button>
        </div>
    </div>

创建game_hall.css

#screen {
    width: 400px;
    height: 200px;
    font-size: 20px;
    background-color: gray;
    color: white;
    border-radius: 10px;

    text-align: center;
    line-height: 100px;
}

#match-button {
    width: 400px;
    height: 50px;
    font-size: 20px;
    color: white;
    background-color: orange;
    border: none;
    outline: none;
    border-radius: 10px;

    text-align: center;
    line-height: 50px;
    margin-top: 20px;
}

#match-button:active {
    background-color: gray;
}

编写js代码获取用户信息

<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
    <script>
        /* 获取用户信息 */
        $.ajax({
            method: 'get',
            url: '/userInfo',
            success: function(data) {
                let screen = document.querySelector('#screen');
                if(data.username == null){
                    alert("当前尚未登录,请先登录!");
                    location.replace("/login.html");
                }
                screen.innerHTML = '玩家: ' + data.username + ', 分数: ' + data.score + "<br> 比赛场次: " + data.totalCount + ", 获胜场次: " + data.winCount;
            }
        });

在这里插入图片描述

7.2.2实现匹配功能

编辑 game_hall.html 的 js 部分代码.

  • 点击匹配按钮, 就会进入匹配逻辑. 同时按钮上提示 “匹配中……(点击停止)” 字样.
  • 再次点击匹配按钮, 则会取消匹配.
  • 当匹配成功后, 服务器会返回匹配成功响应, 页面跳转到 game_room.html
/* 处理匹配功能 */
        let url = 'ws://' + location.host + '/findMatch';
        let websocket = new WebSocket(url);
        let button = document.querySelector('#match-button');
        /* 点击开始匹配 */
        button.onclick = function(){
            /* 这个可以判断websocket是否处于连接状态
            OPEN是一个常数1 ,readstate=1代表连接状态 */
            if(websocket.readyState == websocket.OPEN){
                if(button.innerHTML == '开始匹配'){
                    console.log("开始匹配");
                    /* JSON对象转为字符串 */
                    websocket.send(JSON.stringify({
                        message:'startMatch',
                    }));
                }else if(button.innerHTML == '匹配中……(点击停止)'){
                    console.log("停止匹配");
                    websocket.send(JSON.stringify({
                        message:'stopMatch',
                    }));
                }
            }else{
                console.log("当前你的连接已经断开,请重新连接");
                location.replace('/login.html');
            }
        }

        /* 处理服务器的响应 */
        /* 这个函数是当收到来自服务器的消息时调用的 */
        websocket.onmessage = function(e){
            /* 字符串转为JSON对象 */
            let resp = JSON.parse(e.data);
            if(!resp.ok){
                console.log("游戏大厅发生错误" + resp.reason);
                location.replace('/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.replace('/game_room.html');
            }else if(resp.message == 'repeatConnection'){
                alert("检测到当前为多开,请使用其他账号登录");
                location.replace("/login.html");
            }else{
                console.log("非法的message" + resp.message);
            }
        }
        
        /* 监听窗口关闭事件,当窗口关闭时,主动断开websocket链接,防止还没断开链接就关闭窗口server报错 */
        window.onbeforeunload = function () {
            websocket.close();
        }

在这里插入图片描述

7.3 服务器开发

7.3.1 创建并注册MatchAPI类

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

@Component
public class MatchAPI extends TextWebSocketHandler {
    private ObjectMapper objectMapper = new ObjectMapper();

    @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 {
    }
}

修改 config.WebSocketConfig, 把 MatchAPI 注册进去.

@Configuration
@EnableWebSocket//这个注释可以让Spring知道这是一个WebSocket配置类
public class WebSocketConfig implements WebSocketConfigurer {

    @Autowired
    private TestAPI testAPI;

    @Autowired
    private MatchAPI matchAPI;

    @Autowired
    private GameAPI gameAPI;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        //这个方法可以将一个消息处理器和一个路由关联上,访问这个路由后将使用testAPI的方法进行消息处理
        registry.addHandler(testAPI,"/test");
        //拦截器,可以获取到HttpSession中的session供webSocket中的session使用
        registry.addHandler(matchAPI,"/findMatch").
                addInterceptors(new HttpSessionHandshakeInterceptor());
    }
}
7.3.2 实现用户管理类

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

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

由于存在两个页面, 游戏大厅和游戏房间, 使用两个 哈希表 来分别存储两部分的会话.

涉及线程安全使用ConcurrentHashMap哈希表

@Component
//这个类用来管理用户的在线状态
public class OnlineUserManager {
    private ConcurrentHashMap<Integer, WebSocketSession> game_hall = new ConcurrentHashMap<>();
    private ConcurrentHashMap<Integer, WebSocketSession> game_room = new ConcurrentHashMap<>();

    //用户进入游戏大厅
    public void enterGameHall(int userId,WebSocketSession session){
        game_hall.put(userId,session);
    }

    //用户离开游戏大厅
    public void exitGameHall(int userId){
        game_hall.remove(userId);
    }

    //获取用户信息
    public WebSocketSession getGameHallSession(int userId){
        return game_hall.get(userId);
    }

    //用户进入游戏房间
    public void enterGameRoom(int userId,WebSocketSession session){
        game_room.put(userId,session);
    }

    //用户离开游戏房间
    public void exitGameRoom(int userId){
        game_room.remove(userId);
    }

    //获取用户信息
    public WebSocketSession getGameRoomSession(int userId){
        return game_room.get(userId);
    }

}
7.3.3 创建匹配请求/响应对象

创建 game.MatchRequest

@Data
public class MatchRequest {
    private String message = "";
}

创建 game.MatchResponse

@Data
public class MatchResponse {
    private boolean ok = true;
    private String reason = "";
    private String message = "";
}
7.3.4 处理连接成功

实现MatchAPI中的afterConnectionEstablished方法

@Override
    //处理用户连接
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        //session.getAttributes()获取到的是一个map,里面存放了了HttpSession中的getAttribute里的所有对象
        User user = (User) session.getAttributes().get("user");
        if(user == null){
            //玩家还未登陆就进入游戏大厅了
            MatchResponse response = new MatchResponse();
            response.setOk(false);
            response.setReason("[afterConnectionEstablished]玩家尚未登录!");
            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
            return;
        }
        //检查玩家的上线状态(是否多开)
        //在给玩家设置上线状态时,需要先判断之前玩家是否已经登录过了
        if (onlineUserManager.getGameHallSession(user.getUserId()) != null
        || onlineUserManager.getGameRoomSession(user.getUserId()) != null){
            MatchResponse response = new MatchResponse();
            response.setOk(true);
            response.setReason("当前游戏禁止多开");
            response.setMessage("repeatConnection");
            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
            return;
        }
        //当玩家获取到身份信息后,就可以给玩家设置上线状态了
        onlineUserManager.enterGameHall(user.getUserId(),session);
        System.out.println("当前玩家" + user.getUsername() + "进入游戏大厅");
    }
7.3.5 处理开始匹配/取消匹配

实现MatchAPI中的 handleTextMessage

@Override
    //处理开始/取消匹配
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        User user = (User) session.getAttributes().get("user");
        if(user == null){
            System.out.println("[handleTextMessage]玩家尚未登录");
            return;
        }
        System.out.println("开始匹配" + user.getUserId() + "message" + message.toString());
        //将解析得到的JSON请求数据转换为一个MatchRequest对象
        MatchRequest request = objectMapper.readValue(message.getPayload(),MatchRequest.class);
        MatchResponse response = new MatchResponse();
        if(request.getMessage().equals("startMatch")){
            //加入匹配器中
            //TODO
            match.add(user);
            response.setMessage("startMatch");
        }else if(request.getMessage().equals("stopMatch")){
            //从匹配器中移除
            //TODO
            match.remove(user);
            response.setMessage("stopMatch");
        }else{
            response.setOk(false);
            response.setReason("非法的匹配请求");
        }
        session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
    }
7.3.6 实现匹配器

创建 game.Matcher 类.

涉及线程安全需处理

@Component
//匹配器
public class Match {
    @Autowired
    private OnlineUserManager onlineUserManager;

    @Autowired
    private RoomManager roomManager;

    private ObjectMapper objectMapper = new ObjectMapper();

    //游戏玩家分为三档
    //第一档://2000以下(不含2000)
    //第二档://2000-3000(不含3000)
    //第三档://3000以上
    private Queue<User> normalQueue = new LinkedList<>();
    private Queue<User> highQueue = new LinkedList<>();
    private Queue<User> veryHighQueue = new LinkedList<>();

    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");
        }
    }


    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");
        }
    }

    //启动三个线程循环调用各自的队列
    private Match(){
        new Thread(){
            @Override
            public void run() {
                while(true){
                    handlerMatch(normalQueue);
                }
            }
        }.start();

        new Thread(){
            @Override
            public void run() {
                while(true){
                    handlerMatch(highQueue);
                }
            }
        }.start();

        new Thread(){
            @Override
            public void run() {
                while(true){
                    handlerMatch(veryHighQueue);
                }
            }
        }.start();
    }

    private void handlerMatch(Queue<User> matchQueue){
        synchronized (matchQueue){
            try{
                //五子棋需要两个人,当队列中人数少于2时等待
                while(matchQueue.size() < 2){
                    matchQueue.wait();
                }
                User user1 = matchQueue.poll();
                User user2 = matchQueue.poll();

                System.out.println("匹配出两个玩家" + user1.getUsername() +" " + user2.getUsername());
                WebSocketSession session1 = onlineUserManager.getGameHallSession(user1.getUserId());
                WebSocketSession session2 = onlineUserManager.getGameHallSession(user2.getUserId());
                if(session1 == null){
                    matchQueue.offer(user2);
                    return;
                }
                if(session2 == null){
                    matchQueue.offer(user1);
                    return;
                }
                //防止多开
                if (session1 == session2){
                    matchQueue.add(user1);
                }

                // 将两个玩家加入对战房间
                Room room = new Room();
                roomManager.add(user1.getUserId(),user2.getUserId(),room);


                //给玩家1发送匹配成功的信息
                MatchResponse response1 = new MatchResponse();
                response1.setOk(true);
                response1.setMessage("MatchSuccess");
                session1.sendMessage(new TextMessage(objectMapper.writeValueAsString(response1)));
                //给玩家2发送匹配成功的信息
                MatchResponse response2 = new MatchResponse();
                response2.setOk(true);
                response2.setMessage("MatchSuccess");
                session2.sendMessage(new TextMessage(objectMapper.writeValueAsString(response2)));
            }catch (IOException | InterruptedException e){
                e.printStackTrace();
            }
        }


    }
}
7.3.7 实现房间类

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

创建 game.Room

  • 一个房间要包含一个房间 ID, 使用 UUID 作为房间的唯一身份标识.
  • 房间内要记录对弈的玩家双方信息.
  • 记录先手方的 ID
  • 记录一个 二维数组 , 作为对弈的棋盘.
  • 记录一个 OnlineUserManager, 以备后面和客户端进行交互.
@Data
public class Room {

    //由于Room不能是唯一的,所以不能注入到Spring中,从而也不可以用 Autowired注入这三个bean
    //因此我们需要手动注入这三个bean后续会说怎么处理
    private OnlineUserManager onlineUserManager;
    private RoomManager roomManager;
    private UserMapper userMapper;

    private ObjectMapper objectMapper = new ObjectMapper();

    private String roomId;
    private User user1;
    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];
		public Room() {
        // 使用 uuid 作为唯一身份标识
        roomId = UUID.randomUUID().toString();
    }
7.3.8 实现房间管理器类

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

创建 game.RoomManager

  • 使用一个 Hash 表, 保存所有的房间对象, key 为 roomId, value 为 Room 对象
  • 再使用一个 Hash 表, 保存 userId -> roomId 的映射, 方便根据玩家来查找所在的房间.
  • 提供增, 删, 查的 API. (查包含两个版本, 基于房间 ID 的查询和基于用户 ID 的查询).
@Component
public class RoomManager {

    //存储所有的Room房间
    ConcurrentHashMap<String,Room> rooms = new ConcurrentHashMap<>();
    //存储用户和房间的关联关系
    ConcurrentHashMap<Integer ,String> userIdToRoomId = new ConcurrentHashMap<>();

    public void add(int user1Id,int user2Id,Room room){
        rooms.put(room.getRoomId(),room);
        userIdToRoomId.put(user1Id,room.getRoomId());
        userIdToRoomId.put(user2Id,room.getRoomId());
    }

    public void remove(int user1Id,int userId2,String roomId){
        rooms.remove(roomId);
        userIdToRoomId.remove(user1Id);
        userIdToRoomId.remove(userId2);
    }

    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);
    }
}
7.3.9 处理连接关闭/异常

实现MatchAPI中的afterConnectionClosed

    @Override
    //异常连接处理
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        User user = (User) session.getAttributes().get("user");
        try{
            WebSocketSession tmpSession = onlineUserManager.getGameHallSession(user.getUserId());
            if(tmpSession == session){
                onlineUserManager.exitGameHall(user.getUserId());
            }
            //TODO 从匹配器中移除
            match.remove(user);
            System.out.println("玩家"+ user.getUsername() +"离开游戏大厅");
        }catch (NullPointerException e){
            System.out.println("[handleTransportError]当前用户尚未登录");
        }
    }

    @Override
    //处理玩家断开连接
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        User user = (User) session.getAttributes().get("user");
        try{
            WebSocketSession tmpSession = onlineUserManager.getGameHallSession(user.getUserId());
            if(tmpSession == session){
                onlineUserManager.exitGameHall(user.getUserId());
            }
            //TODO 从匹配器中移除
            match.remove(user);
            System.out.println("玩家"+ user.getUsername() +"离开游戏大厅");
        }catch (NullPointerException e){
            System.out.println("[afterConnectionClosed]当前用户尚未登录");
        }

    }

8. 实现对战模块

8.1 约定前后端交互

在这里插入图片描述

8.2 客户端开发

创建 game_room.html, 表示对战页面.

<div class="nav">
        联机五子棋
    </div>
    <div class="container">
        <div>
            <canvas id="chess" width="450px" height="450px"></canvas>
            <div id="screen">等待玩家连接中...</div>
        </div>
    </div>

    <script src="js/script.js"></script>

创建 css/game_room.css

#screen {
    font-size: 22px;
    text-align: center;
    background-color: rgba(255,255,255,0,7);
    color: yellow;
    margin-bottom: 20px;
}
8.2.1 实现棋盘/棋子绘制

创建 js/script

这段代码可以直接复制粘贴,不需要深究其中含义

gameInfo = {
    roomId: null,
    thisUserId: null,
    thatUserId: null,
    isWhite: true,
}

//
// 设定界面显示相关操作
//

function setScreenText(me) {
    let screen = document.querySelector('#screen');
    if (me) {
        screen.innerHTML = "轮到你落子了!";
    } else {
        screen.innerHTML = "轮到对方落子了!";
    }
}

//
// 初始化 websocket
//
// TODO

//
// 初始化一局游戏
//
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, "#636766");
        } else {
            gradient.addColorStop(0, "#D1D1D1");
            gradient.addColorStop(1, "#F9F9F9");
        }
        context.fillStyle = gradient;
        context.fill();
    }

    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) {
            // TODO 发送坐标给服务器, 服务器要返回结果

            oneStep(col, row, gameInfo.isWhite);
            chessBoard[row][col] = 1;
        }
    }

    // TODO 实现发送落子请求逻辑, 和处理落子响应逻辑. 
}

initGame();

8.2.2 初始化websocket

在刚才代码中加入websocket

//使用location.host 是为了后续部署到云服务器上做准备的
//也可写作127.0.0.1:8080
let websocketUrl = "ws://" + location.host + "/game";
let websocket = new WebSocket(websocketUrl);

websocket.onopen = function() {
    console.log("连接游戏房间成功!");
}

websocket.close = function() {
    console.log("和游戏服务器断开连接!");
}

websocket.onerror = function() {
    console.log("和服务器的连接出现异常!");
}

window.onbeforeunload = function() {
    websocket.close();
}

// 处理服务器返回的响应数据
websocket.onmessage = function(event) {
    console.log("[handlerGameReady] " + event.data);
    let resp = JSON.parse(event.data);

    if (!resp.ok) {
        alert("连接游戏失败! reason: " + resp.reason);
        // 如果出现连接失败的情况, 回到游戏大厅
        location.areplacessign("/game_hall.html");
        return;
    }

    if (resp.message == 'readyGame') {
        gameInfo.roomId = resp.roomId;
        gameInfo.thisUserId = resp.thisUserId;
        gameInfo.thatUserId = resp.thatUserId;
        gameInfo.isWhite = (resp.whiteUserId == resp.thisUserId);

        // 初始化棋盘
        initGame();
        // 设置显示区域的内容
        setScreenText(gameInfo.isWhite);
    } else if (resp.message == 'repeatConnection') {
        alert("检测到游戏多开! 请使用其他账号登录!");
        location.replace("/login.html");
    }
}
8.2.3 发送落子请求

修改刚刚的onclick方法

注释掉原有的 onStep 和 修改 chessBoard 的操作, 放到接收落子响应时处理.

实现 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);

            // 留到浏览器收到落子响应的时候再处理(收到响应再来画棋子)
            // oneStep(col, row, gameInfo.isWhite);
            // chessBoard[row][col] = 1;
        }
    }
    function send(row, col) {
        let req = {
            message: 'putChess',
            userId: gameInfo.thisUserId,
            row: row,
            col: col
        };

        websocket.send(JSON.stringify(req));
    }
8.2.4 处理落子响应

在 initGame 中, 修改 websocket 的 onmessage

websocket.onmessage = function(event) {
        console.log("[handlerPutChess] " + event.data);

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

        // 先判定当前这个响应是自己落的子, 还是对方落的子.
        if (resp.userId == gameInfo.thisUserId) {
            // 我自己落的子
            // 根据我自己子的颜色, 来绘制一个棋子
            oneStep(resp.col, resp.row, gameInfo.isWhite);
        } else if (resp.userId == gameInfo.thatUserId) {
            // 我的对手落的子
            oneStep(resp.col, resp.row, !gameInfo.isWhite);
        } else {
            // 响应错误! userId 是有问题的!
            console.log('[handlerPutChess] resp userId 错误!');
            return;
        }

        // 给对应的位置设为 1, 方便后续逻辑判定当前位置是否已经有子了. 
        chessBoard[resp.row][resp.col] = 1;

        // 交换双方的落子轮次
        me = !me;
        setScreenText(me);

        // 判定游戏是否结束
        let screenDiv = document.querySelector('#screen');
        if (resp.winner != 0) {
            if (resp.winner == gameInfo.thisUserId) {
                // alert('你赢了!');
                screenDiv.innerHTML = '你赢了!';
            } else if (resp.winner = gameInfo.thatUserId) {
                // alert('你输了!');
                screenDiv.innerHTML = '你输了!';
            } else {
                alert("winner 字段错误! " + resp.winner);
            }
            // 回到游戏大厅
            // location.assign('/game_hall.html');

            // 增加一个按钮, 让玩家点击之后, 再回到游戏大厅~
            let backBtn = document.createElement('button');
            backBtn.innerHTML = '回到大厅';
            backBtn.style.backgroundColor = "green";
            backBtn.style.width = "450px";
            backBtn.style.height = "50px";
            backBtn.style.border = "none";
            backBtn.style.borderRadius = "10px";
            backBtn.onclick = function() {
                location.replace('/game_hall.html');
            }
            let fatherDiv = document.querySelector('.container>div');
            fatherDiv.appendChild(backBtn);
        }
    }

8.3 服务器开发

创建 api.GameAPI , 处理 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 {
    }
}

修改 WebSocketConfig, 将 GameAPI 进行注册.

@Configuration
@EnableWebSocket//这个注释可以让Spring知道这是一个WebSocket配置类
public class WebSocketConfig implements WebSocketConfigurer {

    @Autowired
    private TestAPI testAPI;

    @Autowired
    private MatchAPI matchAPI;

    @Autowired
    private GameAPI gameAPI;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        //这个方法可以将一个消息处理器和一个路由关联上,访问这个路由后将使用testAPI的方法进行消息处理
        registry.addHandler(testAPI,"/test");
        //拦截器,可以获取到HttpSession中的session供webSocket中的session使用
        registry.addHandler(matchAPI,"/findMatch").
                addInterceptors(new HttpSessionHandshakeInterceptor());

        registry.addHandler(gameAPI,"/game").
                addInterceptors(new HttpSessionHandshakeInterceptor());
    }
}

8.3.1 创建落子请求/响应对象

创建game.GameRequest

@Data
public class GameRequest {
    private String message = "pusChess";
    private int userId;
    private int row;
    private int col;
}

创建game.GameResponse

@Data
public class GameResponse {
    private String message = "putChess";
    private int userId;
    private int row;
    private int col;
    private int winner;//获胜者id
}

创建 game.GameReadyResponse

@Data
public class GameReadyResponse {
    private String message = "readyGame";
    private boolean ok = true;
    private String reason;
    private String roomId;
    private int thisUserId = 0;
    private int thatUserId = 0;
    private int whiteUserId = 0;
}
8.3.2 处理连接成功

实现 GameAPI 的 afterConnectionEstablished 方法.

@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("[afterConnectionEstablished]当前用户尚未登录");
            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.getGameHallSession(user.getUserId()) != null ||
            onlineUserManager.getGameRoomSession(user.getUserId()) != null){
            resp.setOk(false);
            resp.setReason("当前游戏禁止多开");
            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));
            return;
        }

        //更新用户会话
        //游戏大厅和游戏房间的会话是不一样的
        onlineUserManager.enterGameRoom(user.getUserId(),session);
        //一个房间有两个玩家,因此使用时需要考虑到线程安全
        synchronized (room){
            //设置use1为先手
            if(room.getUser1() == null){
                room.setUser1(user);
                room.setWhiteUserId(user.getUserId());
                System.out.println("玩家1" + user.getUsername() + "准备就绪");
                return;
            }
            if(room.getUser2() == null){
                room.setUser2(user);
                System.out.println("玩家2" + user.getUsername() + "准备就绪");

                //通知玩家1\2\游戏就绪了
                notifyGameReady(room,room.getUser1().getUserId(),room.getUser2().getUserId());
                notifyGameReady(room,room.getUser2().getUserId(),room.getUser1().getUserId());
                return;
            }
            resp.setOk(true);
            resp.setReason("房间已经满了");
            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));
        }

    }
8.3.3 实现通知玩家就绪
private void notifyGameReady(Room room,int thisUserId,int thatUserId) throws IOException {
        GameReadyResponse response = new GameReadyResponse();
        response.setOk(true);
        response.setThisUserId(thisUserId);
        response.setThatUserId(thatUserId);
        response.setWhiteUserId(room.getWhiteUserId());
        WebSocketSession session = onlineUserManager.getGameRoomSession(thisUserId);
        session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
    }
8.3.4 玩家下线处理

也要注意多开

@Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        User user = (User) session.getAttributes().get("user");
        if(user == null){
            return;
        }
        WebSocketSession session1 = onlineUserManager.getGameRoomSession(user.getUserId());
        if(session1 != 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 session1 = onlineUserManager.getGameRoomSession(user.getUserId());
        if(session1 != session){
            System.out.println("当前会话不是游戏中玩家的会话");
            return;
        }
        System.out.println("用户退出 userId = " + user.getUserId());
        onlineUserManager.exitGameRoom(user.getUserId());
        noticeThatUserWin(user);
    }
8.3.5 手动注入bean

在启动类中加入这个
在这里插入图片描述修改room

在这里插入图片描述

8.3.6 处理落子请求
@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());
    }
8.3.7 实现对弈功能

实现 room 中的 putChess 方法.

//用这个方法实现落子响应
    public void putChess(String message) throws IOException {
        GameRequest request = new GameRequest();
        GameResponse response = new GameResponse();
        request = objectMapper.readValue(message,GameRequest.class);
        int row = request.getRow();
        int col = request.getCol();
        //判断是谁下的字
        //做出约定:
        //①如果是玩家一,则下的子为1,
        //②是玩家而,则下的子是2
        int chess = request.getUserId() == user1.getUserId() ? 1 : 2;

        if(chessBoard[row][col] != 0){
            System.out.println("下的子有误" + request);
            return;
        }

        //1.进行落子
        chessBoard[row][col] = chess;
        printBoard();
        //2.检查游戏是否结束
        int winner = checkWinner(chess,row,col);
        System.out.println(winner);
        //3.把响应写回给玩家
        response.setUserId(request.getUserId());
        response.setRow(row);
        response.setCol(col);
        response.setWinner(winner);
        //4.检查玩家的在线状态
        WebSocketSession session1 = onlineUserManager.getGameRoomSession(user1.getUserId());
        WebSocketSession session2 = onlineUserManager.getGameRoomSession(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 respJson = objectMapper.writeValueAsString(response);
        if(session1 != null){
            session1.sendMessage(new TextMessage(respJson));
        }
        if(session2 != null){
            session2.sendMessage(new TextMessage(respJson));
        }

        //5.已经分出胜负,销毁房间
        if(response.getWinner() != 0){
            //更新数据
            userMapper.userWin(response.getWinner() == user1.getUserId() ? user1.getUserId() : user2.getUserId());
            userMapper.userLose(response.getWinner() == user1.getUserId() ? user2.getUserId() : user1.getUserId());
            //销毁房间
            roomManager.remove(user1.getUserId(),user2.getUserId(),roomId);
            System.out.println("游戏结束,房间已销毁 roomId" + roomId + "获胜方" + response.getWinner());
        }
    }
8.3.8 打印棋盘

实现room中的PrintBoard

private void printBoard() {
        System.out.println("打印棋盘信息" + roomId);
        System.out.println("------------------------");
        for(int r = 0 ; r < MAX_ROW ; r++){
            for (int c = 0; c < MAX_COL; c++) {
                System.out.print(chessBoard[r][c] + " ");
            }
            System.out.println();
        }
        System.out.println("------------------------");
    }
8.3.9 判决胜负

实现room中的checkWinner

这个方法其实很简单

(假设为行,其余三种也是一样)当出现五子连珠时,这最后一步肯定在这个五个子中

的一个,那么我们只需判断每次落子后左边4个和右边4个是否和自己颜色一样即可。

在这里插入图片描述

private int checkWinner(int chess, int row, int col) {
        // 以 row, col 为中心
        for (int c = col - 4; c <= col; c++) {
            // 针对其中的一种情况, 来判定这五个子是不是连在一起了~
            // 不光是这五个子得连着, 而且还得和玩家落的子是一样~~ (才算是获胜)
            try {
                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) {
                    // 构成了五子连珠! 胜负已分!
                    return chess == 1 ? user1.getUserId() : user2.getUserId();
                }
            } catch (ArrayIndexOutOfBoundsException e) {
                // 如果出现数组下标越界的情况, 就在这里直接忽略这个异,继续循环下一组数据
                continue;
            }
        }

        // 2. 检查所有列
        for (int r = row - 4; r <= row; r++) {
            try {
                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) {
                    return chess == 1 ? user1.getUserId() : user2.getUserId();
                }
            } catch (ArrayIndexOutOfBoundsException e) {
                continue;
            }
        }

        // 3. 检查左对角线
        for (int r = row - 4, c = col - 4; r <= row && c <= col; r++, c++) {
            try {
                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) {
                    return chess == 1 ? user1.getUserId() : user2.getUserId();
                }
            } catch (ArrayIndexOutOfBoundsException e) {
                continue;
            }
        }

        // 4. 检查右对角线
        for (int r = row - 4, c = col + 4; r <= row && c >= col; r++, c--) {
            try {
                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) {
                    return chess == 1 ? user1.getUserId() : user2.getUserId();
                }
            } catch (ArrayIndexOutOfBoundsException e) {
                continue;
            }
        }

        // 胜负未分, 就直接返回 0 了.
        return 0;
8.3.10 处理玩家中途退出

在 GameAPI 中

//如果玩家掉线通知对手获胜
    private void noticeThatUserWin(User user) throws IOException {
        Room room = roomManager.getRoomByUserId(user.getUserId());
        if(room == null){
            System.out.println("房间已经释放,无需通知");
            return;
        }
        User thatUser = room.getUser1() == user ? room.getUser2() : room.getUser1();
        WebSocketSession session = onlineUserManager.getGameRoomSession(thatUser.getUserId());
        if(session == null){
            //这情况意味着对手也掉线了
            System.out.println("该玩家已掉线,无需通知");
            return;
        }
        //发送响应通知对手
        GameResponse response = new GameResponse();
        response.setUserId(thatUser.getUserId());
        response.setWinner(thatUser.getUserId());
        session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));

        //更新玩家分数
        userMapper.userWin(thatUser.getUserId());
        userMapper.userLose(user.getUserId());
        //销毁房间
        roomManager.remove(user.getUserId(),thatUser.getUserId(),room.getRoomId());
        System.out.println("游戏结束,房间已销毁 roomId" + room.getRoomId() + "获胜方" + user.getUserId());
    }

在这里插入图片描述

9. 部署到云服务器上

9.1 增添数据库

将我们写的db.sql直接复制到云服务器上。

9.2 微调代码

在这里插入图片描述

9.3 打包

通过maven打包
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

9.4 运行

使用java -jar + 包名即可
在这里插入图片描述

9.5 验证

在这里插入图片描述

总结

此项目中包含了许多问题,如多开账号的处理、玩家突然掉线的处理、玩家按了回退之后的处理、多线程下线程安全的问题……但是作为一个项目来说,功能还是不太全面,后续预计将进行改善增添功能如:玩家观战、生成对局回放、生成AI对手等等,现在时间较紧迫,只能先做出这几个功能。

  • 7
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
springcloud-netflix是一个基于Spring Cloud的微服务框架。它提供了一系列工具和组件来简化开发和管理分布式系统的任务。其中包括Eureka、Feign和Zuul等组件。 在搭建springcloud-netflix项目时,需要创建父工程和子工程。父工程是springcloud-netflix-parent,子工程可以是springcloud-netflix-eureka、springcloud-netflix-service-pay等。每个子工程都需要在pom.xml文件中导入相应的依赖。 对于springcloud-netflix-eureka,需要导入spring-cloud-starter-netflix-eureka-server和spring-cloud-starter-netflix-eureka-client等依赖。此外,还需要配置相关的类。 对于springcloud-netflix-service-pay,需要导入spring-cloud-starter-netflix-eureka-client、spring-boot-starter-web和spring-cloud-starter-openfeign等依赖。同样,也需要配置相关的类。 对于Zuul,它是一个API Gateway服务器,提供了动态路由、监控、弹性和安全等边缘服务的框架。在搭建Zuul时,需要导入spring-cloud-starter-netflix-eureka-client、spring-boot-starter-web和spring-cloud-starter-netflix-zuul等依赖。同时,需要配置开启Zuul。 总之,springcloud-netflix是一个基于Spring Cloud的微服务框架,包括了Eureka、Feign和Zuul等组件,可以帮助简化开发和管理分布式系统的任务。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [SpringCloudNetflix](https://blog.csdn.net/Exist_W/article/details/131867868)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

海绵宝宝养的的小窝

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值