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

SpringBoot学习笔记-实现微服务:匹配系统(中)

笔记内容转载自 AcWing 的 SpringBoot 框架课讲义,课程链接:AcWing SpringBoot 框架课。

CONTENTS

  • 1. 同步玩家位置
    • 1.1 游戏信息的记录
    • 1.2 实现多线程同步移动
  • 2. 同步碰撞检测
  • 3. 实现游戏结束界面
  • 4. 持久化游戏状态
    • 4.1 创建数据库表
    • 4.2 保存游戏对局信息

1. 同步玩家位置

1.1 游戏信息的记录

两名玩家初始位置需要由服务器确定,且之后的每次移动都需要在服务器上判断。我们需要在 Game 类中添加 Player 类用来记录玩家的信息,在 consumer.utils 包下创建 Player 类:

package com.kob.backend.consumer.utils;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.util.List;@Data
@NoArgsConstructor
@AllArgsConstructor
public class Player {private Integer id;private Integer sx;private Integer sy;private List<Integer> steps;  // 记录历史走过的每一步方向
}

然后就可以在 Game 中创建玩家:

package com.kob.backend.consumer.utils;import java.util.ArrayList;
import java.util.Arrays;
import java.util.Random;public class Game {...private final Player playerA, playerB;public Game(Integer rows, Integer cols, Integer inner_walls_count, Integer idA, Integer idB) {...playerA = new Player(idA, rows - 2, 1, new ArrayList<>());  // 默认A在左下角B在右上角playerB = new Player(idB, 1, cols - 2, new ArrayList<>());}public Player getPlayerA() {return playerA;}public Player getPlayerB() {return playerB;}...
}

WebSocketServer 中创建 Game 时传入两名玩家的 ID,并且我们将与游戏内容相关的信息全部包装到一个 JSONObject 类中:

package com.kob.backend.consumer;import com.alibaba.fastjson2.JSONObject;
import com.kob.backend.consumer.utils.Game;
import com.kob.backend.consumer.utils.JwtAuthentication;
import com.kob.backend.mapper.UserMapper;
import com.kob.backend.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;import java.io.IOException;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;@Component
@ServerEndpoint("/websocket/{token}")  // 注意不要以'/'结尾
public class WebSocketServer {...private void startMatching() {System.out.println("Start matching!");matchPool.add(this.user);while (matchPool.size() >= 2) {  // 临时调试用的,未来要替换成微服务Iterator<User> it = matchPool.iterator();User a = it.next(), b = it.next();matchPool.remove(a);matchPool.remove(b);Game game = new Game(13, 14, 20, a.getId(), b.getId());game.createMap();JSONObject respGame = new JSONObject();respGame.put("a_id", game.getPlayerA().getId());respGame.put("a_sx", game.getPlayerA().getSx());respGame.put("a_sy", game.getPlayerA().getSy());respGame.put("b_id", game.getPlayerB().getId());respGame.put("b_sx", game.getPlayerB().getSx());respGame.put("b_sy", game.getPlayerB().getSy());respGame.put("map", game.getG());JSONObject respA = new JSONObject();  // 发送给A的信息respA.put("event", "match_success");respA.put("opponent_username", b.getUsername());respA.put("opponent_photo", b.getPhoto());respA.put("game", respGame);users.get(a.getId()).sendMessage(respA.toJSONString());  // A不一定是当前链接,因此要在users中获取JSONObject respB = new JSONObject();  // 发送给B的信息respB.put("event", "match_success");respB.put("opponent_username", a.getUsername());respB.put("opponent_photo", a.getPhoto());respB.put("game", respGame);users.get(b.getId()).sendMessage(respB.toJSONString());}}...
}

前端也需要进行相应的修改,在 store/pk.js 中创建两名玩家的信息:

export default {state: {...a_id: 0,a_sx: 0,a_sy: 0,b_id: 0,b_sx: 0,b_sy: 0,gameObject: null,  // 整个GameMap对象},getters: {},mutations: {...updateGame(state, game) {state.game_map = game.map;state.a_id = game.a_id;state.a_sx = game.a_sx;state.a_sy = game.a_sy;state.b_id = game.b_id;state.b_sx = game.b_sx;state.b_sy = game.b_sy;},updateGameObject(state, gameObject) {state.gameObject = gameObject;},},actions: {},modules: {},
};

GameMap.vue 中需要先将 GameMap 对象存下来,之后会在 PKIndexView 中用到:

...<script>
import { ref, onMounted } from "vue";
import { GameMap } from "@/assets/scripts/GameMap";
import { useStore } from "vuex";export default {setup() {const store = useStore();let parent = ref(null);let canvas = ref(null);onMounted(() => {store.commit("updateGameObject",new GameMap(canvas.value.getContext("2d"), parent.value, store));});return {parent,canvas,};},
};
</script>...

PKIndexView 中要传入从后端获取到的 game 数据:

...<script>
...export default {...setup() {...onMounted(() => {...socket.onmessage = (msg) => {  // 接收到后端消息时会执行...if (data.event === "match_success") {  // 匹配成功...store.commit("updateGame", data.game);  // 更新游戏内容...}};...});...},
};
</script><style scoped></style>

1.2 实现多线程同步移动

我们需要实现两名玩家的客户端以及服务器端的移动同步,假如 Client1 发出了移动指令,那么就会将这个消息发送给服务器,同理另一个客户端 Client2 发出移动指令时也会将消息发送给服务器,服务器在接收到两名玩家的消息后再将消息同步给两名玩家。

我们在 WebSocketServer 中会维护一个游戏 Game,这个 Game 也有自己的执行流程,它会先创建地图 creatMap,接着会一步一步执行,即 nextStep,每一步会等待两名玩家的操作,这个操作可以是键盘输入,也可以是由 Bot 代码执行的微服务返回回来的结果。获取输入后会将结果发送给一个评判系统 judge,来判断两名玩家下一步是不是合法的,如果有一方不合法就游戏结束。

在等待用户输入时会有一个时间限制,比如5秒,如果有一方还没有输入则表示输了,同样也是游戏结束。否则如果两方输入的下一步都是合法的则继续循环 nextStep。这个 nextStep 流程是比较独立的,而且每个游戏对局都有这个独立的过程,如果 Game 是单线程的,那么在等待用户输入时这个线程就会卡死,如果有多个游戏对局的话那么只能先卡死在某个对局中,其他对局的玩家体验就会很差,因此 Game 不能作为一个单线程来处理,每次在等待用户输入时都需要另起一个新的线程,这就涉及到了多线程的通信以及加锁的问题。

我们将 Game 类继承自 Thread 类即可转为多线程,然后需要实现 Thread 的入口函数,使用快捷键 Alt + Insert,选择重写方法,需要重写的是 run() 方法,这是新线程的入口函数:

package com.kob.backend.consumer.utils;import com.alibaba.fastjson2.JSONObject;
import com.kob.backend.consumer.WebSocketServer;import java.util.ArrayList;
import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.locks.ReentrantLock;public class Game extends Thread {private final Integer rows;private final Integer cols;private final Integer inner_walls_count;private final boolean[][] g;private static final int[] dx = { -1, 0, 1, 0 }, dy = { 0, 1, 0, -1 };private final Player playerA, playerB;private Integer nextStepA = null;  // 下一步操作,0、1、2、3分别表示四个方向,null表示还没有获取到private Integer nextStepB = null;private ReentrantLock lock = new ReentrantLock();  // 需要给nextStep变量上锁防止读写冲突private String status = "playing";  // 整局游戏的状态,结束后为finishedprivate String loser = "";  // 输的一方是谁,all表示平局public Game(Integer rows, Integer cols, Integer inner_walls_count, Integer idA, Integer idB) {this.rows = rows;this.cols = cols;this.inner_walls_count = inner_walls_count;this.g = new boolean[rows][cols];playerA = new Player(idA, rows - 2, 1, new ArrayList<>());  // 默认A在左下角B在右上角playerB = new Player(idB, 1, cols - 2, new ArrayList<>());}public Player getPlayerA() {return playerA;}public Player getPlayerB() {return playerB;}public void setNextStepA(Integer nextStepA) {  // 未来会在另一个线程中调用lock.lock();  // 操作nextStep变量前先上锁try {this.nextStepA = nextStepA;} finally {lock.unlock();  // 操作完后无论是否有异常都解锁}}public void setNextStepB(Integer nextStepB) {lock.lock();try {this.nextStepB = nextStepB;} finally {lock.unlock();}}public boolean[][] getG() {return g;}private boolean check_connectivity(int sx, int sy, int tx, int ty) {if (sx == tx && sy == ty) return true;g[sx][sy] = true;for (int i = 0; i < 4; i++) {int nx = sx + dx[i], ny = sy + dy[i];if (!g[nx][ny] && check_connectivity(nx, ny, tx, ty)) {g[sx][sy] = false;  // 注意在这里我们用的g就是原始数组,因此修改后要记得还原return true;}}g[sx][sy] = false;  // 记得还原return false;}private boolean drawMap() {// 初始化障碍物标记数组for (int i = 0; i < this.rows; i++) {Arrays.fill(g[i], false);}// 给地图四周加上障碍物for (int r = 0; r < this.rows; r++) {g[r][0] = g[r][this.cols - 1] = true;}for (int c = 0; c < this.cols; c++) {g[0][c] = g[this.rows - 1][c] = true;}// 添加地图内部的随机障碍物,需要有对称性因此枚举一半即可,另一半对称生成Random random = new Random();for (int i = 0; i < this.inner_walls_count / 2; i++) {for (int j = 0; j < 10000; j++) {int r = random.nextInt(this.rows);  // 返回0~this.rows-1的随机整数int c = random.nextInt(this.cols);if (g[r][c] || g[this.rows - 1 - r][this.cols - 1 - c]) continue;if (r == this.rows - 2 && c == 1 || r == 1 && c == this.cols - 2) continue;g[r][c] = g[this.rows - 1 - r][this.cols - 1 - c] = true;break;}}return check_connectivity(this.rows - 2, 1, 1, this.cols - 2);}public void createMap() {for (int i = 0; i < 10000; i++) {if (drawMap()) {break;}}}private boolean nextStep() {  // 等待两名玩家的下一步操作,在该方法中也会操作nextStep变量try {Thread.sleep(500);  // 前端的蛇每秒走2格,因此走一格需要500ms,每次后端执行下一步时需要先sleep,否则快速的多次输入将会覆盖掉之前输入的信息} catch (InterruptedException e) {e.printStackTrace();}for (int i = 0; i < 50; i++) {try {Thread.sleep(100);  // 每回合循环50次,每次睡眠100ms,即一回合等待用户输入的时间为5slock.lock();try {if (nextStepA != null && nextStepB != null) {  // 两名玩家的下一步操作都读到了playerA.getSteps().add(nextStepA);playerB.getSteps().add(nextStepB);return true;}} finally {lock.unlock();}} catch (InterruptedException e) {e.printStackTrace();}}return false;}private void judge() {  // 判断两名玩家下一步操作是否合法}private void sendAllMessage(String message) {  // 向两个Client发送消息WebSocketServer.users.get(playerA.getId()).sendMessage(message);WebSocketServer.users.get(playerB.getId()).sendMessage(message);}private void sendMove() {  // 向两个Client发送移动消息lock.lock();try {JSONObject resp = new JSONObject();resp.put("event", "move");resp.put("a_direction", nextStepA);resp.put("b_direction", nextStepB);sendAllMessage(resp.toJSONString());nextStepA = nextStepB = null;} finally {lock.unlock();}}private void sendResult() {  // 向两个Client公布结果JSONObject resp = new JSONObject();resp.put("event", "result");resp.put("loser", loser);sendAllMessage(resp.toJSONString());}@Overridepublic void run() {for (int i = 0; i < 1000; i++) {  // 游戏最多走的步数不会超过1000if (nextStep()) {  // 是否获取了两条蛇的下一步操作judge();if ("playing".equals(status)) {  // 如果游戏还在进行中则需要将两名玩家的操作广播给两个ClientsendMove();} else {sendResult();break;}} else {status = "finished";lock.lock();try {if (nextStepA == null && nextStepB == null) {loser = "all";} else if (nextStepA == null) {loser = "A";} else {loser = "B";}} finally {lock.unlock();}sendResult();  // 这一步结束后需要给两个Client发送消息break;}}}
}

然后前端 GameMap.js 中在移动时需要向后端通信,现在两名玩家的键盘输入操作就只需要 W/S/A/D 了:

import { AcGameObject } from "./AcGameObject";
import { Wall } from "./Wall";
import { Snake } from "./Snake";export class GameMap extends AcGameObject {...add_listening_events() {this.ctx.canvas.focus();  // 使Canvas聚焦this.ctx.canvas.addEventListener("keydown", e => {let d = -1;if (e.key === "w") d = 0;else if (e.key === "d") d = 1;else if (e.key === "s") d = 2;else if (e.key === "a") d = 3;if (d !== -1) {this.store.state.pk.socket.send(JSON.stringify({event: "move",direction: d,}));}});}...
}

WebSocketServer 对于每局游戏对局都会创建一个 Game 类,通过 start() 方法可以新开一个线程运行 Game 中的 run() 方法,由于我们需要在 Game 中使用 WebSocketServerusers,还需要将 users 修改为 public,然后需要接收前端的移动请求:

package com.kob.backend.consumer;import com.alibaba.fastjson2.JSONObject;
import com.kob.backend.consumer.utils.Game;
import com.kob.backend.consumer.utils.JwtAuthentication;
import com.kob.backend.mapper.UserMapper;
import com.kob.backend.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;import java.io.IOException;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;@Component
@ServerEndpoint("/websocket/{token}")  // 注意不要以'/'结尾
public class WebSocketServer {// ConcurrentHashMap是一个线程安全的哈希表,用于将用户ID映射到WS实例public static final ConcurrentHashMap<Integer, WebSocketServer> users = new ConcurrentHashMap<>();// CopyOnWriteArraySet也是线程安全的private static final CopyOnWriteArraySet<User> matchPool = new CopyOnWriteArraySet<>();  // 匹配池private User user;private Session session = null;private Game game = null;private static UserMapper userMapper;@Autowiredpublic void setUserMapper(UserMapper userMapper) {WebSocketServer.userMapper = userMapper;}@OnOpenpublic void onOpen(Session session, @PathParam("token") String token) throws IOException {this.session = session;System.out.println("Connected!");Integer userId = JwtAuthentication.getUserId(token);this.user = userMapper.selectById(userId);if (user != null) {users.put(userId, this);} else {this.session.close();}}@OnClosepublic void onClose() {System.out.println("Disconnected!");if (this.user != null) {users.remove(this.user.getId());matchPool.remove(this.user);}}@OnMessagepublic void onMessage(String message, Session session) {  // 一般会把onMessage()当作路由System.out.println("Receive message!");JSONObject data = JSONObject.parseObject(message);String event = data.getString("event");  // 取出event的内容if ("start_match".equals(event)) {  // 开始匹配this.startMatching();} else if ("stop_match".equals(event)) {  // 取消匹配this.stopMatching();} else if ("move".equals(event)) {  // 移动move(data.getInteger("direction"));}}@OnErrorpublic void onError(Session session, Throwable error) {error.printStackTrace();}public void sendMessage(String message) {  // 从后端向当前链接发送消息synchronized (this.session) {  // 由于是异步通信,需要加一个锁try {this.session.getBasicRemote().sendText(message);} catch (IOException e) {e.printStackTrace();}}}private void startMatching() {System.out.println("Start matching!");matchPool.add(this.user);while (matchPool.size() >= 2) {  // 临时调试用的,未来要替换成微服务Iterator<User> it = matchPool.iterator();User a = it.next(), b = it.next();matchPool.remove(a);matchPool.remove(b);game = new Game(13, 14, 20, a.getId(), b.getId());game.createMap();users.get(a.getId()).game = game;users.get(b.getId()).game = game;game.start();  // 开一个新的线程JSONObject respGame = new JSONObject();respGame.put("a_id", game.getPlayerA().getId());respGame.put("a_sx", game.getPlayerA().getSx());respGame.put("a_sy", game.getPlayerA().getSy());respGame.put("b_id", game.getPlayerB().getId());respGame.put("b_sx", game.getPlayerB().getSx());respGame.put("b_sy", game.getPlayerB().getSy());respGame.put("map", game.getG());JSONObject respA = new JSONObject();  // 发送给A的信息respA.put("event", "match_success");respA.put("opponent_username", b.getUsername());respA.put("opponent_photo", b.getPhoto());respA.put("game", respGame);users.get(a.getId()).sendMessage(respA.toJSONString());  // A不一定是当前链接,因此要在users中获取JSONObject respB = new JSONObject();  // 发送给B的信息respB.put("event", "match_success");respB.put("opponent_username", a.getUsername());respB.put("opponent_photo", a.getPhoto());respB.put("game", respGame);users.get(b.getId()).sendMessage(respB.toJSONString());}}private void stopMatching() {System.out.println("Stop matching!");matchPool.remove(this.user);}private void move(Integer direction) {if (game.getPlayerA().getId().equals(user.getId())) {game.setNextStepA(direction);} else if (game.getPlayerB().getId().equals(user.getId())) {game.setNextStepB(direction);}}
}

最后在 PKIndexView 中处理接收到后端发来的移动消息以及游戏结束消息:

<template><PlayGround v-if="$store.state.pk.status === 'playing'" /><MatchGround v-else />
</template><script>
import PlayGround from "@/components/PlayGround.vue";
import MatchGround from "@/components/MatchGround.vue";
import { onMounted, onUnmounted } from "vue";
import { useStore } from "vuex";export default {components: {PlayGround,MatchGround,},setup() {const store = useStore();let socket = null;let socket_url = `ws://localhost:3000/websocket/${store.state.user.jwt_token}/`;onMounted(() => {socket = new WebSocket(socket_url);store.commit("updateOpponent", {username: "我的对手",photo: "https://cdn.acwing.com/media/article/image/2022/08/09/1_1db2488f17-anonymous.png",});socket.onopen = () => {  // 链接成功建立后会执行console.log("Connected!");store.commit("updateSocket", socket);};socket.onmessage = (msg) => {  // 接收到后端消息时会执行const data = JSON.parse(msg.data);  // Spring传过来的数据是放在消息的data中console.log(data);if (data.event === "match_success") {  // 匹配成功store.commit("updateOpponent", {  // 更新对手信息username: data.opponent_username,photo: data.opponent_photo,});store.commit("updateGame", data.game);  // 更新游戏内容setTimeout(() => {  // 3秒后再进入游戏地图界面store.commit("updateStatus", "playing");}, 3000);} else if (data.event === "move") {  // 两名玩家的移动const gameObject = store.state.pk.gameObject;const [snake0, snake1] = gameObject.snakes;snake0.set_direction(data.a_direction);snake1.set_direction(data.b_direction);} else if (data.event === "result") {  // 游戏结束const gameObject = store.state.pk.gameObject;const [snake0, snake1] = gameObject.snakes;if (data.loser === "all" || data.loser === "A") {snake0.status = "die";}if (data.loser === "all" || data.loser === "B") {snake1.status = "die";}}};socket.onclose = () => {  // 关闭链接后会执行console.log("Disconnected!");store.commit("updateStatus", "matching");  // 进入游戏地图后玩家点击其他页面应该是默认退出游戏};});onUnmounted(() => {socket.close();  // 如果不断开链接每次切换页面都会创建新链接,就会导致有很多冗余链接});},
};
</script><style scoped></style>

2. 同步碰撞检测

现在还需要将碰撞检测放到后端进行判断,先将 Snake.js 中的碰撞检测判断代码删掉,并将死后变白的逻辑放到 render() 函数中:

...export class Snake extends AcGameObject {...next_step() {  // 将蛇的状态变为走下一步...// if (!this.gamemap.check_next_valid(this.next_cell)) {  // 下一步不合法//   this.status = "die";// }}...render() {...ctx.fillStyle = this.color;if (this.status === "die") {ctx.fillStyle = "white";}...}
}

接下来需要实现后端中的 judge() 方法,在判断的时候需要知道当前蛇的身体有哪些,先在 comsumer.utils 包下创建 Cell 类表示身体的每一格:

package com.kob.backend.consumer.utils;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@AllArgsConstructor
@NoArgsConstructor
public class Cell {int x;int y;
}

然后在 Player 类中创建一个方法能够根据玩家历史走过的路径找出当前这条蛇身体的每一格:

package com.kob.backend.consumer.utils;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.util.ArrayList;
import java.util.List;@Data
@NoArgsConstructor
@AllArgsConstructor
public class Player {private Integer id;private Integer sx;private Integer sy;private List<Integer> steps;  // 记录历史走过的每一步方向private boolean check_tail_increasing(int step) {  // 检测当前回合蛇的长度是否增加if (step <= 7) return true;  // 前7回合每一回合长度都增加return step % 3 == 1;  // 之后每3回合增加一次长度}public List<Cell> getCells() {  // 返回蛇的身体,每次都根据蛇历史走的方向将其每一格找出来List<Cell> cells = new ArrayList<>();int[] dx = { -1, 0, 1, 0 }, dy = { 0, 1, 0, -1 };int x = sx, y = sy;int step = 0;cells.add(new Cell(x, y));for (int d: steps) {x += dx[d];y += dy[d];cells.add(new Cell(x, y));if (!check_tail_increasing(++step)) {cells.remove(0);  // 删掉蛇尾,即第一个起始的位置}}return cells;}
}

最后即可在 Game 类中实现 judge() 方法:

...public class Game extends Thread {...private boolean check_valid(List<Cell> cellsA, List<Cell> cellsB) {  // 判断A是否合法int n = cellsA.size();Cell headCellA = cellsA.get(n - 1);  // A的头,也就是最后一个Cellif (g[headCellA.x][headCellA.y]) {return false;}for (int i = 0; i < n - 1; i++) {  // 判断除了头以外的其他身体部分if (cellsA.get(i).x == headCellA.x && cellsA.get(i).y == headCellA.y) {return false;}if (cellsB.get(i).x == headCellA.x && cellsB.get(i).y == headCellA.y) {return false;}}return true;}private void judge() {  // 判断两名玩家下一步操作是否合法List<Cell> cellsA = playerA.getCells();List<Cell> cellsB = playerB.getCells();boolean validA = check_valid(cellsA, cellsB);boolean validB = check_valid(cellsB, cellsA);if (!validA || !validB) {status = "finished";if (!validA && !validB) {loser = "all";} else if (!validA) {loser = "A";} else {loser = "B";}}}...
}

3. 实现游戏结束界面

首先我们需要将输的玩家记录到前端的全局变量中,在 store/pk.js 中添加 loser 变量:

export default {state: {...loser: "none",  // none表示没人输,all表示平局,A/B表示A/B赢},getters: {},mutations: {...updateLoser(state, loser) {state.loser = loser;},},actions: {},modules: {},
};

然后在 PKIndexView 组件的游戏结束处理语句块中添加更新 loser 的语句:

store.commit("updateLoser", data.loser);

游戏结束后需要给用户给用户展示谁赢谁输的界面,并提供一个重开按钮,在 components 目录下创建 ResultBoard.vue

<template><div class="card text-bg-secondary text-center"><div class="card-header" style="font-size: 26px;">游戏结束</div><div class="card-body" style="background-color: rgba(255, 255, 255, 0.4);"><div class="result_board_text" v-if="$store.state.pk.loser === 'all'">Draw</div><div class="result_board_text" v-else-if="$store.state.pk.loser === 'A' && $store.state.pk.a_id.toString() === $store.state.user.id">Lose</div><div class="result_board_text" v-else-if="$store.state.pk.loser === 'B' && $store.state.pk.b_id.toString() === $store.state.user.id">Lose</div><div class="result_board_text" v-else>Win</div><div class="result_board_btn"><button @click="returnHome" type="button" class="btn btn-info btn-lg">返回主页</button></div></div></div>
</template><script>
import { useStore } from "vuex";export default {setup() {const store = useStore();const returnHome = () => {  // 需要复原一些全局变量store.commit("updateStatus", "matching");store.commit("updateLoser", "none");store.commit("updateOpponent", {username: "我的对手",photo: "https://cdn.acwing.com/media/article/image/2022/08/09/1_1db2488f17-anonymous.png",});};return {returnHome,};},
};
</script><style scoped>
.card {width: 30vw;position: absolute;top: 25vh;left: 35vw;
}.result_board_text {color: white;font-size: 50px;font-weight: bold;font-style: italic;padding: 5vh 0;
}.result_board_btn {padding: 3vh 0;
}
</style>

4. 持久化游戏状态

4.1 创建数据库表

最后我们还需要将游戏过程存到数据库中,方便用户之后回看游戏录像,在数据库中创建 record 表用来记录每局对战的信息:

  • id: int(主键、自增、非空)
  • a_id: int
  • a_sx: int
  • a_sy: int
  • b_id: int
  • b_sx: int
  • b_sy: int
  • a_steps: varchar(1000)
  • b_steps: varchar(1000)
  • map: varchar(1000)
  • loser: varchar(10)
  • createtime: datetime

创建该数据库表的 SQL 语句如下:

CREATE TABLE `kob`.`record` (`id` int NOT NULL AUTO_INCREMENT,`a_id` int NULL,`a_sx` int NULL,`a_sy` int NULL,`b_id` int NULL,`b_sx` int NULL,`b_sy` int NULL,`a_steps` varchar(1000) NULL,`b_steps` varchar(1000) NULL,`map` varchar(1000) NULL,`loser` varchar(10) NULL,`createtime` datetime NULL,PRIMARY KEY (`id`)
);

pojo 包下创建 Record 类如下:

package com.kob.backend.pojo;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.util.Date;@Data
@NoArgsConstructor
@AllArgsConstructor
public class Record {@TableId(value = "id", type = IdType.AUTO)private Integer id;private Integer aId;private Integer aSx;  // 注意别忘了驼峰命名private Integer aSy;private Integer bId;private Integer bSx;private Integer bSy;private String aSteps;private String bSteps;private String map;private String loser;@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")private Date createtime;
}

mapper 包下创建 RecordMapper 类如下:

package com.kob.backend.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.kob.backend.pojo.Record;
import org.apache.ibatis.annotations.Mapper;@Mapper
public interface RecordMapper extends BaseMapper<Record> {
}

4.2 保存游戏对局信息

可以在向前端发送游戏结果消息之前将对局信息存下来,首先需要在 WebSocketServer 中将 RecordMapper 创建出来:

...@Component
@ServerEndpoint("/websocket/{token}")  // 注意不要以'/'结尾
public class WebSocketServer {...public static RecordMapper recordMapper;  // 要在Game中调用@Autowiredpublic void setRecordMapper(RecordMapper recordMapper) {WebSocketServer.recordMapper = recordMapper;}...
}

然后在 Player 中创建辅助函数用来返回 steps 的字符串形式:

...@Data
@NoArgsConstructor
@AllArgsConstructor
public class Player {...private List<Integer> steps;  // 记录历史走过的每一步方向...public String getStringSteps() {  // 将steps转换成字符串StringBuilder res = new StringBuilder();for (int d: steps) {res.append(d);}return res.toString();}
}

最后就可以在 Game 中将游戏记录保存至数据库中:

...public class Game extends Thread {...private String getStringMap() {  // 将g转换成01字符串StringBuilder res = new StringBuilder();for (int i = 0; i < rows; i++) {for (int j = 0; j < cols; j++) {res.append(g[i][j] ? 1 : 0);}}return res.toString();}private void saveRecord() {  // 将对局信息存到数据库中Record record = new Record(null,playerA.getId(),playerA.getSx(),playerA.getSy(),playerB.getId(),playerB.getSx(),playerB.getSy(),playerA.getStringSteps(),playerB.getStringSteps(),getStringMap(),loser,new Date());WebSocketServer.recordMapper.insert(record);}private void sendResult() {  // 向两个Client公布结果JSONObject resp = new JSONObject();resp.put("event", "result");resp.put("loser", loser);saveRecord();  // 在发送结束消息给前端之前先将游戏记录存下来sendAllMessage(resp.toJSONString());}...
}

相关文章:

SpringBoot学习笔记-实现微服务:匹配系统(中)

笔记内容转载自 AcWing 的 SpringBoot 框架课讲义&#xff0c;课程链接&#xff1a;AcWing SpringBoot 框架课。 CONTENTS 1. 同步玩家位置1.1 游戏信息的记录1.2 实现多线程同步移动 2. 同步碰撞检测3. 实现游戏结束界面4. 持久化游戏状态4.1 创建数据库表4.2 保存游戏对局信息…...

【复杂网络建模】——基于代理的社会网络建模(Agent-Based Modeling,ABM)[Python实现]

目录 一、复杂网络建模方法 二、基于代理的社会网络建模实现及Python实现代码 一、复杂网络建模方法 复杂网络是一种由大量相互连接的元素(节点或顶点)组成的网络结构,这些连接通常是非常复杂和动态的。这些网络可以在各种领域中发现,包括社交网络、生物学系统、信息技术…...

RSA实现中弱密钥漏洞分析(Analyzing Weak Key Vulnerabilities in RSA Implementation)

点我完整下载&#xff1a;《RSA实现中弱密钥漏洞分析》本科毕业论文一万字.doc RSA实现中弱密钥漏洞分析 "Analyzing Weak Key Vulnerabilities in RSA Implementation" 目录 目录 2 摘要 3 关键词 4 第一章 引言 4 1.1 研究背景 4 1.2 研究目的 5 1.3 研究意义 6 第…...

【管理运筹学】背诵手册(六)| 图与网络分析(最大流问题,最小费用最大流问题)

六、图与网络分析 最大流问题 最大流问题的数学规划模型为&#xff1a; max ⁡ v f 12 f 13 { f 12 f 13 − f 57 − f 67 0 f 13 f 23 f 34 f 35 . . . 0 ≤ f i j ≤ c i j \max vf_{12}f_{13}\\ \begin{cases} f_{12}f_{13}-f_{57}-f_{67}0 \\ f_{13}f_{23}f_{34}f…...

C语言之结构体详解

C语言之结构体详解 文章目录 C语言之结构体详解1. 结构体类型的声明2. 结构体变量的创建和初始化3. 结构体的特殊声明4. 结构体的自引用结构体的自引用匿名结构体的自引用 5. 结构体内存对齐5.1 练习一5.2 练习三 6. 为什么存在内存对⻬? 1. 结构体类型的声明 struct tag {me…...

学习canvas

Canvas是一个基于像素的渲染引擎&#xff0c;它使用JavaScript API在画布上绘制图像。以下是它的一些优点和缺点&#xff1a; 优点&#xff1a; Canvas的渲染速度快&#xff0c;适合处理大量图像和高度动态的图像。 可以直接操作像素&#xff0c;从而能够创建出高质量、流畅的…...

浏览器的渲染原理

以下内容来源于渡一前端大师课&#xff0c;仅作个人学习记录。 渲染的第一步是 解析HTML 解析过程中遇到CSS解析CSS&#xff0c;遇到JS执行JS。为了提高解析效率&#xff0c;浏览器在开始解析之前&#xff0c;会启动一个预解析的线程&#xff0c;率先下载HTML中的外部CSS文件和…...

从 JSON 转 Java 实体的多种方法详解

将 JSON 数据转换为 Java 对象是现代应用程序开发中常见的任务。在 Java 中&#xff0c;有多种方法可以实现这一目标。本文将详细介绍几种常见的方法&#xff0c;以及它们的优缺点。 1. 手动映射&#xff08;Manual Mapping&#xff09; 手动映射是最基础的方法之一&#xff…...

数据库的多表查询(MYSQL)表表联立

根据以上三张表格&#xff0c;对三张表格进行不同的联立&#xff0c;查询并显示符合条件的内容。 1. 查出至少有一个员工的部门。显示部门编号、部门名称、部门位置、部门人数。 mysql> SELECT d.deptno AS 部门编号, d.dname as 部门名称, d.loc as 部门位置, COUNT(e.emp…...

P8650 [蓝桥杯 2017 省 A] 正则问题(dfs )

多重括号&#xff0c;利用回溯来对上一层括号中的内容进行反馈 实现&#xff1a; 若为 x 长度加一 若为 &#xff08; 进入递归计算 (计算相当于子表达式) 若为 &#xff09; 结束当前递归 若为 | …...

【ESP32】手势识别实现笔记:红外温度阵列 | 双三次插值 | 神经网络 | TensorFlow | ESP-DL

目录 一、开发环境搭建与新建工程模板1.1、开发环境搭建与卸载1.2、新建工程目录1.3、自定义组件 二、驱动移植与应用开发2.1、I2C驱动移植与AMG8833应用开发2.2、SPI驱动移植与LCD应用开发2.3、绘制温度云图2.4、启用PSRAM&#xff08;可选&#xff09;2.5、画面动静和距离检测…...

No matching version found for @babel/compat-data@^7.23.5 处理

npm ERR! notarget No matching version found for babel/compat-data^7.23.5 处理 报错信息 npm WARN ERESOLVE overriding peer dependency npm ERR! code ETARGET npm ERR! notarget No matching version found for babel/compat-data^7.23.5. npm ERR! notarget In most …...

手持机|三防智能手机_4寸/5寸/6寸安卓系统三防手机PDA手持终端方案

随着科技的不断发展&#xff0c;三防手持机作为一种多功能设备&#xff0c;正逐渐在各行业得到广泛应用。这款手持机采用高性能处理器&#xff0c;支持高精度北斗定位和工业本安防爆功能&#xff0c;并具备IP67级防水防尘性能和1.5米防跌落能力。因此&#xff0c;它在仓储管理、…...

蓝桥杯算法心得——仙界诅咒(dfs)

大家好&#xff0c;我是晴天学长&#xff0c;搜索型的dfs&#xff0c;差点开二维矩阵了&#xff0c;仔细一想&#xff0c;没那么夸张啊&#xff0c;哈哈哈&#xff0c;需要的小伙伴可以关注支持一下哦&#xff01;后续会继续更新的。&#x1f4aa;&#x1f4aa;&#x1f4aa; 1…...

List集合,遍历,数据结构

一.List常见的方法&#xff1a; 二. List集合的遍历方式 除了 迭代器遍历 增强for遍历 Lambda表达式遍历&#xff0c;还有自己独有的普通for遍历&#xff0c;列表迭代器遍历 1.迭代器遍历 2.增强for遍历 3.Lambda表达式遍历 4.普通for遍历 5.列表迭代器遍历 列表迭代器相对于…...

2的幂运算

2的幂 描述 : 给你一个整数 n&#xff0c;请你判断该整数是否是 2 的幂次方。如果是&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 如果存在一个整数 x 使得 n 2x &#xff0c;则认为 n 是 2 的幂次方。 题目 : LeetCode 231.2的幂 : 231. 2 的幂 分…...

优先队列经典例题leetcode思路代码详解

目录 leetcode215题.数组中的第k个最大元素 leetcode347题.前k个高频元素 leetcode295题.数据流的中位数 对优先队列感兴趣的朋友可以去看我上一篇文章。 优先队列基础讲解-CSDN博客 leetcode215题.数组中的第k个最大元素 215. 数组中的第K个最大元素 - 力扣&#xff08;…...

新型Python环境与依赖管理工具——pipenv

文章目录 pipenv介绍pipenv安装pipenv使用创建虚拟环境删除虚拟环境安装依赖查看包之间的依赖图卸载依赖在虚拟环境中执行命令shell环境下通过requirements.txt安装依赖导出requirements.txt文件查看虚拟环境的路径 pipenv介绍 pipenv可以看做是pip和virtualenv的组合体&#…...

FastDFS+Nginx - 本地搭建文件服务器同时实现在外远程访问「内网穿透」

文章目录 前言1. 本地搭建FastDFS文件系统1.1 环境安装1.2 安装libfastcommon1.3 安装FastDFS1.4 配置Tracker1.5 配置Storage1.6 测试上传下载1.7 与Nginx整合1.8 安装Nginx1.9 配置Nginx 2. 局域网测试访问FastDFS3. 安装cpolar内网穿透4. 配置公网访问地址5. 固定公网地址5.…...

kendo-splitter动态分配分隔框大小

通过size方法&#xff0c;动态改变框大小&#xff0c;参考链接&#xff1a;https://docs.telerik.com/kendo-ui/api/javascript/ui/splitter/methods/size vue画面 <kendo-button type"primary" click"changePane">button</kendo-button><…...

JVM垃圾回收机制全解析

Java虚拟机&#xff08;JVM&#xff09;中的垃圾收集器&#xff08;Garbage Collector&#xff0c;简称GC&#xff09;是用于自动管理内存的机制。它负责识别和清除不再被程序使用的对象&#xff0c;从而释放内存空间&#xff0c;避免内存泄漏和内存溢出等问题。垃圾收集器在Ja…...

五年级数学知识边界总结思考-下册

目录 一、背景二、过程1.观察物体小学五年级下册“观察物体”知识点详解&#xff1a;由来、作用与意义**一、知识点核心内容****二、知识点的由来&#xff1a;从生活实践到数学抽象****三、知识的作用&#xff1a;解决实际问题的工具****四、学习的意义&#xff1a;培养核心素养…...

06 Deep learning神经网络编程基础 激活函数 --吴恩达

深度学习激活函数详解 一、核心作用 引入非线性:使神经网络可学习复杂模式控制输出范围:如Sigmoid将输出限制在(0,1)梯度传递:影响反向传播的稳定性二、常见类型及数学表达 Sigmoid σ ( x ) = 1 1 +...

C# 求圆面积的程序(Program to find area of a circle)

给定半径r&#xff0c;求圆的面积。圆的面积应精确到小数点后5位。 例子&#xff1a; 输入&#xff1a;r 5 输出&#xff1a;78.53982 解释&#xff1a;由于面积 PI * r * r 3.14159265358979323846 * 5 * 5 78.53982&#xff0c;因为我们只保留小数点后 5 位数字。 输…...

rnn判断string中第一次出现a的下标

# coding:utf8 import torch import torch.nn as nn import numpy as np import random import json""" 基于pytorch的网络编写 实现一个RNN网络完成多分类任务 判断字符 a 第一次出现在字符串中的位置 """class TorchModel(nn.Module):def __in…...

Python基于历史模拟方法实现投资组合风险管理的VaR与ES模型项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档&#xff09;&#xff0c;如需数据代码文档可以直接到文章最后关注获取。 1.项目背景 在金融市场日益复杂和波动加剧的背景下&#xff0c;风险管理成为金融机构和个人投资者关注的核心议题之一。VaR&…...

AI+无人机如何守护濒危物种?YOLOv8实现95%精准识别

【导读】 野生动物监测在理解和保护生态系统中发挥着至关重要的作用。然而&#xff0c;传统的野生动物观察方法往往耗时耗力、成本高昂且范围有限。无人机的出现为野生动物监测提供了有前景的替代方案&#xff0c;能够实现大范围覆盖并远程采集数据。尽管具备这些优势&#xf…...

宇树科技,改名了!

提到国内具身智能和机器人领域的代表企业&#xff0c;那宇树科技&#xff08;Unitree&#xff09;必须名列其榜。 最近&#xff0c;宇树科技的一项新变动消息在业界引发了不少关注和讨论&#xff0c;即&#xff1a; 宇树向其合作伙伴发布了一封公司名称变更函称&#xff0c;因…...

日常一水C

多态 言简意赅&#xff1a;就是一个对象面对同一事件时做出的不同反应 而之前的继承中说过&#xff0c;当子类和父类的函数名相同时&#xff0c;会隐藏父类的同名函数转而调用子类的同名函数&#xff0c;如果要调用父类的同名函数&#xff0c;那么就需要对父类进行引用&#…...

《Docker》架构

文章目录 架构模式单机架构应用数据分离架构应用服务器集群架构读写分离/主从分离架构冷热分离架构垂直分库架构微服务架构容器编排架构什么是容器&#xff0c;docker&#xff0c;镜像&#xff0c;k8s 架构模式 单机架构 单机架构其实就是应用服务器和单机服务器都部署在同一…...