- 🎥 个人主页:Dikz12
- 📕格言:吾愚多不敏,而愿加学
- 欢迎大家👍点赞✍评论⭐收藏
目录
项目介绍
棋海争锋:实现一个网页版的五子棋对战,分为三个模块:用户模块、匹配模块、对战模块.
技术栈: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 {
@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());
}
@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("连接关闭");
}
}
@Configuration
@EnableWebSocket //告诉Spring,这是WebSocket配置
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private TestSocket testSocket;
@Override
public 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
}
后端代码实现:
@Autowired
private 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();
}
//存session
HttpSession 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 不存在, 则返回 false
reason: '', // 错误原因
message: 'startMatch' / 'stopMatch'
}
[响应2] //匹配成功后的响应
{
ok: true, // 是否成功. 成功true, 否则 false
reason: '', // 错误原因
message: 'matchSuccess',
}
- 页面这端拿到匹配响应之后, 就跳转到游戏房间.
- 如果返回的响应 ok 为 false, 则弹框的方式显示错误原因, 并跳转到登录页面.
创建并配置 websocket 请求的入口类
该类继承自 TextWebsocketHandler,作为处理 websocket 请求的入口类;
ObjectMapper, 后续用来处理 JSON 数据(字符串和对象之间的转换)
@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 {
}
}
配置:
在
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进行修改
//会产生线程安全问题 改用ConcurrentHashMap
ConcurrentHashMap<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 来管理用户的在线状态.
- 禁止多开,采用账号登录成功之后,禁止在其它地方在登陆的方式;先判定用户是否是已经在线, 如果在线则直接返回出错 .
- 设置玩家为上线状态.
@Override
public 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字符串,在包装上一层TextMessage
session.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 个元素及其以上, 才唤醒线程消费队列.
@Override
protected 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,往队列里添加一个元素是不能往下执行的
// 所以此处,使用while
while (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());
//理论上,匹配队列中的玩家一定是在线状态, 前面逻辑以及处理过
// 最后还是在进行判定,尽量避免出现bug
if (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, // 玩家自己的 id
thatUserId: 2, // 对手的 id
whiteUser: 1, // 先手方的 id
}
[落子请求]
{
message: 'putChess',
userId: 1,
row: 0,
col: 0
}
[落子响应]
{
message: 'putChess',
userId: 1,
row: 0,
col: 0,
winner: 0
}
创建并配置 websocket 请求的入口类
跟匹配模块的操作一样,这里就不在进行过多阐述!!
创建Room对象(游戏房间)
public class Room {
/**
* 这个类表示游戏房间
*/
//用String,方便保证房间号唯一
private String roomId;
//玩家一
private UserInfo userInfo1;
//玩家二
private UserInfo userInfo2;
// 先手方的id
private Integer whiteUser;
public Room() {
// 生成随机盐,保证房间号唯一
roomId = UUID.randomUUID().toString();
//Room对象不能唯一,不能使用注解交给Spring管理
//所以通过入口类中记录的context来手动获取到前面的RoomManager和 OnlineUserManager
onlineUserManager = 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;
//当前用户id
private Integer thisUserId;
//对手id
private Integer thatUserId;
// 先手方的id
private 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");
//当两个玩家都准备好了,服务器就需要个两个客户端发送响应
//玩家1
noticeGameReady(room,room.getUserInfo1(),room.getUserInfo2());
//玩家2
noticeGameReady(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中情况:
- 行
- 列
- 对角线(左和右)
情况一:
情况二:
情况三:
项目验证
登录成功界面:
匹配界面:
判定胜负界面:
分数更新成功