【项目】棋海争锋

  • 🎥 个人主页:Dikz12
  • 📕格言:吾愚多不敏,而愿加学
  • 欢迎大家👍点赞✍评论⭐收藏

目录

项目介绍

WebSocket介绍

 使用

项目创建 

数据库设计  

用户模块 

登录接口

 注册接口

获取用户信息接口

匹配模块

websocket交互接口

创建并配置 websocket 请求的入口类

创建匹配请求/响应对象

创建用户管理器 

 处理websocket连接成功

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

处理连接关闭和连接异常

对战模块 

websocket交互接口

创建并配置 websocket 请求的入口类

创建Room对象(游戏房间) 

创建落子请求/响应对象

 处理连接成功

处理连接异常和连接关闭  

 处理落子请求

实现对弈功能

 主要的判定输赢逻辑

​编辑项目验证


项目介绍

棋海争锋:实现一个网页版的五子棋对战,分为三个模块:用户模块、匹配模块、对战模块.

技术栈:Spring、Spring Boot、SpringMVC、MyBatis、 MySQL、WebSocket

 前端:html、css、js、ajax.

WebSocket介绍

传统的Http协议是一种“一问一答”结构的协议模型,也就是客户端发送一个请求,服务器就会回一个响应,这种情况下,服务器是不能主动给客户端发消息的;

而想这个项目,五子棋双人对战,是需要服务器主动给客户端发消息这种场景的,就可以使用WebSocket来实现“消息推送机制”.

而 WebSocket 则是更接近于 TCP 这种级别的通信方式. 一旦连接建立完成, 客户端或者服务器都可以主动的向对方发送数据.

 使用

 Spring 内置了 Websocket,可以直接使用;

比如:Test 类继承 TextWebsocketHandler,在实现四个方法,还需要把该配置告诉Spring,这是WebSocket配置。

@Component
public class TestSocket extends TextWebSocketHandler {@Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {System.out.println("连接成功");}@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {System.out.println("收到消息: "+message.getPayload());}@Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {System.out.println("连接异常");}@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {System.out.println("连接关闭");}
}
@Configuration
@EnableWebSocket //告诉Spring,这是WebSocket配置
public class WebSocketConfig implements WebSocketConfigurer {@Autowiredprivate TestSocket testSocket;@Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {//当前前断页面访问这个url,就会执行 TestSocket这个类registry.addHandler(testSocket, "/test");}
}

 效果:

WebSocket实现消息推送最重要的就是掌握四个关键节点,也就是四个核心时机;

建立连接之后、收到消息之后、出现连接异常、出现连接关闭.

项目创建 

在创建项目Spring Boot项目时时引入Spring Web、MyBatis、MySQL、WebSocket、以及Lombok相关依赖.

数据库设计  

数据库就基于项目功能进行简单设计,需要用到id(自增主键)、用户名、用户密码、天梯分、总场数、获胜场数;也就是一个简单的实体表; 

create table user
(id          int primary key auto_increment,user_name   varchar(20) unique,password    varchar(128),score       int, -- 积分total_count int, -- 总场数win_count   int -- 胜利场数
) ENGINE = INNODB DEFAULT CHARSET = utf8mb4;

用户模块 

 该模块用实现用户注册和登录功能,以及管理用户的天梯分数、总场数、获胜场数;使用 MySQL 数据库存储数据,服务器端基于 Spring + MyBatis 来实现数据库的增删改查.

登录接口

[请求]
/user/login?userName=?&&password=?[响应]
{"id": 6,"userName": "gaodong","password": "d6ea380dee1f46758bf13b90a9aeb011bb0d40e9d84f042182e6a60cda9a0d27","score": 100,"totalCount": 0,"winCount": 0
}

 注册接口

[请求]
/user/register?userName=?&&password=?[响应]
{"id": 0,"userName": "cboh","password": "b6e029af45e949ad9a221a111c237e6290c0c4620c2fa242fe3daa401092c8c0","score": 0,"totalCount": 0,"winCount": 0
}

 这里密码进行了加密处理,采用的是摘要算法:MD5算法。(密码加随机盐生成一个MD5)

获取用户信息接口

用来实现登录成功后显示用户分数的信息.

[请求]
/user/getUserInfo[响应]
{"id": 6,"userName": "gaodong","password": "d6ea380dee1f46758bf13b90a9aeb011bb0d40e9d84f042182e6a60cda9a0d27","score": 100,"totalCount": 4,"winCount": 2
}

 后端代码实现:

    @Autowiredprivate UserService userService;/*** 登录* @param userName* @param password* @return*/@ResponseBody@RequestMapping(  "/login")public Object login(String userName, String password, HttpServletRequest request) {UserInfo user = userService.selectByName(userName);if (user == null || !SecurityUtils.verifyUser(password,user.getPassword())) {return new UserInfo();}//存sessionHttpSession session = request.getSession(true); //没有会话就创建一个会话session.setAttribute("user",user);log.info("密码正确");return user;}/*** 注册* @param userName* @param password* @return*/@ResponseBody@RequestMapping("/register")public UserInfo register(String userName,String password) {UserInfo user = userService.insertUser(userName, password);return user;}/*** 获取用户信息  用来实现登录成功后显示用户分数的信息.*/@RequestMapping("/getUserInfo")public Object getUserInfo(HttpServletRequest request) {try {HttpSession httpSession = request.getSession(false);UserInfo user = (UserInfo) httpSession.getAttribute("user");// 拿着这个 user 对象, 去数据库中找, 找到最新的数据UserInfo newUser = userService.selectByName(user.getUserName());return newUser;} catch (NullPointerException e) {return new UserInfo();}}

匹配模块

用户登录成功, 则进入游戏大厅页面.

游戏大厅中, 能够显示用户的名字, 天梯分数, 比赛场数和获胜场数.

同时显示一个 "匹配按钮".

点击匹配按钮则用户进入匹配队列, 并且界面上显示为 "取消匹配" .

再次点击则把用户从匹配队列中删除.

如果匹配成功, 则跳转进入到游戏房间页面.

页面加载时和服务器建立 websocket 连接. 双方通过 websocket 来传输 "开始匹配", "取消匹配", "匹配成功" 这样的信息.

websocket交互接口

[连接]
ws://127.0.0.1:8080/findMatch[请求]
{message: 'startMatch' / 'stopMatch',
}[响应1] //收到请求后响应
{ok: true,                // 是否成功. 比如用户 id 不存在, 则返回 falsereason: '',                // 错误原因message: 'startMatch' / 'stopMatch'
}[响应2] //匹配成功后的响应
{ok: true,                // 是否成功. 成功true, 否则 falsereason: '',                // 错误原因message: 'matchSuccess',    
}
  • 页面这端拿到匹配响应之后, 就跳转到游戏房间.
  • 如果返回的响应 ok 为 false, 则弹框的方式显示错误原因, 并跳转到登录页面.

创建并配置 websocket 请求的入口类

该类继承自  TextWebsocketHandler,作为处理 websocket 请求的入口类;

ObjectMapper, 后续用来处理 JSON 数据(字符串和对象之间的转换)

@Component
public class MatchAPI extends TextWebSocketHandler {private ObjectMapper objectMapper = new ObjectMapper();@Componentpublic class MatchAPI extends TextWebSocketHandler {}@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {}@Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {}@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {}
}

配置: 

在 addHandler 之后,在加上 new HttpSessionHandshakeIntercept0r(), 这样可以把之前登       录过程中往 HttpSession 中存放的session, 放到 WebSocket 的 session 中. 方便后面的代码中获取到当前用户信息.

创建匹配请求/响应对象

@Data
public class MatchRequest {/*** 表示一个websocket匹配请求*/private String message = "";
}
@Data
public class MatchResponse {/*** 表示一个websocket匹配响应*/private boolean ok = true;private String reason;  //错误信息private String message; // 发送的消息
}

创建用户管理器 

 用于管理当前用户的在线状态. 本质上是 哈希表 的结构. key 为用户 id, value 为用户的 WebSocketSession.借助这个类, 一方面可以判定用户是否是在线, 同时也可以进行方便的获取到 Session 从而给客户端回话.

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

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

@Component
public class OnlineUserManager {/*** 使用一个哈希表,来保存当前用户的在线状态:*    key -> id    value -> websocket会话*/// 多个用户和服务器建立连接或者断开连接,此时服务器并发执行对HashMap进行修改//会产生线程安全问题 改用ConcurrentHashMapConcurrentHashMap<Integer, WebSocketSession> gameHall = new ConcurrentHashMap<>();//表示在用户在游戏放假的在线状态ConcurrentHashMap<Integer, WebSocketSession> gameRoom = new ConcurrentHashMap<>();//上线public void enterGameHall(Integer id,WebSocketSession webSocketSession) {gameHall.put(id,webSocketSession);}//退出public void exitGameHall(Integer id) {gameHall.remove(id);}//查找public WebSocketSession getFromGameHall(Integer id) {return gameHall.get(id);}public void enterGameRoom(Integer id,WebSocketSession webSocketSession) {gameRoom.put(id,webSocketSession);}public void exitGameRoom(Integer id) {gameRoom.remove(id);}public WebSocketSession getSessionFromGameRoom(Integer id) {return gameRoom.get(id);}
}

 处理websocket连接成功

 实现 afterConnectionEstablished 方法.

  • 通过参数中的 session 对象, 拿到之前登录时设置的 用户 信息.
  • 使用 onlineUserManager 来管理用户的在线状态.
  • 禁止多开,采用账号登录成功之后,禁止在其它地方在登陆的方式;先判定用户是否是已经在线, 如果在线则直接返回出错 .
  • 设置玩家为上线状态.
    @Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {//玩家上线,添加到 onlineUserManager中try{// 1.先获取到当前的用户信息(谁在游戏大厅中,建立连接)UserInfo userInfo =  (UserInfo) session.getAttributes().get("user");// 当用户登录成功之后,禁止多开; 采用的是禁止在其它地方登录的方式(根源解决问题)if (onlineUserManager.getFromGameHall(userInfo.getId()) != null|| onlineUserManager.getSessionFromGameRoom(userInfo.getId()) != null) {//此时,用户已经登录了MatchResponse matchResponse = new MatchResponse();matchResponse.setOk(true);matchResponse.setReason("禁止多开!!");matchResponse.setMessage("repeatConnection");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(matchResponse)));return;}//userInfo 可能为null,没有经过登录操作,直接通过url访问game_hall.html// 2. 拿到用户信息之后,设置成在线状态onlineUserManager.enterGameHall(userInfo.getId(),session);System.out.println("玩家进入游戏大厅userName: "+userInfo.getUserName());} catch (NullPointerException e) {System.out.println("[MatchAPI.afterConnectionEstablished] 当前用户未登录!");
//            e.printStackTrace(); //可控异常MatchResponse matchResponse = new MatchResponse();matchResponse.setOk(false);matchResponse.setReason("你尚未登录!不能进行匹配操作!!!");//先通过 ObjectMapper把MatchRequest转换成JSON字符串,在包装上一层TextMessagesession.sendMessage(new TextMessage(objectMapper.writeValueAsString(matchResponse)));}}

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

  • 先从会话中拿到当前玩家的信息.
  • 解析客户端发来的请求
  • 判定请求的类型, 如果是 startMatch, 则把用户对象加入到匹配队列. 如果是 stopMatch, 则把用户对象从匹配队列中删除.
  • 此处需要实现一个 匹配器 对象, 来处理匹配的实际逻辑.

匹配器对象:

1.匹配队列这里采用的是搞三个队列, 根据玩家分数分为三个区间:

Normal:score < 500

High:score >= 500 && score < 1000

VeryHigh:score >= 1000

2.接下来还要在搞三个线程,针对三个队列进行扫描(判断队列是否 > 2),就可以把这一对玩家取出来,放到游戏房间里。

3.采用多线程编程要多考虑,线程安全安全问题(websocket本身就属于一个线程了,有写了一组线程,所以会出现线程安全问题),多个线程访问不同的队列不会出现线程安全问题,必须是多个线程操作同一个队列,才需要进行加锁。所以,使用synchronized选取加锁对象时,就可以选取这三个队列对象本身。

4.避免出现忙等问题。引入wait()和notify(), 线程如入口使用 wait 来等待, 直到队列中达到 2 个元素及其以上, 才唤醒线程消费队列.

 @Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {UserInfo userInfo = (UserInfo) session.getAttributes().get("user");//获取到客户端发送的数据内容String payload = message.getPayload();//当前这个数据载荷是一个JSON字符串,就需要转换成Java对象,MatchRequest 对象MatchRequest matchRequest = objectMapper.readValue(payload, MatchRequest.class);MatchResponse response = new MatchResponse();if (matchRequest.getMessage().equals("startMatch")) {//进入匹配队列//先创建一个匹配队列,把当前用户加进去matcher.add(userInfo);//就可以个客户端一个响应response.setOk(true);response.setMessage("startMatch");} else if(matchRequest.getMessage().equals("stopMatch")) {//退出匹配对队列// 把用户从匹配队列中移出matcher.remove(userInfo);// 返回个客户端一个响应response.setOk(true);response.setMessage("stopMatch");} else {//非法请求response.setOk(false);response.setReason("非法的匹配请求");}String jsonStr = objectMapper.writeValueAsString(response);session.sendMessage(new TextMessage(jsonStr));}
   //主要的处理逻辑private void handleMatch(Queue<UserInfo> matchQueue) {synchronized (matchQueue) {try {//队列初始情况,可能为null,往队列里添加一个元素是不能往下执行的// 所以此处,使用whilewhile (matchQueue.size() < 2) {matchQueue.wait();}// 1.从队列中取出两个玩家UserInfo player1 = matchQueue.poll();UserInfo player2 = matchQueue.poll();log.info("匹配到了两个玩家: " + player1.getUserName() + " ," + player2.getUserName());// 2.拿到玩家的websocket会话 目的就是为了告诉玩家,匹配到对手了WebSocketSession session1 = onlineUserManager.getFromGameHall(player1.getId());WebSocketSession session2 = onlineUserManager.getFromGameHall(player2.getId());//理论上,匹配队列中的玩家一定是在线状态, 前面逻辑以及处理过// 最后还是在进行判定,尽量避免出现bugif (session1 == null) {//玩家1,下线需要把玩家二重新放到匹配队列中matchQueue.offer(player2);return;}if (session2 == null) {matchQueue.offer(player1);return;}//这种情况理论上也不存在 1.前面的逻辑已经避免多开 2.玩家下线,就会移出匹配队列if (session1 == session2) {matchQueue.offer(player1);return;}// 3.匹配成功,把两个玩家放到游戏房间Room room = new Room();roomManager.addRoom(room,player1.getId(), player2.getId());// 4.给玩家反馈信息,告诉玩家你匹配成功了// 通过websocket 返回一个Message (JSON字符串) 为 "matchSuccess"的响应MatchResponse response1 = new MatchResponse();response1.setMessage("matchSuccess");String json1 = objectMapper.writeValueAsString(response1);session1.sendMessage(new TextMessage(json1));MatchResponse response2 = new MatchResponse();response2.setMessage("matchSuccess");String json2 = objectMapper.writeValueAsString(response2);session2.sendMessage(new TextMessage(json2));} catch (IOException | InterruptedException e) {e.printStackTrace();}}}

处理连接关闭和连接异常

  • 主要的工作就是把玩家从 onlineUserManager 中退出.
  • 退出的时候要注意判定, 当前玩家是否是多开的情况(一个userId, 对应到两个 websocket 连接). 如果一个玩家开启了第二个 websocket 连接, 那么这第二个 websocket 连接不会影响到玩家从 OnlineUserManager 中退出.
  • 如果玩家当前在匹配队列中, 则直接从匹配队列里移除.

对战模块 

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

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

在游戏房间页面中, 能够显示五子棋棋盘. 玩家点击棋盘上的位置实现落子功能.

并且五子连珠则触发胜负判定, 显示 "你赢了" "你输了".

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

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

websocket交互接口

[连接]ws://127.0.0.1:8080/game
[连接响应]当两个玩家都连接好了, 则给双方都返回一个数据表示就绪{message: 'gameReady',    // 游戏就绪ok: true,                // 是否成功. reason: '',                // 错误原因roomId: 'abcdef',        // 房间号. 用来辅助调试. thisUserId: 1,            // 玩家自己的 idthatUserId: 2,            // 对手的 idwhiteUser: 1,            // 先手方的 id
}
[落子请求]{message: 'putChess',userId: 1,row: 0,col: 0
}
[落子响应]{message: 'putChess',userId: 1,    row: 0,col: 0, winner: 0
}

创建并配置 websocket 请求的入口类

 跟匹配模块的操作一样,这里就不在进行过多阐述!!

创建Room对象(游戏房间) 

public class Room {/*** 这个类表示游戏房间*///用String,方便保证房间号唯一private String roomId;//玩家一private UserInfo userInfo1;//玩家二private UserInfo userInfo2;// 先手方的idprivate Integer whiteUser;public Room() {// 生成随机盐,保证房间号唯一roomId = UUID.randomUUID().toString();//Room对象不能唯一,不能使用注解交给Spring管理//所以通过入口类中记录的context来手动获取到前面的RoomManager和 OnlineUserManageronlineUserManager = JavaGobangApplication.context.getBean(OnlineUserManager.class);roomManager = JavaGobangApplication.context.getBean(RoomManager.class);userInfoMapper = JavaGobangApplication.context.getBean(UserInfoMapper.class);}
}

创建落子请求/响应对象

@Data
//客户端连接到游戏房间后,服务器返回的响应
public class GameReadyResponse {private String message;private boolean ok;private String reason;private String roomId;//当前用户idprivate Integer thisUserId;//对手idprivate Integer thatUserId;// 先手方的idprivate Integer whiteUser;
}
@Data
//落子响应类
public class GameResponse {private String message;private Integer userId;private int row;private int col;// 判断谁获胜private Integer winner;
}
@Data
//表示落子请求类
public class GameRequest {private  String message;private Integer userId;private int row;private int col;
}

 处理连接成功

  • 首先需要检测用户的登录状态. 从 Session 中拿到当前用户信息.
  • 然后要判定当前玩家是否是在房间中.
  • 接下来进行多开判定.如果玩家已经在游戏中, 则不能再次连接.
  • 把两个玩家放到对应的房间对象中. 当两个玩家都建立了连接, 房间就放满了.这个时候通知两个玩家双方都准备就绪.
  • 如果有第三个玩家尝试也想加入房间, 则给出一个提示, 房间已经满了.
  public void afterConnectionEstablished(WebSocketSession session) throws Exception {GameReadyResponse response = new GameReadyResponse();//1.先获取用户信息UserInfo userInfo = (UserInfo) session.getAttributes().get("user");if (userInfo == null) {response.setOk(false);response.setReason("用户未登录!");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));return;}//2.通过用户id,查询是否已经进入游戏房间Room room = roomManager.getRoomByUserId(userInfo.getId());if (room == null) {//没有找到房间,表示玩家还在匹配中response.setOk(false);response.setReason("尚未匹配到棋友!");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));return;}// 3.判定当前用户是不是多开if (onlineUserManager.getFromGameHall(userInfo.getId()) != null|| onlineUserManager.getSessionFromGameRoom(userInfo.getId()) != null) {// 一个账号,一边在游戏大厅,一边在游戏房间,也认为是多开response.setOk(true);response.setReason("禁止多开游戏页面!");response.setMessage("repeatConnection");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));return;}// 4.把玩家设置成上线onlineUserManager.enterGameRoom(userInfo.getId(), session);// 5.把两个玩家加入到游戏房间中//    前面的创建房间/匹配过程, 是在 game_hall.html 页面中完成的.//    因此前面匹配到对手之后, 需要经过页面跳转, 来到 game_room.html 才算正式进入游戏房间(才算玩家准备就绪)//    当前这个逻辑是在 game_room.html 页面加载的时候进行的.//    执行到当前逻辑, 说明玩家已经页面跳转成功了!!synchronized (room) {if (room.getUserInfo1() == null) {//第一个玩家加入房间room.setUserInfo1(userInfo);//先进的设置为先手room.setWhiteUser(userInfo.getId());log.info("玩家 " + userInfo.getUserName() + " 已经准备就绪! 作为玩家1");return;}if (room.getUserInfo2() == null) {//第二个玩家进入房间room.setUserInfo2(userInfo);room.setWhiteUser(userInfo.getId());log.info("玩家 " + userInfo.getUserName() + " 已经准备就绪! 作为玩家2");//当两个玩家都准备好了,服务器就需要个两个客户端发送响应//玩家1noticeGameReady(room,room.getUserInfo1(),room.getUserInfo2());//玩家2noticeGameReady(room,room.getUserInfo2(),room.getUserInfo1());return;}}// 6. 此处如果又有玩家尝试连接同一个房间, 就提示报错.//    这种情况理论上是不存在的, 还是做一个判定和提示.response.setOk(false);response.setReason("当时房间已经满了,加入房间失败");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));

处理连接异常和连接关闭  

关闭和异常情况的处理一样! 

    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {UserInfo userInfo = (UserInfo) session.getAttributes().get("user");if (userInfo == null) {return;}WebSocketSession existSession = onlineUserManager.getSessionFromGameRoom(userInfo.getId());if (existSession == session) {onlineUserManager.exitGameRoom(userInfo.getId());}log.info("用户退出! userId=" + userInfo.getId());//通知对手获胜 noticeThatUserWin(userInfo);}

 处理落子请求

  •  先获取到用户信息
  • 根据玩家 id 获取到房间对象
  • 通过 room 对象来处理这次具体的请求 
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {// 1.先获取到用户信息UserInfo userInfo  = (UserInfo)session.getAttributes().get("user");if (userInfo == null) {log.info("GameAPI[handleTextMessage]: 玩家未登录!");return;}// 2. 根据玩家 id 获取到房间对象Room room = roomManager.getRoomByUserId(userInfo.getId());if (room == null) {return;}// 3. 通过 room 对象来处理这次具体的请求room.putChess(message.getPayload());}

实现对弈功能

实现 room 中的 putChess 方法.

  • 先把请求解析成请求对象.
  • 根据请求对象中的信息, 往棋盘上落子.(判定当前子的位置是用户1的还是用户2的)
  • 落子完毕之后, 为了方便调试, 可以打印出棋盘的当前状况.
  • 判定胜负操作.(约定如果玩家1获胜,返回玩家1的用户id,玩家二获胜就返回玩家二的用户id;胜负未分就返回0)
  • 构造落子响应, 写回给每个玩家.
  • 写回的时候如果发现某个玩家掉线, 则判定另一方为获胜.
  • 如果游戏胜负已分, 则修改玩家的分数, 并销毁房间.

 主要的判定输赢逻辑

输赢逻辑判定操作写在服务器进行. 赢分为如下3中情况:

  1. 对角线(左和右) 

 情况一:

情况二:

情况三: 

项目验证

登录成功界面: 

 

 匹配界面:

判定胜负界面:

 分数更新成功

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/303082.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

华为S5735S核心交换配置实例

以下脚本实现创建vlan2,3&#xff0c;IP划分&#xff0c;DHCP启用&#xff0c;接口划分&#xff0c;ssh,telnet,http,远程登录启用 默认用户创建admin/admin123提示首次登录需要更改用户密码 sysname test-Hxvlan 2 description to test1…

【快捷部署】016_Ollama(CPU only版)

&#x1f4e3;【快捷部署系列】016期信息 编号选型版本操作系统部署形式部署模式复检时间016Ollama&#xff08;CPU only&#xff09;latestCentOS 7.XDocker单机2024-04-10 注意事项&#xff1a; 1、目前镜像及大模型下载速度尚可&#xff0c;但由于容量较大&#xff0c;所以…

汽车4S行业的信息化特点与BI建设挑战

汽车行业也是一个非常大的行业&#xff0c;上下游非常广&#xff0c;像主机厂&#xff0c;上游的零配件&#xff0c;下游的汽车流通&#xff0c;汽车流通之后的汽车后市场&#xff0c;整个链条比较长。今天主要讲的是汽车流通&#xff0c;汽车4S集团。一个汽车4S集团下面授权代…

gitlab、jenkins安装及使用文档二

安装 jenkins IP地址操作系统服务版本192.168.75.137Rocky9.2jenkins 2.450-1.1 jdk 11.0.22 git 2.39.3192.168.75.138Rocky9.2gitlab-ce 16.10.0 结合上文 jenkins安装 前期准备&#xff1a; yum install -y epel-release yum -y install net-tools vim lrzsz wget…

【双指针】成最多水的容器

给定一个长度为 n 的整数数组 height 。有 n 条垂线&#xff0c;第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。 找出其中的两条线&#xff0c;使得它们与 x 轴共同构成的容器可以容纳最多的水。 返回容器可以储存的最大水量。 示例 1&#xff1a; 输入&#xff1a;[1,…

科技云报道:从“奇点”到“大爆炸”,生成式AI开启“十年周期”

科技云报道原创。 世界是复杂的&#xff0c;没有人知道未来会怎样&#xff0c;但如果单纯从技术的角度&#xff0c;我们总是能够沿着技术发展的路径&#xff0c;找到一些主导未来趋势的脉络。 从Sora到Suno&#xff0c;从OpenAI到Copilot、Blackwell&#xff0c;这些热词在大…

【微服务】------微服务架构技术栈

目前微服务早已火遍大江南北&#xff0c;对于开发来说&#xff0c;我们时刻关注着技术的迭代更新&#xff0c;而项目采用什么技术栈选型落地是开发、产品都需要关注的事情&#xff0c;该篇博客主要分享一些目前普遍公司都在用的技术栈&#xff0c;快来分享一下你当前所在用的技…

Java语言实现文件分割与合并

一&#xff1a; 题目&#xff1a; 写一个方法,将feige.exe文件分割为每份1MB大小的若干份(最后一份可以不满1MB), 存储在一个temp的文件夹中(每份文件名自己定义,例如1.temp 2.temp), 然后再写一个方法,将temp文件夹中的若干份合并为一个文件fg.exe 代码&#xff1a; main…

Linux查看系统配置信息的命令【lscpu】【free】【df】【uname】【lsblk】

目录 1.查看CPU信息【lscpu】 2.查看内存信息【free】 3.查看文件系统信息【df】 4.查看系统信息【uname】 知识扩展&#xff1a;Red Hat Enterprise Linux 和 Debian GNU/Linux 两者的发展介绍 知识扩展&#xff1a;Centos 和 ubuntu的区别 知识扩展&#xff1a;更多 …

51单片机入门_江协科技_25~26_OB记录的笔记_蜂鸣器教程

25. 蜂鸣器 25.1. 蜂鸣器介绍 •蜂鸣器是一种将电信号转换为声音信号的器件&#xff0c;常用来产生设备的按键音、报警音等提示信号 •蜂鸣器按驱动方式可分为有源蜂鸣器和无源蜂鸣器&#xff08;开发板上用的无源蜂鸣器&#xff09; •有源蜂鸣器&#xff1a;内部自带振荡源&a…

C语言 知识点 + 笔记(2w6千字 持续更新...)

前言 本篇以笔记为主的C语言详解,全篇一共十章内容,2万6千多字,会持续更新基础内容,争取做到更详细。多一句没有,少一句不行! 形而上学者谓之道,形而下学者谓之器 第 1 章 C语言的流程 (1) C程序经历的六个阶段 编辑(Edit)预处理(Preprocess)编译(Compile)汇编(Assemb…

llama2.c与chinese-baby-llama2语言模型本地部署推理

文章目录 简介Github文档克隆源码英文模型编译运行中文模型&#xff08;280M&#xff09;main函数 简介 llama2.c是一个极简的Llama 2 LLM全栈工具&#xff0c;使用一个简单的 700 行 C 文件 ( run.c ) 对其进行推理。llama2.c涉及LLM微调、模型构建、推理端末部署&#xff08…

Windows系统上运行appium连接iOS真机自动化测试

步骤: 1、windows安装tidevice工具 2、Mac系统打包安装WebDriverAgent(WDA)工具 3、安装Appium 4、连接iOS手机 iOS自动化的实现和执行都依赖Mac系统,因为需要通过Xcodebuild编译安装WDA (WebDriverAgent)到iOS设备中,通过WDA实现对被测应用进行操作。而Windows系统无…

汽车疲劳测试试验平台技术要求(北重厂家)

汽车疲劳测试试验平台技术要求通常包括以下几个方面&#xff1a; 车辆加载能力&#xff1a;测试平台需要具备足够的承载能力&#xff0c;能够同时测试多种车型和不同重量的车辆。 动力系统&#xff1a;测试平台需要具备稳定可靠的动力系统&#xff0c;能够提供足够的力和速度来…

# ABAP SQL 字符串处理

经常我都要在ABAP的sql语句中对字符串进行处理&#xff0c;现在就总结一下可以用到的方法 文章目录 字符串处理拼接字段运行结果 填充字符串运行结果 截取字符串 SUBSTRING运行结果 CAST转换类型程序运行结果 字符串处理 在SQL语句中&#xff0c;有时候会有需要拼接字段或者是…

无影云电脑不能连接到本机的调试串口的解决方案

目录 概述 解决方案 云端电脑中的操作 本地USBDK驱动程序的更新 概述 我从1月份开始使用阿里的无影云电脑进行嵌入式开发板的测试&#xff0c;主要的原因有两个&#xff1a;一是平时使用的笔记本资源过于紧张&#xff0c;二是方便移动办公&#xff0c;这样我只要平时拿着开…

SCI一区 | Matlab实现OOA-TCN-BiGRU-Attention鱼鹰算法优化时间卷积双向门控循环单元融合注意力机制多变量时间序列预测

SCI一区 | Matlab实现OOA-TCN-BiGRU-Attention鱼鹰算法优化时间卷积双向门控循环单元融合注意力机制多变量时间序列预测 目录 SCI一区 | Matlab实现OOA-TCN-BiGRU-Attention鱼鹰算法优化时间卷积双向门控循环单元融合注意力机制多变量时间序列预测预测效果基本介绍模型描述程序…

HarmonyOS实战开发-如何使用 geolocation 实现获取当前位置经纬度

介绍 本示例使用 geolocation 实现获取当前位置的经纬度,然后通过 http 将经纬度作为请求参数,获取到该经纬度所在的城市。通过 AlphabetIndexer 容器组件实现按逻辑结构快速定位容器显示区域。 效果预览 使用说明 1.进入主页,点击国内热门城市,配送地址会更新为选择的城…

AI大模型探索之路-应用篇2:Langchain框架ModelIO模块—数据交互的秘密武器

目录 前言 一、概述​​​​​​​ 二、Model 三、Prompt 五、Output Parsers 总结 前言 随着人工智能技术的不断进步&#xff0c;大模型的应用场景越来越广泛。LangChain框架作为一个创新的解决方案&#xff0c;专为处理大型语言模型的输入输出而设计。其中&#xff0c;…

如何通过navicat连接SQL Server数据库

本文介绍如何通过Navicat 连接SQL Server数据库。如果想了解如何连接Oracle数据库&#xff0c;可以参考下边这篇文章。如何通过Navicat连接Oracle数据库https://sgknight.blog.csdn.net/article/details/132064235 1、新建SQL Server连接配置 打开Navicat软件&#xff0c;点击…