From 108ae844f0900180f177921c6412216f02a985b0 Mon Sep 17 00:00:00 2001 From: barney <15270405776@163.com> Date: Sun, 4 Sep 2022 18:33:58 +0800 Subject: [PATCH] =?UTF-8?q?6.2=20=E5=AE=9E=E7=8E=B0=E5=BE=AE=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=EF=BC=9A=E5=8C=B9=E9=85=8D=E7=B3=BB=E7=BB=9F=EF=BC=88?= =?UTF-8?q?=E4=B8=AD=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kob/backend/consumer/WebSocketServer.java | 52 +++- .../com/kob/backend/consumer/utils/Cell.java | 15 ++ .../com/kob/backend/consumer/utils/Game.java | 234 ++++++++++++++++-- .../kob/backend/consumer/utils/Player.java | 63 +++++ .../kob/backend/controller/pojo/Record.java | 33 +++ .../com/kob/backend/mapper/RecordMapper.java | 13 + web/src/assets/scripts/GameMap.js | 24 +- web/src/assets/scripts/Snake.js | 5 - web/src/components/GameMap.vue | 2 +- web/src/components/MatchGround.vue | 2 - web/src/components/ResultBoard.vue | 78 ++++++ web/src/store/pk.js | 47 +++- web/src/views/pk/PkIndexView.vue | 87 ++++--- 13 files changed, 565 insertions(+), 90 deletions(-) create mode 100644 backend/src/main/java/com/kob/backend/consumer/utils/Cell.java create mode 100644 backend/src/main/java/com/kob/backend/consumer/utils/Player.java create mode 100644 backend/src/main/java/com/kob/backend/controller/pojo/Record.java create mode 100644 backend/src/main/java/com/kob/backend/mapper/RecordMapper.java create mode 100644 web/src/components/ResultBoard.vue diff --git a/backend/src/main/java/com/kob/backend/consumer/WebSocketServer.java b/backend/src/main/java/com/kob/backend/consumer/WebSocketServer.java index b599c17..18ecdc3 100644 --- a/backend/src/main/java/com/kob/backend/consumer/WebSocketServer.java +++ b/backend/src/main/java/com/kob/backend/consumer/WebSocketServer.java @@ -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 users = new ConcurrentHashMap<>(); + public final static ConcurrentHashMap users = new ConcurrentHashMap<>(); // 保存当前的匹配池 - final private static CopyOnWriteArraySet matchpool = new CopyOnWriteArraySet<>(); + private final static CopyOnWriteArraySet 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 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")); } } diff --git a/backend/src/main/java/com/kob/backend/consumer/utils/Cell.java b/backend/src/main/java/com/kob/backend/consumer/utils/Cell.java new file mode 100644 index 0000000..5489b69 --- /dev/null +++ b/backend/src/main/java/com/kob/backend/consumer/utils/Cell.java @@ -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; +} diff --git a/backend/src/main/java/com/kob/backend/consumer/utils/Game.java b/backend/src/main/java/com/kob/backend/consumer/utils/Game.java index 67a9ff4..3728f9b 100644 --- a/backend/src/main/java/com/kob/backend/consumer/utils/Game.java +++ b/backend/src/main/java/com/kob/backend/consumer/utils/Game.java @@ -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 cellsA,List 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 cellsA = playerA.getCells(); + List 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; + } + } + } + + } diff --git a/backend/src/main/java/com/kob/backend/consumer/utils/Player.java b/backend/src/main/java/com/kob/backend/consumer/utils/Player.java new file mode 100644 index 0000000..5186c33 --- /dev/null +++ b/backend/src/main/java/com/kob/backend/consumer/utils/Player.java @@ -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 steps; + + private boolean check_tail_increasing(int steps) { + if (steps <= 10) { + return true; + } + if (steps % 3 == 1) { + return true; + } + return false; + } + + public List getCells() { + List 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(); + } + + +} diff --git a/backend/src/main/java/com/kob/backend/controller/pojo/Record.java b/backend/src/main/java/com/kob/backend/controller/pojo/Record.java new file mode 100644 index 0000000..75d2b71 --- /dev/null +++ b/backend/src/main/java/com/kob/backend/controller/pojo/Record.java @@ -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; +} diff --git a/backend/src/main/java/com/kob/backend/mapper/RecordMapper.java b/backend/src/main/java/com/kob/backend/mapper/RecordMapper.java new file mode 100644 index 0000000..60c69c6 --- /dev/null +++ b/backend/src/main/java/com/kob/backend/mapper/RecordMapper.java @@ -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 { + +} diff --git a/web/src/assets/scripts/GameMap.js b/web/src/assets/scripts/GameMap.js index d7d1f5c..97f3c18 100644 --- a/web/src/assets/scripts/GameMap.js +++ b/web/src/assets/scripts/GameMap.js @@ -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, + })); }); } diff --git a/web/src/assets/scripts/Snake.js b/web/src/assets/scripts/Snake.js index 1fe2e13..fe3bb11 100644 --- a/web/src/assets/scripts/Snake.js +++ b/web/src/assets/scripts/Snake.js @@ -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) { // 通过读取用户输入来设置方向 diff --git a/web/src/components/GameMap.vue b/web/src/components/GameMap.vue index fdc808b..30a72d6 100644 --- a/web/src/components/GameMap.vue +++ b/web/src/components/GameMap.vue @@ -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 { diff --git a/web/src/components/MatchGround.vue b/web/src/components/MatchGround.vue index cebe655..34c66a4 100644 --- a/web/src/components/MatchGround.vue +++ b/web/src/components/MatchGround.vue @@ -45,8 +45,6 @@ export default { event: "start-matching", })); - - }else { match_btn_info.value = "开始匹配"; diff --git a/web/src/components/ResultBoard.vue b/web/src/components/ResultBoard.vue new file mode 100644 index 0000000..ecd0093 --- /dev/null +++ b/web/src/components/ResultBoard.vue @@ -0,0 +1,78 @@ + + + + + + \ No newline at end of file diff --git a/web/src/store/pk.js b/web/src/store/pk.js index bc166a5..02b72da 100644 --- a/web/src/store/pk.js +++ b/web/src/store/pk.js @@ -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中调用修改全局变量的函数 diff --git a/web/src/views/pk/PkIndexView.vue b/web/src/views/pk/PkIndexView.vue index ecd2462..112094e 100644 --- a/web/src/views/pk/PkIndexView.vue +++ b/web/src/views/pk/PkIndexView.vue @@ -2,66 +2,83 @@ + +