标准项目-----网页五子棋(4)-----游戏大厅+匹配+房间代码

发布于:2025-08-02 ⋅ 阅读:(21) ⋅ 点赞:(0)

页面实现

hall.html

<!DOCTYPE html>
<html lang="ch">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>游戏大厅</title>
    <link rel="stylesheet" href="css/common.css">
    <link rel="stylesheet" href="css/hall.css">
</head>
<body>
    <div class="nav">五子棋匹配大厅</div>
    <div class="container">
        <div class="dialog">
            <!-- 展示用户信息 -->
            <div id="screen"></div>
            <!-- 开始匹配 -->
            <button id="match" onclick="findMatch()">开始匹配</button>
        </div>
    </div>
    <script src="js/jquery.min.js"></script>

</body>
</html>

 hall.html

.container {
    height: calc(100% - 50px);
    display: flex;
    justify-content: center;
    align-items: center;
}
 
.container .dialog {
    height: 350px;
    width: 299px;
    background-color: white;
    border-radius: 20px;
    padding-top: 30px;
    display: flex;
    justify-content: center;
    /* align-items: center; */
    flex-wrap: wrap
}
 
.dialog *{
    display: flex;
    justify-content: center;
    align-items: center;
}
 
.dialog #screen {
    width: 250px;
    height: 150px;
    background-color: wheat;
    border-radius: 10px;
}
 
.dialog #match {
    width: 150px;
    height: 40px;
    background-color: rgb(255, 159, 33);
    border-radius: 10px;
}
 
.dialog #match:active {
    background-color: rgb(204, 128, 21);
}

获取用户信息接口

当用户进入 游戏大厅时,就应该获取到登录用户的信息显示到页面上,我们使用js代码从访问后端接口获取信息

  <script src="js/jquery.min.js"></script>
    <script> 
            $.ajax({
                url:"/user/getUserInfo",
                type:"get",
                success: function(result) {
                if(result.username != null) {
                    let screen = document.querySelector("#screen");
                    screen.innerHTML = '当前玩家:' + result.username + '<br>天梯积分:' + 
                    result.score + '<br>比赛场次:' + result.totalCount + 
                    '<br>获胜场次:' + result.winCount;
                }else{
                    alert("获取用户信息失败,请重新登录");
                    location.href = "/login.html";
                }
            },
            error: function() {
                alert("获取用户信息失败");
            }
            })



    </script>

WebSocket前端代码

当用户点击匹配按钮时,需要告知服务器该用户要进行匹配,服务器如果接收到则立即回复表示正在匹配,当匹配成功服务器则又需要发送匹配信息给客户端。这里涉及到服务器主动给客户端发送消息的场景,所以我们使用websocket实现

 初始化websocket
   var webSocket= new WebSocket("ws://localhost:8080/game"); 
         webSocket.onopen = function() {
            console.log("连接成功");
        }
        webSocket.onclose = function() {
            console.log("连接关闭");
        }
        webSocket.onerror = function() {
            console.log("error");
        }
 
        //页面关闭时释放webSocket
        window.onbeforeunload = function() {
            webSocket.close();
        }
 
        //处理服务器发送的消息
        webSocket.onmessage = function(e) {
 
        }
实现findMatch()方法

点击开始匹配按钮后就会执行findMatch方法,进入匹配状态,此时我们可以把开始匹配按钮替换成取消匹配按钮,再次点击则会向服务器发送取消匹配请求

 function findMatch() {
            //检查websocket连接
            if(webSocket.readyState == webSocket.OPEN) {
                if($("#match").text() == '开始匹配') {
                    console.log("开始匹配");
                    webSocket.send(JSON.stringify({
                        message: 'startMatch' //约定startMatch表示开始匹配
                    }));
                }else if($("#match").text() == '匹配中...') {
                    console.log("停止匹配");
                    webSocket.send(JSON.stringify({
                        message: 'stopMatch' //约定stopMatch表示停止匹配
                    }));
                }
            }else{
                alert("连接断开,请重新登录");
                location.href = "/login.html";
            }
        }
实现onmessage

我们约定服务器返回的响应为包含以下三个字段的json:

ok: true/false,  //表示请求成功还是失败
errMsg: "错误信息",  //请求失败返回错误信息
message: 'startMatch' 开始匹配 / 'stopMatch' 停止匹配/ 'success' 匹配成功 / 'no_login' 用户未登录 / ’repeat_login'该账号重复登录

 webSocket.onmessage = function(e) {
            //解析json字符串为js对象
            let resp = JSON.parse(e.data);
            if(resp.message == 'startMatch') {
                //开始匹配请求发送成功正在匹配
                //替换按钮描述
                $("#match").text("匹配中...");
            }else if(resp.message == 'stopMatch') {
                //取消匹配请求发送成功已取消匹配
                //替换按钮描述
                $("#match").text("开始匹配");
            }else if(resp.message == 'success'){
                //匹配成功
                console.log("匹配成功! 进入游戏房间");
                location.assign("/room.html");
                console.log("进入游戏房间");
            }else if(resp.message == 'repeat_login') {
                alert("该账号已在别处登录");
                location.href = "/login.html";
            }else if(resp.message == 'no_login') {
                alert("当前还未登录");
                location.href = "/login.html";
            }else {
                alert("非法响应 errMsg:" + resp.errMsg);
            }
 
        }

WebSocket后端代码

注册websocket  

创建TextWebSocketHandler子类,重写如下方法: 

package org.ting.j20250110_gobang.websocket;
 
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
 
@Component
public class MatchWebSocket extends TextWebSocketHandler {
    //连接成功后执行
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        super.afterConnectionEstablished(session);
    }
    //接收到请求后执行
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        super.handleTextMessage(session, message);
    }
    //连接异常时执行
    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        super.handleTransportError(session, exception);
    }
    //连接正常断开后执行
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        super.afterConnectionClosed(session, status);
    }
}

注册socket:

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Autowired
    private TextWebSocketHandler textWebSocketHandler;
    @Autowired
    private MatchWebSocket matchWebSocket;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(textWebSocketHandler, "/test");
        registry.addHandler(matchWebSocket, "/findMatch") //注意路径和前端对应
                //添加拦截器获取到session,方便获取session中的用户信息
                .addInterceptors(new HttpSessionHandshakeInterceptor());

    }
}

维护在线用户

用户登录成功后,我们可以维护好用户的websocket会话,把用户表示为在线状态,方便获取到用户的websocket会话

@Component
public class OnlineUserManager {
    //使用ConcurrentHashMap保证线程安全
    private Map<Integer, WebSocketSession> onlineUser = new ConcurrentHashMap<>();

    public void enterGameHall(int userId, WebSocketSession session) {
        //用户上线
        onlineUser.put(userId, session);
    }

    public void exitGameHall(int userId) {
        //用户下线
        onlineUser.remove(userId);
    }

    public WebSocketSession getFromHall(int userId) {
        //获取用户的websocket会话
        return onlineUser.get(userId);
    }
}

实现webSocket相关方法

上期我们定义了webSocket的处理类,但是并没有完成重写的方法,接下来我们借助维护的在线用户具体实现如下方法

在实现这些方法之前,我们还需要按照上期约定好的信息交互形式定义两个实体类,代表请求和响应:

public class MatchRequest {
    private String message;

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}
@Data
public class MatchResponse {
    private boolean ok;
    private String errMsg;
    private String message;


}
连接成功
  public void afterConnectionEstablished(WebSocketSession session) throws Exception {
          try {
              User user = (User) session.getAttributes().get("user");
              if (onlineUserManager.getFromHall(user.getUserId()) != null) {
                  MatchResponse response = new MatchResponse();
                  response.setOk(false);
                  response.setErrMsg("已经在别处登录");
                  response.setMessage("repeat_login");
                  session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
                  // 此处直接关闭有些太激进了, 还是返回一个特殊的 message , 供客户端来进行判定, 由客户端负责进行处理
                  // session.close();
                  return;
              } else {
                  onlineUserManager.enterGameHall(user.getUserId(), session);
                  System.out.println("用户:" + user.getUsername() + " 已上线");
              }
          }catch (NullPointerException e){
              System.out.println("[MatchAPI.afterConnectionEstablished] 当前用户未登录!");
              // e.printStackTrace();
              // 出现空指针异常, 说明当前用户的身份信息是空, 用户未登录呢.
              // 把当前用户尚未登录这个信息给返回回去~~
              MatchResponse response = new MatchResponse();
              response.setOk(false);
              response.setErrMsg("您尚未登录! 不能进行后续匹配功能!");
              response.setMessage("no_login");
              session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
          }

    }
连接断开
 public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        try {
            User user = (User)session.getAttributes().get("user");
            //防止重复登录时删除正常登录的在线信息
            if(onlineUserManager.getFromHall(user.getUserId()).equals(session)) {
                onlineUserManager.exitGameHall(user.getUserId());
                System.out.println("用户:" + user.getUsername() + " 已下线");
            }
        }catch (NullPointerException e) {
            System.out.println("[MatchAPI.handleTransportError] 当前用户未登录!");
            MatchResponse response = new MatchResponse();
            response.setOk(false);
            response.setErrMsg("用户未登录");
            response.setMessage("no_login");
            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
        }
    }
    //连接正常断开后执行
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        try {
            User user = (User)session.getAttributes().get("user");

            //防止重复登录时删除正常登录的在线信息
            if(onlineUserManager.getFromHall(user.getUserId()).equals(session)) {
                onlineUserManager.exitGameHall(user.getUserId());
                System.out.println("用户:" + user.getUsername() + " 已下线");
            }

        }catch (NullPointerException e) {
            System.out.println("[MatchAPI.afterConnectionClosed] 当前用户未登录!");
            MatchResponse response = new MatchResponse();
            response.setOk(false);
            response.setErrMsg("用户未登录");
            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
        }
    }
}
定义Mather类
@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;

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

    // 当玩家点击停止匹配的时候, 就需要把玩家从匹配队列中删除
    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!");
        }
    }

}
处理匹配请求
@Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        User user = (User) session.getAttributes().get("user");
        // 获取到客户端给服务器发送的数据
        String payload = message.getPayload();
        // 当前这个数据载荷是一个 JSON 格式的字符串, 就需要把它转成 Java 对象. MatchRequest
        MatchRequest request = objectMapper.readValue(payload, MatchRequest.class);
        MatchResponse response = new MatchResponse();
        if (request.getMessage().equals("startMatch")) {
            // 进入匹配队列
            matcher.add(user);
            // 把玩家信息放入匹配队列之后, 就可以返回一个响应给客户端了.
            response.setOk(true);
            response.setMessage("startMatch");
        } else if (request.getMessage().equals("stopMatch")) {
            // 退出匹配队列
            matcher.remove(user);
            // 移除之后, 就可以返回一个响应给客户端了.
            response.setOk(true);
            response.setMessage("stopMatch");
        } else {
            response.setOk(false);
            response.setErrMsg("非法的匹配请求");
        }
        String jsonString = objectMapper.writeValueAsString(response);
        session.sendMessage(new TextMessage(jsonString));
    }
 游戏房间实体类
package com.example.demo;

import com.example.demo.dao.User;

import java.util.UUID;
//每个房间都是不通的,所以不能给spring管理
public class Room {
    private String roomId;
    private User user1;
    private User user2;
    // 先手方的玩家 id
    private int whiteUser;

    public int getWhiteUser() {
        return whiteUser;
    }

    public void setWhiteUser(int whiteUser) {
        this.whiteUser = whiteUser;
    }

    public Room() {
        roomId = UUID.randomUUID().toString();
    }
    public String getRoomId() {
        return roomId;
    }

    public void setRoomId(String roomId) {
        this.roomId = roomId;
    }

    public User getUser1() {
        return user1;
    }

    public void setUser1(User user1) {
        this.user1 = user1;
    }

    public User getUser2() {
        return user2;
    }

    public void setUser2(User user2) {
        this.user2 = user2;
    }
}
实现匹配功能
创建线程扫描队列

我们为每个匹配队列创建一个线程,用来实现匹配功能,我们在构造方法中创建线程:

 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()方法进行匹配
 public void handlerMatch(Queue<User> matchQueue) {
        try {
            //对操作的队列加锁保证线程安全
            synchronized (matchQueue) {
                //1.检测队列中是否有两个元素
                while(matchQueue.size() < 2) {
                    matchQueue.wait();
                }

                //2.从队列中取出两个玩家
                User user1 = matchQueue.poll();
                User user2 = matchQueue.poll();

                //3.获取到两个玩家的会话信息
                WebSocketSession session1 = onlineUserManager.getFromHall(user1.getUserId());
                WebSocketSession session2 = onlineUserManager.getFromHall(user2.getUserId());

                //4.todo 把两个玩家放到一个游戏房间中

                //5.给用户返回匹配成功的响应
                MatchResponse response = new MatchResponse();
                response.setOk(true);
                response.setMessage("success");
                String json = objectMapper.writeValueAsString(response);
                session1.sendMessage(new TextMessage(json));
                session2.sendMessage(new TextMessage(json));
            }
        }catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }

    }
修改websocket后端代码
@Component
public class MatchWebSocket extends TextWebSocketHandler {
    @Autowired
    private Matcher matcher;
    @Autowired
    private OnlineUserManager onlineUserManager;
    ObjectMapper objectMapper=new ObjectMapper();
    //连接成功后执行
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
          try {
              User user = (User) session.getAttributes().get("user");
              if (onlineUserManager.getFromHall(user.getUserId()) != null) {
                  MatchResponse response = new MatchResponse();
                  response.setOk(true);
                  response.setErrMsg("已经在别处登录");
                  response.setMessage("repeat_login");
                  session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
                  // 此处直接关闭有些太激进了, 还是返回一个特殊的 message , 供客户端来进行判定, 由客户端负责进行处理
                  // session.close();
                  return;
              } else {
                  onlineUserManager.enterGameHall(user.getUserId(), session);
                  System.out.println("用户:" + user.getUsername() + " 已上线");
              }
          }catch (NullPointerException e){
              System.out.println("[MatchAPI.afterConnectionEstablished] 当前用户未登录!");
              // e.printStackTrace();
              // 出现空指针异常, 说明当前用户的身份信息是空, 用户未登录呢.
              // 把当前用户尚未登录这个信息给返回回去~~
              MatchResponse response = new MatchResponse();
              response.setOk(true);
              response.setErrMsg("您尚未登录! 不能进行后续匹配功能!");
              response.setMessage("no_login");
              session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
          }

    }
    //接收到请求后执行
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        User user = (User) session.getAttributes().get("user");
        // 获取到客户端给服务器发送的数据
        String payload = message.getPayload();
        // 当前这个数据载荷是一个 JSON 格式的字符串, 就需要把它转成 Java 对象. MatchRequest
        MatchRequest request = objectMapper.readValue(payload, MatchRequest.class);
        MatchResponse response = new MatchResponse();
        if (request.getMessage().equals("startMatch")) {
            // 进入匹配队列
            matcher.add(user);
            // 把玩家信息放入匹配队列之后, 就可以返回一个响应给客户端了.
            response.setOk(true);
            response.setMessage("startMatch");
        } else if (request.getMessage().equals("stopMatch")) {
            // 退出匹配队列
            matcher.remove(user);
            // 移除之后, 就可以返回一个响应给客户端了.
            response.setOk(true);
            response.setMessage("stopMatch");
        } else {
            response.setOk(false);
            response.setErrMsg("非法的匹配请求");
        }
        String jsonString = objectMapper.writeValueAsString(response);
        session.sendMessage(new TextMessage(jsonString));
    }
    //连接异常时执行
    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        try {
            User user = (User)session.getAttributes().get("user");
            //防止重复登录时删除正常登录的在线信息
            if(onlineUserManager.getFromHall(user.getUserId()).equals(session)) {
                onlineUserManager.exitGameHall(user.getUserId());
                System.out.println("用户:" + user.getUsername() + " 已下线");
                matcher.remove(user);
            }
        }catch (NullPointerException e) {
            System.out.println("[MatchAPI.handleTransportError] 当前用户未登录!");
            MatchResponse response = new MatchResponse();
            response.setOk(false);
            response.setErrMsg("用户未登录");
            response.setMessage("no_login");
            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
        }
    }
    //连接正常断开后执行
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        try {
            User user = (User)session.getAttributes().get("user");

            //防止重复登录时删除正常登录的在线信息
            if(onlineUserManager.getFromHall(user.getUserId()).equals(session)) {
                onlineUserManager.exitGameHall(user.getUserId());
                System.out.println("用户:" + user.getUsername() + " 已下线");
                matcher.remove(user);
            }

        }catch (NullPointerException e) {
            System.out.println("[MatchAPI.afterConnectionClosed] 当前用户未登录!");
            MatchResponse response = new MatchResponse();
            response.setOk(false);
            response.setErrMsg("用户未登录");
            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
        }
    }

}
房间管理器实体类

这里我们创建了两个哈希表,一个维护房间id到游戏房间的映射,一个维护用户id到游戏房间的映射,此时我们就可以通过add方法把两个用户加入一个游戏房间内

package com.example.demo;

import org.springframework.stereotype.Component;

import java.util.concurrent.ConcurrentHashMap;

@Component
public class RoomManager {
    //通过房间id来获得房间
    private ConcurrentHashMap<String, Room> roomIdToRoom = new ConcurrentHashMap<>();
    //通过用户id来获取房间id,然后再获取房间
    private ConcurrentHashMap<Integer, String> userIdToRoomId = new ConcurrentHashMap<>();
    public void add(String roomId, Room room, Integer userId1, Integer userId2) {
        roomIdToRoom.put(roomId, room);
        userIdToRoomId.put(userId1, roomId);
        userIdToRoomId.put(userId2, roomId);
    }
    public void remove(String roomId, int userId1, int userId2) {
        roomIdToRoom.remove(roomId);
        userIdToRoomId.remove(userId1);
        userIdToRoomId.remove(userId2);
    }
    public Room getRoomByRoomId(String roomId) {
        return roomIdToRoom.get(roomId);
    }
    public Room getRoomByUserId(Integer userId) {
        return roomIdToRoom.get(userIdToRoomId.get(userId));
    }
}
进入房间代码
Room room = new Room();
roomManager.add(room.getRoomId(), room, user1.getUserId(), user2.getUserId());
游戏房间前端代码

room.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>游戏房间</title>
    <link rel="stylesheet" href="css/common.css">
    <link rel="stylesheet" href="css/game_room.css">
</head>
<body>
    <div class="nav">五子棋对战</div>
    <div class="container">
        <div>
            <!-- 棋盘区域, 需要基于 canvas 进行实现 -->
            <canvas id="chess" width="450px" height="450px">

            </canvas>
            <!-- 显示区域 -->
            <div id="screen"> 等待玩家连接中... </div>
        </div>
    </div>
    <script src="js/script.js"></script>
    
</body>
</html>

common.css

/* 公共样式 */
 
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
 
html, body {
    height: 100%;
    background-image: url(../img/kk.png);
    background-repeat: no-repeat;
    background-position: center;
    background-size: cover;
}
 
 
.nav {
    height: 50px;
    background-color: gray;
    color: white;
    font-size: 20px;
    display: flex;
    justify-content: center;
    align-items: center;
}
.container {
    width: 100%;
    height: calc(100% - 50px);

    display: flex;
    align-items: center;
    justify-content: center;
}

game_room.css

#screen {
    width: 450px;
    height: 50px;
    margin-top: 10px;
    background-color: #fff;
    font-size: 22px;
    line-height: 50px;
    text-align: center;
}

.return-btn {
    width: 450px;
    height: 50px;
    margin-top: 5px;
    background-color: orange;
    color: #fff;
    font-size: 22px;
    line-height: 50px;
    text-align: center;
}

script.js

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 = "img/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;
        }
    }
}
 
initGame();

下篇文章我们来写对战的相关代码