6.2 实现微服务:匹配系统(中)

master
barney 2 years ago
parent 1b4d8d808a
commit 108ae844f0
  1. 52
      backend/src/main/java/com/kob/backend/consumer/WebSocketServer.java
  2. 15
      backend/src/main/java/com/kob/backend/consumer/utils/Cell.java
  3. 234
      backend/src/main/java/com/kob/backend/consumer/utils/Game.java
  4. 63
      backend/src/main/java/com/kob/backend/consumer/utils/Player.java
  5. 33
      backend/src/main/java/com/kob/backend/controller/pojo/Record.java
  6. 13
      backend/src/main/java/com/kob/backend/mapper/RecordMapper.java
  7. 24
      web/src/assets/scripts/GameMap.js
  8. 5
      web/src/assets/scripts/Snake.js
  9. 2
      web/src/components/GameMap.vue
  10. 2
      web/src/components/MatchGround.vue
  11. 78
      web/src/components/ResultBoard.vue
  12. 47
      web/src/store/pk.js
  13. 87
      web/src/views/pk/PkIndexView.vue

@ -4,6 +4,7 @@ import com.alibaba.fastjson.JSONObject;
import com.kob.backend.consumer.utils.Game;
import com.kob.backend.consumer.utils.JwtAuthentication;
import com.kob.backend.controller.pojo.User;
import com.kob.backend.mapper.RecordMapper;
import com.kob.backend.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@ -25,26 +26,32 @@ public class WebSocketServer {
// 保存所有用户对应的请求链接
// 线程安全的一个HashMap
final private static ConcurrentHashMap<Integer,WebSocketServer> users = new ConcurrentHashMap<>();
public final static ConcurrentHashMap<Integer,WebSocketServer> users = new ConcurrentHashMap<>();
// 保存当前的匹配池
final private static CopyOnWriteArraySet<User> matchpool = new CopyOnWriteArraySet<>();
private final static CopyOnWriteArraySet<User> matchpool = new CopyOnWriteArraySet<>();
// 发起请求链接对应的用户
private User user;
private Session session = null;
// 注入userMapper(和之前的有点不一样)
private static UserMapper userMapper;
// 注入RecordMapper
public static RecordMapper recordMapper;
// // 保存地图
// private Game game = null;
private Game game = null;
@Autowired
public void setUserMapper(UserMapper userMapper) {
WebSocketServer.userMapper = userMapper;
}
@Autowired
public void setRecordMapper(RecordMapper recordMapper) {
WebSocketServer.recordMapper = recordMapper;
}
@OnOpen
public void onOpen(Session session, @PathParam("token") String token) throws IOException {
// 建立连接
@ -78,7 +85,6 @@ public class WebSocketServer {
private void startMatching() {
System.out.println("start-----matching----");
matchpool.add(this.user);
System.out.println(matchpool.size());
while (matchpool.size() >= 2) {
Iterator<User> it = matchpool.iterator();
@ -86,14 +92,32 @@ public class WebSocketServer {
matchpool.remove(a);
matchpool.remove(b);
Game game = new Game(13,14,20);
Game 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(); // 重新开一个新线程
// 两名玩家的id,位置以及地图保存在respGame中
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();
respA.put("event","start-matching");
respA.put("opponent_username",b.getUsername());
respA.put("opponent_photo",b.getPhoto());
respA.put("gamemap",game.getG());
respA.put("game",respGame);
users.get(a.getId()).sendMessage(respA.toJSONString());
@ -101,10 +125,12 @@ public class WebSocketServer {
respB.put("event","start-matching");
respB.put("opponent_username",a.getUsername());
respB.put("opponent_photo",a.getPhoto());
respB.put("gamemap",game.getG());
respB.put("game",respGame);
users.get(b.getId()).sendMessage(respB.toJSONString());
// System.out.println(respGame);
}
}
@ -113,6 +139,14 @@ public class WebSocketServer {
matchpool.remove(this.user);
}
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接收消息(读取前端发来的信息)
@ -125,6 +159,8 @@ public class WebSocketServer {
}
else if ("stop-matching".equals(event)){
stopMatching();
}else if ("move".equals(event)) {
move(data.getInteger("direction"));
}
}

@ -0,0 +1,15 @@
package com.kob.backend.consumer.utils;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author zfp
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Cell {
private int x,y;
}

@ -1,39 +1,58 @@
package com.kob.backend.consumer.utils;
import com.alibaba.fastjson.JSONObject;
import com.kob.backend.consumer.WebSocketServer;
import com.kob.backend.controller.pojo.Record;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Random;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author zfp
*/
public class Game {
final private Integer rows;
final private Integer cols;
final private Integer inner_walls_count;
final private int[][] g;
public class Game extends Thread {
private final Integer rows;
private final Integer cols;
private final Integer inner_walls_count;
private final int[][] g;
private final Player playerA, playerB;
private final static int[] dx = {-1, 0, 1, 0};
private final static int[] dy = {0, 1, 0, -1};
private Integer nextStepA = null; // 玩家A的下一步操作,没有操作设置为null
private Integer nextStepB = null;
final private static int[] dx = {-1,0,1,0};
final private static int[] dy = {0,1,0,-1};
private ReentrantLock lock = new ReentrantLock(); // 多线程之间读写操作需要加锁
private String status = "playing"; // playing -> finished
private String loser = "";// "all": 平局 "A": A输 "B": B输
public Game(Integer rows,Integer cols,Integer inner_walls_count) {
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];
this.playerA = new Player(idA, rows - 2, 1, new ArrayList<>());
this.playerB = new Player(idB, 1, cols - 2, new ArrayList<>());
}
public int[][] getG() {
return g;
}
private boolean check_connectivity(int sx,int sy,int tx,int ty) {
private boolean check_connectivity(int sx, int sy, int tx, int ty) {
if (sx == tx && sy == ty) {
return true;
}
g[sx][sy] = 1;
for (int i = 0; i < 4; i++) {
int x = sx + dx[i] , y = sy + dy[i];
if (x >= 0 && x < this.rows && y >= 0 && y < this.cols && g[x][y] == 0 ) {
if (check_connectivity(x,y,tx,ty)) {
int x = sx + dx[i], y = sy + dy[i];
if (x >= 0 && x < this.rows && y >= 0 && y < this.cols && g[x][y] == 0) {
if (check_connectivity(x, y, tx, ty)) {
g[sx][sy] = 0;
return true;
}
@ -71,7 +90,7 @@ public class Game {
int r = random.nextInt(this.rows);
int c = random.nextInt(this.cols);
// 判断对称的位置是否有障碍物
if (g[r][c] == 1 || g[this.rows - 1 - r][this.cols - 1 - c] == 1){
if (g[r][c] == 1 || g[this.rows - 1 - r][this.cols - 1 - c] == 1) {
continue;
}
// 不要生成到蛇初始的位置上
@ -83,7 +102,7 @@ public class Game {
break;
}
}
return this.check_connectivity(this.rows - 2, 1,1,this.cols - 2);
return this.check_connectivity(this.rows - 2, 1, 1, this.cols - 2);
}
public void createMap() {
@ -94,4 +113,191 @@ public class Game {
}
}
}
public Player getPlayerA() {
return playerA;
}
public Player getPlayerB() {
return playerB;
}
// 多个client可能会同时进行写操作,需要加锁
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 boolean nextStep() {
try {
// 保证前端不会遗漏操作,前端是每200ms移动一个格子
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 10s倒计时操作,10s内没有输入判输
for (int i = 0; i < 100; 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) {
e.printStackTrace();
}
}
return false;
}
private void sendAllMessage(String message) { // 向两个client传递信息
WebSocketServer.users.get(playerA.getId()).sendMessage(message);
WebSocketServer.users.get(playerB.getId()).sendMessage(message);
}
private String getMapString() {
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 saveRecords() {
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() { // 向两个client发送结果
JSONObject resp = new JSONObject();
resp.put("event", "result");
resp.put("loser", loser);
saveRecords();
sendAllMessage(resp.toJSONString());
}
private boolean check_valid(List<Cell> cellsA,List<Cell> cellsB) {
int n = cellsA.size();
Cell c = cellsA.get(n-1);
if (g[c.getX()][c.getY()] == 1) {return false;}
for (int i = 0; i < n-1; i++) {
if (cellsA.get(i).getX() == c.getX() && cellsA.get(i).getY() == c.getY()) {
return false;
}
}
for (int i = 0; i < n-1; i++) {
if (cellsB.get(i).getX() == c.getX() && cellsB.get(i).getY() == c.getY()) {
return false;
}
}
return true;
}
private void judge() { // 判断两名玩家的下一步是否合法
List<Cell> cellsA = playerA.getCells();
List<Cell> cellsB = playerB.getCells();
boolean valid_A = check_valid(cellsA,cellsB);
boolean valid_B = check_valid(cellsB,cellsA);
if (!valid_A || !valid_B) {
status = "finished";
if (!valid_A && !valid_B) {
loser = "all";
}else if (!valid_A) {
loser = "A";
}else {
loser = "B";
}
}
}
private void sendMove() { // 向两名玩家传递蛇的移动信息
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();
}
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
if (nextStep()) {
judge();
if ("playing".equals(status)) {
sendMove();
} 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();
break;
}
}
}
}

@ -0,0 +1,63 @@
package com.kob.backend.consumer.utils;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
/**
* @author zfp
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Player{
// 玩家id
private Integer id;
// 玩家的起始坐标(x,y)
private Integer sx;
private Integer sy;
// 所有历史指令
private List<Integer> steps;
private boolean check_tail_increasing(int steps) {
if (steps <= 10) {
return true;
}
if (steps % 3 == 1) {
return true;
}
return false;
}
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;
int step = 0;
res.add(new Cell(x,y));
for (int d : this.steps) {
x += dx[d];
y += dy[d];
res.add(new Cell(x,y));
step++;
if (!check_tail_increasing(step)) {
res.remove(0);
}
}
return res;
}
public String getStepsString() {
StringBuilder res = new StringBuilder();
for (int i : this.steps) {
res.append(i);
}
return res.toString();
}
}

@ -0,0 +1,33 @@
package com.kob.backend.controller.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;
/**
* @author zfp
*/
@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;
}

@ -0,0 +1,13 @@
package com.kob.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.kob.backend.controller.pojo.Record;
import org.apache.ibatis.annotations.Mapper;
/**
* @author zfp
*/
@Mapper
public interface RecordMapper extends BaseMapper<Record> {
}

@ -3,7 +3,7 @@ import { Snake } from "./Snake";
import { Wall } from "./Wall";
export class GameMap extends AcGameObject {
constructor(ctx, parent,store) {
constructor(ctx, parent, store) {
super(); // 继承类一直要先调用父类的构造函数
this.ctx = ctx;
@ -28,7 +28,7 @@ export class GameMap extends AcGameObject {
create_walls() { // 判断是否生成有效的地图
const g = this.store.state.pk.gamemap;
for (let r = 0; r < this.rows; r++) {
for (let c = 0; c < this.cols; c++) {
if (g[r][c]) {
@ -41,18 +41,18 @@ export class GameMap extends AcGameObject {
add_listening_events() { // 绑定监听事件
this.ctx.canvas.focus(); // 聚焦
const [snake0, snake1] = this.snakes;
// 蓝方用wsad控制,红方用上下左控制
// 蓝方用wsad控制,红方用上下左控制
this.ctx.canvas.addEventListener("keydown", e => {
if (e.key === 'w') snake0.set_direction(0);
else if (e.key === 'd') snake0.set_direction(1);
else if (e.key === 's') snake0.set_direction(2);
else if (e.key === 'a') snake0.set_direction(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);
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;
this.store.state.pk.socket.send(JSON.stringify({
event: "move",
direction: d,
}));
});
}

@ -75,11 +75,6 @@ export class Snake extends AcGameObject {
// 1,2,3 => 1,1,2,3
this.cells[i] = JSON.parse(JSON.stringify(this.cells[i-1]));
}
if (!this.gamemap.check_vaild(this.next_cell)) { // 下一步不合法, 蛇直接去世
this.status = "die";
}
}
set_direction(d) { // 通过读取用户输入来设置方向

@ -16,7 +16,7 @@ export default {
let canvas = ref(null);
const store = useStore();
onMounted(() => {
new GameMap(canvas.value.getContext('2d'), parent.value,store)
store.commit("updateGameObject", new GameMap(canvas.value.getContext('2d'), parent.value,store));
});
return {

@ -45,8 +45,6 @@ export default {
event: "start-matching",
}));
}else {
match_btn_info.value = "开始匹配";

@ -0,0 +1,78 @@
<template>
<div class="result-board">
<div class="result-text" v-if="$store.state.pk.loser === 'all'">
Draw
</div>
<div class="result-text" v-else-if="$store.state.pk.loser === 'A' && $store.state.pk.a_id === parseInt($store.state.user.id)">
You Lose(A)
</div>
<div class="result-text" v-else-if="$store.state.pk.loser === 'B' && $store.state.pk.b_id == parseInt($store.state.user.id)">
You Lose(B)
</div>
<div class="result-text" v-else>
You Win
</div>
<div class="reset-btn">
<button @click="reset" class="btn btn-warning lg">再来一次!</button>
</div>
</div>
</template>
<script>
import { useStore } from 'vuex';
export default {
setup() {
const store = useStore();
const reset = () => {
store.commit("updateStatus","matching");
store.commit("updateLoser","none");
store.commit("updateOpponent", {
username: "my opponent",
photo:
"https://cdn.acwing.com/media/article/image/2022/08/09/1_1db2488f17-anonymous.png",
});
}
return {
reset,
}
}
}
</script>
<style scoped>
.result-board {
width: 30vw;
height: 30vh;
background-color: rgba(34, 20, 77, 0.5);
position: absolute;
top: 30vh;
left: 35vw;
}
.result-text {
font-size: 50px;
color:yellowgreen;
font-weight: bold;
text-align: center;
font-style: italic;
padding-top: 4vh;
padding-bottom: 4vh;
}
.reset-btn {
width: 100%;
display: flex;
justify-content: center;
}
</style>

@ -6,23 +6,44 @@ export default {
opponent_username: "", //对手的用户名
opponent_photo: "", // 对手的头像
gamemap: null, // 对战的地图
a_id: 0,
a_sx: 0,
a_sy: 0,
b_id: 0,
b_sx: 0,
b_sy: 0,
gameObject: null,
loser: "none" //"none","all","A","B" none表示还没有结果
},
getters: {
},
mutations: { // 用于修改全局数据
updateSocket(state,socket) {
state.socket = socket;
},
updateOpponent(state,opponent) {
state.opponent_username = opponent.username;
state.opponent_photo = opponent.photo;
},
updateStatus(state,status) {
state.status = status;
},
updateGameMap(state,gamemap) {
state.gamemap = gamemap;
},
updateSocket(state, socket) {
state.socket = socket;
},
updateOpponent(state, opponent) {
state.opponent_username = opponent.username;
state.opponent_photo = opponent.photo;
},
updateStatus(state, status) {
state.status = status;
},
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;
},
updateGameObject(state, gameObject) {
state.gameObject = gameObject;
},
updateLoser(state, loser) {
state.loser = loser;
}
},
actions: { // 在actions中调用修改全局变量的函数

@ -2,66 +2,83 @@
<!-- status等于playing才进入对战界面 -->
<PlayGround v-if="$store.state.pk.status === 'playing'" />
<MatchGround v-if="$store.state.pk.status === 'matching'" />
<ResultBoard v-if="$store.state.pk.loser !== 'none'"/>
</template>
<script>
import PlayGround from "../../components/PlayGround.vue";
import MatchGround from "../../components/MatchGround.vue";
import { onMounted, onUnmounted } from "vue"; //
import ResultBoard from '@/components/ResultBoard.vue';
import { onMounted, onUnmounted } from "vue"; //
import { useStore } from "vuex";
export default {
components: {
PlayGround,
MatchGround,
},
setup() {
const store = useStore();
const socket_url = `ws://localhost:3000/websocket/${store.state.user.token}/`;
let socket = null;
// (pk)
// onMountedsetup
components: {
PlayGround,
MatchGround,
ResultBoard,
},
setup() {
const store = useStore();
const socket_url = `ws://localhost:3000/websocket/${store.state.user.token}/`;
let socket = null;
// (pk)
// onMountedsetup
onMounted(() => {
store.commit("updateOpponent", {
username: "my opponent",
photo:
"https://cdn.acwing.com/media/article/image/2022/08/09/1_1db2488f17-anonymous.png",
});
store.commit("updateOpponent", {
username: "my opponent",
photo:
"https://cdn.acwing.com/media/article/image/2022/08/09/1_1db2488f17-anonymous.png",
});
// socket
socket = new WebSocket(socket_url);
socket = new WebSocket(socket_url);
socket.onopen = () => {
socket.onopen = () => {
store.commit("updateSocket", socket);
console.log("connnected!");
};
//
socket.onmessage = (msg) => {
};
//
socket.onmessage = (msg) => {
const data = JSON.parse(msg.data);
//
if (data.event === "start-matching") {
console.log(data)
store.commit("updateOpponent", {
username: data.opponent_username,
photo: data.opponent_photo,
});
setTimeout(() => {
store.commit("updateOpponent", {
username: data.opponent_username,
photo: data.opponent_photo,
});
setTimeout(() => {
store.commit("updateStatus", "playing");
}, 2000);
//
store.commit("updateGameMap", data.gamemap);
}, 500); // 0.5
// (id)
store.commit("updateGame", data.game);
} else if (data.event === "move") {
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") {
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";
}
store.commit("updateLoser",data.loser);
}
};
socket.onclose = () => {
socket.onclose = () => {
console.log("disconnected!");
store.commit("updateStatus", "matching");
};
};
});
// pk
onUnmounted(() => {
socket.close();
socket.close();
});
},
};

Loading…
Cancel
Save