1.SpringBoot解决跨域问题
package com.kob.backend.config;
import org.springframework.context.annotation.Configuration;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Configuration
public class CorsConfig implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) res;
HttpServletRequest request = (HttpServletRequest) req;
String origin = request.getHeader("Origin");
if(origin!=null) {
response.setHeader("Access-Control-Allow-Origin", origin);
}
String headers = request.getHeader("Access-Control-Request-Headers");
if(headers!=null) {
response.setHeader("Access-Control-Allow-Headers", headers);
response.setHeader("Access-Control-Expose-Headers", headers);
}
response.setHeader("Access-Control-Allow-Methods", "*");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Credentials", "true");
chain.doFilter(request, response);
}
@Override
public void init(FilterConfig filterConfig) {
}
@Override
public void destroy() {
}
}
2.SpringBoot配置Spring Security
是用户认证操作,一种授权机制,目的是安全。
(1)添加依赖:
spring-boot-starter-security
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.7.0</version>
</dependency>
刷新之后显示登陆:
默认的叫 **Username **是 user ,密码自动生成。
(2)Spring Security使用密文添加用户
@GetMapping("/user/add/{userId}/{username}/{password}/")
public String addUser(
@PathVariable int userId,
@PathVariable String username,
@PathVariable String password) {
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encodedPassword = passwordEncoder.encode(password);
User user = new User(userId, username, encodedPassword);
userMapper.insert(user);
return "Add User Successfully";
}
3.SpringBoot实现JwtToken验证
(1)添加依赖:
在 pom.xml 中添加下列依赖:
- jjwt-api
- jjwt-impl
- jjwt-jackson
添加之后点击重新加载。
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
(2) 编写、修改相关类
- 实现 JwtUtil 类,在 backend 目录下创建软件包 utils 并创建 JwtUtil 类。
JwtUtil 类为jwt 工具类,用来创建、解析 jwt token。
package com.kob.backend.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;
@Component
public class JwtUtil {
public static final long JWT_TTL = 60 * 60 * 1000L * 24 * 14; // 有效期14天
public static final String JWT_KEY = "SDFGjhdsfalshdfHFdsjkdsfds121232131afasdfac";
public static String getUUID() {
return UUID.randomUUID().toString().replaceAll("-", "");
}
public static String createJWT(String subject) {
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());
return builder.compact();
}
private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
SecretKey secretKey = generalKey();
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if (ttlMillis == null) {
ttlMillis = JwtUtil.JWT_TTL;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
return Jwts.builder()
.setId(uuid)
.setSubject(subject)
.setIssuer("sg")
.setIssuedAt(now)
.signWith(signatureAlgorithm, secretKey)
.setExpiration(expDate);
}
public static SecretKey generalKey() {
byte[] encodeKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
return new SecretKeySpec(encodeKey, 0, encodeKey.length, "HmacSHA256");
}
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(jwt)
.getBody();
}
}
- 实现 JwtAuthenticationTokenFilter 类
在 backend 的 config 目录下创建 config 软件包,并创建 JwtAuthenticationTokenFilter 类。
实现 JwtAuthenticationTokenFilter 类,用来验证 jwt token ,如果验证成功,则将 User 信息注入上下文中。
package com.kob.backend.config.filter;
import com.kob.backend.mapper.UserMapper;
import com.kob.backend.pojo.User;
import com.kob.backend.service.impl.utils.UserDetailsImpl;
import com.kob.backend.utils.JwtUtil;
import com.sun.istack.internal.NotNull;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private UserMapper userMapper;
@Override
protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("Authorization");
if (!StringUtils.hasText(token) || !token.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
token = token.substring(7);
String userid;
try {
Claims claims = JwtUtil.parseJWT(token);
userid = claims.getSubject();
} catch (Exception e) {
throw new RuntimeException(e);
}
User user = userMapper.selectById(Integer.parseInt(userid));
if (user == null) {
throw new RuntimeException("用户名未登录");
}
UserDetailsImpl loginUser = new UserDetailsImpl(user);
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
}
}
配置config.SecurityConfig类
放行登录、注册等接口。
package com.kob.backend.config;
import com.kob.backend.config.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
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;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/user/account/token/", "/user/account/register/").permitAll()
.antMatchers(HttpMethod.OPTIONS).permitAll()
.anyRequest().authenticated();
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
}
4.Springboot实现匹配系统
(1)匹配系统的流程
要实现匹配系统起码要有两个客户端client1,client2,当客户端打开对战页面并开始匹配时,会给后端服务器server发送一个请求,而匹配是一个异步的过程,什么时候返回结果是不可预知的,所以我们要写一个专门的匹配系统,维护一堆用户的集合,当用户发起匹配请求时,请求会先传给后端服务器,然后再传给匹配系统处理,匹配系统会不断地在用户里去筛选,将rating较为相近的的用户匹配到一组。当成功匹配后,匹配系统就会返回结果给springboot的后端服务器,继而返回给客户端即前端。然后我们就能在前端看到匹配到的对手是谁啦。
(2)websocket协议
因为匹配是异步的过程,且需要前后端双向交互,而普通的http协议是单向的,一问一答式的,属于立即返回结果的类型,不能满足我们的异步需求,因此我们需要一个新的协议websocket:不仅客户端可以主动向服务器端发送请求,服务器端也可以主动向客户端发送请求,是双向双通的,且支持异步。简单来说就是客户端向后端发送请求,经过不确定的时间,会返回一次或多次结果给客户端。
基本原理: 每一个ws连接都会在后端维护起来,客户端连接服务器的时候会创建一个WebSocketServer类。每创建一个链接就是new一个WebSocketServer类的实例,所有与链接相关的信息,都会存在这个类里面。
(3)前面逻辑的优化
由于每次刷新都会刷新不同的地图,为了公平起见,应该把地图的生成放在服务器端,然后返回结果给前端。
为了防止作弊,游戏的一系列操作、判断逻辑都应该放在服务器端,前端只是呈现动画。
我们可以从客户端获取输入,也可以通过微服务从代码端获得输入。
简单的流程如下: Game -> Create map -> 返回给客户端 -> 客户端等待匹配waiting (sleep) -> 匹配成功则进行一系列游戏逻辑
用线程安全的set定义匹配池:
final private static CopyOnWriteArraySet<Users> matchPoll = new CopyOnWriteArraySet<>();
开始匹配时,将用户放进拼配池里,取消匹配时将用户移除匹配池,匹配过程在目前调试阶段可以简单地两两匹配
解决同步问题
前文也提到过,生成地图,游戏逻辑等与游戏相关的操作都应该放在服务端,不然的话客户每次刷新得到的地图都不一样,游戏的公平性也不能得到保证。因此,我们要将之前在前端写的游戏逻辑全部转移到后端(云端),前端只负责动画的演示即可。
实现游戏同步
实际上我们在游戏对战的时候存在三个棋盘,两个是对战双方客户端里存在的棋盘,一个是云端存在的棋盘,我们要求实现云端与两个客户端之间的同步。
实现方法:
游戏总流程示意图:
Matching System <---------- WS服务器 <--------------Client1 、client2
·····································|
·····································|
·····································|(维护)
·····································|
···································Game
·····································|
·································CreateMap
·····································|
······(有时间限制)·············Next Step(等待玩家或bot输入) <------Client1 (Client2…) 或 bot
·····································|
································Judge System判断两名玩家下一步走法是否合法
·····································|
··································Result (Nest Step超时的话直接返回Result)
引入线程:
为了优化游戏体验度,我们的Game不能作为单线程去处理,每一个Game要另起一个新线程来做。
从Next Step开始的操作可以当成一个线程,获取用户操作可以当成另一个线程。
这里我们涉及到两个线程之间进行通信的问题,以及线程开锁解锁的问题。
每一局单独的游戏都会new 一个新的Game类,都是一个单独的线程
将类改成多线程
继承一个 Thread类,并且ALT + INS重写run()方法
我们开始进行线程的执行的时候,线程的入口函数就是这个run()函数consumer/utils/Game.java.
将用户的操作nextStep存起来,方便外面的线程调用,
在Game线程里面会读取两个玩家的操作nextStepA/B的值,
在外面Client线程里面则会修改这两个变量的值,
这里涉及到了线程的读写同步问题!
需要加上进程同步锁
一般来说就是先上锁再读写,后解锁
try{} finally {lock.unlock();}可以保证报异常的情况下也可以解锁而不会产生死锁
简单总结一下就是:先上锁再操作,具体可以参考OS相关的内容o(╯□╰)o
所以以下涉及到nextStepA 和 nextStepB 的,不管是读还是写,只要出现了的话就要考虑到上锁和解锁方面的问题了,consumer/utils/Game.java
实现接受客户端玩家输入的操作
后端接受前端两名玩家输入的操作后,才开始进行下一步操作。为了游戏的流畅性,提高玩家的游戏体验感,我们规定,如果超过一定的时间后,另一名玩家仍然未能给予操作,我们就判定这个玩家lose了。
可以用sleep函数来实现等待效果,定最长等待时间为5s。
这里可以按照自己的情况合理地规定等待时间,可以通过增加循环次数,减少sleep时间优化玩家操作手感,以牺牲服务器的计算量换取玩家的操作的流畅性。
tips:要在循环里面上锁,在外面上锁会死锁!
还需要注意的是,我们前端设置1s走5步,200ms走一步,所以为了操作顺利,不会因为操作太快而读入多个操作,我们每一次读取前都要先sleep 200ms,规范一下
实现Try again逻辑
接下来我们把Try again按钮实现一下,玩家可以在游戏结束后点击这个按钮再来一局游戏。
实现逻辑也比较简单,每次点击按钮,把游戏页面展示状态status从playing 改成 matching即可,这样整个游戏页面就返回到匹配页面了。
不要忘记了要updateLoser改成none,即重新开始游戏前还没有loser
还有把对手头像updateOpponent成默认的灰头像。
设计录像数据库
为了后期存储对战录像,我们需要先设计一个存储对象的数据库。
数据库内容包括
Id record的id 自增 主键
a_id 用户a的id
a_sx 用户a在地图中的横坐标
a_sy 用户a在地图中的纵坐标
b_id 用户b的id
b_sx 用户b在地图中的横坐标
b_sy 用户b在地图中的纵坐标
a_steps 用户a走的步数
b_steps 用户a走的步数
Map 两个用户对战的地图信息
Loser 记录失败方的名字
Createtime 创建时间
建立相应的pojo,mapper层。
准备工作完成后,我们就可以开始写将数据写入数据库的逻辑了
consumer/utils/Game.java
private String getMapString() {
StringBuilder res = new StringBuilder();
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
if (mark[i][j]) res.append(1);
else res.append(0);
}
}
return res.toString();
}
private void saveRecord() {
Record record = new Record(
null, //因为之前创建数据库时是把id定义为自动递增,所以这里不用手动传id
playerA.getId(),
playerA.getSx(),
playerA.getSy(),
playerB.getId(),
playerB.getSx(),
playerB.getSy(),
playerA.getStepsString(),
playerB.getStepsString(),
getMapString(),
loser,
new Date()
);
WebSocketServer.recordMapper.insert(record); //ws里数据库的注入
}
至此,就完成了联机匹配和存储游戏对局数据的大部分内容。
JAVA注解大概分为两类
(1)一类是使用Bean,即是把已经在xml文件中配置好的Bean拿来用,完成属性、方法的组装;比如@Autowired , @Resource,可以通过byTYPE(@Autowired)、byNAME(@Resource)的方式获取Bean;
(2)一类是注册Bean,@Component , @Repository , @ Controller , @Service , @Configration这些注解都是把你要实例化的对象转化成一个Bean,放在IoC容器中,等你要用的时候,它会和上面的@Autowired , @Resource配合到一起,把对象、属性、方法完美组装。
@controller :标注控制层,也可以理解为接收请求处理请求的类。
@service:标注服务层,也就是内部逻辑处理层。
@repository:标注数据访问层,也就是用于数据获取访问的类(组件)。
@component 其他不属于以上三类的类,但是会同样注入spring容器以被获取使用。它的作用就是实现bean的注入
@AutoWired 就是在你声明了注册类后,可以用该注解注入进当前写的类中。
凡是子类及带属性、方法的类都注册Bean到Spring中,交给它管理;@Bean用在方法上,告诉Spring容器,你可以从下面这个方法中拿到一个Bean。调用的时候和@Component一样,用@Autowired 调用有@Bean注解的方法,多用于第三方类无法写@Component的情况。
微服务实现匹配系统
根据上一part的设计逻辑,我们可以用微服务去代替之前调试用的匹配系统,使匹配系统功能更加完善。
微服务:是一个独立的程序,可以认为是另起了一个新的springboot。
我们把这个新的springboot叫做Matching System作为我们的匹配系统,与之对应的是Matching Server,即匹配的服务器后端。
当我们之前的springboot也就是游戏对战的服务器后端backend Server获取了两个匹配的玩家信息后,会向Matching Server服务器后端发送一个http请求,而当Matching Server接收到了请求后,会开一个独立的线程Matching开始进行玩家匹配。
匹配逻辑也非常简单,即每隔1s会扫描当前已有的所有玩家,判断当前玩家的rating是否相近,能否匹配出来,若能匹配出来则将结果返回给backend Server(通过http返回)
实现手法:Spring Cloud
创建backendcloud
我们项目的结构会出现变化,要先创建一个新的springboot项目backendcloud作为父项目,包含两个并列的子项目Matching System和backend。
注意:backendcloud 创建时要引入Spring Web依赖,不然的话后面自己要在pom.xml里手动添加!
因为父级项目是不用写逻辑的,可以把他的整个src文件删掉。
配置pom.xml
<packaging>pom</packaging>
加上Spring Cloud依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2021.0.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
在backendcloud项目文件夹下创建两个模块:MatchingSystem, backend,相当于两个并列的springboot项目。
Matching System
配置pom.xml
将父项目里的spring web依赖转移到Matching System的pom.xml里
配置端口
在resources文件夹里创建文件application.properties
server.port = 3001
匹配服务的实现
和之前写的业务逻辑一样,先写个匹配的服务接口MatchingService,然后在Impl里实现对应的接口
这里提供参考逻辑:matchingsystem\service\impl\MatchingServiceImpl.java
@Service
public class MatchingServiceImpl implements MatchingService {
@Override
public String addPlayer(Integer userId, Integer rating) {
System.out.println("add player: " + userId + " " + rating);
return "add player successfully";
}
@Override
public String removePlayer(Integer userId) {
System.out.println("remove player: " + userId);
return "remove player successfully";
}
}
实现匹配的Controller
matchingsystem\controller\MatchingController.java
@RestController
public class MatchingController {
@Autowired
private MatchingService matchingService;
@PostMapping("/player/add/")
public String addPlayer(@RequestParam MultiValueMap<String, String> data) {
Integer userId = Integer.parseInt(Objects.requireNonNull(data.getFirst("user_id")));
Integer rating = Integer.parseInt(Objects.requireNonNull(data.getFirst("rating")));
return matchingService.addPlayer(userId, rating);
}
@PostMapping("/player/remove/")
public String removePlayer(@RequestParam MultiValueMap<String, String> data) {
Integer userId = Integer.parseInt(Objects.requireNonNull(data.getFirst("user_id")));
return matchingService.removePlayer(userId);
}
}
注意:这里用的是MultiValueMap,即一个键值key可以对应多个value值,一个key对应一个列表list
定义:MultiValueMap<String, String> valueMap = new LinkedMultiValueMap<>();
这里如果用@Requestparam + map接收所有参数的话会不严谨,因为若url返回的是多个参数的话,map只能接受一个参数,即一个value,有时候匹配的会返回多个rating相近的人的结果,这时候如果用map接收可能会产生一些蜜汁错误,因此用MultiValueMap的话可以省事点。。。
用到的api:
MultiValueMap.getFirst(key)返回对应key的value列表的第一个值。
设置网关
为了防止用户破坏系统,我们应该设置一定的访问权限,让自己的系统更加安全
这里可以仿照之前写过的SecurityConfig
添加spring security依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.7.1</version>
</dependency>
配置SecurityConfig
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
...
.antMatchers("/player/add/","/player/remove/").hasIpAddress("127.0.0.1") //只允许本地访问
...
}
设置Matching System项目的启动入口
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MatchingSystemApplication {
public static void main(String[] args){
SpringApplication.run(MatchingSystemApplication.class,args);
}
}
实现收到请求后的匹配具体逻辑
思路:把所有当前匹配的用户放在一个数组(matchinPool)里,每隔1s扫描一遍数组,把rating较接近的两名用户匹配在一起,随着时间的推移,两名用户允许的rating差可以不断扩大,保证了所有用户都可以匹配在一起。
在Impl文件夹里新建一个utils工具包,编写MatchingPool.java和Player.java类(对应于上面的数组和用户信息)
MatchingPool.java是一个多线程的类,要继承自Thread类
在匹配服务里把实现添加与删除用户的逻辑
匹配逻辑:搞个无限循环,周期性执行,每次sleep(1000),若没有匹配的人选,则等待时间++,若有匹配的人选则进行匹配。匹配的rating差会随着等待时间而增加(rating差每等待1s则*10)。
匹配原则:为了提高用户体验,等待时间越长的玩家越优先匹配。即列表players从前往后匹配。用一个标记数组标记有没有匹配过即可,checkMatched()是判断这两个玩家是否能成功匹配在一起。sendResult()是发送匹配结果。
TIPS:这里标注一下我初学遇到的坑点,ArrayList循环删除某个元素不能直接循环一遍然后remove,因为每次循环的时候,ArrayList的size()都会改变,所以循环是有问题的,这样只能保证你删掉一个符合要求的元素,而不能实现循环删掉所有符合要求的元素,因此我们要从另一个角度思考问题,用一个新的ArrayList存放每一个不需要删除的元素,然后原来的引用直接指向新的List即可。
这里也提供另一种实现循环remove的方法:用迭代器Iterator
Iterator<Player> iterator = players.iterator();
while (iterator.hasNext()) {
if (要删除的条件) iterator.remove();
}
但是我们上面的删除还涉及到used数组,所以迭代器删除法并不适合,所以要用新列表赋值法!!
对于checkMatch判断两个玩家是否能成功匹配,还要考虑其等待时间,要判断分差能不能小于等于a与b的等待时间的最小值*10即 ratingDelta<=min(waitingTimea,waitingTimeb)∗10
private boolean checkMatched(Player a, Player b) { //判断两名玩家是否匹配
int ratingDelta = Math.abs(a.getRating() - b.getRating());
int waitingTime = Math.min(a.getWaitingTime(), b.getWaitingTime());
return ratingDelta <= waitingTime * 10;
}
接收匹配成功的信息
我们要在backend端再写一个接受MatchingSystem端匹配成功的信息的Service和相应的Controller
Matching System调用ws端的接口
为了实现springboot之间的通信,我们要像前文一样使用一个Bean类,方法为调用RestTemplate类。即上文的RestTemplateConfig.java
为了能让Spring里面的Bean注入进来,需要在MatchingPool.java里加上@Component
对于匹配时断开连接的处理
如果一名玩家开始匹配后断开了连接,按照我们上面的做法,断开连接后的玩家会一直处于匹配池中,这样我们的Matching System后端会报错,因为我们凡是要获取玩家信息的时候,该玩家已经掉线了,不存在了,会get一个空玩家信息,空信息是没有属性的,而我们后面会调用玩家属性,这是不合理的,肯定会报错的,我们需要修改这个bug:在每次get之前都要判断一下玩家信息是否为空,若不为空再进行下面的逻辑。
5.Springboot实现简易AI对战
Springboot实现类似OJ的代码执行功能
由于我们这个项目是可以通过执行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即可,这里就不再赘述了。
如上,我们的BotRunningSystem后端框架就搭建完了,还需要更改前端让用户可以选择用bot对战还是人工对战
工程课 SpringBoot-7.1. Springboot实现简易ai对战(https://www.acwing.com/solution/content/133480/)
6.Springboot创建排行榜和录像回放
我采用了Elo Rating System来作为我的rating变化机制
Elo Rating System是由匈牙利裔美国物理学家Arpad Elo创建的一个衡量各类对弈活动水平的评价方法,是当今对弈水平评估的公认的权威方法。被广泛用于国际象棋、围棋、足球、篮球等运动。网络游戏英雄联盟、魔兽世界内的竞技对战系统也采用此分级制度。
总结:
从Elo的工作模式中我们可以得出以下几点:
Elo会给出玩家一场对局的获胜概率。Elo积分相差越大,积分高的一方获胜概率就越大;
每一场对局后,对阵双方都会进行一部分积分交换,胜者得分,败者失分;
如果两名玩家的积分相差很大,代表高分方获胜的概率极大,因此即便赢了也涨不了多少分,败方也掉不了多少分。但倘若被低分方爆出冷门,那高分方将失去大量分数。