蛇的控制逻辑,增长逻辑,碰撞检测改为后端实现 + 结果计分板实现

This commit is contained in:
2023-03-03 16:51:58 +08:00
parent 4b3ebf9d77
commit ae4d8ef42d
10 changed files with 475 additions and 23 deletions
@@ -27,7 +27,7 @@ public class WebSocketServer {
使用线程安全的 Hash 表 ConcurrentHashMap<>(),
将 userId 映射到 webSocketServer
*/
private static final ConcurrentHashMap<Integer, WebSocketServer> users = new ConcurrentHashMap<>();
public static final ConcurrentHashMap<Integer, WebSocketServer> users = new ConcurrentHashMap<>();
// 使用 CopyOnWriteArraySet 创建一个线程安全的匹配池 matchPool
private static final CopyOnWriteArraySet<User> matchPool = new CopyOnWriteArraySet<>();
// 在 WebSocketServer 中注入数据库的方法演示-> 使用 static 定义为独一份的变量
@@ -36,6 +36,7 @@ public class WebSocketServer {
private Session session = null;
// 用户信息:定义一个成员变量
private User user;
private Game game = null;
// 注入方法
@Autowired
@@ -99,14 +100,24 @@ public class WebSocketServer {
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; // 将 game 赋给 a 玩家
users.get(b.getId()).game = game;
game.start(); // 开启新线程,执行函数
JSONObject respGameData = new JSONObject();
respGameData.put("game_map",game.getG());
respGameData.put("rows",game.getRows());
respGameData.put("cols",game.getCols());
respGameData.put("inner_walls_count",game.getInnerWallsCount());
respGameData.put("a_id",game.getPlayerA().getId());
respGameData.put("a_sx",game.getPlayerA().getSx());
respGameData.put("a_sy",game.getPlayerA().getSy());
respGameData.put("b_id",game.getPlayerB().getId());
respGameData.put("b_sx",game.getPlayerB().getSx());
respGameData.put("b_sy",game.getPlayerB().getSy());
// 将 a 配对成功的消息传回客户端
JSONObject respA = new JSONObject();
@@ -133,6 +144,18 @@ public class WebSocketServer {
matchPool.remove(this.user);
}
// direction 传入 move(移动) 方向参数
private void move(int direction){
// 判断角色:如果是 A 角色,则将获取到的方向设置为 A 的 nextStep 方向
// user.getId() 是获取当前链接的用户 id
if(game.getPlayerA().getId().equals(user.getId())){
game.setNextStepA(direction);
}
else if(game.getPlayerB().getId().equals(user.getId())){
game.setNextStepB(direction);
}
}
// @OnMessage 用于从前端接收请求: 一般 onMessage 当成路由使用,做为消息判断处理的中间部分
@OnMessage
public void onMessage(String message, Session session) {
@@ -148,6 +171,8 @@ public class WebSocketServer {
startMatching();
} else if ("stop-matching".equals(event)) {
stopMatching();
} else if ("move".equals(event)) {
move(data.getInteger("direction"));
}
}
@@ -0,0 +1,12 @@
package com.kob.backend.consumer.utils;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Cell {
int x, y;
}
@@ -1,9 +1,17 @@
package com.kob.backend.consumer.utils;
import java.util.Random;
import com.alibaba.fastjson2.JSONObject;
import com.kob.backend.consumer.WebSocketServer;
// 用来管理整个游戏流程
public class Game {
import javax.swing.event.InternalFrameEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Random;
import java.util.concurrent.locks.ReentrantLock;
// 用来管理整个游戏流程:这里需要多线程
public class Game extends Thread {
// 游戏地图:行数,列数,内部障碍物数量
private final Integer rows;
private final Integer cols;
@@ -12,25 +20,68 @@ public class Game {
private final int[][] g;
// 定义"上右下左"四个方向的 dx, dy偏移量
private final int[] dx = {-1, 0, 1, 0}, dy = {0, 1, 0, -1};
private final Player playerA, playerB;
// 玩家下一步操作状态定义
private Integer nextStepA = null;
private Integer nextStepB = null;
// 加锁:解决读写冲突用
private ReentrantLock lock = new ReentrantLock();
// 整个游戏的当前状态: playing(正在进行) --> finished(游戏结束)
private String status = "playing";
// 定义失败者: all(平局), A(A输), B(B输)
private String loser = "";
// 初始化构造函数
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];
playerA = new Player(idA, rows - 2, 1, new ArrayList<>());
playerB = new Player(idB, 1, cols - 2, new ArrayList<>());
}
public int getRows(){
public Player getPlayerA() {
return playerA;
}
public Player getPlayerB() {
return playerB;
}
public int getRows() {
return rows;
}
public int getCols(){
public int getCols() {
return cols;
}
public int getInnerWallsCount(){
public int getInnerWallsCount() {
return inner_walls_count;
}
public void setNextStepA(Integer nextStepA) {
// 为了防止两方读写冲突,需要加线程锁之后操作 nextStep
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() {
return g;
@@ -117,4 +168,162 @@ public class Game {
break;
}
}
// 获取两名玩家的下一步操作
private boolean nextStep() {
try {
// 返回下一步操作之前,先睡眠 200 毫秒,用于等待前端渲染完成,避免渲染速度跟不上后端运算处理速度
// 前端定义了每 1 秒钟 5 个格子,则每 200 毫秒走一格(每一步至少需要 200 毫秒的时间)
Thread.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 循环 50 秒钟,每次 100 毫秒,判断用户输入(50 * 100 = 5000 ms后如果没有接收到输入,则判断输赢)
for (int i = 0; i < 50; i++) {
try {
// 两次接收输入的等待间隔
Thread.sleep(100);
lock.lock();
try {
// 判断两名玩家是否都已经读取到输入:如果读入都非空,则表示读取结束,返回 true
if (nextStepA != null && nextStepB != null) {
// 将下一步操作存下来
playerA.getSteps().add(nextStepA);
playerB.getSteps().add(nextStepB);
// 成功获取到两名玩家的下一步操作,返回 true
return true;
}
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return false;
}
// 碰撞合法性检测辅助函数:需要判断 snakeA snakeB 是否重合
private boolean checkValid(List<Cell> cellsA, List<Cell> cellsB) {
int n = cellsA.size(); // cellsA 的当前长度
Cell cell = cellsA.get(n - 1); // 取出 cellsA 的蛇头位置(数组最后一位)(第0个元素是蛇尾巴)
if (g[cell.x][cell.y] == 1) return false; // 判断 A 的最后一位(蛇头)的坐标是否是障碍物(值为1),如果是障碍物则返回false(判断已碰撞)
// 判断 A蛇头位置是否和A除了蛇头以外的其他身体部分坐标有重合
for (int i = 0; i < n - 1; i++) {
if (cellsA.get(i).x == cell.x && cellsA.get(i).y == cell.y)
return false;
}
for (Cell cellB : cellsB) { // 判断 A蛇头位置是否和B蛇身体各部分坐标有重合:有重合返回不合法(表示已经碰撞)
if (cellB.x == cell.x && cellB.y == cell.y)
return false;
}
// 以上判断条件如果都合法,则返回 true合法(表示没有碰撞)
return true;
}
// 判断两名玩家下一步操作是否合法
private void judge() {
// 取出 A,B 两蛇
List<Cell> cellsA = playerA.getCells();
List<Cell> cellsB = playerB.getCells();
boolean validA = checkValid(cellsA, cellsB);
boolean validB = checkValid(cellsB, cellsA);
// 如果 A B 有人这一步不合法(已经产生碰撞),将游戏状态改为 "finished",表示游戏结束
if (!validA || !validB) {
status = "finished";
// 游戏结束时,判断输赢
if (!validA && !validB) {
loser = "all";
} else if (!validA && validB) {
loser = "A";
} else if (validA && !validB) {
loser = "B";
}
}
}
// 向两个 Client 广播信息
private void sendAllMessage(String message) {
WebSocketServer.users.get(playerA.getId()).sendMessage(message);
WebSocketServer.users.get(playerB.getId()).sendMessage(message);
}
// 向两个 Client 传递移动信息
private void sendMove() {
//由于这里需要读入 nextStepA, nextStepB, 这里需要加一下线程锁
lock.lock();
try {
JSONObject resp = new JSONObject();
resp.put("event", "move"); // event(事件类型): move(移动信息)
resp.put("a_direction", nextStepA); // a_direction( playerA 移动的方向)
resp.put("b_direction", nextStepB); // b_direction( playerB 移动的方向)
sendAllMessage(resp.toJSONString());
// 本次获取完下一步操作的同时需要进行再下一步操作输入,在这之前,需要将当前的 nextStep 清空
nextStepA = nextStepB = null;
} finally {
lock.unlock();
}
}
// 向两个 Client 公布结果
private void sendResult() {
lock.lock();
try {
JSONObject resp = new JSONObject();
resp.put("event", "result"); // event(事件类型): result(结果)
resp.put("loser", loser); // 失败者
// 将最后碰撞时的蛇的眼睛指向传给前端
resp.put("a_eyes_finally_direction", nextStepA);
resp.put("b_eyes_finally_direction", nextStepB);
sendAllMessage(resp.toJSONString());
} finally {
lock.unlock();
}
}
// 重写线程函数
@Override
public void run() {
// 循环 1000 : 13格x14格x(最大)3步/ < 1000保证 1000 次一定可以走完
for (int i = 0; i < 1000; i++) {
if (nextStep()) { // 是否获取到两条蛇的下一步操作
// 判断下一步操作是否合法
judge();
// 如果游戏状态还是 playing,则向两个玩家广播移动信息
if ("playing".equals(status)) {
sendMove();
} else if ("finished".equals(status)) {
// 如果游戏状态已经更改为了 finished,则将结果返回给 Client break 中断执行后续操作
sendResult();
break;
}
} else {
// 未获取到某蛇的下一步操作,则将游戏状态改为 finished
status = "finished";
// 判断失败者:这里需要加锁,因为涉及到对 nextStep 的读操作
lock.lock();
try {
if (nextStepA == null && nextStepB == null) {
loser = "all";
} else if (nextStepA == null) {
loser = "A";
} else {
loser = "B";
}
} finally {
lock.unlock();
}
}
}
}
}
@@ -0,0 +1,51 @@
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 Integer id;
private Integer sx;
private Integer sy;
// 玩家走过的路径中每一步的方向
private List<Integer> steps;
// 检验当前回合蛇的长度(蛇尾)是否需要增加
public boolean checkTailIncreasing(int step) {
// 10 回合每次都增加一节蛇尾,后面每 3 回合增加一节蛇尾
if (step <= 10) return true;
if (step % 3 == 1) return true;
return false;
}
// 蛇身 Cell 列表:记录蛇身各部分坐标
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; // 定义当前回合数: 开始时为 0 回合
res.add(new Cell(x, y)); // 现将蛇头添加进 List
for (int d : steps) { // 使用下一步方向求出下一个结点的坐标
x += dx[d];
y += dy[d];
res.add(new Cell(x, y)); // 添加新的身体结点
step++; // 回合数 +1
// 判断蛇尾要不要增加:如果本回合蛇尾不增加,则将本回合新生成的蛇尾(列表的第0个元素)删掉
if (!checkTailIncreasing(step)) {
res.remove(0);
}
}
return res;
}
}