实现bot代码执行的微服务 botrunningsystem, 主服务前后端做出对应修改

This commit is contained in:
2023-03-09 10:46:31 +08:00
parent fffb1d5a60
commit ebd3f3c610
31 changed files with 627 additions and 45 deletions
@@ -49,7 +49,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
.and()
.authorizeRequests()
.antMatchers("/user/account/token/", "/user/account/register/").permitAll()
.antMatchers("/pk/start/").hasIpAddress("127.0.0.1")
.antMatchers("/pk/start/","/pk/receive/bot/move/").hasIpAddress("127.0.0.1")
.antMatchers(HttpMethod.OPTIONS).permitAll()
.anyRequest().authenticated();
@@ -3,14 +3,15 @@ package com.kob.backend.consumer;
// WebSocket用于前后端通信
import com.alibaba.fastjson2.JSONObject;
import com.kob.backend.config.RestTemplateConfig;
import com.kob.backend.consumer.utils.Game;
import com.kob.backend.consumer.utils.JwtAuthenticationUtil;
import com.kob.backend.mapper.BotMapper;
import com.kob.backend.mapper.RecordMapper;
import com.kob.backend.mapper.UserMapper;
import com.kob.backend.pojo.Bot;
import com.kob.backend.pojo.Record;
import com.kob.backend.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.WebProperties;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@@ -19,11 +20,8 @@ import org.springframework.web.client.RestTemplate;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import javax.xml.stream.events.StartDocument;
import java.io.IOException;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
@Component
@ServerEndpoint("/websocket/{token}") // 注意不要以'/'结尾
@@ -36,16 +34,17 @@ public class WebSocketServer {
*/
public static final ConcurrentHashMap<Integer, WebSocketServer> users = new ConcurrentHashMap<>();
// 用于和 MatchingSystem 进行通信
private static RestTemplate restTemplate;
public static RestTemplate restTemplate;
// 在 WebSocketServer 中注入数据库的方法演示-> 使用 static 定义为独一份的变量
private static UserMapper userMapper;
private static BotMapper botMapper;
// 注入 RecordMapper 用于调用实现游戏数据到数据库的存储
public static RecordMapper recordMapper;
// 后端向前端发送信息,首先需要创建一个 session
private Session session = null;
// 用户信息:定义一个成员变量
private User user;
private Game game = null;
public Game game = null;
// addPlayer 添加用户到匹配池的 URL; removePlayer 从匹配池移除用户的 URL
private final static String addPlayerUrl = "http://127.0.0.1:3001/player/add/";
private final static String removePlayerUrl = "http://127.0.0.1:3001/player/remove/";
@@ -63,12 +62,16 @@ public class WebSocketServer {
WebSocketServer.userMapper = userMapper;
}
@Autowired
public void setBotMapper(BotMapper botMapper) {
WebSocketServer.botMapper = botMapper;
}
@Autowired
public void setRecordMapper(RecordMapper recordMapper) {
WebSocketServer.recordMapper = recordMapper;
}
// @OnOpen 创建链接时自动触发这个函数
@OnOpen
public void onOpen(Session session, @PathParam("token") String token) throws IOException {
@@ -108,12 +111,22 @@ public class WebSocketServer {
}
// 匹配逻辑函数
public static void startGame(Integer aId, Integer bId) {
public static void startGame(Integer aId, Integer aBotId, Integer bId, Integer bBotId) {
User a = userMapper.selectById(aId);
User b = userMapper.selectById(bId);
Bot botA = botMapper.selectById(aBotId);
Bot botB = botMapper.selectById(bBotId);
// 匹配成功时,创建联机地图
Game game = new Game(13, 14, 20, a.getId(), b.getId());
Game game = new Game(
13,
14,
20,
a.getId(),
botA,
b.getId(),
botB
);
game.createMap(); // 初始化地图
// 当用户不为空时,才能执行下面的操作(防止有人退出游戏后用户已经成为空指针)
if (users.get(a.getId()) != null)
@@ -124,6 +137,7 @@ public class WebSocketServer {
game.start(); // 开启新线程,执行函数
// 打包地图数据信息
JSONObject respGameData = new JSONObject();
respGameData.put("game_map", game.getG());
respGameData.put("rows", game.getRows());
@@ -157,15 +171,15 @@ public class WebSocketServer {
}
// 开始匹配的逻辑部分:开始匹配时向 MatchingSystem 服务端发一个请求
private void startMatching() {
private void startMatching(Integer botId) {
System.out.println("start matching");
// 这里的参数列表需要与 MatchingSystem(微服务)->MatchingController->addPlayer 的参数对应
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
data.add("user_id", this.user.getId().toString());
data.add("rating", this.user.getRating().toString());
data.add("bot_id", botId.toString()); // 将选中的 botId 传给匹配系统
// postForObject(请求的URL,传出的数据,返回值的类型.class) -> Java 反射机制
String res = restTemplate.postForObject(addPlayerUrl, data, String.class);
System.out.println(this.user.getId().toString() + " " + this.user.getUsername() + " " + res);
restTemplate.postForObject(addPlayerUrl, data, String.class);
}
// 取消匹配的逻辑部分:向 MatchingSystem 发送请求:从匹配池移除一个用户
@@ -173,18 +187,20 @@ public class WebSocketServer {
System.out.println("stop matching");
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
data.add("user_id", this.user.getId().toString());
String res = restTemplate.postForObject(removePlayerUrl, data, String.class);
System.out.println(this.user.getId().toString() + " " + this.user.getUsername() + " " + res);
restTemplate.postForObject(removePlayerUrl, data, String.class);
}
// direction 传入 move(移动) 方向参数
// direction 传入 move(移动) 方向参数(将人的操作传给Gama)
private void move(int direction) {
// 判断角色:如果是 A 角色,则将获取到的方向设置为 A 的 nextStep 方向
// user.getId() 是获取当前链接的用户 id
if (game.getPlayerA().getId().equals(user.getId())) {
game.setNextStepA(direction);
// 判断当前是否是人工操作(botId == -1),是人工操作时,返回人工操作结果
if (game.getPlayerA().getBotId() == -1)
game.setNextStepA(direction); // 亲自出马时,才接受这个用户键盘输入
} else if (game.getPlayerB().getId().equals(user.getId())) {
game.setNextStepB(direction);
if (game.getPlayerB().getBotId() == -1)
game.setNextStepB(direction);
}
}
@@ -200,7 +216,8 @@ public class WebSocketServer {
// 匹配状态函数调用
if ("start-matching".equals(event)) {
startMatching();
// 传入前端 MatchGround.vue->click_match_btn->JSON.stringify() 封装传过来的 bot_id
startMatching(data.getInteger("bot_id"));
} else if ("stop-matching".equals(event)) {
stopMatching();
} else if ("move".equals(event)) {
@@ -2,10 +2,11 @@ package com.kob.backend.consumer.utils;
import com.alibaba.fastjson2.JSONObject;
import com.kob.backend.consumer.WebSocketServer;
import com.kob.backend.pojo.Bot;
import com.kob.backend.pojo.Record;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import javax.swing.event.InternalFrameEvent;
import java.sql.Time;
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
@@ -24,21 +25,40 @@ public class Game extends Thread {
private Integer nextStepA = null;
private Integer nextStepB = null;
// 加锁:解决读写冲突用
private ReentrantLock lock = new ReentrantLock();
private final ReentrantLock lock = new ReentrantLock();
// 整个游戏的当前状态: playing(正在进行) --> finished(游戏结束)
private String status = "playing";
// 定义失败者: all(平局), A(A输), B(B输)
private String loser = "";
// 传送消息给 BotRunningSystem 的链接
private static final String addBotUrl = "http://127.0.0.1:3002/bot/add/";
// 初始化(有参)构造函数
public Game(Integer rows, Integer cols, Integer inner_walls_count, Integer idA, Integer idB) {
public Game(Integer rows, Integer cols, Integer inner_walls_count, Integer idA, Bot botA, Integer idB, Bot botB) {
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<>());
// 定义默认 botId : botId 等于 -1 时表示玩家"亲自出战",非负则表示由对应 id bot 操作(Ai)
Integer botIdA = -1, botIdB = -1;
// 定义默认 bot 代码值
String botCodeA = "", botCodeB = "";
// 判断 botA 不等于 null , botA id code 重新赋值;否则取默认值
if (botA != null) {
botIdA = botA.getId();
// botA.getContent() bot 的代码区域
botCodeA = botA.getContent();
}
// 判断 botB 不等于 null , botB id code 重新赋值
if (botB != null) {
botIdB = botB.getId();
botCodeB = botB.getContent();
}
playerA = new Player(idA, botIdA, botCodeA, rows - 2, 1, new ArrayList<>());
playerB = new Player(idB, botIdB, botCodeB, 1, cols - 2, new ArrayList<>());
}
public Player getPlayerA() {
@@ -61,14 +81,17 @@ public class Game extends Thread {
return inner_walls_count;
}
// 将地图信息转换成字符串
private String getGameMapString() {
StringBuilder res = new StringBuilder();
// 将地图数据展开成一维
/* for (int i = 0; i < rows; i++) {
/*
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
res.append(g[i][j]);
}
}*/
}
*/
for (int[] row : g) {
for (int col : row) {
res.append(col);
@@ -185,6 +208,44 @@ public class Game extends Thread {
}
}
// 将当前的局面信息编码成字符串
private String getInput(Player player) {
Player me, you;
if (playerA.getId().equals(player.getId())) {
me = playerA;
you = playerB;
} else {
me = playerB;
you = playerA;
}
// 地图#me.sx#me.sy#我的操作#you.sx#you.sy#对手操作
// getStepsString() 操作序列可能是空的,为了防止分割出错,在这里加上()
return getGameMapString() + "#"
+ me.getSx() + "#"
+ me.getSy() + "#("
+ me.getStepsString() + ")#"
+ you.getSx() + "#"
+ you.getSy() + "#("
+ you.getStepsString() + ")";
}
// 发送 bot 代码让服务器自动执行
private void sendBotCode(Player player) {
//// botId 等于 -1 时表示玩家"亲自出战",非负则表示由对应 id bot 操作(Ai)
if (player.getBotId() == -1) return; // 当人来亲自操作时,直接 return, 不需要执行代码
// 不是人来控制时(改为 Bot 按照代码逻辑自动执行)
// 封装数据包
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
data.add("user_id", player.getId().toString());
data.add("bot_code", player.getBotCode());
data.add("input", getInput(player));
// BotRunningSystem 发送消息
WebSocketServer.restTemplate.postForObject(addBotUrl, data, String.class);
}
// 获取两名玩家的下一步操作
private boolean nextStep() {
try {
@@ -195,6 +256,10 @@ public class Game extends Thread {
throw new RuntimeException(e);
}
sendBotCode(playerA);
sendBotCode(playerB);
// 循环 50 秒钟,每次 100 毫秒,判断用户输入(50 * 100 = 5000 ms后如果没有接收到输入,则判断输赢)
for (int i = 0; i < 50; i++) {
try {
@@ -258,9 +323,9 @@ public class Game extends Thread {
// 游戏结束时,判断输赢
if (!validA && !validB) {
loser = "all";
} else if (!validA && validB) {
} else if (!validA) {
loser = "A";
} else if (validA && !validB) {
} else {
loser = "B";
}
}
@@ -13,6 +13,8 @@ import java.util.List;
@NoArgsConstructor
public class Player {
private Integer id;
private Integer botId; // botId 等于 -1 时表示玩家"亲自出战",非负则表示由对应 id bot 操作(Ai)
private String botCode; // bot 的代码
private Integer sx;
private Integer sy;
// 玩家走过的路径中每一步的方向
@@ -0,0 +1,24 @@
package com.kob.backend.controller.pk;
import com.kob.backend.service.pk.ReceiveBotMoveService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Objects;
@RestController
public class ReceiveBotMoveController {
@Autowired
private ReceiveBotMoveService receiveBotMoveService;
@PostMapping("/pk/receive/bot/move/")
public String receiveBotMove(@RequestParam MultiValueMap<String, String> data) {
Integer userId = Integer.parseInt(Objects.requireNonNull(data.getFirst("user_id")));
Integer direction = Integer.parseInt(Objects.requireNonNull(data.getFirst("direction")));
return receiveBotMoveService.receiveBotMove(userId, direction);
}
}
@@ -19,7 +19,9 @@ public class StartGameController {
@PostMapping("/pk/start/")
public String startGame(@RequestParam MultiValueMap<String, String> data) {
Integer aId = Integer.parseInt(Objects.requireNonNull(data.getFirst("a_id")));
Integer aBotId = Integer.parseInt(Objects.requireNonNull(data.getFirst("a_bot_id")));
Integer bId = Integer.parseInt(Objects.requireNonNull(data.getFirst("b_id")));
return startGameService.startGame(aId, bId);
Integer bBotId = Integer.parseInt(Objects.requireNonNull(data.getFirst("b_bot_id")));
return startGameService.startGame(aId, aBotId, bId, bBotId);
}
}
@@ -0,0 +1,34 @@
package com.kob.backend.service.impl.pk;
import com.kob.backend.consumer.WebSocketServer;
import com.kob.backend.consumer.utils.Game;
import com.kob.backend.service.pk.ReceiveBotMoveService;
import org.springframework.stereotype.Service;
@Service
public class ReceiveBotMoveServiceImpl implements ReceiveBotMoveService {
@Override
public String receiveBotMove(Integer userId, Integer direction) {
System.out.println("receive bot move: -> " + "userid: " + userId + "direction: " + direction);
// 看看操作的 user 是否还存在
if (WebSocketServer.users.get(userId) != null) {
// 取出 game
Game game = WebSocketServer.users.get(userId).game;
// 判断 game 是否存在
if (game != null) {
// Bot 的操作传给 game
// 判断角色:如果是 A 角色,则将获取到的方向设置为 botA nextStep 方向
if (game.getPlayerA().getId().equals(userId)) {
game.setNextStepA(direction);
} else if (game.getPlayerB().getId().equals(userId)) {
game.setNextStepB(direction);
}
}
}
return "receive bot move success";
}
}
@@ -7,9 +7,9 @@ import org.springframework.stereotype.Service;
@Service
public class StartGameServiceImpl implements StartGameService {
@Override
public String startGame(Integer aId, Integer bId) {
public String startGame(Integer aId, Integer aBotId, Integer bId, Integer bBotId) {
System.out.println("start game " + aId + " " + bId);
WebSocketServer.startGame(aId, bId); // 调用 WebSocketServer->startGame 函数
WebSocketServer.startGame(aId, aBotId, bId, bBotId); // 调用 WebSocketServer->startGame 函数
return "start game success";
}
}
@@ -0,0 +1,6 @@
// 接收 botrunningsystem 传回的 bot -> move 信息
package com.kob.backend.service.pk;
public interface ReceiveBotMoveService {
String receiveBotMove(Integer userId,Integer direction); // 接收传入的用户 id 和运动方向信息
}
@@ -3,5 +3,5 @@ package com.kob.backend.service.pk;
public interface StartGameService {
// 参数是匹配的两位玩家的 id
String startGame(Integer aId, Integer bId);
String startGame(Integer aId, Integer aBotId, Integer bId, Integer bBotId);
}