Springboot实现类似OJ的代码执行功能
Bot Running System
由于我们这个项目是可以通过执行bot代码来实现AI的操作的,因此我们要设计一个新的微服务Bot Running System
专门去跑我们的代码,类似于OJ
,但又并不是传统意义上的OJ
评测,而是通过代码的运行来完成AI的操作。
同样的,Bot Running System
也会有一个独立的线程Bot Pool
去存放我们的每一个bot代码且将其执行,并将代码执行结果返回给步骤判断阶段,也就是ws端的Next Step
阶段。
以上就是我们代码执行微服务的主要逻辑思路。
创建后端BotRunningSystem
- 与前面类似,在根目录
backendcloud
下创建一个新模块BotRunningSystem
- 将模块
MatchingSystem
的依赖都复制到BotRunningSystem
里 - 添加新依赖
joor-java-8
:可以在Java
中动态编译Java
代码 - 未来如果想要执行其他语言的代码,可以在云端创建一个有内存上限的
docker
,在docker
里面执行代码 - 给该
springboot
项目创建端口3002(server.port=3002) \resources\application.properties
把Main
文件重命名为常见的BotRunningSystemApplication
作为该springboot
的入口
\botrunningsystem\BotRunningSystemApplication
@SpringBootApplication
public class BotRunningSystemApplication {
public static void main(String[] args) {
SpringApplication.run(BotRunningSystemApplication.class, args);
}
}
编写BotRunningSystem的api接口
与前面所有的springboot
项目类似,我们要实现接口就得先创建好controller
层和service
层,在service
层里新建接口,在impl
里实现相应的接口,最后controller
层定义相对应的url
调用对应的服务。
service
层
package com.popgame.botrunningsystem.service;
public interface BotRunningService {
String addBot(Integer userId, String botCode, String input);
}
实现该接口
@Service
public class BotRunningServiceImpl implements BotRunningService {
@Override
public String addBot(Integer userId, String botCode, String input) {
System.out.println("add bot: " + userId + " " + botCode + " " + input);
return "add bot successfully";
}
}
编写Controller
层
BotRunningController.java
package com.popgame.botrunningsystem.controller;
import com.popgame.botrunningsystem.service.BotRunningService;
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 BotRunningController {
@Autowired
private BotRunningService botRunningService;
@PostMapping("/bot/add/")
public String addBot(@RequestParam MultiValueMap<String, String> data) {
Integer userId = Integer.parseInt(Objects.requireNonNull(data.getFirst("user_id")));
String botCode = data.getFirst("bot_code");
String input = data.getFirst("input");
return botRunningService.addBot(userId,botCode,input);
}
}
给该url
添加网关
与上一章写的config
目录下的RestTemplateConfig.java
与SecurityConfig.java
类似,只需要更改url
为/bot/add/
即可,这里就不再赘述了。
如上,我们的BotRunningSystem
后端框架就搭建完了,还需要更改前端让用户可以选择用bot
对战还是人工对战。
别忘了给BotRunningSystem
微服务设置端口号:
/resources/application.properties
server.port=3002
修改前端
在匹配对战界面MatchGround.vue
添加select
选项表单,可以让用户选择是出动bot对战还是自己人工对战(player moudle
)
<div class="col-4">
<div class="user-select-bot">
<select class="form-select" aria-label="Default select example">
<option value="-1" selected>Player Module</option>
<option v-for="bot in bots" :key="bot.id" :value="bot.id"> {{bot.title}}</option>
</select>
</div>
</div>
简单设计一下css
样式
.user-select-bot {
padding-top: 20vh;
}
.user-select-bot>select {
width: 80%;
/*居中*/
margin: 0 auto;
}
有了框架后接下来需要动态获取bot
列表
...
const refresh_bots = () => {
$.ajax({
url: "http://127.0.0.1:3000/user/bot/getlist/",
type: "get",
headers: {
Authorization: "Bearer " + store.state.user.token,
},
success(resp) {
bots.value = resp;
// console.log(resp);
},
error() {
//console.log(resp);
}
});
}
refresh_bots();//从云端动态获取bots
...
绑定事件
在select
表单里绑定事件select_bot
以表示当前用户选择的是哪一项选项,
可以用v-model
来实现双向绑定:select_bot
的值就是你选择的选项。
<select v-model="select_bot" class="form-select" aria-label="Default select example">
...
</select>
传送bot_id
因为我们添加了bot
对战的功能,所以我们要更改前面写的代码。
我们前面前端client
往ws
后端(3000),ws
后端往Matching System
后端(3001)传送数据时只传送了userId
是不够的,还需要传送bot_id
。
因为我们要靠bot_id
取出相应bot
的代码从而实现bot
的对战!
MatchGround.vue
...
const click_match_btn = () => {
if (match_btn_info.value === "Match") {
match_btn_info.value = "Cancel";
store.state.pk.socket.send(JSON.stringify({
event: "start matching",
bot_id: select_bot.value;
}));
} else {
...
}
}
...
更改后端
backend\consumer\WebSocketService.java
...
private void startMatching(Integer botId) {
System.out.println("start matching!");
//向后端发请求
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());
restTemplate.postForObject(addPlayerUrl, data, String.class); //发送请求
}
...
@OnMessage
public void onMessage(String message, Session session) {
// 从Client接收消息
System.out.println("receive message!");
JSONObject data = JSONObject.parseObject(message);
String event = data.getString("event");
if ("start matching".equals(event)) {
startMatching(data.getInteger("bot_id")); //传送bot_id
} else if ("stop matching".equals(event)) {
stopMatching();
} else if ("move".equals(event)) {
int d = data.getInteger("direction");
move(d);
}
}
...
类似滴,需要在Matching System
后端(3001)把涉及到的Controller
层与Service
层都传个botId
参数。
最后MatchingPool
向ws
端返回结果时也需要加上玩家a和b的各自的botId
matchingsystem\service\utils\MatchingPool.java
private void sendResult(Player a, Player b) { // 返回匹配结果给ws端
System.out.println("send result: " + a + " " + b);
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
data.add("a_id", a.getUserId().toString());
data.add("a_bot_id", a.getBotId().toString()); //
data.add("b_id", b.getUserId().toString());
data.add("b_bot_id", b.getBotId().toString()); //
restTemplate.postForObject(startGameURL, data, String.class);
}
ws
端(startGameURL
)接收的时候也需要加上botId
参数(相应的Controller
层)
\backend\controller\pk\StartGameController.java
@RestController
public class StartGameController {
@Autowired
private StartGameService startGameService;
@PostMapping("/pk/start/game/")
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")));
Integer bBotId = Integer.parseInt(Objects.requireNonNull(data.getFirst("b_bot_id")));
return startGameService.startGame(aId, aBotId, bId, bBotId); //要完善
}
}
顺着完善一下相应的Service
层 与其实现方法 startGameService.startGame()
backend\service\impl\pk\StartGameServiceImpl.java
@Service
public class StartGameServiceImpl implements StartGameService {
@Override
public String startGame(Integer aId, Integer aBotId, Integer bId, Integer bBotId) {
...
WebSocketServer.startGame(aId, aBotId, bId, bBotId);
...
}
}
顺着完善一下ws
端的startGame
方法 (顺藤摸瓜就好了。。但是好累QAQ)
backend\consumer\WebSocketService.java
public static void startGame(Integer aId, Integer aBotId, Integer bId, Integer bBotId) {
...
...
}
目前为止我们完善了botId
在Matching System端
与ws
端与Client
前端之间的相互传递!!
依照botId取出相应的代码
backend\consumer\WebSocketService.java
...
private static BotsMapper botsMapper;
@Autowired
public void setBotsMapper(BotsMapper botsMapper) {
WebSocketServer.botsMapper = botsMapper;
}
...
public static void startGame(Integer aId, Integer aBotId, Integer bId, Integer bBotId) {
Users a = usersMapper.selectById(aId), b = usersMapper.selectById(bId);
Bots botA = botsMapper.selectById(aBotId), botB = botsMapper.selectById(bBotId);
Game game = new Game(13, 14, 36, a.getId(), botA, b.getId(), botB);
...
}
对应的Game
类也要修改
backend\consumer\utils\Game.java
...
public Game(
Integer rows,
Integer cols,
Integer inner_walls_count,
Integer idA,
Bots botA,
Integer idB,
Bots botB) {
this.rows = rows;
this.cols = cols;
this.inner_walls_count = inner_walls_count;
this.mark = new boolean[rows][cols];
Integer botIdA = -1, botIdB = -1;
String botCodeA = "", botCodeB = "";
if (botA != null) {
botIdA = botA.getId();
botCodeA = botA.getContent();
}
if (botB != null) {
botIdB = botB.getId();
botCodeB = botB.getContent();
}
playerA = new Player(idA, botIdA, botCodeA, this.rows - 2, 1, new ArrayList<>());
playerB = new Player(idB, botIdB, botCodeB, 1, this.cols - 2, new ArrayList<>());
}
...
对应的Player
类也要修改,顺便把bot
的代码存一下
backend\consumer\utils\Player.java
public class Player {
private Integer id;
private Integer botId; //-1表示自己来玩,其他表示用bot
private String botCode; //取出代码
private Integer sx;
private Integer sy;
private List<Integer> steps;
...
}
NextStep判断处理
NextStep
步骤(在ws端)要判断当前是pvp模式还是pve模式,即当前传入的是bot代码的操作还是人的操作,如果是bot代码的操作则向BoRunningSystem
微服务发送请求,让他去跑一遍代码,读入代码的操作。如果是人的操作则要等待用户输入。
分类讨论:需不需要向BotRunningSystem
发送一段代码让其自动执行。
代码的运行可以调用上文写的api
,在BotRunningController
里。
getInput
负责将当前的局面信息编码成字符串,
编码格式为:
地图#我的横坐标#我的纵坐标#我的操作#对手的横坐标#对手的纵坐标#对手的操作
每一项用#
隔开,其中为了防止操作为空,可以用()把操作括起来进行区分。
backend\consumer\utils\Game.java
...
private String getInput(Player player) { //将当前的局面信息编码成字符串
//格式:地图#我的横坐标#我的纵坐标#我的操作#对手的横坐标#对手的纵坐标#对手的操作
Player me, opponent;
if (playerA.getId().equals(player.getId())) {
me = playerA;
opponent = playerB;
} else {
me = playerB;
opponent = playerA;
}
return getMapString() + '#' +
me.getSx() + '#' +
me.getSy() + "#(" +
me.getStepsString() + ")#" +
opponent.getSx() + '#' +
opponent.getSy() + "#(" +
opponent.getStepsString() + ")#";
}
private void sendBotCode(Player player) { //向BotRunningSystem端发送代码
if (player.getBotId() == -1) return; //表示人操作,不需要执行代码
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端发送请求,由BotRunningController那里接收
WebSocketServer.restTemplate.postForObject(addBotURL, data, String.class);//这里要在ws端将RestTemplate改成public
}
...
private boolean nextStep() {
//等待玩家的下一步操作
try {
Thread.sleep(200); //前端1s走5步,200ms走一步,因此为了操作顺利,每一次读取都要先sleep 200ms
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
sendBotCode(playerA);
sendBotCode(playerB);
...
}
...
若是bot代码对战的话要把人的操作屏蔽掉,不需要接受前端的输入
backend\consumer\WebSocketServer.java
...
public void move(int direction) {
if (game.getPlayerA().getId().equals(user.getId())) { //蛇A
if (game.getPlayerA().getBotId() == -1) // 人玩
game.setNextStepA(direction);
} else if (game.getPlayerB().getId().equals(user.getId())) { //蛇B
if (game.getPlayerB().getBotId() == -1) //人玩
game.setNextStepB(direction);
}
}
...
前面说了这么多可能会有点乱,这里理清一下我们的思路与流程:
简单来说就是我们在前端把匹配信息传到ws后端服务器——> 再传到
Matching System
服务器——>把玩家放到匹配池去匹配——>把匹配成功信息再返回给ws后端服务器——>ws后端服务器会调用Game——>Game里面会Create Map产生对战地图——>玩家可以开始玩游戏(bot or yourself)——>把每一步信息传到Next Step
判断是否合法——>若是bot玩则把每一步信息传到微服务Bot Running System
将代码跑一遍(放到Bot Pool里)——>consumer(bot)函数运行代码(通过joor)——> 返回结果给ws端——> 最后判断对局结果
实现微服务 Bot Running System
功能:不断接收用户的输入,当接收的代码比较多时,要把代码放到一个队列里(Bot Pool
),用队列存储每一个代码任务信息。
本质:生产者消费者模型
生产者发送一个任务过来,我们会把他存到队列里面,
消费者是一个单独的线程,会不断等待任务的到来,每完成一个任务会检查任务队列是否为空,若不为空则从队头取一个任务过来执行,以此为例,循环往复。
特别的,虽然这里的Bot Pool
与匹配系统里的Match Pool
类似,都是一个单独的线程,但是实现方法与Matchi Pool
有所不同。我们Match Pool
每次去匹配的时候都是通过不断地sleep
1s来让用户等待匹配,这是用户可以接受的。但是若我们Bot Pool
里也按照这种方式,则用户在玩游戏的过程中延迟会太高,游戏体验不好,在游戏过程中让用户等待太长时间是无法接受的。因此,我们实现Bot Pool
时要改用Condition Variable条件变量。如果空的话就阻塞线程,一旦有消息要处理则发一个信号量唤醒线程!
实现消费者线程Bot Pool
- 这是一个多线程任务,要继承自
Thread
- 记得重写
run
函数 - 定义:锁,条件变量,队列(
Bot
类) - 新建
Bot
类:userId
,botCode
,input
- 队列不需要定义成线程安全的队列,普通队列即可,我们可以通过加锁与解锁来维护他的安全性
- 涉及到读写冲突的都要先加锁再工作后面再解锁
Queue
涉及到两边的操作,一边是生产者给他不断加入任务,另一边是消费者不断取出任务,因此要先上锁后解锁- 有关
Queue
的都要想到锁 - 在启动
springboot
前启动线程BotPool: BotRunningServiceImpl.botPool.start();
- 线程有关:每次
start()
后会开一个新的线程执行run()
里面的内容
条件变量相关
定义:private final Condition condition = lock.newCondition(); //条件变量
api
:
condition.await()
:阻塞当前线程,直到该线程被唤醒或这个线程中断。await()
后会自动将lock
释放。
condition.signal()
: 唤醒线程。
condition.signalAll()
:唤醒所有线程。
botrunningsystem/service/impl/utils/BotPool.java
package com.gameforces.botrunningsystem.service.impl.utils;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class BotPool extends Thread {
private final ReentrantLock lock = new ReentrantLock();//单例模式,加不加static都可以
private final Condition condition = lock.newCondition(); //条件变量
private final Queue<Bot> bots = new LinkedList<>(); //消费队列
public void addBot(Integer userId, String botCode, String input) { //在BotRunningServiceImpl里调用
lock.lock(); //只要对Queue进行修改都要加锁
try {
bots.add(new Bot(userId, botCode, input));
condition.signalAll();//生产者往消费队列生产完任务后,唤醒被阻塞的线程
} finally {
lock.unlock();
}
}
private void consume(Bot bot) { //一共等待5s钟,每个bot等待2s钟,剩下1s做冗余
Consumer consumer = new Consumer();
consumer.startTimeout(2000, bot);
}
@Override
public void run() {
while (true) {
lock.lock();
if (bots.isEmpty()) {
try {
condition.await(); //若当前消费队列为空,则将当前线程阻塞 (await包含将锁释放的操作)
} catch (InterruptedException e) {
e.printStackTrace();
lock.unlock();
break;
}
} else {
//将队头任务取出
Bot bot = bots.remove();
lock.unlock();
//执行(消费)当前任务
consume(bot); //比较耗时,可能会执行几秒钟
}
}
}
}
botrunningsystem/service/BotRunningServiceImpl.java
@Service
public class BotRunningServiceImpl implements BotRunningService {
public final static BotPool botPool = new BotPool(); // 静态变量,把消费者线程存下来
@Override
public String addBot(Integer userId, String botCode, String input) {
System.out.println("add bot: " + userId + " " + botCode + " " + input);
botPool.addBot(userId, botCode, input); //将当前待运行(待消费)的代码放入消费队列botPool
return "add bot successfully";
}
}
botrunningsystem/BotRunningSystemApplication
在botrunningsystem
微服务(springboot
)的入口启动消费者线程
import com.gameforces.botrunningsystem.service.impl.BotRunningServiceImpl;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class BotRunningSystemApplication {
public static void main(String[] args) {
BotRunningServiceImpl.botPool.start(); // 启动botPool消费者线程
SpringApplication.run(BotRunningSystemApplication.class, args);
}
}
consume(bot)
consume(bot)
:执行当前队头代码的任务,在执行函数consume(bot)
之前记得要先解锁,因为编译执行代码会很慢。如果不解锁的话,未来往队列里加任务的时候可能会阻塞进程,这是没有必要的。我们取出队头任务后,就与队列无关了,没有读写冲突,所以要先解锁,再执行代码任务。
操作本质:手动实现一个消费队列。
这里的consume(bot)
只是简单的利用java
里的api
实现java
代码,并不能实现其他语言的代码,以后若要进行优化的话,可以把代码放到一个沙箱里去运行,可以把这个函数改成基于docker
执行代码。
joor
:动态实现java
代码
运行代码
为了防止玩家恶意输入死循环代码,我们可以用一个独立的线程Consumer
控制代码运行时间,如果超时的话可以自动断掉这个Consumer
运行线程。
Consumer.java
package com.gameforces.botrunningsystem.service.impl.utils;
import org.joor.Reflect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.PrintWriter;
import java.util.UUID;
import java.util.function.Supplier;
@Component // 为了在类中能够注入Bean,即能通過@Autowired注入RestTemplate
public class Consumer extends Thread {
private Bot bot;
private final static String receiveBotMoveURL = "http://127.0.0.1:3000/pk/receive/bot/move/";
private static RestTemplate restTemplate;
@Autowired
public void setRestTemplate(RestTemplate restTemplate) {
Consumer.restTemplate = restTemplate;
}
public void startTimeout(long timeout, Bot bot) { //timeout: 当前线程最多执行多少时间
this.bot = bot;
this.start(); //启动当前线程
//控制当前线程执行时间
try {
this.join(timeout); //最多等待timeout秒
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
this.interrupt(); //中断当前线程
}
}
private String addUid(String code, String uid) { //在code中的Bot类名后添加uid
int k = code.indexOf(" implements java.util.function.Supplier<Integer>");
return code.substring(0, k) + uid + code.substring(k);
}
@Override
public void run() {
UUID uuid = UUID.randomUUID();
String uid = uuid.toString().substring(0, 8); //返回前8位随机字符串
Supplier<Integer> botInterface = Reflect.compile(
"com.gameforces.botrunningsystem.utils.Bot" + uid,
addUid(bot.getBotCode(), uid)
).create().get(); //joor的api,用来动态编译代码
File file = new File("input.txt");
try (PrintWriter fout = new PrintWriter(file)) {
fout.println(bot.getInput());
fout.flush(); //清缓冲区,防止读不到信息
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
Integer direction = botInterface.get();
System.out.println("move-direction: " + bot.getUserId() + " " + direction);
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
data.add("user_id", bot.getUserId().toString());
data.add("direction", direction.toString());
restTemplate.postForObject(receiveBotMoveURL, data, String.class);
}
}
API
:
join(timeout)
:控制线程执行时间,最多等待timeout
时间(单位秒),会执行后面的操作。
逻辑流程如下:1. 首先this.start()
会开启一个新线程执行run()
里面的任务,执行完任务后会自动跳过this.join(timeout)
从而执行其后面的操作语句。2. 若在this.start()
开启线程执行run()
里面任务后,等待时间超过了timeout
(秒)则会放弃执行run()
里面的任务直接执行this.join(timeout)
后面的操作语句。
interrupt()
: 中断当前线程
为了方便编程,先定义一个接口给用户编写自己的bot
botrunningsystem\utils\BotInterface.java
public interface BotInterface { //用户编写自己的bot的接口
Integer nextMove(String input);//下一步要走的方向是什么
}
实现BotInterface
接口
botrunningsystem\utils\Bot.java
package com.popgame.botrunningsystem.utils;
public class Bot implements com.popgame.botrunningsystem.utils.BotInterface {
@Override
public Integer nextMove(String input) {
return 0; //向上走
}
}
初识joor
因为joor
执行代码的api
不能同时运行类名相同的代码,因此要在类名前加一个随机字符串,可以用UUID
实现
UUID uuid = UUID.randomUUID();
String uid = uuid.toString().substring(0,8);
botrunningsystem\service\impl\utils\Consumer.java
package com.popgame.botrunningsystem.service.impl.utils;
import com.popgame.botrunningsystem.utils.BotInterface;
import org.joor.Reflect;
import java.util.UUID;
public class Consumer extends Thread {
private Bot bot;
public void startTimeout(long timeout, Bot bot) {
this.bot = bot;
this.start(); //启动当前线程
//控制当前线程执行时间
try {
this.join(timeout); //最多等待timeout秒
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
this.interrupt(); //中断当前进程
}
}
private String addUid(String code, String uid) { //在code中的Bot类名添加uid
int k = code.indexOf(" implements com.popgame.botrunningsystem.utils.BotInterface");
return code.substring(0, k) + uid + code.substring(k);
}
@Override
public void run() {
UUID uuid = UUID.randomUUID();
String uid = uuid.toString().substring(0, 8); //返回前8位随机字符串
BotInterface botInterface = Reflect.compile(
"com.popgame.botrunningsystem.utils.Bot" + uid,
addUid(bot.getBotCode(), uid)
).create().get(); //joor的api
Integer direction = botInterface.nextMove(bot.getInput());
System.out.println("move-direction: " + bot.getUserId() + " " + direction);
}
}
将代码运行结果返回给ws
端(3000)
与前面一样,编写对应的service
层ReceiveBotMoveService
和controller
层ReceiveBotMoveController
tips:记得在SecurityConfig
开放controller
层的url
网关!
Controller
层
botrunningsystem/controller/pk/ReceiveBotMoveController.java
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);
}
}
botrunningsystem/service/pk/impl/ReceiveBotMoveService.java
service
层接口
package com.gameforces.backend.service.pk;
public interface ReceiveBotMoveService {
String receiveBotMove(Integer userId, Integer direction);
}
实现Running Bot System 与 ws端的通信
用RestTemplate
!!!
在Consumer.java
里定义RestTemplate
!!传给ws
端(通过上面Controller
层写的URL
)
注意:为了在类中能够注入Bean
,即能通過@Autowired
注入RestTemplate
,要在Consumer.java
里面加上@Component
实现service层的接口
我们将代码运行的结果返回给ws
端,还要一路下传给到NextStep
判断部分
ReceiveBotMoveServiceImpl.java
@Service
public class ReceiveBotMoveServiceImpl implements ReceiveBotMoveService {
@Override
public String receiveBotMove(Integer userId, Integer direction) {
System.out.println("receive bot move: " + userId + " " + direction);
if (WebSocketServer.users.get(userId) != null) {
Game game = WebSocketServer.users.get(userId).game;
if (game != null) {
if (game.getPlayerA().getId().equals(userId)) { //蛇A
game.setNextStepA(direction);
} else if (game.getPlayerB().getId().equals(userId)) { //蛇B
game.setNextStepB(direction);
}
}
}
return "receive bot move successfully";
}
}
最后NextStep
会将结果传给ws
端,然后再返回给前端,这样我们就实现了AI对战了!
最后贴一下简易的AI代码:
botrunningsystem\utils\Bot.java
package com.popgame.botrunningsystem.utils;
import java.util.ArrayList;
import java.util.List;
public class Bot implements com.popgame.botrunningsystem.utils.BotInterface {
static class Cell {
public int x, y;
public Cell(int x, int y) {
this.x = x;
this.y = y;
}
}
private boolean check_tail_increasing(int step) { //检测当前回合蛇的长度是否增加
if (step <= 10) return true;
else {
return step % 3 == 1;
}
}
public List<Cell> getCells(int sx, int sy, String steps) {
steps = steps.substring(1, steps.length() - 1);
List<Cell> res = new ArrayList<>(); //存放蛇的身体
int[][] fx = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}};
int x = sx, y = sy;
res.add(new Cell(x, y));
int step = 0; //回合数
for (int i = 0; i < steps.length(); i++) {
int ch = steps.charAt(i);
int d = steps.charAt(i) - '0';
x += fx[d][0];
y += fx[d][1];
res.add(new Cell(x, y));
if (!check_tail_increasing(++step)) {
res.remove(0);
}
}
return res;
}
@Override
public Integer nextMove(String input) {
String[] strs = input.split("#");
int[][] g = new int[13][14];
for (int i = 0, k = 0; i < 13 && k < strs[0].length(); i++) {
for (int j = 0; j < 14; j++, k++) {
if (strs[0].charAt(k) == '1') {
g[i][j] = 1;
}
}
}
int aSx = Integer.parseInt(strs[1]), aSy = Integer.parseInt(strs[2]);
int bSx = Integer.parseInt(strs[4]);
int bSy = Integer.parseInt(strs[5]);
List<Cell> aCells = getCells(aSx, aSy, strs[3]);
List<Cell> bCells = getCells(bSx, bSy, strs[6]);
for (Cell c : aCells) g[c.x][c.y] = 1;
for (Cell c : bCells) g[c.x][c.y] = 1;
int[] dx = {-1, 0, 1, 0}, dy = {0, 1, 0, -1};
for (int i = 0; i < 4; i++) {
int x = aCells.get(aCells.size() - 1).x + dx[i]; // a的最后一个元素:蛇头
int y = aCells.get(aCells.size() - 1).y + dy[i];
if (x < 0 || x >= 13 || y < 0 || y >= 14 || g[x][y] == 1) continue;
return i;
}
return 0;
}
}
一些小细节
改良前端记分牌ResultBoard
的显现流畅性,在从对战界面切换回pk页面的时候,要把其隐藏起来
PKindex.vue
setup() {
...
store.commit("updateLoser", "none"); //每次打开pk界面都先把记分牌隐藏起来
...
}
太棒啦