谨记:找不出问题是最大的问题
这一次的课程笔记,就不再把全部写过的代码都拆开一行一行,咋写的啥时候写的写成什么样详细的写出。不然就变成了y总的视频内容复述员了。对于这一次代码仅仅将其拆分成两个部分:逻辑的开发过程以及问题处理的方法,
项目地址
一.基本逻辑
游戏的基本逻辑
用Bot进行判断的逻辑流程
如下图所示,红线是媒介bot_id在整个大系统的三个模块中的传递方向
对于Bot
的服务,整体处理在BotRunningSystem
中。在BotRunningSystem
中有一个单独的线程Botpool
,这个线程会不断的执行代码,每次执行一次代码就去把执行的结果(移动的过程)返回给处理移动的nextstep
,如果在等待时间之内nextstep
函数没有能够接收到处理的指令,那么游戏就结束,如果两名Bot的下一步操作都有获取到,那么就进入到Judge
来判断合法性,直到结束
在这里面用到了一个依赖,依赖为joor
用它来接收一段代码,将代码插入到队列当中,每次从队列里取出代码运行,运行结束后将代码返还给用户
(一).创建后端模块BotRunningSystem
在模块的pom.xml
中添加依赖joor
这个依赖就是能够负责在Java中动态编译Java代码。由于目前实现的逻辑相对较为简单,因此想要实现其他语言的话,后面在把项目上线的时候,需要把BootPool
线程替换成docker
,给docker
设置一个内存上限,再去docker
中执行一段代码。用setTimeOut
去限定时间,这样既可以保证安全又可以支持其他语言,
1.后端代码的构建
首先给Main
入口添加注解@SpringBootApplication
然后开启线程服务
BotRunningServiceImpl.botPool.start();
1.1 创建一个API
使用后端三件套service
、controller
、impl
来创建一个API,使得可以接收一个Bot的代码,然后把它放进BootPool
线程里。
1.1.1首先定义接口BotRunningService
,
在接口里实现函数addBot()
其中传入几个信息:bot
所属的userId
,bot
的代码,bot
的输入
1.1.2 实现接口impl
为了方便调试把传递过去的参数进行一下输出
1.1.3 调用接口controller
添加注解并且定义url
为/bot/add/
,注入@Service
注解,用MultiValueMap
来传递三个参数
要注意的是在传入参数的时候要记得添加requireNonNull
最后直接返回
return botRunningService.addBot(userId,botCode,input)
1.1.4下一步:放行网关
复制RestTemplate
与SecurityConfig
,在里面将/url/add/
放行。
在application.properties
中添加端口3002,以启动项目查看是否能够运行
(二).修改前端与后端
1.实现页面加框
在bootstrap
中找一个合适的样式,并且调整两个游戏对象以及中间的选择框的占比为1/4.
对于整个选择框来说,默认的value值为-1,如果不是人的话,默认为非负数
2.动态获取当前的bot
列表
在UserBotIndedView
中实现了一个函数为refresh_bots()
用来动态的获取当前的bot
列表.
将refresh_bots()
函数放进MatchGround.vue
中,首先在里面定义一个空的列表为bots
返回refresh_bots()
(一定要返回)然后返回bots
数组,来让云端拉取。
在option
组件中添加v-for
,用bot_id
以循环的形式输出到前端页面中
v-for="bot in bots" :key="bot.id" :value="bot.id"
{{ bot.title}}
3.传递Bot信息给前端
在前端中添加双向数据绑定,并且初始化为-1,将其返回
let select_bot =ref(-1)
并且给选择的<select>
标签进行v-model
绑定
3.1修改所有的通信,将后端的通信添加bot_id
第一步是从客户端往3000端口服务器传递时,需要传递bot_id
也就是onMessage
的通信的位置
第二步是server
端在向Matchingsystem
模块(3001)中发送信息的时候,需要一个bot_id
也就是在调用这个线程的startMatching()
函数中,添加传递的参数为botId
在3001后端的模块里要接收这个botId
需要在controller
层的addPlayer
中添加botId
,并且返回给service
层的MtachingService
.
在给service
层中加上botId
之后就需要在impl
中也加上botId
当botId
传进MatchingSystem
之后,需要把它存进Player
类里。在MtachingPool
中有函数负责添加玩家,将botId
放进去
第四步 辅助函数sendResult
在从MatchingPool
中返回结果时
需要用到botId
来帮助匹配,因此不仅需要返回userId
还需要返回botId
此外接收的一方也需要接收botId
,对应的在匹配成功之后的启动游戏的controller,
即StartGameController
,
接收的一方位于backend
,即3000端口的后端
以接收对战双方A,B的bptId
由于controller
层经历了修改那么因此StartGameService
和StartServiceImpl
也需要传入两个botId
第五步:在impl
层里将两个botId
传给WebSocketServer
对应的StartMatching
函数
通过这样的方法,整个后端的BotId
的传送链已经形成,
下一步的操作,是根据BotId
能够把代码取出来
至于为什么要传递bot,可以有如下的解释原因:匹配系统模块需要直到用户是谁,用户的bot
是谁,因此需要用到botId
,需要返还botId
给server
端,因此匹配的时候就要存下来botId,否则就不能传回botId
.
对于Server
端,需要用botId
来初始化角色,因此每一步都需要存下botId
来用作传递。
4.根据BotId
把bot
的代码取出来
要想取出bot
的代码,需要的是从数据库中取出,从数据库中取出数据需要用到Mapper
.在WebSocketServer
中定义Mapper并且注入。
在开始游戏前的进程StartGame
函数里,取出两个Bot
,取出来的bot
需要传给Game
类进行后端渲染并且代码执行
在Game
里面进行构造。取出bot
的Id
与代码
在获取的时候由于botId
可能为-1(也就是人来操作),这种情况下的botCode
为空,因此需要在这个前提下添加判空操作
在初始化两个对象playerA
与plyaerB
的时候,将Id
与代码统统传过去
5.判断是人还是代码
对于这一步的操作,如果判断是人,需要向BotRunningSystem
中传递代码并且编译代码来让蛇移动。如果是人则需要等待输入。这一部分的操作,通过用在Game
类里的sendBotCode()
来进行判断并且存储数据。
然后在nextStep()
函数里分别两名用户调用并且判断。因为两名玩家选择是使用人还是使用Bot
的选择不一致。
在sendbotCode
函数里当判断为bot
的时候,需要传入三个参数,分别是user_id
、bot_code
以及input
对于input
函数,由于需要传过去当前点游戏局面,因此需要定义辅助函数getInput()
为了能够严格的屏蔽人或者bot
的混淆操作,需要在move()
中添加判断操作
if(game.getPlayerA().getBotId().equals(-1))
以判断是否是人工,当为人工的时候放行操作
5.1 辅助函数getInput()
getInput
函数的最主要的任务是将当前的局面信息编码成字符串存储,主要的工作就是信息压缩,具体的压缩方式为
地图#meSx#meSy#w我的操作#对手的Sx#对手的Sy#对手的操作序列。
在进行信息压缩之前,首先需要判断的是谁是主谁是客。
在进行信息压缩的时候,由于操作序列可能为空,因此要人为的添加括号防止出现空的问题
(三).实现微服务BotRunningSystem
对于BotRunningSystem
需要实现的是不断的接收用户的输入,当接收的代码比较多的时候,将这些代码放进队列中,这是一个典型的生产者-消费者模型。
简单的介绍:生产者-消费者模型
有一个队列来存储当前的所有的任务,生产者每发送一个任务,就去把任务放到队列里,消费者说白了就是一个线程,一个苦力,每完成一个工作,就去检查队列是否为空,如果不空,则就从队头拿出来一个代码执行一遍,执行完之后再检查是否为空,循环往复。
1.实现消费者线程
消费者线程中需要处理Bot
的代码,因此要求有任务立即执行,这样的做法以保证用户体验,要想实现这样的效果,会用到conditionvirable
创建的Bootpool
需要继承自Thread
里面需要一个run
函数,在这个run
函数里,首先需要的是定义一些锁在这个循环与其他的不同的地方在于。如果队列为空将其堵塞,如果有消息则需要唤醒
在BotPool
中定义一个队列用来存储信息。由于队列里存的都是Bot
,因此需要存一个辅助的类为Bot
1.1Bot
类
依据存什么用什么的原则,在Bot
类里需要存下userId
、bot
的代码、bot
的当前的局面
1.1.1关于BootPool
中的run
里面的线程
首先先来解读一下原理:具体来说,这是一个死循环,每次操作队列,队列会在两个线程里面操作,一个是生产者不断的加任务,一个是消费者不断的消耗任务。两个线程会出现读写冲突,因此需要加锁,当当前队列为空的时候就需要阻塞,
阻塞线程用condtion
的await
的api,唤醒线程用signnal
或者signnall
当队列不空的时候,则需要取出队头,如果队列为空则需要阻塞。当队列不空并且解锁的话就需要去消费下任务。
消费一下队列需要用到consumer
函数
关于consumer
函数
对于这个函数的调用一定要在Lock
锁的后面,因为这个函数比较耗时,可能会执行几秒钟。其中的整个编译过程是很慢的,因此在执行这个代码之前的操作一定是先解锁,如果不解锁未来往队列里加代码的操作时会被阻塞掉,但是没有必要阻塞掉,只有涉及到读写冲突的时候才需要堵塞。一旦取完队头,进程与队列就没有关系了,这样就不会产生读写冲突,因此锁在执行之前一定要提前释放。
1.1.2 定义函数addBot
以实现在队列里插入一个bot
这个函数主要是向队列添加一个新的bot
这个函数会在BotRunningServiceimpl
中调用
在impl
中调用的时候,需要单开一个线程,因此需要开一个静态变量把所有线程都存下来
1.2 实现线程的启动
线程的启动仿照之前的匹配系统matchingSystem
对于启动的入口中调用匹配池的botPool
函数启动线程。
综上所述:消息队列就是这么实现的,首先有一个地方可以不断的往里加任务,加任务的时候会有锁,获取到锁则增加任务,如果锁被另外线程拿住的话,会阻塞在lock.lock()
中,当获取完这个锁之后,会将Bot
加到队列里面,在加完之后一定要唤醒另一个线程
再说明Signal
:唤醒任意线程 singnalll
唤醒所有的线程。
2.实现Bot
代码的编译,即实现Consumer
函数
声明:当前实现的仅仅是Java代码的编译,如果想能够编译其他的代码,前往搜索引擎搜索:Java
如何执行docker
代码
在一开始已经说过,实现编译用到的是Joor
以实现动态的编译。
为了能够保证整个过程的时间可控,把它放进线程中,以支持当超时的时候,断掉运行程序。
2.1首先先定义一个进程
定义一个类为Consumer
,Consumer
类继承自Thread
在类里面重载一个run
函数
为了能够执行进程操作,定义函数为startTimeOut()
在这个函数中,重要的是控制线程的执行时间,因为超时会断掉运行程序。
this.join(tiemout)
解读整个流程:线程启动,去开一个新的线程执行run
函数,当前线程执行join
。当前线程在join
函数下等待tiemout
/新的线程执行完毕,之后再去执行后面的线程操作。如果在最多timeout
的时间间隔里还没有执行结束,则中断掉。
中断的API
为this.interrupt()
为何不能选择sleep
:sleep
函数具有限制性,尽管时间结束,但是依旧要跑完时间,join
操作是在时间结束的时候立刻去执行下一步,相比之下join
执行更具有立即性。
2.2 在run
函数里编译代码
实现编译bot
的代码,需要定义辅助接口BotInterface
,来用来实现前端用户编写AI的接口,在这里面定义接口函数nextMove()
,用来获取下一步的方向。
在run函数调用这个辅助接口,以实现编译。
BotInterface botInterface =Reflect
其中这个Reflect
是添加的依赖,可以动态的实现编译代码。
使用人和一个简单的Bot
进行配置,其中的运行情况如下:
2.2.1 模拟前端传入编译
模拟整个从前端提取代码然后把Bot
代码编译的过程。
定义Bot
,继承BotInterface
这样在Reflect
对象里使用的名称为com.kob.newbotrunningsystem.utils.bot
,代码就是Bot
的代码
对于编译有一个问题,就是说,同名的类仅仅会编译一次,由于每个用户的代码都不一样
解决这个问题需要用到随机字符串为UUID uuid =UUID.random(UUID)
返回前8位,在名称的后面添加这个随机字符串。
不仅如此,代码的类名也需要添加随机字符串,添加这样的字符串就需要用到一个逻辑去加。
逻辑为,新开一个字符串,匹配某一行代码的后面的字符串,匹配完成后在字符串的前面+随机字符串,具体的样式类型为
Bot + 要匹配出来的字符串,
具体的匹配的逻辑如下:
将这个类编译完之后创建一个类,以保证获取
.create().get()
2.3 实现最后的进程运行—BotPool
中的Consumer
函数
在这个函数里,调用之前的startTimeOut()
函数,来定义Bot的执行时间
对于Bot
的运行时间,两个Bot
要求一共等待5秒,一个Bot一共等待或者执行2s,拿出一秒来做冗余是一种比较合理的运行状态
2.4 Server
端接收代码的操作
在BotPool
模块中编译完成代码之后,需要把编译出来的移动方向的direction
在server
端取出来,以映射到前端页面。
实现这个接口就需要用到三件套server
impl
controller
分别为
ReceiveBotMoveService
ReceiveBotMoveServiceImpl
ReceiveBotMoveController
在controller
中,用post
方法进行链接。链接方法为post
方法,链接为/pk/receive/bot/move/
,然后把这个 链接进行放行。
下一步,就需要把编译的代码的得到的移动结果传递给nextstep
在WebSocketServer
中的move
函数的逻辑是来执行人的操作,执行机器的操作传递给Game
类的方法与之是类似的。具体写法见图
通过这样的方式把操作从server
传到Game
中,等待前端渲染
2.5 将代码编译出来的移动信息返还给后端server
首先要做的是给Bot的代码模块:BotRunningSystem
来创建restTemplateConfig
以注入restTemplate
,这样就可以去cousumer
类中的run
函数把整个的移动路径给返回。然后定义全局urlreceiveBotMoveUrl
把数据信息加载到链接上,最终通过restTemplate
将数据返回。
部分的代码逻辑如下所示:
(四)。写一个相对较为智能的AI
较为智能智能在了可以自动的规避障碍物。在这里面一开始需要用到cell以存储这条蛇的所有的位置,标注x和y为蛇的初始位置。
在这个AI里面需要编码解码
String [] strs =input.split("#")
先初始化地图,在初始化地图的时候需要枚举一个一维数组,这是一个蛇的身体,第一位为蛇头,在初始化的时候将其标注出来。
下一步就是取出画出两条蛇的移动路径。
在最原始的bakcend
的模块中的utils
里的Player
类里有相关的函数getCells()
与check_tail_increasing
来控制整个蛇身的变换
传入sx,sy
以及String
类型的Steps
这样就需要改成采用蛇的身体steps.length()
来循环枚举
定义方向坐标d,这个方向坐标定义为:d=steps.charAt(i)-'0'
,-0
的目的就是将其转化成一个整数。
之后去nextMove
中将两条蛇的身体取出来。
为了方便首先先取出两条蛇的起点,人为规定1 是A的起点的横坐标2是A的起点的纵坐标,3为操作4为B起点横坐标,5为B起点纵坐标,6为B的操作。
取出身体并且标注身体之后,用上下左右的偏移量枚举四个方向,判断哪个方向能走,以描出整个的蛇的运动路径。
注意在解码的时候,要人为的把括号去掉,否则在编译的时候会报错,如下所示:
修改这种问题需要写成这样的语句
steps =steps.substring(1,steps.length()-1);//去掉之前编码的括号
具体的实现逻辑见项目的utils
的Bot
后续如果想需要更厉害的Bot可以用到α
剪枝。
二.问题处理
问题一.Maven
的配置出现问题。
每次项目配置一次Maven
,每次都有新错误。
这一次的错误如下:
但是这个一直解决无果,于是删除了模块重新创建模块,在重新创建之后,pom.xml
显示已忽略
解决这种显示已经忽略的方法为:在设置中找到maven
,在maven
里将被打勾的选项注释掉,以解决方案。
解决方法见下图:
重新配置后,发现还是出现报错,这次出现的是未解析的依赖项
原本以为能够套用之前的错误方法来解决后来发现未果,与其他已经配置好的pom.xml
进行比对,发现少了这一部分,添加之后即可解决问题
少了的这一部分见下图:
问题二:Bot的解析有误
出现这些问题的原因,首先是截取的字段少了implements
,导致匹配错误
再度报错出现问题是因为run
函数中的name
写成了com.kob.Bot
而不是com.kob.BotInterface
问题三,当从一个界面跳到另一个界面的时候,提示输赢的哪个框会一直存在
因此需要优化一下前端
在ResultBoard
里的逻辑已经表明如果想要关掉这个页面,先给updateLoser
中传空,因此在PKIndexView
中的setup
函数里,每次先加上一个状态更新,以清空输赢状态
问题四:socket
在连接一段时间后会断开
这个问题到目前来看一致无解,如果出现这种情况只能关机重启解决
问题五:两个Bot的问题
其实问题在于,两条蛇在越过障碍物之后不会停止更不会判断输赢,像下图:
原因其实是前端的页面的刷新的时间与Bot
代码编译的时候两者直接处理反馈的时间不一致,导致一快一慢在返回给前端停止页面变化的时候,双端出现了延迟
因此解决方案就是修改两个时间使其保持一致。
在重启之后即可保存正常
而且两个client
端也能够保持同步
佬!