当前位置: 首页 > news >正文

[网页五子棋][匹配对战]落子实现思路、发送落子请求、处理落子响应

文章目录

  • 落子实现思路
  • 发送落子请求
  • 处理落子响应
    • 两种棋盘的区别
    • 实现 handleTextMessage
    • 实现对弈功能
    • 控制台打印棋盘
    • 完善前端逻辑

落子实现思路

先来实现:点击棋盘,能发送落子请求

image.png|456

  • 客户端 1 点击了棋盘位置,先不着急画子,而是给服务器发送一个 websocket 请求(前面约定的落子请求格式,是谁在哪个位子上落子)
  • 服务器内部需要维护一个棋盘
    • 服务器根据落子请求,在棋盘上进行更新
    • 还需要进行胜负判定
    • 最后将响应(格式为前面约定的落子响应格式)返回给两个客户端
  • 两个客户端收到落子响应之后,再在棋盘上绘制棋子

服务器需要把棋盘上的变化,广播给房间中的每个玩家

image.png|359

发送落子请求

修改 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) {  // TODO 发送坐标给服务器,服务器要返回结果  send(row, col);  // 留到浏览器收到落子响应的时候再处理(收到响应再来画棋子)  // oneStep(col, row, gameInfo.isWhite);        // chessBoard[row][col] = 1;    }  
}  
// TODO 实现发送落子请求逻辑和处理落子响应逻辑  
function send(row, col) {  console.log("send");  let request = {  message: "putChess",  userId: gameInfo.thisUserId,  row: row,  col: col,  }  websocket.send(JSON.stringify(request));  
}

处理落子响应

两种棋盘的区别

服务器的棋盘实现:

public class Room {  // ......private int[][] board = new int[15][15];//......
}
  • 这个二维数组用来表示棋盘
  • 约定:
    1. 使用 0 表示当前位置未落子,初始化好的 int 二维数组,相当于是全 0
    2. 使用 1 表示 user1 的落子位置
    3. 使用 2 表示 user2 的落子位置

客户端和服务器这两边的二维数组的区别

  • 服务器的数组元素有三种状态
    • 服务器这边的二维数组,起到的效果就是进行判定胜负,就得知道玩家 1 和玩家 2 的子都在哪
  • 客户端的数组元素只有两种状态 image.png|273
    • 客户端的二维数组只是用来判定这里有没有子,就不需要区分这个子是谁
    • 判定有没有子,主要是为了避免重复落子

如果直接在客户端来判定胜负关系,是否可行呢?

  • 不太可行
  • 游戏中的关键逻辑,还是要交给服务器来进行判定

外挂:工作过程主要就是篡改客户端这边的数据和逻辑
为什么吃鸡的外挂多?

  • 因为这种射击类的游戏,要求网络延时极低,不然残血时一颗子弹打向你,等你大包打起来了这颗子弹的扣血响应才来
  • 所以这类游戏的关键判定只能放在客户端,就给了不法分子篡改客户端数据的机会,就造成了射击类游戏外挂泛滥

实现 handleTextMessage

实现 gameAPI 中的 handleTextMessage 方法

@Override  
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {  // 1. 先从 session 里拿到当前用户的身份信息  User user = (User) session.getAttributes().get("user");  if (user == null) {  System.out.println("[handleTextMessage] 当前玩家尚未登录!");  return;  }  // 2. 根据玩家 id 获取到房间对象  Room room = roomManager.getRoomByUserId(user.getUserId());  // 3. 通过 room 对象来处理这次具体的请求  room.putChess(message.getPayload());  
}

实现对弈功能

实现 room 中的 putChess 方法

  • 先把请求解析成请求对象
  • 根据请求对象中的信息,往棋盘上落子
  • 落子完毕后,为了方便测试,可以打印出棋盘的当前状况
  • 检查游戏是否结束
  • 构造落子响应,写回给每个玩家
  • 写回的时候如果发现某个玩家掉线,则判定另一方为获胜
  • 如果游戏胜负已分,则修改玩家的分数,并销毁房间
package org.example.java_gobang.game;  import com.fasterxml.jackson.databind.ObjectMapper;  
import org.example.java_gobang.JavaGobangApplication;  
import org.example.java_gobang.model.User;  
import org.example.java_gobang.model.UserMapper;  
import org.springframework.web.socket.TextMessage;  
import org.springframework.web.socket.WebSocketSession;  import java.io.IOException;  
import java.util.UUID;  // 表示一个游戏房间  
public class Room {  // 此处我们使用字符串的类型来表示,方便生成唯一值  private String roomId;  private User user1;  private User user2;  // 先手方的玩家 id    private int whiteUser;  private static final int MAX_ROW = 15;  private static final int MAX_COL = 15;  // 这个二维数组用来表示棋盘  // 约定:  // 1. 使用 0 表示当前位置未落子,初始化好的 int 二维数组,相当于是 全0  // 2. 使用 1 表示 user1 的落子位置  // 3. 使用 2 表示 user2 的落子位置  private int[][] board = new int[MAX_ROW][MAX_COL];  // 创建 ObjectMapper 用来转换 JSON    private ObjectMapper objectMapper = new ObjectMapper();  // 引入 OnlineUserManager    // @Autowired    private OnlineUserManager onlineUserManager;  private UserMapper userMapper;  // 引入 RoomManager,用于房间销毁  // @Autowired  private RoomManager roomManager;  // 通过这个方法来处理一次落子操作  // 要做的事情:  // 1. 记录当前落子的位置  // 2. 进行胜负判定  // 3. 给客户端返回响应  public void putChess(String jsonString) throws IOException {  // 1. 记录当前落子的位置  GameRequest request = objectMapper.readValue(jsonString, GameRequest.class);  GameResponse response = new GameResponse();  // 当前这个子是玩家1 落的还是玩家2 落的。根据这个玩家1 和玩家2 来决定往数组中是写 1 还是 2        int chess = request.getUserId() == user1.getUserId() ? 1 : 2;  int row = request.getRow();  int col = request.getCol();  if (board[row][col] != 0) {  // 在客户端已经针对重复落子进行过判定了,此处为了程序更加稳健,在服务器再判定  System.out.println("当前位置 (" + row + ", " + col + ") 已经有子了!");  return;  }  board[row][col] = chess;  // 2. 打印出当前的棋盘信息,方便来观察局势,也方便后面验证胜负关系的判定  printBoard();  // 2. 进行胜负判定  int winner = checkWinner(row, col);  // 3. 给房间中的所有客户端都返回响应  response.setMessage("putChess");  response.setUserId(request.getUserId());  response.setRow(row);  response.setCol(col);  response.setWinner(winner);  // 要想给用户发送 websocket 数据,就需要获取到这个用户的 WebSocketSession        WebSocketSession session1 = onlineUserManager.getFromGameRoom(getUser1().getUserId());  WebSocketSession session2 = onlineUserManager.getFromGameRoom(getUser2().getUserId());  // 万一当前查到的会话为空(玩家已经下线了)特殊处理一下  if (session1 == null) {  // 玩家1 已经下线了,直接认为玩家2 获胜!  response.setWinner(user2.getUserId());  System.out.println("玩家1 掉线!");  }  if (session2 == null) {  // 玩家2 已经下线,直接认为玩家1 获胜!  response.setWinner(user2.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) {  System.out.println("游戏结束!房间即将销毁! roomId=" + roomId + " 获胜方为:" + response.getWinner());  // 销毁房间  roomManager.remove(roomId, user1.getUserId(), user2.getUserId());  }  }//......//......
}

控制台打印棋盘

实现打印棋盘的逻辑

private void printBoard() {  // 打印出棋盘  System.out.println("[打印棋盘信息] ");  System.out.println("=======================================");  for (int r = 0; r < MAX_ROW; r++) {  for (int c = 0; c < MAX_COL; c++) {  // 针对一行之内的若干列,不要打印换行  System.out.print(board[r][c] + " ");  }  // 每次遍历完一行之后,再打印换行  System.out.println();  }  System.out.println("=======================================");  
}

完善前端逻辑

  • 之前 websocket.onmessage 主要是用来处理游戏就绪响应,在游戏就绪之后,初始化完毕之后,也就不再有游戏就绪响应了
  • 就在 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[row][col] = 1;  // 交换双方的落子轮次  me != me;  setScreenText(me);  // 判定游戏是否结束  if (resp.winner != 0) {  if (resp.winner == gameInfo.thisUserId){  alert('你赢了!');  } else if (resp.winner == gameInfo.thatUserId) {  alert('你输了!');  } else {  console.log("winner 字段错误!" + resp.winner);  }  // 胜负已分,回到游戏大厅  location.assign('game_hall.html');  }  
}

相关文章:

  • 论文略读:Auto-Regressive Moving Diffusion Models for Time Series Forecasting
  • 【nm】nm命令的使用:查看.so中的符号信息
  • RocketMQ介绍与部署
  • NodeJS全栈WEB3面试题——P6安全与最佳实践
  • SDU棋界精灵——实现硬件程序ESP32的FreeRTOS任务
  • 【LeetCode 热题100】动态规划实战:打家劫舍、完全平方数与零钱兑换(LeetCode 198 / 279 / 322)(Go语言版)
  • 【QT控件】QWidget 常用核心属性介绍 -- 万字详解
  • Laplace 噪声
  • 案例:TASK OA
  • YOLOv5 :训练自己的数据集
  • wow Warlock shushia [Dreadsteed]
  • 简单了解string类的特性及使用(C++)
  • MDP的curriculums部分
  • volatile,synchronized,原子操作实现原理,缓存一致性协议
  • 基于Python学习《Head First设计模式》第四章 工厂方法+抽象工厂
  • “等待-通知”机制优化(一次性申请)循环等待
  • HarmonyOS5 仓颉入门:和 ArkTs 互操作
  • 初识vue3(vue简介,环境配置,setup语法糖)
  • RGB888色彩格式转RGB565格式
  • VMware安装Ubuntu全攻略
  • 优书网注册/上海seo推广
  • 电话交换机ip地址/专业seo网站优化推广排名教程
  • 网站建设定制公司/企业推广策略
  • 做网站用html/google官方版下载
  • 金融直播室网站建设/网络优化
  • 网站开发技术发展历程/专业培训心得体会