本节实现功能
Bot代码执行微服务
思路细化
总
简单来说就是我们在前端把匹配信息传到ws后端服务器——> 再传到Matching System服务器——>把玩家放到匹配池去匹配——>把匹配成功信息再返回给ws后端服务器——>ws后端服务器会调用Game——>Game里面会Create Map产生对战地图——>玩家可以开始玩游戏(bot or yourself)——>把每一步信息传到Next Step判断是否合法——>若是bot玩则把每一步信息传到微服务Bot Running System将代码跑一遍(放到Bot Pool里)——>consumer(bot)函数运行代码(通过joor)——> 返回结果给ws端——> 最后判断对局结果
前半部分
- 首先要实现的是一个微服务,所以要创建一个模块botrunningsystem
- botrunningsystem首先实现的功能是接受网页后端传过来的用户id,bot代码,地图的相关信息
- 经典三建 接口(BotRunningService),接口的实现(BotRunningServiceImpl),接口的调用(BotRunningController)
- BotRunningController中写了一个PostMapping有相应的链接地址,用于接收后端传过来的相应信息,因为是后端信息的交互,所以需要在网关中放行,同时信息的交互需要RestTemplate,所以要引入这个类,直接复制matcingsystem的config,当然端口的配置需要改一下,改成3002
- 下面的话就应在前端页面实现bot的选择(文本框),因为bot的创建与删除,所以在匹配界面需要刷新也就是复制refresh_bots ,在select表单里绑定事件select_bot以表示当前用户选择的是哪一项选项,
可以用v-model来实现双向绑定:select_bot的值就是你选择的选项。 - 前端点击选择bot传给后端,后端WebSocketServer接收到匹配请求,将bot传给匹配服务,Matching System接收到botId成功匹配后传给BackEnd,BackEnd将bot传给BotRunningSystem服务,WebSocketServer,MatchingController,MatchingService,MatchingServiceImpl,MatchingPool,Player,MatchingPool ,然后BackEnd接收匹配成功返回的botId
- 已经匹配成功的话,所以BackEnd接收信息时的操作,修改代码,StartGameController,StartGameService,StartGameServiceImpl,WebSocketServer,BackEnd再次接受
8.通过Player,WebSocketServer,Game取出代码 - 判断一下当前的玩家点击的是人还是代码、如果是代码的话就需要像我们的微服务发送一段代码、然后让她自动去算、如果是人来操作的话就要等待用户的输入,若是bot代码对战的话要把人的操作屏蔽掉,不需要接受前端的输入
后半部分
实现微服务 Bot Running System
功能:不断接收用户的输入,当接收的代码比较多时,要把代码放到一个队列里(Bot Pool),用队列存储每一个代码任务信息。
本质:生产者消费者模型
生产者发送一个任务过来,我们会把他存到队列里面,
消费者是一个单独的线程,会不断等待任务的到来,每完成一个任务会检查任务队列是否为空,若不为空则从队头取一个任务过来执行,以此为例,循环往复。
特别的,虽然这里的Bot Pool与匹配系统里的Match Pool类似,都是一个单独的线程,但是实现方法与Matchi Pool
有所不同。我们Match Pool每次去匹配的时候都是通过不断地sleep1s来让用户等待匹配,这是用户可以接受的。但是若我们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():唤醒所有线程。
consume(bot)
consume(bot):执行当前队头代码的任务,在执行函数consume(bot)之前记得要先解锁,因为编译执行代码会很慢。如果不解锁的话,未来往队列里加任务的时候可能会阻塞进程,这是没有必要的。我们取出队头任务后,就与队列无关了,没有读写冲突,所以要先解锁,再执行代码任务。
操作本质:手动实现一个消费队列。
这里的consume(bot)只是简单的利用java里的api实现java代码,并不能实现其他语言的代码,以后若要进行优化的话,可以把代码放到一个沙箱里去运行,可以把这个函数改成基于docker执行代码。
运行代码
为了防止玩家恶意输入死循环代码,我们可以用一个独立的线程Consumer控制代码运行时间,如果超时的话可以自动断掉这个Consumer运行线程。
api:
join(timeout):控制线程执行时间,最多等待timeout时间(单位秒),会执行后面的操作。或者上一个线程执行完会执行后面的操作。
interrupt(): 中断当前线程
为了方便编程,先定义一个接口给用户编写自己的bot
botrunningsystem\utils\BotInterface.java
实现BotInterface接口
botrunningsystem\utils\Bot.java
因为joor执行代码的api不能重复运行类名相同的代码,因此要在类名前加一个随机字符串,可以用UUID实现
UUID uuid = UUID.randomUUID();
String uid = uuid.toString().substring(0,8);
botrunningsystem\service\impl\utils\Consumer.java
将代码运行结果返回给ws端
与前面一样,编写对应的service层ReceiveBotMoveService和controller层ReceiveBotMoveController
实现Running Bot System 与 ws端的通信
用RestTemplate!!!
在Consumer里定义RestTemplate!!传给ws端(通过上面Controller层写的URL)实现service层的接口
我们将代码运行的结果返回给ws端,还要一路下传给到NextStep判断部分
ReceiveBotMoveServiceImpl.java
最后NextStep会将结果传给ws端,然后再返回给前端,这样我们就实现了AI对战了!
让BotRunning System获得到前端选择的Bot
新建Bot执行微服务项目
导入配置包
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>backendcloud</artifactId>
<groupId>com.kob</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.kob.botrunningsystem</groupId>
<artifactId>botrunningsystem</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.7.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.jooq/joor-java-8 -->
<dependency>
<groupId>org.jooq</groupId>
<artifactId>joor-java-8</artifactId>
<version>0.9.14</version>
</dependency>
</dependencies>
</project>
创建BotRunningSystem用于接收前端所传的信息
* 在创建好的BotRuningSystem下的启动文件重构为BotRunningSystemApplication
package com.kob.botrunningsystem;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class BotRunningSystemApplication {
public static void main(String[] args) {
SpringApplication.run(BotRunningSystemApplication.class, args);
}
}
- botrunningsystem下建包service,controller,config
- 在service中建立一个接口(BotRunningService)
package com.kob.botrunningsystem.service;
public interface BotRunningService {
String addBot(Integer userId, String botCode, String input);
}
- service下建包impl,用于实现接口(BotRunningServiceImpl)
package com.kob.botrunningsystem.service.impl;
import com.kob.botrunningsystem.service.BotRunningService;
import org.springframework.stereotype.Service;
@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 success";
}
}
- 在controller中调用BotRunningService(BotRunningController)
package com.kob.botrunningsystem.controller;
import com.kob.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 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);
}
}
- 在网关中放开权限(config)
package com.kob.botrunningsystem.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/bot/add/").hasIpAddress("127.0.0.1")
.antMatchers(HttpMethod.OPTIONS).permitAll()
.anyRequest().authenticated();
}
}
- 通过建立RestTemplateConfig实现不同服务之间的通信
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
- 端口配置的话在resources中创建Application.properties文件
server.port=3002
Bot_id的传递
* 前端选择Bot
MatchGround.vue
<template>
<div class="matchground">
<div class="row">
<div class="col-4">
...
</div>
<div class="col-4">
<div class="user-select-bot">
<select class="form-select" aria-label="Default select example" v-model="select_bot">
<option value="-1" selected>亲自出马</option>
<option v-for="bot in bots" :key="bot.id" :value="bot.id">{{ bot.title }}</option>
</select>
</div>
</div>
<div class="col-4">
...
</div>
<div class="col-12" style="text-align: center; padding-top: 15vh;">
...
</div>
</div>
</div>
</template>
<script>
import { ref } from 'vue';
import { useStore } from 'vuex';
import $ from 'jquery';
export default {
setup() {
...
let bots = ref([]);
let select_bot = ref("-1");
const click_match_btn = () => {
if(match_btn_info.value === "开始匹配") {
match_btn_info.value = "取消";
store.state.pk.socket.send(JSON.stringify({
event: "start-matching",
bot_id: select_bot.value,
}));
}
...
};
const refresh_bots = () => {//起一个刷新作用,让Bot实时刷新
$.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;
}
})
};
refresh_bots();
return {
...
bots,
select_bot,
}
}
}
</script>
<style scoped>
...
div.user-select-bot {
padding-top: 20vh;
}
div.user-select-bot > select {
width: 60%;
margin: 0 auto;
}
</style>
- 网页后端接受Bot
一系列都要改变
WebSocketServer
package com.kob.backend.consumer;
...
@Component
@ServerEndpoint("/websocket/{token}")
public class WebSocketServer {
...
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) {
...
if("start-matching".equals(event)) {
startMatching(data.getInteger("bot_id"));
}
...
}
...
}
MatchingController
package com.kob.matchingsystem.controller;
...
import java.util.Objects;
@RestController
public class MatchingController {
@Autowired
private MatchingService matchingService;
// 参数不能使用普通map,MultiValueMap和普通map的区别时,这个是一个键对应多个值
@PostMapping("/player/add/")
public String addPlayer(@RequestParam MultiValueMap<String, String> data) {
...
Integer botId = Integer.parseInt(Objects.requireNonNull(data.getFirst("bot_id")));
return matchingService.addPlayer(userId, rating, botId);
}
...
}
MatchingService
package com.kob.matchingsystem.service;
public interface MatchingService {
String addPlayer(Integer userId, Integer rating, Integer botId);
...
}
MatchingServiceImpl
package com.kob.matchingsystem.service.impl;
...
@Service
public class MatchingServiceImpl implements MatchingService {
public static final MatchingPool matchingPool = new MatchingPool();
@Override
public String addPlayer(Integer userId, Integer rating, Integer botId) {
System.out.println("Add Player: " + userId + " " + rating + " " + botId);
matchingPool.addPlayer(userId, rating, botId);
return "add player success";
}
...
}
MatchingPool
package com.kob.matchingsystem.service.impl.utils;
...
// 匹配池是多线程的
@Component
public class MatchingPool extends Thread {
...
public void addPlayer(Integer userId, Integer rating, Integer botId) {
// 在多个线程(匹配线程遍历players时,主线程调用方法时)会操作players变量,因此加锁
lock.lock();
try {
players.add(new Player(userId, rating, botId, 0));
} finally {
lock.unlock();
}
}
...
}
Player
package com.kob.matchingsystem.service.impl.utils;
...
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Player {
private Integer userId;
private Integer rating;
private Integer botId;
private Integer waitingTime;
}
MatchingPool
package com.kob.matchingsystem.service.impl.utils;
...
// 匹配池是多线程的
@Component
public class MatchingPool extends Thread {
...
private void sendResult(Player a, Player b) { // 返回匹配结果
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);
}
...
}
BackEnd接收匹配成功返回的bot_id
- StartGameController
package com.kob.backend.controller.pk;
...
import java.util.Objects;
@RestController
public class StartGameController {
...
@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);
}
}
- StartGameService
package com.kob.backend.service.pk;
public interface StartGameService {
String startGame(Integer aId, Integer aBotId, Integer bId, Integer bBotId);
}
- StartGameServiceImpl
package com.kob.backend.service.impl.pk;
...
@Service
public class StartGameServiceImpl implements StartGameService {
@Override
public String startGame(Integer aId, Integer aBotId, Integer bId, Integer bBotId) {
System.out.println("start gameL: " + aId + " " + bId);
WebSocketServer.startGame(aId, aBotId, bId, bBotId);
return "start game success";
}
}
- WebSocketServer
package com.kob.backend.consumer;
...
import com.kob.backend.pojo.Bot;
import com.kob.backend.mapper.BotMapper;
@Component
// url链接:ws://127.0.0.1:3000/websocket/**
@ServerEndpoint("/websocket/{token}") // 注意不要以'/'结尾
public class WebSocketServer {
...
public static BotMapper botMapper;
...
@Autowired
public void setBotMapper(BotMapper botMapper) {
WebSocketServer.botMapper = botMapper;
}
...
public static void startGame(Integer aId, Integer aBotId, Integer bId, Integer bBotId) {
User a = userMapper.selectById(aId), b = userMapper.selectById(bId);
Bot botA = botMapper.selectById(aBotId), botB = botMapper.selectById(bBotId);
Game game = new Game(
13,
14,
20,
a.getId(),
botA,
b.getId(),
botB);
...
}
...
}
- Player
package com.kob.backend.consumer.utils;
...
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Player {
private Integer id;
private Integer botId; // -1表示亲自出马,否则表示用AI打
private String botCode;
...
}
- WebSocketServer
//将RestTemplate变成public,若是代码输入则屏蔽人的输入
package com.kob.backend.consumer;
...
@Component
@ServerEndpoint("/websocket/{token}")
public class WebSocketServer {
...
public static RestTemplate restTemplate;
private void move(int direction) {
if(game.getPlayerA().getId().equals(user.getId())) { //蛇A
if(game.getPlayerA().getBotId().equals(-1)) // 亲自出马则接收输入
game.setNextStepA(direction);
} else if(game.getPlayerB().getId().equals(user.getId())) { //蛇B
if(game.getPlayerB().getBotId().equals(-1))
game.setNextStepB(direction);
}
}
...
}
- Game
package com.kob.backend.consumer.utils;
...
import com.kob.backend.pojo.Bot;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
public class Game extends Thread {
...
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,
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];
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, rows - 2, 1, new ArrayList<>());
playerB = new Player(idB, botIdB, botCodeB, 1, cols - 2, new ArrayList<>());
}
private String getInput(Player player) { //将当前的局面信息编码成字符串
//格式:地图#我的横坐标#我的纵坐标#我的操作#对手的横坐标#对手的纵坐标#对手的操作
Player me, you;
if(playerA.getId().equals(player.getId())) {
me = playerA;
you = playerB;
} else {
me = playerB;
you = playerA;
}
return getMapString() + "#" +
me.getSx() + "#" +
me.getSy() + "#(" +
me.getStepsString() + ")#" + // 加()是为了预防操作序列为空
you.getSx() + "#" +
you.getSy() + "#(" +
you.getStepsString() + ")";
}
private void sendBotCode(Player player) {//向BotRunningSystem端发送代码
if(player.getBotId().equals(-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端发送请求
WebSocketServer.restTemplate.postForObject(addBotUrl, data, String.class);
}
private boolean nextStep() {
//等待玩家的下一步操作
try {
Thread.sleep(200);//前端1s走5步,200ms走一步,因此为了操作顺利,每一次读取都要先sleep 200ms
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
sendBotCode(playerA);
sendBotCode(playerB);
...
}
...
}
- 测试
BotRunning System中代码的实现
评测器是一个经典的生产者消费者模型,此服务生产者会将任务放进一个队列中,
消费者是单独的一个线程,当有任务就会从队头立即执行;并且关键问题是评测
器的执行代码不能单纯的用sleep 1s去判断是否有任务,这样很影响评测体验,
因此需要用到Condition Variable,当有任务执行,无任务等待。
- 文件结构
botrunningsystem
service
impl
utils
Bot.java
BotPool.java
Consumer.java
utils
Bot.java
BotInterface.java
- utils.Bot
package com.kob.botrunningsystem.service.impl.utils;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Bot {
Integer userId;
String botCode;
String input;
}
- BotPool
//虽然队列没用消息队列,但是因为我们写了条件变量与锁的操作,所以等价于消息队列
package com.kob.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 {
// 以下的锁和条件变量加不加static都可以,因为BotPool只有一个
private final ReentrantLock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private final Queue<Bot> bots = new LinkedList<>();
public void addBot(Integer userId, String botCode, String input) {
lock.lock();
try {
bots.add(new Bot(userId, botCode, input));
condition.signalAll();
} finally {
lock.unlock();
}
}
private void consume(Bot bot) {
}
@Override
public void run() {
while(true) {
lock.lock();
if(bots.isEmpty()) {
try {
// 若执行了await会自动释放锁
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
lock.unlock();
break;
}
} else {
Bot bot = bots.remove();
lock.unlock();
// 耗时操作,因此要在释放锁之后执行
consume(bot);
}
}
}
}
- BotRunningServiceImpl
//实现botpool
package com.kob.botrunningsystem.service.impl;
...
import com.kob.botrunningsystem.service.impl.utils.BotPool;
@Service
public class BotRunningServiceImpl implements BotRunningService {
public static final 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);
return "add bot success";
}
}
- BotPool线程的启动
package com.kob.botrunningsystem;
...
@SpringBootApplication
public class BotRunningSystemApplication {
public static void main(String[] args) {
BotRunningServiceImpl.botPool.start();
SpringApplication.run(BotRunningSystemApplication.class, args);
}
}
- 用户写Bot实现的接口(BotInterface)
package com.kob.botrunningsystem.utils;
public interface BotInterface {//用户编写自己的bot的接口
Integer nextMove(String input);//下一步要走的方向是什么
}
- Bot(写蛇的AI代码的地方)
package com.kob.botrunningsystem.utils;
public class Bot implements BotInterface {
@Override
public Integer nextMove(String input) {
return 0; // 向上走
}
}
- consumer实现(运用一个依赖joor)
//AI代码的实现
package com.kob.botrunningsystem.service.impl.utils;
import com.kob.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();//启动当前线程
// 在 程序运行结束后 或 程序在指定timeout时间后还未执行完毕 直接中断代码执行
try {
this.join(timeout);//最多等待timeout秒
this.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
this.interrupt(); //中断当前进程
}
}
private String addUid(String code, String uid) { // 在code中的Bot类名后添加uid
int k = code.indexOf(" implements 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.kob.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);
}
}
- 在BotPool中调用consumer
package com.kob.botrunningsystem.service.impl.utils;
...
public class BotPool extends Thread {
...
private void consume(Bot bot) {
Consumer consumer = new Consumer();
consumer.startTimeout(2000, bot);
}
...
}
- 测试
- 简单的前端bug修改
PkIndexView.vue
<template>
...
</template>
<script>
...
export default {
...
setup() {
const store = useStore();
const socketUrl = `ws://127.0.0.1:3000/websocket/${store.state.user.token}/`;
store.commit("updateLoser", 'none');
...
}
}
</script>
<style scoped>
</style>
将Bot执行结果传递给前端
backend接受结果需要创建相应的接口(ReceiveBotMoveService),接口的实现(ReceiveBotMoveServiceImpl),接口的调用(ReceiveBotMoveController)
ReceiveBotMoveService
package com.kob.backend.service.pk;
public interface ReceiveBotMoveService {
String receiveBotMove(Integer userId, Integer direction);
}
- ReceiveBotMoveServiceImpl 将代码运行的结果返回给ws端,还要一路下传给到NextStep判断部分
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 + " " + direction + " ");
if(WebSocketServer.users.get(userId) != null) {
Game game = WebSocketServer.users.get(userId).game;
if(game != null) {
if(game.getPlayerA().getId().equals(userId)) {
game.setNextStepA(direction);
} else if(game.getPlayerB().getId().equals(userId)) {
game.setNextStepB(direction);
}
}
}
return "receive bot move success";
}
}
- ReceiveBotMoveController
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);
}
}
- 网关
package com.kob.backend.config;
...
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
...
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 放行这两个接口
.antMatchers("/user/account/token/", "/user/account/register/", "/getKaptcha").permitAll()
.antMatchers("/pk/start/game/", "/pk/receive/bot/move/").hasIpAddress("127.0.0.1")
.antMatchers(HttpMethod.OPTIONS).permitAll()
.anyRequest().authenticated();
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
...
}
- BotRunningSystem返回Bot执行结果
package com.kob.botrunningsystem.service.impl.utils;
import com.kob.botrunningsystem.utils.BotInterface;
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.util.UUID;
@Component
public class Consumer extends Thread {
private Bot bot;
private static RestTemplate restTemplate;
private static final String receiveBotMoveUrl = "http://127.0.0.1:3000/pk/receive/bot/move/";
@Autowired
public void setRestTemplate(RestTemplate restTemplate) {
Consumer.restTemplate = restTemplate;
}
...
@Override
public void run() {
UUID uuid = UUID.randomUUID();
String uid = uuid.toString().substring(0, 8);
BotInterface botInterface = Reflect.compile(
"com.kob.botrunningsystem.utils.Bot" + uid,
addUid(bot.getBotCode(), uid)
).create().get();
Integer direction = botInterface.nextMove(bot.getInput());
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);
}
}
- 测试
简单的AI
package com.kob.botrunningsystem.utils;
import java.util.ArrayList;
import java.util.List;
public class Bot implements 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;
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[] 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 i = 0; i < steps.length(); i++) {
int d = steps.charAt(i) - '0';
x += dx[d];
y += dy[d];
res.add(new Cell(x, y));
if(!check_tail_increasing(++step)) {
res.remove(0);
}
}
return res;
}
@Override
public Integer nextMove(String input) {
// 地图#my.sx#my.sy#(my操作)#you.sx#you.sy#(you操作)
String[] strs = input.split("#");
int[][] g = new int[13][14];
for(int i = 0, k = 0; i < 13; 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]), 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];
int y = aCells.get(aCells.size() - 1).y + dy[i];
if(x >= 0 && x < 13 && y >= 0 && y < 14 && g[x][y] == 0) {
return i;
}
}
return 0;
}
}