本节实现功能
联机对战
计分板
对局回放
联机对战
操作思路
1.后端:同步玩家位置信息
2.前端:接收两名玩家位置以及地图信息
3.后端:实现一局游戏的逻辑
4.前端: 发送移动指令给后端
5.后端: 处理接收移动的事件
6.前端: 接收后端发送过来的移动事件
7.前端:根据后端返回结果将死了的蛇变白
8.后端:将前端的裁判程序移到后端
细化思路
1.后端:同步玩家位置信息
- 创建一个Player类,存储玩家的Id,起始坐标,蛇的移动方向
- 在Game类中调用Player类,初始化玩家的Id,起始坐标,创建两个函数,分别返回两个玩家的id和起始坐标
- 在WebSocketServer类中封装一个JSONObject,通过链接向前端发送用户的id,起始坐标,地图信息
2.前端:接收两名玩家位置以及地图信息(同步)
- 在Pk.js中创建用户的id和起始坐标的全局变量,创建updateGame用以更新地图和用户信息
- 在PkIndexView.vue中匹配成功下调用 store.commit(“updateGame”, data.game),进行相关信息的更新
3.后端:实现一局游戏的逻辑
- 需要考虑到多线程的问题 两个线程同时读写一个变量、这样就会有读写冲突、涉及到顺序问题
- WebSocketServer中将private改成public,因为Game类需要使用( public static ConcurrentHashMap[HTML_REMOVED] users = new ConcurrentHashMap<>();),将game赋给对应的链接
- 因为线程原因,所以Game类继承Thread接口,同时在Game类中建立变量 用户的操作 当前游戏的状态 A,B的对局状态,创建玩家的下一步操作函数,实现新线程的入口函数run(),通过入口函数调用实现 蛇的移动方法,蛇的结束方法,游戏的结果方法
4.前端: 发送移动指令给后端
- 在GameMap.js中创建一个变量d表示蛇的移动
- 如果移动发送给后端
5.后端: 处理接收移动的事件
- 在WebSocketServer接受前端传来的信息,根据状态信息判断执行哪个方法(写一个移动方法)
6.前端: 接收后端发送过来的移动事件
- 设立全局变量,可以对地图更新
- 在GameMap.vue中调用updateGameObject地图的更新
- Pk页面:接收后端的移动事件
7.前端:根据后端返回结果将死了的蛇变白
- 删除前端判断蛇的操作是否有效
- 设立一个蛇的状态的全局变量loser,用于更新蛇的状态
- 在PkIndexView.vue中判断蛇的状态
8.后端:将前端的裁判程序移到后端
- 在后端创建Cell用于存放坐标
- 在Player中实现蛇的增长,减短,移动逻辑
- 在Game中创建boolean check_valid 用于判断蛇撞墙和蛇自己是否身子重复,创建judge 判断两名玩家操作是否合法
* 同步两个玩家的初始位置
同步玩家的位置我们可以标记一下、至于谁在A谁在B我们需要在云端确定
确定完之后我们会把每一个玩家的位置传给前端,我们可以傻瓜式的确定a在左下角b在
右上角、我们在存地图的时候需要存一下玩家的id和位置
在game这个类里我们需要加一个player类来维护玩家的位置信息
一般开发思路需要用什么定义什么、先定义需要用到的各种函数
有参构造函数无参构造函数、存一下每个玩家每一次的指令是什么
- 棋盘同步原理
现在有三个棋盘、还有一个在云端
有两个浏览器就是有两个client、状态同步的机制
client向云端发送消息表示这个蛇动了一下、当服务器接收到两个蛇的移动之后
服务器就会把两个蛇移动的信息分别返回给Client1client2
同步给两名玩家、这样我们就实现了三个棋盘的同步
- 玩家类
package com.kob.backend.consumer.utils;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Player {
private Integer id;
private Integer sx;
private Integer sy;
// 蛇每个球的方向
private List<Integer> steps;
}
- 游戏类
package com.kob.backend.consumer.utils;
import java.util.ArrayList;
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) {
this.rows = rows;
this.cols = cols;
this.inner_walls_count = inner_walls_count;
this.g = new int[rows][cols];
//调用Player类初始化两个起始点
playerA = new Player(idA, rows - 2, 1, new ArrayList<>());
playerB = new Player(idB, 1, cols - 2, new ArrayList<>());
}
public Player getPlayerA() {
return playerA;
}
public Player getPlayerB() {
return playerB;
}
...
}
- WebSocketServer操作类
//在匹配完之后,初始化地图时,将玩家传给Game构造函数
package com.kob.backend.consumer;
...
@Component
// url链接:ws://127.0.0.1:3000/websocket/**
@ServerEndpoint("/websocket/{token}") // 注意不要以'/'结尾
public class WebSocketServer {
...
private void startMatching() {
...
while(matchPool.size() >= 2) {
...
Game game = new Game(13, 14, 20, a.getId(), b.getId());
game.createMap();
JSONObject respGame = new JSONObject();
// 玩家的id以及横纵信息
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());
// 发送给A的信息
JSONObject respA = new JSONObject();
...
respA.put("game", respGame);
// 获取A的链接向前端发送信息
users.get(a.getId()).sendMessage(respA.toJSONString());
// 发送给B的信息
JSONObject respB = new JSONObject();
...
respB.put("game", respGame);
// 获取B的链接向前端发送信息
users.get(b.getId()).sendMessage(respB.toJSONString());
}
}
...
}
接收两名玩家位置以及地图信息
- 前端Pk.js
export default {
state: {
...
gamemap: null,
a_id: 0,
a_sx: 0,
a_sy: 0,
b_id: 0,
b_sx: 0,
b_sy: 0,
},
...
mutations: {
...
updateGame(state, game) {
state.gamemap = 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;
}
},
...
}
- 前端PkIndexView.vue
<template>
...
</template>
<script>
...
export default {
...
setup() {
...
onMounted(() => {
...
// 回调函数:接收到后端信息调用
socket.onmessage = msg => {
// 返回的信息格式由后端框架定义,django与spring定义的不一样
const data = JSON.parse(msg.data);
if(data.event === "start-matching") {
...
setTimeout(() => {
store.commit("updateStatus", "playing");
}, 200);
store.commit("updateGame", data.game);
}
}
...
});
...
}
}
</script>
<style scoped>
</style>
实现一局游戏的逻辑
补充知识
什么是线程为什么要用多线程?
Game不能作为单线程来处理、线程:一个人干就是单线程,两个人干就是多线程
涉及到两个线程之间的通信以及加锁的问题
我们需要先把game变成一个支持多线程的类
就变成多线程了、我们需要实现thread类的一个入口函数
alt+insert就可以实现、重载run函数
start函数就是thread函数的一个api、可以另起一个线程来执行这个函数
为了方便我们需要先把我们的game存放到这个类里面
我们的线程就要一步一步等待下一步操作的操作
这里设计到两个线程同时读写一个变量、这样就会有读写冲突、涉及到顺序问题
- WebSocketServer
//将Game作为一局游戏的一个线程,使Game线程启动
package com.kob.backend.consumer;
...
@Component
@ServerEndpoint("/websocket/{token}") // 注意不要以'/'结尾
public class WebSocketServer {
...
// 将private改成public,因为Game类需要使用
public static ConcurrentHashMap<Integer, WebSocketServer> users = new ConcurrentHashMap<>();
...
private Game game = null;
...
private void startMatching() {
System.out.println("start matching!");
matchPool.add(this.user);
while(matchPool.size() >= 2) {
...
game.createMap();
game.start();//Thread 的一个API
//将game赋给对应的链接
users.get(a.getId()).game = game;
users.get(b.getId()).game = game;
...
}
}
...
}
- 游戏类
/*一局游戏的操作,首先执行run方法。只有读写、写写有冲突,此处关于nextStep,
我们会接收前端的nextStep输入 或 bots代码的输入,而且会频繁的读,因此需要加锁。*/
package com.kob.backend.consumer.utils;
import com.alibaba.fastjson.JSONObject;
import com.kob.backend.consumer.WebSocketServer;
import java.util.ArrayList;
import java.util.Random;
import java.util.concurrent.locks.ReentrantLock;
public class Game extends Thread {
...
private Integer nextStepA = null; // A的操作
private Integer nextStepB = null; // B的操作
private ReentrantLock lock = new ReentrantLock();
private String status = "playing"; // playing -> finished
private String loser = ""; // all: 平局,A:A输,B:B输
...
// 在主线程会读两个玩家的操作,并且玩家随时可能输入操作,存在读写冲突
public void setNextStepA(Integer nextStepA) {
lock.lock();
try {
this.nextStepA = nextStepA;
} finally {
lock.unlock();
}
}
public void setNextStepB(Integer nextStepB) {
lock.lock();
try {
this.nextStepB = nextStepB;
} finally {
lock.unlock();
}
}
public int[][] getG() {
...
}
private boolean check_connectivity(int sx, int sy, int tx, int ty) {
...
}
// 画地图
private boolean draw() {
...
}
public void createMap() {
...
}
// 接收玩家的下一步操作
private boolean nextStep() {
// 每秒五步操作,因此第一步操作是在200ms后判断是否接收到输入。并给地图初始化时间
try {
Thread.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
/*
* 个人理解:此循环循环了5000ms,也就是5s,前端是一秒移动5步,
* 后端接收玩家键盘输入是5s内玩家的一个输入,若在一方先输入,
* 一方还未输入,输入的一方多此操作,以最后一次为准。
*/
// 因为会读玩家的nextStep操作,因此加锁
for(int i = 0; i < 50; i++) {
try {
Thread.sleep(100);
lock.lock();
try {
if(nextStepA != null && nextStepB != null) {
playerA.getSteps().add(nextStepA);
playerB.getSteps().add(nextStepB);
return true;
}
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
return false;
}
private void judge() { // 判断两名玩家操作是否合法
}
private void senAllMessage(String message) {
WebSocketServer.users.get(playerA.getId()).sendMessage(message);
WebSocketServer.users.get(playerB.getId()).sendMessage(message);
}
private void sendMove() { // 向两名玩家传递移动信息
// 因为需要读玩家的下一步操作,所以需要加锁
lock.lock();
try {
JSONObject resp = new JSONObject();
resp.put("event", "move");
resp.put("a_direction", nextStepA);
resp.put("b_direction", nextStepB);
senAllMessage(resp.toJSONString());
nextStepA = nextStepB = null;
} finally {
lock.unlock();
}
}
private void sendResult() { // 向两名玩家发送游戏结果
JSONObject resp = new JSONObject();
resp.put("event", "result");
resp.put("loser", loser);
senAllMessage(resp.toJSONString());
}
@Override
public void run() {
// 一局游戏,地图大小总共13 * 14 = 182 ≈ 200,蛇每三步长一个格子,两条蛇总长度若200,每三步长一格,最多600步 < 1000
for(int i = 0; i < 1000; i++) {
// 是否获取到两条蛇的下一步操作
if(nextStep()) {
judge();
if(status.equals("playing")) {
sendMove();
} else {
sendResult();
break;
}
} else {
status = "finished";
// 因为读了nextStep操作,因此也要加锁
lock.lock();
// try finally是为了出异常也会抛锁
try {
if(nextStepA == null && nextStepB == null) {
loser = "all";
} else if(nextStepA == null) {
loser = "A";
} else {
loser = "B";
}
} finally {
lock.unlock();
}
// 游戏结束
sendResult();
break;
}
}
}
}
发送移动指令给后端
- GameMap.js
import { AcGameObject } from "./AcGameObject";
import { Wall } from "./Wall";
import { Snake } from './Snake';
export class GameMap extends AcGameObject {
...
add_listening_events() {
this.ctx.canvas.focus();
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;
// else if(e.key === 'ArrowUp') snake1.set_direction(0);
// else if(e.key === 'ArrowRight') snake1.set_direction(1);
// else if(e.key === 'ArrowDown') snake1.set_direction(2);
// else if(e.key === 'ArrowLeft') snake1.set_direction(3);
// 若移动了,发送给后端
if(d >= 0) {
this.store.state.pk.socket.send(JSON.stringify({
event: "move",
direction: d,
}));
}
});
}
...
}
处理接收移动的事件
- WebSocketServer
package com.kob.backend.consumer;
import com.alibaba.fastjson.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 move(int direction) {
if(game.getPlayerA().getId().equals(user.getId())) {
game.setNextStepA(direction);
} else if(game.getPlayerB().getId().equals(user.getId())) {
game.setNextStepB(direction);
}
}
@OnMessage
public void onMessage(String message, Session session) {
// 从Client接收消息
System.out.println("receive message: " + message);
JSONObject data = JSONObject.parseObject(message);
String event = data.getString("event");
if("start-matching".equals(event)) {
startMatching();
} else if("stop-matching".equals(event)) {
stopMatching();
} else if("move".equals(event)) {
move(data.getInteger("direction"));
}
}
...
}
接收后端发送过来的移动事件
- 设立全局变量,可以对地图更新
export default {
state: {
...
gameObject: null,
},
getters: {
},
mutations: {
...
updateGameObject(state, gameobject) {
state.gameObject = gameobject;
}
},
actions: {
},
modules: {
}
}
- GameMap存储(也就是地图的更新)
<template>
...
</template>
<script>
...
export default {
setup() {
...
onMounted(() => {
store.commit(
"updateGameObject",
new GameMap(canvas.value.getContext('2d'), parent.value, store)
);
});
...
}
}
</script>
<style scoped>
...
</style>
- Pk页面:接收后端的移动事件
<template>
...
</template>
<script>
...
export default {
components: {
PlayGround,
MatchGround,
},
setup() {
...
onMounted(() => {
...
// 回调函数:接收到后端信息调用
socket.onmessage = msg => {
const data = JSON.parse(msg.data);
if(data.event === "start-matching") {
...
} else if(data.event === "move") {
console.log(data);
const game = store.state.pk.gameObject;
const [snake0, snake1] = game.snakes;
snake0.set_direction(data.a_direction);
snake1.set_direction(data.b_direction);
} else if(data.event === "result") {
console.log(data);
}
}
...
});
onUnmounted(() => {
...
});
}
}
</script>
<style scoped>
</style>
根据后端返回结果将死了的蛇变白
- Snake.js
//删除前端判断蛇的操作是否有效
import { AcGameObject } from "./AcGameObject";
import { Cell } from "./Cell";
export class Snake extends AcGameObject {
...
// 将蛇状态变为走下一步
next_step() {
...
// 让蛇在下一回合长一个格子
const k = this.cells.length;
for(let i = k; i > 0; i--) {
this.cells[i] = JSON.parse(JSON.stringify(this.cells[i - 1]));
}
// 删除前端判断蛇的操作是否有效
}
...
}
- PkIndexView.vue
<template>
...
</template>
<script>
...
export default {
...
setup() {
...
onMounted(() => {
...
// 回调函数:接收到后端信息调用
socket.onmessage = msg => {
const data = JSON.parse(msg.data);
if(data.event === "start-matching") {
...
} else if(data.event === "move") {
...
} else if(data.event === "result") {
console.log(data);
const game = store.state.pk.gameObject;
const [snake0, snake1] = game.snakes;
if(data.loser === "all" || data.loser === "A") {
snake0.status = "die";
}
if(data.loser === "all" || data.loser === "B") {
snake1.status = "die";
}
}
}
...
});
onUnmounted(() => {
...
});
}
}
</script>
<style scoped>
</style>
将前端的裁判程序移到后端
- Cell.java
package com.kob.backend.consumer.utils;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Cell {
public int x;
public int y;
}
- Player.java
package com.kob.backend.consumer.utils;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Player {
...
// 检查当前会和,蛇的长度是否会增加
private boolean check_tail_increasing(int step) {
if(step <= 10) return true;
return step % 3 == 1;
}
public List<Cell> getCells() {
List<Cell> res = new ArrayList<>();
int[] dx = {-1, 0, 1, 0}, dy = {0, 1, 0, -1};
int x = sx, y = sy;
res.add(new Cell(x, y));
int step = 0;
for(int d : steps) {
x += dx[d];
y += dy[d];
/**
* 每一步移动都会把蛇头移动到下一个格子(注:蛇头有两个cell,详看前端Snake.js的next_step()与update_move()逻辑),
* 若当前长度增加,蛇头正好移到新的一个格子,剩下的蛇身长度不变,因此长度 + 1;若长度不增加,则删除蛇尾
*/
res.add(new Cell(x, y));
if(!check_tail_increasing(++step)) {
/*
关键:
为什么此处删除0呢,回合数增加,说明蛇可以行走,蛇的长度变长,
但不符合变长规律的话,需要将最后一个格子(也就是上回合的蛇尾)
删掉
*/
res.remove(0);
}
}
return res;
}
}
- Game.java
package com.kob.backend.consumer.utils;
import com.alibaba.fastjson.JSONObject;
import com.kob.backend.consumer.WebSocketServer;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.locks.ReentrantLock;
public class Game extends Thread {
...
private boolean check_valid(List<Cell> cellsA, List<Cell> cellsB) {
int n = cellsA.size();
Cell cell = cellsA.get(n - 1);
// 如果是墙,则非法
if(g[cell.x][cell.y] == 1) return false;
// 遍历A除最后一个Cell
/**
* 关键:
* 首先我在Player中已经解释getCells的函数返回的res是蛇尾到蛇头的位置。
* 因此以下两个for循环分别判断的是蛇头是否和两条蛇的蛇身重合!
* 那么为什么不用判断两个蛇头是否重合呢?可能是地图大小为13 * 14,
* 两个蛇头的位置初始为(1, 1)和(11, 12),两个蛇头的位置横纵之和分别为偶数
* 和奇数,因此两个蛇头永远不会走到同一个格子!
*/
for(int i = 0; i < n - 1; i++) {
// 和A蛇身是否重合
if(cellsA.get(i).x == cell.x && cellsA.get(i).y == cell.y) {
return false;
}
}
// 遍历B除最后一个Cell
for(int i = 0; i < n - 1; i++) {
// 和B蛇身是否重合
if(cellsB.get(i).x == cell.x && cellsB.get(i).y == cell.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 valibB = check_valid(cellsB, cellsA);
if(!validA || !valibB) {
status = "finished";
if(!validA && !valibB) {
loser = "all";
} else if(!validA) {
loser = "A";
} else {
loser = "B";
}
}
}
private void senAllMessage(String message) {
...
}
private void sendMove() { // 向两名玩家传递移动信息
...
}
private void sendResult() { // 向两名玩家发送游戏结果
...
}
@Override
public void run() {
...
}
}
计分板
- 在前端创建一个ResultBoard.vue 实现分别向不同的用户发送不同的输赢信息和再次匹配功能
- 创建全局变量Statu,记录初始化 用户当前的状态,失败的人,匹配过程
-
在PkIndexView.vue中引入计分板组件即可
-
ResultBoard.vue
<template>
<div class="result-board">
<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===parseInt($store.state.user.id)">
Lose!!!
</div>
<div class="result-board-text" v-else-if="$store.state.pk.loser==='B'&&$store.state.pk.b_id==parseInt($store.state.user.id)">
Lose!!!
</div>
<div class="result-board-text" v-else>
Win!!!
</div>
<div class="result-board-btn">
<button @click="restart" type="button" class="btn btn-light">再来</button>
</div>
</div>
</template>
<script>
import { useStore } from 'vuex';
export default{
setup(){
const store=useStore();
const restart=()=>{
store.commit("updateStatu","matching");
store.commit("updateLoser","none");
store.commit("updateOpponent",{
username:"我的对手",
photo:"https://ts1.cn.mm.bing.net/th/id/R-C.49e27901805f7d4a5af5ba4f0a8344e9?rik=DGDuc%2fIiXgm6Hw&riu=http%3a%2f%2fwww.gx8899.com%2fuploads%2fallimg%2f2017111809%2fa0f102gwlnw.jpg&ehk=VHM%2fAdYSYer8l8OWKksECcdGeqQmaiQ7L6nH7%2fV7Xzk%3d&risl=&pid=ImgRaw&r=0",
})
}
return{
restart,
}
}
}
</script>
<style scoped>
.result-board{
height: 30vh;
width: 30vw;
background-color: rgba(50, 50, 50, 0.5);
position: absolute;
top: 34vh;
left: 35vw;
}
.result-board-text{
text-align: center;
color: aquamarine;
font-size: 50px;
font-weight: 600;
font-style: italic;
padding-top: 5vh;
}
.result-board-btn{
padding-top: 10vh;
text-align: center;
}
</style>
- PkIndexView.vue
src/views/pk/PkIndexView.vue
<template>
...
<ResultBoard v-if="$store.state.pk.loser != 'none'" />
</template>
<script>
...
import ResultBoard from '../../components/ResultBoard.vue';
...
export default {
components: {
...
ResultBoard,
},
setup() {
...
onMounted(() => {
...
// 回调函数:接收到后端信息调用
socket.onmessage = msg => {
// 返回的信息格式由后端框架定义,django与spring定义的不一样
const data = JSON.parse(msg.data);
if(data.event === "start-matching") {
...
} else if(data.event === "move") {
...
} else if(data.event === "result") {
...
store.commit("updateLoser", data.loser);
}
}
...
});
...
}
}
</script>
<style scoped>
</style>
对局回放
1. 创建一个数据库表中的列:
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
2. 写一个Pojo
* Record.java
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
@AllArgsConstructor
@NoArgsConstructor
public class Record {
@TableId(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;
}
3. 写一个Mapper
* RecordMapper.java
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. 在WebSocketServer注入RecordMapper,为了保存对局信息
* 在WebSocketServer
package com.kob.backend.consumer;
...
import com.kob.backend.mapper.RecordMapper;
import com.kob.backend.mapper.UserMapper;
...
@Component
@ServerEndpoint("/websocket/{token}")
public class WebSocketServer {
...
private static UserMapper userMapper;
public static RecordMapper recordMapper;
...
@Autowired
public void setRecordMapper(RecordMapper recordMapper) {
WebSocketServer.recordMapper = recordMapper;
}
...
}
5.在Player中将玩家的蛇的方向偏移量转化成String
* Player
package com.kob.backend.consumer.utils;
...
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Player {
...
public String getStepsString() {
StringBuilder res = new StringBuilder();
for(int d : steps) {
res.append(d);
}
return res.toString();
}
}
6.在Game中将对局信息保存至数据库
* Game
package com.kob.backend.consumer.utils;
import com.alibaba.fastjson.JSONObject;
import com.kob.backend.consumer.WebSocketServer;
import com.kob.backend.pojo.Record;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Random;
import java.util.concurrent.locks.ReentrantLock;
public class Game extends Thread {
...
private String getMapString() {//将地图展开成一维的01字符串
StringBuilder res = new StringBuilder();
for(int i = 0; i < rows; i++) {
for(int j = 0; j < cols; j++) {
res.append(g[i][j]);
}
}
return res.toString();
}
private void saveToDataBase() {
Record record = new Record(
null,
playerA.getId(),
playerA.getSx(),
playerA.getSy(),
playerB.getId(),
playerB.getSx(),
playerB.getSy(),
playerA.getStepsString(),
playerB.getStepsString(),
getMapString(),
loser,
new Date()
);
WebSocketServer.recordMapper.insert(record);
}
private void sendResult() { // 向两名玩家发送游戏结果
...
saveToDataBase();
senAllMessage(resp.toJSONString());
}
...
}