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


    目录

    1、同步两个玩家的位置思路解析

    2、实现了三个棋盘的同步原理

    3、初始化一下我们的playerAplayerB

    4、什么是线程为什么要用多线程?

    5、如何去实现等待两名玩家输入

    6、前端向后端发消息

    7、在数据库中创建表record


    1、同步两个玩家的位置思路解析

     除了地图同步以外、我们还需要同步两个玩家的位置
    同步玩家的位置我们可以标记一下、至于谁在A谁在B我们需要在云端确定
    确定完之后我们会把每一个玩家的位置传给前端,我们可以傻瓜式的确定a在左下角b在
    右上角、我们在存地图的时候需要存一下玩家的id和位置
    在game这个类里我们需要加一个player类来维护玩家的位置信息
    一般开发思路需要用什么定义什么、先定义需要用到的各种函数
    有参构造函数无参构造函数、存一下每个玩家每一次的指令是什么


    2、实现了三个棋盘的同步原理

    现在有三个棋盘、还有一个在云端
    有两个浏览器就是有两个client、状态同步的机制
    client向云端发送消息表示这个蛇动了一下、当服务器接收到两个蛇的移动之后
    服务器就会把两个蛇移动的信息分别返回给Client1client2
    同步给两名玩家、这样我们就实现了三个棋盘的同步 


    3、初始化一下我们的playerAplayerB

    首先我们构造map的时候传入两名玩家的userid、初始化一下我们的playerAplayerB
    为了需要访问到我们的player、我们需要写两个函数
    后端就可以把两个玩家的信息传过去、前端做出相应修改

     


    4、什么是线程为什么要用多线程?

    Game不能作为单线程来处理、线程:一个人干就是单线程,两个人干就是多线程
    涉及到两个线程之间的通信以及加锁的问题
    我们需要先把game变成一个支持多线程的类
    就变成多线程了、我们需要实现thread类的一个入口函数
    alt+insert就可以实现、重载run函数
    start函数就是thread函数的一个api、可以另起一个线程来执行这个函数
    为了方便我们需要先把我们的game存放到这个类里面
    我们的线程就要一步一步等待下一步操作的操作
    这里设计到两个线程同时读写一个变量、这样就会有读写冲突、涉及到顺序问题 


    5、如何去实现等待两名玩家输入

    两名玩家都输入我们就进行下一步
    如果超过一定时间之后两名玩家还没有输入的话
    我们要结束这个操作、告诉我们哪个玩家没有输入
    就输了、可以用sleep函数、如果是正在进行中的话
    我们应该将这一步操作广播给两位玩家、需要同步一下
    我们从服务器分别接收到两名玩家的输入之后、需要将两名玩家的输入分别广
    播给两个人、比如说我们两个玩家,同时都向服务器发送了请求
    c1不知道c2的操作s向c1c2广播操作 


    6、前端向后端发消息

    当我们移动的时候、之前我们是在gamemap里面判断的
    两个线程同时操纵一个变量、至少有一个变量是写的话那就需要加锁子
    前端写完之后后端需要接收到这个请求
    gameobject需要存下来才能访问到蛇、每一个新的游戏都会new一个新的类
    都会开一个新的线程 


    7、在数据库中创建表record

    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

     

    联机对战:同步玩家的操作

    文件结构

    backend
        consumer
            utils
                Cell.java
                Player.java

    src
        components
            ResultBoard.vue

    1.后端:同步玩家位置信息

    玩家类:需要联机对战肯定就有玩家,因此建立玩家类 

    1. package com.kill9.backend.consumer.utils;
    2. import lombok.AllArgsConstructor;
    3. import lombok.Data;
    4. import lombok.NoArgsConstructor;
    5. import java.util.List;
    6. @NoArgsConstructor
    7. @AllArgsConstructor
    8. @Data
    9. public class Player {
    10. private Integer id;
    11. private Integer sx;
    12. private Integer sy;
    13. //每一次的指令
    14. private List steps;
    15. }

    游戏类

    一场游戏包含两名玩家

    1. private final Player playerA,playerB;
    2. final int[][] g;
    3. public Game(
    4. Integer rows,
    5. Integer cols,
    6. Integer innerWallsCount,
    7. Integer idA,
    8. Integer idB)
    9. {
    10. this.rows = rows;
    11. this.cols = cols;
    12. this.innerWallsCount = innerWallsCount;
    13. this.g = new int[rows][cols];
    14. playerA = new Player(idA,rows-2,1,new ArrayList<>());
    15. playerB = new Player(idB,1,cols-2,new ArrayList<>());
    16. }
    17. public Player getPlayerA() {
    18. return playerA;
    19. }
    20. public Player getPlayerB() {
    21. return playerB;
    22. }

    websocket操作类

    在匹配完之后,初始化地图时,将玩家传给Game构造函数

    1. Game game = new Game(13,14,20,a.getId(),b.getId());
    2. game.createMap();
    3. //封装游戏信息
    4. JSONObject respGame = new JSONObject();
    5. respGame.put("a_id",game.getPlayerA().getId());
    6. respGame.put("a_sx",game.getPlayerA().getSx());
    7. respGame.put("a_sy",game.getPlayerA().getSy());
    8. respGame.put("b_id",game.getPlayerB().getId());
    9. respGame.put("b_sx",game.getPlayerB().getSx());
    10. respGame.put("b_sy",game.getPlayerB().getSy());
    11. respGame.put("map",game.getG());
    12. respA.put("game",respGame);
    13. respB.put("game",respGame);

    2.前端:接收两名玩家位置以及地图信息

    pk类的存储

    1. import ModuleUser from './user'
    2. export default {
    3. state: {
    4. socket:null ,//ws连接
    5. opponent_username:"",
    6. opponent_photo:"",
    7. status:"matching",matching表示匹配界面,playing表示对战界面
    8. gamemap:null,
    9. a_id:0,
    10. a_sx:0,
    11. a_sy:0,
    12. b_id:0,
    13. b_sx:0,
    14. b_sy:0,
    15. },
    16. getters: {
    17. },
    18. mutations: {
    19. updateSocket(state,socket){
    20. state.socket = socket;
    21. },
    22. updateOpponent(state,opponent){
    23. state.opponent_username = opponent.username,
    24. state.opponent_photo = opponent.photo;
    25. },
    26. updateStatus(state,status){
    27. state.status = status;
    28. },
    29. updateGame(state,game){
    30. state.gamemap = game.map;
    31. state.a_id = game.a_id;
    32. state.a_sx = game.a_sx;
    33. state.a_sy = game.a_sy;
    34. state.b_id = game.b_id;
    35. state.b_sx = game.b_sx;
    36. state.b_sy = game.b_sy;
    37. }
    38. },
    39. actions: {
    40. },
    41. modules: {
    42. user: ModuleUser,
    43. }
    44. }

    pk界面

    修改pk界面的接收地图相关参数

    1. export default {
    2. ...
    3. setup() {
    4. ...
    5. onMounted(() => {
    6. ...
    7. // 回调函数:接收到后端信息调用
    8. socket.onmessage = msg => {
    9. // 返回的信息格式由后端框架定义,django与spring定义的不一样
    10. const data = JSON.parse(msg.data);
    11. if(data.event === "start-matching") {
    12. ...
    13. setTimeout(() => {
    14. store.commit("updateStatus", "playing");
    15. }, 200);
    16. store.commit("updateGame", data.game);
    17. }
    18. }
    19. ...
    20. });
    21. ...
    22. }
    23. }

     

     

    3.后端:一局游戏的逻辑

    Webscoket操作类

    将Game作为一局游戏的一个线程,使Game线程启动 

    1. private Game game = null;
    2. ...
    3. private void startMatching() {
    4. System.out.println("start matching!");
    5. matchPool.add(this.user);
    6. while(matchPool.size() >= 2) {
    7. ...
    8. game.createMap();
    9. // 一局游戏一个线程,会执行game类的run方法
    10. game.start();
    11. users.get(a.getId()).game = game;
    12. users.get(b .getId()).game = game;
    13. ...
    14. }
    15. }

    游戏类

    一局游戏的操作,首先执行run方法。只有读写、写写有冲突,此处关于nextStep,我们会接收前端的nextStep输入 或 bots代码的输入,而且会频繁的读,因此需要加锁。

    Game.java

    1. package com.kill9.backend.consumer.utils;
    2. import java.util.ArrayList;
    3. import java.util.Random;
    4. import java.util.concurrent.locks.ReentrantLock;
    5. //支持多线程
    6. public class Game extends Thread{
    7. private final Integer rows;
    8. private final Integer cols;
    9. private final Integer innerWallsCount;
    10. private final static int[] dx = {-1,0,1,0},dy={0,1,0,-1};
    11. private final Player playerA,playerB;
    12. private Integer nextStepA = null;
    13. private Integer nextStepB = null;
    14. //定义锁
    15. private ReentrantLock lock = new ReentrantLock();
    16. final int[][] g;
    17. public Game(
    18. Integer rows,
    19. Integer cols,
    20. Integer innerWallsCount,
    21. Integer idA,
    22. Integer idB)
    23. {
    24. this.rows = rows;
    25. this.cols = cols;
    26. this.innerWallsCount = innerWallsCount;
    27. this.g = new int[rows][cols];
    28. playerA = new Player(idA,rows-2,1,new ArrayList<>());
    29. playerB = new Player(idB,1,cols-2,new ArrayList<>());
    30. }
    31. public int[][] getG(){
    32. return g;
    33. }
    34. public Player getPlayerA(){
    35. return playerA;
    36. }
    37. public Player getPlayerB(){
    38. return playerB;
    39. }
    40. public void setNextStepA(Integer nextStepA){
    41. //上锁
    42. lock.lock();
    43. try{
    44. this.nextStepA = nextStepA;
    45. }finally {
    46. lock.unlock();
    47. }
    48. }
    49. public void setNextStepB(Integer nextStepB){
    50. //上锁
    51. lock.lock();
    52. try{
    53. this.nextStepB = nextStepB;
    54. }finally {
    55. lock.unlock();
    56. }
    57. }
    58. //判断连通性 flood fill
    59. private boolean check_connectivity(int sx,int sy,int tx,int ty){
    60. if(sx==tx&&sy==ty) return true;
    61. g[sx][sy] = 1;
    62. for(int i = 0;i<4;i++){
    63. int x = sx+dx[i],y = sy+dy[i];
    64. if(x<this.rows && x>=0 && y<this.cols && y>=0 && g[x][y]==0){
    65. if(check_connectivity(x,y,tx,ty)){
    66. //恢复原来的数组
    67. g[sx][sy] = 0;
    68. return true;
    69. }
    70. }
    71. }
    72. //恢复现场
    73. g[sx][sy] = 0;
    74. return false;
    75. }
    76. //画地图
    77. private boolean draw(){
    78. //初始化
    79. for(int i = 0;i<this.rows;i++){
    80. for(int j = 0;j<this.cols;j++){
    81. g[i][j] = 0;
    82. }
    83. }
    84. //给四周加墙
    85. for(int r = 0;r<this.rows;r++){
    86. g[r][0] = g[r][this.cols-1] = 1;
    87. }
    88. for(int c = 0;c<this.cols;c++){
    89. g[0][c] = g[this.rows-1][c] = 1;
    90. }
    91. Random random = new Random();
    92. for(int i = 0;i<this.innerWallsCount/2;i++){
    93. for(int j = 0;j<1000;j++){
    94. int r = random.nextInt(this.rows);
    95. int c = random.nextInt(this.cols);
    96. //画过的不画
    97. if(g[r][c]==1||g[this.rows-1-r][this.cols-1-c]==1) continue;
    98. g[r][c] = g[this.rows-1-r][this.cols-1-c] = 1;
    99. break;
    100. }
    101. }
    102. return check_connectivity(this.rows-2,1,1,this.cols-2);
    103. }
    104. public void createMap(){
    105. for(int i = 0;i<1000;i++){
    106. if(draw()) break;
    107. }
    108. }
    109. private boolean nextStep(){
    110. //每秒五步操作,因此第一步操作是在200ms后判断是否接收到输入。并给地图初始化时间
    111. //如果两名玩家操作非常快 比如1s操作了50次,但是返回前端,每动完一格之后才会去判断下一步,
    112. //如果我们在动一格的期间获取了很多下一步的操作的话,我们就会把中间结果都覆盖掉,在前端渲染的时候就会遗漏一些步数
    113. //所以在进行下一步操作时先睡200ms ,因为200ms才能走一格,如果200ms内多输入了很多,它之只会留最后一步,就会 覆盖掉
    114. try {
    115. Thread.sleep(200);
    116. } catch (InterruptedException e) {
    117. throw new RuntimeException(e);
    118. }
    119. //等待两名玩家的下一步操作,上锁
    120. for(int i = 0;i<5;i++){
    121. //此循环循环了5000ms,也就是5s,前端是一秒移动5步,
    122. //后端接收玩家键盘输入是5s内玩家的一个输入,若在一方先输入,
    123. //一方还未输入,输入的一方多此操作,以最后一次为准。
    124. try {
    125. Thread.sleep(1000);
    126. //等待玩家输入
    127. lock.lock();//读玩家输入要上锁
    128. try{
    129. if(nextStepA != null && nextStepB != null){
    130. playerA.getSteps().add(nextStepA);
    131. playerB.getSteps().add(nextStepB);
    132. return true;
    133. }
    134. }finally {
    135. //由于报异常的话 可能不会解锁,所以要在finally解锁
    136. lock.unlock();
    137. }
    138. } catch (InterruptedException e) {
    139. e.printStackTrace();
    140. }
    141. }
    142. return false;
    143. }
    144. //入口函数
    145. @Override
    146. public void run() {
    147. super.run();
    148. }
    149. }

    先搭好整体框架 

    1. private void sendResult(){ //向两名玩家返回结果
    2. }
    3. private void judge(){//判断两名玩家是否合法操作
    4. }
    5. private void sendMove(){ //向两个client公布信息
    6. }
    7. //入口函数
    8. @Override
    9. public void run() {
    10. for(int i = 0;i<1000;i++){
    11. //先判断是否获取两条蛇的下一步操作
    12. if(nextStep()){ // 是否获取了两条蛇的下一步的操作
    13. judge();
    14. if(status.equals("playing")){
    15. //将每名玩家的操作广播给两名玩家,实现同步
    16. sendMove();
    17. }else{
    18. //向前端返回结果
    19. sendResult();
    20. break;
    21. }
    22. }else{
    23. //这里也要加锁 涉及到nextStep的读 防止边界情况 ,在超时的边界输入
    24. lock.lock();
    25. status = "finished";
    26. try{
    27. if(nextStepA == null && nextStepB == null){
    28. loser = "all";
    29. }else if(nextStepA == null){
    30. loser = "A";
    31. }else{
    32. loser = "B";
    33. }
    34. }finally {
    35. lock.unlock();
    36. }
    37. sendResult();
    38. break;// 表示整个游戏结束
    39. }
    40. }
    41. }
    42. }
    1. private void sendAllMessage(String message){
    2. WebSocketServer.users.get(playerA.getId()).sendMessage(message);
    3. WebSocketServer.users.get(playerB.getId()).sendMessage(message);
    4. }
    5. private void sendResult(){ //向两名玩家返回结果
    6. //向本局的两名玩家分别广播
    7. JSONObject resp = new JSONObject();
    8. resp.put("event","result");
    9. resp.put("loser",loser);
    10. sendAllMessage(resp.toJSONString());
    11. }
    12. private void judge(){//判断两名玩家是否合法操作
    13. }
    14. private void sendMove(){ //向两个client公布信息
    15. lock.lock();
    16. try{
    17. JSONObject resp = new JSONObject();
    18. resp.put("event","move");
    19. resp.put("a_direction",nextStepA);
    20. resp.put("b_direction",nextStepB);
    21. //清空操作
    22. nextStepA = nextStepB = null;
    23. }finally {
    24. lock.unlock();
    25. }
    26. }

    4.前端: 发送移动指令给后端

    GameMap.js

    1. add_listening_events() {
    2. this.ctx.canvas.focus();
    3. this.ctx.canvas.addEventListener("keydown", e => {
    4. let d = -1;
    5. if(e.key === 'w') d = 0;
    6. else if(e.key === 'd') d = 1;
    7. else if(e.key === 's') d = 2;
    8. else if(e.key === 'a') d = 3;
    9. // else if(e.key === 'ArrowUp') snake1.set_direction(0);
    10. // else if(e.key === 'ArrowRight') snake1.set_direction(1);
    11. // else if(e.key === 'ArrowDown') snake1.set_direction(2);
    12. // else if(e.key === 'ArrowLeft') snake1.set_direction(3);
    13. // 若移动了,发送给后端
    14. if(d >= 0) {
    15. this.store.state.pk.socket.send(JSON.stringify({
    16. event: "move",
    17. direction: d,
    18. }));
    19. }
    20. });
    21. }

    5.后端处理接收移动的事件

    1. private void move(int d){
    2. if(game.getPlayerA().getId().equals(user.getId())) {
    3. game.setNextStepA(d);
    4. }else if(game.getPlayerB().getId().equals(user.getId())){
    5. game.setNextStepB(d);
    6. }
    7. }
    8. //路由
    9. @OnMessage
    10. public void onMessage(String message, Session session) {
    11. // 从Client接收消息
    12. System.out.println("receive message!");
    13. //将字符串转换成json对象
    14. JSONObject data = JSONObject.parseObject(message);
    15. String event = data.getString("event");
    16. //防止空指针
    17. if("start-matching".equals(event)){
    18. startMatching();
    19. }else if("stop-matching".equals(event)){
    20. stopMatching();
    21. }else if("move".equals(event)){
    22. move(data.getInteger("direction"));
    23. }
    24. }

    6前端接收后端发送过来的移动事件

    设立GameMap的存储

    1. src/store/pk.js
    2. export default {
    3. state: {
    4. ...
    5. gameObject: null,
    6. },
    7. getters: {
    8. },
    9. mutations: {
    10. ...
    11. updateGameObject(state, gameobject) {
    12. state.gameObject = gameobject;
    13. }
    14. },
    15. actions: {
    16. },
    17. modules: {
    18. }
    19. }

    GameMap存储

    1. src/components/GameMap.vue

    Pk页面:接收后端的移动事件

    1. src/views/pk/PkIndexView.vue

    7.前端:根据后端返回结果将死了的蛇变白

    Snake.js

    删除前端判断蛇的操作是否有效

    1. export class Snake extends AcGameObject {
    2. ...
    3. // 将蛇状态变为走下一步
    4. next_step() {
    5. ...
    6. // 让蛇在下一回合长一个格子
    7. const k = this.cells.length;
    8. for(let i = k; i > 0; i--) {
    9. this.cells[i] = JSON.parse(JSON.stringify(this.cells[i - 1]));
    10. }
    11. // 删除前端判断蛇的操作是否有效
    12. }
    13. ...
    14. }

    PkIndexView.vue

    根据后端结果将失败的一方颜色变白

    1. // 回调函数:接收到后端信息调用
    2. socket.onmessage = msg => {
    3. // 返回的信息格式由后端框架定义,django与spring定义的不一样
    4. const data = JSON.parse(msg.data);
    5. if(data.event === "start-matching") {
    6. ...
    7. } else if(data.event === "move") {
    8. ...
    9. } else if(data.event === "result") {
    10. console.log(data);
    11. const game = store.state.pk.gameObject;
    12. const [snake0, snake1] = game.snakes;
    13. if(data.loser === "all" || data.loser === "A") {
    14. snake0.status = "die";
    15. }
    16. if(data.loser === "all" || data.loser === "B") {
    17. snake1.status = "die";
    18. }
    19. }
    20. }

    8.后端:将前端的裁判程序移到后端

    注意,此地方有两处代码有迷惑性,分别是:
    1. 在Player.java的getCells函数里为什么删除的是第0个元素
    2. 在Game.java的check_valid函数里为什么不判断最后一个元素是否重合
    以上两个答案在下方代码的关键处有解释

    Cell.java

    1. package com.kill9.backend.consumer.utils;
    2. import lombok.AllArgsConstructor;
    3. import lombok.Data;
    4. import lombok.NoArgsConstructor;
    5. @Data
    6. @AllArgsConstructor
    7. @NoArgsConstructor
    8. public class Cell {
    9. private Integer x;
    10. private Integer y;
    11. }

    Player.java 

    1. package com.kill9.backend.consumer.utils;
    2. import lombok.AllArgsConstructor;
    3. import lombok.Data;
    4. import lombok.NoArgsConstructor;
    5. import java.util.ArrayList;
    6. import java.util.List;
    7. @NoArgsConstructor
    8. @AllArgsConstructor
    9. @Data
    10. public class Player {
    11. private Integer id;
    12. private Integer sx;
    13. private Integer sy;
    14. //每一次的指令
    15. private List steps;
    16. private boolean check_tail_increasing(int step){//检验当前回合蛇的长度是否增加
    17. if(step<=10) return true;
    18. return step % 3 == 1;
    19. }
    20. public List getCells(){
    21. List res = new ArrayList<>();
    22. int[] dx={-1,0,1,0},dy = {0,1,0,-1};
    23. int x = sx,y = sy;
    24. int step = 0;
    25. /**
    26. * 每一步移动都会把蛇头移动到下一个格子(注:蛇头有两个cell,详看前端Snake.js的next_step()与update_move()逻辑),
    27. * 若当前长度增加,蛇头正好移到新的一个格子,剩下的蛇身长度不变,因此长度 + 1;若长度不增加,则删除蛇尾
    28. */
    29. res.add(new Cell(x,y));
    30. for(int d: steps){
    31. x += dx[d];
    32. y += dy[d];
    33. res.add(new Cell(x,y));
    34. if(!check_tail_increasing(++step)){
    35. /**
    36. * 关键:
    37. * 为什么此处删除0呢,首先存储蛇身、且判定是否增加、且画蛇的逻辑此时还是在前端,我们只是将
    38. * 判断蛇是否撞到 墙和蛇身 移到后端。并且我们在后端保存的是是蛇头的x、y坐标和蛇身相对
    39. * 于上一步操作的方向,但是在我们做了第一个操作后蛇尾才是蛇头,意思就是res逆序才是蛇
    40. * 头到蛇尾的位置!
    41. */
    42. res.remove(0);//删掉蛇尾
    43. }
    44. return res;
    45. }
    46. }
    47. }

     Game.java

    1. public class Game extends Thread {
    2. ...
    3. private boolean check_valid(List cellsA, List cellsB) {
    4. int n = cellsA.size();
    5. Cell cell = cellsA.get(n - 1);
    6. // 如果是墙,则非法
    7. if(g[cell.x][cell.y] == 1) return false;
    8. // 遍历A除最后一个Cell
    9. /**
    10. * 关键:
    11. * 首先我在Player中已经解释getCells的函数返回的res是蛇尾到蛇头的位置。
    12. * 因此以下两个for循环分别判断的是蛇头是否和两条蛇的蛇身重合!
    13. * 那么为什么不用判断两个蛇头是否重合呢?可能是地图大小为13 * 14,
    14. * 两个蛇头的位置初始为(1, 1)和(11, 12),两个蛇头的位置横纵之和分别为偶数
    15. * 和奇数,因此两个蛇头永远不会走到同一个格子!
    16. */
    17. for(int i = 0; i < n - 1; i++) {
    18. // 和蛇身是否重合
    19. if(cellsA.get(i).x == cell.x && cellsA.get(i).y == cell.y) {
    20. return false;
    21. }
    22. }
    23. // 遍历B除最后一个Cell
    24. for(int i = 0; i < n - 1; i++) {
    25. // 和B蛇身是否重合
    26. if(cellsB.get(i).x == cell.x && cellsB.get(i).y == cell.y) {
    27. return false;
    28. }
    29. }
    30. return true;
    31. }
    32. private void judge() { // 判断两名玩家操作是否合法
    33. List cellsA = playerA.getCells();
    34. List cellsB = playerB.getCells();
    35. boolean validA = check_valid(cellsA, cellsB);
    36. boolean valibB = check_valid(cellsB, cellsA);
    37. if(!validA || !valibB) {
    38. status = "finished";
    39. if(!validA && !valibB) {
    40. loser = "all";
    41. } else if(!validA) {
    42. loser = "A";
    43. } else {
    44. loser = "B";
    45. }
    46. }
    47. }
    48. private void senAllMessage(String message) {
    49. ...
    50. }
    51. private void sendMove() { // 向两名玩家传递移动信息
    52. ...
    53. }
    54. private void sendResult() { // 向两名玩家发送游戏结果
    55. ...
    56. }
    57. @Override
    58. public void run() {
    59. ...
    60. }
    61. }

    9.结果板的实现

    pk信息存储类

    1. src/store/pk.js
    2. export default {
    3. state: {
    4. ...
    5. loser: "none", // all、A、B
    6. },
    7. getters: {
    8. },
    9. mutations: {
    10. ...
    11. updateLoser(state, loser) {
    12. state.loser = loser;
    13. }
    14. },
    15. actions: {
    16. },
    17. modules: {
    18. }
    19. }

     ResultBoard.vue

    1. src/components/ResultBoard.vue

    Pk页面

    1. src/views/pk/PkIndexView.vue

    完善pk页面 对局中添加玩家头像,确认自己是哪条蛇

    PlayGround.vue

    对局回放

    通过保存对局每个状态的信息实现对局的回放功能

    backend
        pojo
            Record.java
        mapper
            RecordMapper.java 

    创建数据库

     

    Record.java

    1. package com.kill9.backend.pojo;
    2. import com.baomidou.mybatisplus.annotation.IdType;
    3. import com.baomidou.mybatisplus.annotation.TableId;
    4. import com.fasterxml.jackson.annotation.JsonFormat;
    5. import lombok.AllArgsConstructor;
    6. import lombok.Data;
    7. import lombok.NoArgsConstructor;
    8. import java.util.Date;
    9. @Data
    10. @AllArgsConstructor
    11. @NoArgsConstructor
    12. public class Record {
    13. @TableId(type = IdType.AUTO)
    14. private Integer id;
    15. private Integer aId;
    16. private Integer aSx;
    17. private Integer aSy;
    18. private Integer bId;
    19. private Integer bSx;
    20. private Integer bSy;
    21. private String aSteps;
    22. private String bSteps;
    23. private String map;
    24. private String loser;
    25. @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
    26. private Date createtime;
    27. }

    RecordMapper.java

    1. package com.kill9.backend.mapper;
    2. import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    3. import com.kill9.backend.pojo.Record;
    4. import org.apache.ibatis.annotations.Mapper;
    5. @Mapper
    6. public interface RecordMapper extends BaseMapper {
    7. }

    WebSocketServer.java

    注入RecordMapper,为了保存对局信息

    1. @Component
    2. // url链接:ws://127.0.0.1:3000/websocket/**
    3. @ServerEndpoint("/websocket/{token}") // 注意不要以'/'结尾
    4. public class WebSocketServer {
    5. ...
    6. private static UserMapper userMapper;
    7. public static RecordMapper recordMapper;
    8. ...
    9. @Autowired
    10. public void setRecordMapper(RecordMapper recordMapper) {
    11. WebSocketServer.recordMapper = recordMapper;
    12. }
    13. ...
    14. }

     Player.java

    将玩家的蛇的方向偏移量转化成String

    1. @Data
    2. @AllArgsConstructor
    3. @NoArgsConstructor
    4. public class Player {
    5. ...
    6. public String getStepsString() {
    7. StringBuilder res = new StringBuilder();
    8. for(int d : steps) {
    9. res.append(d);
    10. }
    11. return res.toString();
    12. }
    13. }

    Game.java

    将对局信息保存至数据库

    1. public class Game extends Thread {
    2. ...
    3. private String getMapString() {
    4. StringBuilder res = new StringBuilder();
    5. for(int i = 0; i < rows; i++) {
    6. for(int j = 0; j < cols; j++) {
    7. res.append(g[i][j]);
    8. }
    9. }
    10. return res.toString();
    11. }
    12. private void saveToDataBase() {
    13. Record record = new Record(
    14. null,
    15. playerA.getId(),
    16. playerA.getSx(),
    17. playerA.getSy(),
    18. playerB.getId(),
    19. playerB.getSx(),
    20. playerB.getSy(),
    21. playerA.getStepsString(),
    22. playerB.getStepsString(),
    23. getMapString(),
    24. loser,
    25. new Date()
    26. );
    27. WebSocketServer.recordMapper.insert(record);
    28. }
    29. private void sendResult() { // 向两名玩家发送游戏结果
    30. ...
    31. saveToDataBase();
    32. senAllMessage(resp.toJSONString());
    33. }
    34. ...
    35. }

     完成~~

  • 相关阅读:
    CSS 选择器
    uni-app——网络请求、数据缓存
    华为云两台机器内网互联
    链表的基本操作
    小巧有劲的按摩好手,能装兜里的护理工具,小鸟斗士筋膜枪体验
    【美团3.18校招真题2】
    c++后端开发书籍推荐
    Qt扩展-KDDockWidgets 的使用
    刷题-最长回文子串-C++解法
    网站页头被挂马状态及新增了index.html文件解决思路
  • 原文地址:https://blog.csdn.net/weixin_49884716/article/details/127949262