项目结构和课程内容

  • 项目结构

红色为当节课内容

1.png

  • 代码执行,此项目只支持 java 代码的执行,用的是 joor java 8 实现

可扩展为docker实现,设置内存上限,时间,用命令可执行所有语言代码,并具备一定安全性,因为docker与运行环境隔绝

让 BotRunning System 获得到前端选择的 Bot

1.新建 Bot 执行微服务项目

右键backendcloud->新建->模块

1.png

2.导包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
botrunningsystem/pom.xml

<?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>

3.BotRunningSystem 接收前端选择的 botId

  • 文件结构
1
2
3
4
5
6
7
8
9
10
11
botrunningsystem
config
RestTemplateConfig.java
SecurityConfig.java
controller
BotRunningController.java
service
impl
BotRunningServiceImpl.java
BotRunningService.java
BotRunningSystemApplication.java
  • 将 BotRunningSystem/Main.java 更名为 BotRunningSystemApplication.java
1
2
3
4
5
6
7
8
9
10
11
12
13
BotRunningSystemApplication.java

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);
}
}
  • 接口
1
2
3
4
5
6
7
8
BotRunningService.java

package com.kob.botrunningsystem.service;

public interface BotRunningService {

String addBot(Integer userId, String botCode, String input);
}
  • 接口实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
BotRunningServiceImpl.java

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";
}
}
  • 控制器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
BotRunningController.java

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);
}
}
  • 权限控制(网关)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
SecurityConfig.java

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();
}
}
  • 服务间发送消息的 RestTemplate
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
RestTemplateConfig.java

package com.kob.botrunningsystem.config;

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新建文件

1
2
3
Application.properties

server.port=3002

4.前端选择 Bot

  • 匹配界面

添加选择操作方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
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 = () => {
$.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>

5.后端接收 bot

  • BackEnd 接收 Bot

WebSocketServer接收到匹配请求,将bot传给匹配服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
WebSocketServer.java

package com.kob.backend.consumer;

...

@Component
// url链接:ws://127.0.0.1:3000/websocket/**
@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"));
}

...
}

...
}
  • Matching System 接收 Bot

Matching System 接收到 backend 传的 botId,将 bot 传给 BotRunningSystem 服务

控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
MatchingController.java

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);
}

...
}

接口

1
2
3
4
5
6
7
8
9
10
MatchingService.java

package com.kob.matchingsystem.service;

public interface MatchingService {

String addPlayer(Integer userId, Integer rating, Integer botId);

...
}

接口实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
MatchingServiceImpl.java

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";
}

...
}

匹配池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
MatchingPool.java

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Player.java

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;
}

匹配池返回结果加上 botId

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 接收匹配成功返回的 botId

StartGameController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
StartGameController.java

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.java

1
2
3
4
5
6
7
8
StartGameService.java

package com.kob.backend.service.pk;

public interface StartGameService {

String startGame(Integer aId, Integer aBotId, Integer bId, Integer bBotId);
}

StartGameServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
StartGameServiceImpl.java

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.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
WebSocketServer.java

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.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Player.java

package com.kob.backend.consumer.utils;

...

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Player {
private Integer id;
private Integer botId; // -1表示亲自出马,否则表示用AI打
private String botCode;

...
}

WebSocketServer.java

将RestTemplate变成public,若是代码输入则屏蔽人的输入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
WebSocketServer.java

package com.kob.backend.consumer;

...

@Component
// url链接:ws://127.0.0.1:3000/websocket/**
@ServerEndpoint("/websocket/{token}") // 注意不要以'/'结尾
public class WebSocketServer {
...

public static RestTemplate restTemplate;

private void move(int direction) {
if(game.getPlayerA().getId().equals(user.getId())) {
if(game.getPlayerA().getBotId().equals(-1)) // 亲自出马则接收输入
game.setNextStepA(direction);
} else if(game.getPlayerB().getId().equals(user.getId())) {
if(game.getPlayerB().getBotId().equals(-1))
game.setNextStepB(direction);
}
}

...
}

Game.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
Game.java

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) { // 将当前局面信息编码成字符串
// 地图#my.sx#my.sy#my操作#you.sx#you.sy#you操作
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) {
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));
WebSocketServer.restTemplate.postForObject(addBotUrl, data, String.class);
}

// 接收玩家的下一步操作
private boolean nextStep() {
// 每秒五步操作,因此第一步操作是在200ms后判断是否接收到输入。
try {
Thread.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}

sendBotCode(playerA);
sendBotCode(playerB);

...
}

...

}
  • 测试

BotRunning System 的实现

思路:

1
2
3
4
评测器是一个经典的生产者消费者模型,此服务生产者会将任务放进一个队列中,
消费者是单独的一个线程,当有任务就会从队头立即执行;并且关键问题是评测
器的执行代码不能单纯的用sleep 1s去判断是否有任务,这样很影响评测体验,
因此需要用到Condition Variable,当有任务执行,无任务等待。
  • 文件结构
1
2
3
4
5
6
7
8
9
10
matchingsystem
service
impl
utils
Bot.java
BotPool.java
Consumer.java
utils
Bot.java
BotInterface.java
  • Bot 的实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Bot.java

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;
}
  • BotPoll 的实现

虽然队列没用消息队列,但是因为我们写了条件变量与锁的操作,所以等价于消息队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
BotPool.java

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.java

加任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
BotRunningServiceImpl.java

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 线程的启动
1
2
3
4
5
6
7
8
9
10
11
12
13
BotRunningSystemApplication.java

package com.kob.botrunningsystem;

...

@SpringBootApplication
public class BotRunningSystemApplication {
public static void main(String[] args) {
BotRunningServiceImpl.botPool.start();
SpringApplication.run(BotRunningSystemApplication.class, args);
}
}
  • BotInterface.java

用户写Bot实现的接口

1
2
3
4
5
6
7
8
BotInterface.java

package com.kob.botrunningsystem.utils;

public interface BotInterface {
Integer nextMove(String input);
}

  • Bot.java
1
2
3
4
5
6
7
8
9
10
11
Bot.java

package com.kob.botrunningsystem.utils;

public class Bot implements BotInterface {

@Override
public Integer nextMove(String input) {
return 0; // 向上走
}
}
  • Consumer 的实现

docker 与沙箱分别有什么区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
Consumer.java

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);
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);
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);
}
}
  • BotPool.java

调用Consumer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
BotPool.java

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 的修改

在游戏结束后,点到其他页面再点回pk页面,结果没有消失

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 执行结果传给前端

1.BackEnd 接收 Bot 代码的结果

  • 文件结构
1
2
3
4
5
6
7
8
9
10
backend
controller
pk
ReceiveBotMoveController.java
service
impl
pk
ReceiveBotMoveServiceImpl.java
pk
ReceiveBotMoveService.java
  • 接口
1
2
3
4
5
6
7
8
ReceiveBotMoveService.java

package com.kob.backend.service.pk;

public interface ReceiveBotMoveService {

String receiveBotMove(Integer userId, Integer direction);
}
  • WebSocketServer 操作类

将game改为public

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
WebSocketServer.java

package com.kob.backend.consumer;

...

@Component
// url链接:ws://127.0.0.1:3000/websocket/**
@ServerEndpoint("/websocket/{token}") // 注意不要以'/'结尾
public class WebSocketServer {
...

public Game game = null;

...
}
  • 接口实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
ReceiveBotMoveServiceImpl.java

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";
}
}
  • 控制器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ReceiveBotMoveController.java

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);
}
}
  • 权限控制(网关)

放行接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
SecurityConfig.java

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);
}

...
}

2.BotRunningSystem 返回 Bot 执行结果

  • Consumer.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
Consumer.java

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);
}
}
  • 测试

y总bot代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
Bot.java

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;
}
}