Kob
更新日志
- 完善用户展示页面 2022.10.21
- 添加第三方登录 2022.10.14
- 加入哈希值修改文件冲突问题 2022.10.9
- 修复贪吃蛇人机 2022.10.8
- 黑白棋: 完成人机对战 + 优化录像以及游戏界面 + 添加代码模板; 404页面完善 2022.10.7
- 修复bug : 删除贴子总是删掉第一个; 优化个人信息为组件
- 完成黑白棋代码执行 + 录像功能 2022.10.6
- 完成黑白棋基本逻辑, 实现人人对战 2022.10.4-5
- 初步完成前端黑白棋逻辑, 后端简单匹配逻辑完成, 实现用户间通信。后续将游戏逻辑放到后端 2022.9.29上午
- 修改前端结构, 方便添加游戏, 搭建黑白棋 2022.9.28
- 完成多语言执行Bot代码 2022.9.27
- 优化代码执行速度, 保证python2 python3 java cpp都能流畅执行 2022.9.26 下午+ 晚上
- 拆解pk模块, 方便后续添加多种游戏 2022.9.26 上午
- 添加多种语言, 但是游戏体验不是很好, 持续优化 2022.9.22-25
- 添加聊天模块, 真人对战前可以聊天~ ------ 2022.9.20-21
- 完善游戏介绍, 优化玩家掉线匹配池任然存在, 测试添加聊天模块 ------ 2022.9,19
- 添加自动匹配机器人, 变更前端添加游戏说明 + 自动打包上传脚本 ------ 2022.9.18
上线 SuperOfBot 1.0版本 – 2022年9月17号
对局记录 + 回放 + 排行榜 – 2022年9月15, 16
1. 分页的实现
- 后端
// new Page<>(第几页, 每页多少个);
IPage<T> IPage = new Page<>(page, 10);
QueryWrapper<T> query = new QueryWrapper();
query.orderByDesc("id");
// 相应api
List<T> lists = mapper.selectPage(recordIPage, query).getRecords();
- 前端
- 拉取某一页内容
const pull_page = page => {
current_page = page;
$.ajax({
url: "https://app3426.acapp.acwing.com.cn/api/record/page",
type: "get",
data: {
page
},
headers: {
Authorization: "Bearer " + store.state.user.token,
},
success (resp) {
records.value = resp.data.records;
total_records = resp.data.recordsCount;
update_page();
},
error (resp) {
console.log(resp);
}
})
}
- 展示分页组件, 效果: 最多展示五页, 点击哪页哪页亮, 并且动态计算前后两页是否存在
const update_page = () => {
// 计算最大页数
let max_pages = parseInt(Math.ceil(total_records / 10));
let new_pages = [];
// 遍历当前页的前两页后两页
for (let i = current_page - 2; i <= current_page + 2; i++) {
if (i >= 1 && i <= max_pages) { // 合法存储下来
new_pages.push({
number: i,
is_active: i === current_page ? "active" : "", // 当前页特定样式
});
}
}
pages.value = new_pages;
}
- 点击展示页面
const click_page = page => {
if (page === -2) page = current_page - 1; // 前进
else if (page === -1) page = current_page + 1; // 后退
let max_pages = parseInt(Math.ceil(total_records / 10));
if (page >= 1 && page <= max_pages) {
pull_page(page);
}
}
2. 录像实现
vuex-persistedstate组件持久化store, 使得刷新不会影响录像
import createPersistedstate from 'vuex-persistedstate'
plugins: [
createPersistedstate({
key: 'save',
paths: ['record', 'user']
})
]
- 复用PlayGround组件, 添加is_record标记
store记录信息:
1. 两名玩家的步数
2. 失败的玩家
3. 两名玩家基本信息
- 判断录像/直播, 通过setInterval函数不断执行下一步
if (this.store.state.record.is_record) {
let k = 0;
const a_steps = this.store.state.record.a_steps;
const b_steps = this.store.state.record.b_steps;
const loser = this.store.state.record.record_loser;
const [snake0, snake1] = this.snakes;
const interval_id = setInterval(() => {
if (k >= a_steps.length - 1) {
if (loser === "all" || loser === "A") {
snake0.status = "die";
}
if (loser === "all" || loser === "B") {
snake1.status = "die";
}
clearInterval(interval_id);
} else {
// 通过接口设置两条蛇的方向
snake0.set_direction(parseInt(a_steps[k]));
snake1.set_direction(parseInt(b_steps[k]));
}
k++;
}, 300);
}
Bot代码执行 – 2022年9月13-14日
修改前端, 传递bot信息
- 传递路径:
前端选择人或Bot开始匹配 ->
3000服务websocket中startMatching函数 ->
匹配系统添加玩家 ->
匹配池添加玩家(Player类添加botId信息), 进行匹配 ->
匹配成功, 发送信息添加BotId ->
3000服务接收匹配系统传递的数据, 调用startGame(添加botId参数) ->
Game类中添加相应玩家的bot信息, 在nextStep中判断是人工操作还是机器人操作向BotRunning服务发送信息
添加BotRunning服务
- 设计:
- Bot池:
单独的线程, 存储3000服务发送的bot信息, 使用自制消息队列控制池中bot
- run方法中循环方式:
只有当队列不为空时, 去执行相应方法, 其他时间阻塞; 使用Condition进行控制
- run方法中循环方式:
- Consumer:
单独的线程, 用来执行Bot代码
- Controller + Service :
提供相应的接口添加Bot信息
- Bot池:
- Bot池:
生产者消费者模型, 在对bot的操作时需要加锁, 因为涉及多个线程
- addBot方法:
提供给外界添加任务的方法
- condition.signalAll(): 当有任务进来时, 唤醒所有线程即当前阻塞的BOT_POOL, 会自己释放锁
- consume:
消费bot, 即开启线程去执行Bot代码
- run方法:
- 池为空时:
condition.await(), 释放当前锁, 阻塞当前线程; 异常需要手动释放锁
- 不为空:
拿出bot并进行消费, consume; 先释放锁再去消费, 因为执行代码比较耗时
- 池为空时:
- addBot方法:
- Consumer:
执行代码, 单独开启线程
- startTimeout(timeout, bot):
设置代码执行最长时间对线程进行控制, 当超出时间或者执行完毕中断当前线程
- 进来开启线程this.start(), 设置bot信息
- 如何进行控制:
join(timeout)方法: 线程执行完毕或timeout时间后, 执行join后面的代码(this.interrpt())
- run方法:
执行代码
- Reflect.compile(“package name”, “code”).create.get(); :
需要保证类名不一致, 即在类名后添加随机Id
- 生成的实例去执行接口响应的方法:
nextMove(当前局面)
, 将返回值发送给3000服务
- Reflect.compile(“package name”, “code”).create.get(); :
- startTimeout(timeout, bot):
- 3000服务接收下一步信息
- 我们已经中断了从前端获取输入进行移动, 需要重新调用之前进行移动的方法
game.setNextStepA(direction);
- 我们已经中断了从前端获取输入进行移动, 需要重新调用之前进行移动的方法
游戏完整的流程
- client1, client2点击开始匹配
- 3000服务通过websocket接受玩家信息, 发送给matching服务
- matching服务匹配池接收3000服务发送的玩家信息, 通过相应的策略匹配两名玩家, 发送给3000服务
- 3000服务接收对战玩家信息, 开启游戏startGame
- startGame创建Game线程(创建地图即相关信息), 通过nextStep获取输入
- nextStep
- 用户手动输入
- 判断输入合法性
- 合法: 发送信息给前端, 继续获取下一步输入
- 不合法: 结束游戏, 判断输赢
- 判断输入合法性
- Bot执行
- 发送bot信息给BotRunning服务
- BotRunning服务通过BOT_POOL接收bot信息进行处理
- Consumer消费bot, 生成下一步走向
- 发送给3000服务
- 判断输入合法性
- 合法: 发送信息给前端, 继续获取下一步输入
- 不合法: 结束游戏, 判断输赢
- 判断输入合法性
- 用户手动输入
匹配系统(下), 完善玩家匹配策略 – 2022年9月8日
新建微服务 - MatchSystem : 实现通过分值匹配玩家
- 创建匹配池
- 包含参数
- players:
池中的玩家
- lock :
3000服务会通过路由向匹配池添加/删除玩家, 匹配池中也会对玩家进程读写操作, 所以需要加锁控制
- RestTemplate :
发送请求需要的类
- players:
- 添加addPlayer, removePlayer方法 :
使用lock.lock try{ .. }finally{lock.unlock} 控制
- 实现匹配策略:
通过分值 + 时间匹配, 每增加一秒分值差距提升10
- increaseWaitingTime :
增加所有人的等待时间
- matchPlayers:
匹配玩家
- increaseWaitingTime :
- 包含参数
- 匹配池run函数
- while(true) + sleep(1000):
实现每隔一秒匹配一次
- lock.lock :
increaseWaitingTime与matchPlayers都对player有操作, 需要加锁
- while(true) + sleep(1000):
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
lock.lock();
try {
increaseWaitingTime();
matchPlayers();
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
}
-
MatchSystem添加API:
addPlayer与removePlayer
供3000服务添加与移除匹配玩家 -
3000服务接收匹配系统匹配玩家信息
public String startGame(Integer aId, Integer aBotId, Integer bId, Integer bBotId) {
System.out.println("start game: " + aId + " " + bId);
WebSocketServer.startGame(aId, aBotId, bId, bBotId);
return "start game success";
}
匹配系统(中) – 2022年9月7日
1. 前后端通信
- 图解
- 两个用户的一局游戏单独开一个线程实现: Game
- 游戏玩法是回合制, 所以需要lock的信息就是两名玩家的下一步 nextStep
- Game中所有涉及nextStep变量的地方都需要加锁
lock.lock();
try{
// do someting
}finally{
lock.unlock();
}
- 从run方法进行整体分析
- run方法中for循环1000次:
因为13 * 14的地图, 三步增长一次, 最多大概600步, 这里循环1000次保证正确
- nextStep函数, 返回一个boolean, 表示获取下一步成功或失败
- 进入直接sleep 200ms :
前端1s走5格. 200ms走1格, 这里保证了前端画完再去读取下一步操作
- for循环50次, 每次sleep100ms :
总时间5s, 如果5s内没有输入, 则判断获取下一步失败, 标记为finished
- sleep 100ms :
1s -> 100ms, 优化用户体验
- 进入直接sleep 200ms :
- 获取下一步成功
- 将前端
judge函数
放到后端 - judge后,
- 两条蛇运动都是正常的:
sendMove函数
- 否则,
sendResult函数
- 两条蛇运动都是正常的:
- 将前端
- 获取下一步失败
- 结束游戏:
修改游戏状态
- 通过nextStep判断平局, A输, B输
- 结束游戏:
- run方法中for循环1000次:
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
if (nextStep()) { // 是否获取两条蛇下一操作
judge();
if (status.equals("playing")) {
sendMove();
} else {
sendResult();
break;
}
} else {
status = "finished";
lock.lock();
try {
if (nextStepA == null && nextStepB == null) {
loser = "all";
} else if (nextStepA == null) {
loser = "A";
} else {
loser = "B";
}
} finally {
lock.unlock();
}
sendResult();
break;
}
}
}
- 信息交互
- 发送
- sendMove : 获取到两名玩家的nextStep ->封装信息 -> 通过WebSocketServer中的sendMessage发送给客户端
- sendResult : 封装信息 -> 通过WebSocketServer中的sendMessage发送给客户端
- 接收信息
- onMessage: 通过event处理对应信息
- 发送
匹配系统(上) – 2022年9月6日
- 配置websocket
- 实现匹配页面, 有待优化~
- 匹配信息存储
- socket
- 对手信息
- 地图信息 : 后端创建地图返回
优化前端, 添加额外的查看用户界面 –2022年9月5日
优化前端, 添加头像更新功能 – 2022年9月3日
- 简陋的添加头像hh
优化页面 + 集成个人空间项目 – 2022年9月2日
个人中心页面 + 优化 – 2022年9月1日
登录注册模块 + bot增删改查 – 2022年8-30~31
- 后端
- spring-security
- 配置
- 实现UserDetailsService类
- 实现UserDetails
- JwtAuthenticationTokenFilter
- SecurityConfig
- 配置
- Account 登录注册
- Bot 增删改查
- @Validated + 实体类传参 注意导入依赖
- spring-security
- 前端
- 集成登录注册页面
- 刷新不修改登录状态
导航栏 + 画蛇 + 键盘输入操作– 2022年8月29日
- Navbar
- 前端渲染 :
<router-link class="navbar-brand" :to="{ name: 'home' }"
:to="{name : '路由设置的名称'}"
- 设置后半部分导航栏靠右
class="navbar-nav me-auto mb-2 mb-lg-0" -> class="navbar-nav"
- 点击导航栏,点哪亮哪 ~ 动态确认active
const route = useRoute();
let route_name = computed(() => route.name);
:class="route_name == '对应路由名称' ? 'nav-link active' : 'nav-link'"
- 前端渲染 :
- router
- 去除#
createWebHashHistory -> createWebHistory
- 首先创建对应页面 :
pk、ranklist、record、bot、error、index(拓展使用)
- 路由格式
{path:"路径", component:"对应组件", name:"自定义名字"}
- 404路径细节
path : "/:catchAll(.*)"
- 去除#
- 游戏基类 : AcGameObject
- 游戏地图 : GameMap
- 参数
ctx : 画笔、 parent : 动态设置宽高
- 确认单位长度L
- min(parent.clientWidth / cols, parent.clientHeight / rows)
- 画地图, 通过奇偶确认格子颜色
- 参数
- 画障碍物 Wall
- 封住四周
- 随机封住中间位置
- 13行13列装换为13行14列?
- 13-13地图两条蛇的坐标分别是(11, 1)、(1, 11)相加为偶数, 之后的每一步可以确认
奇 -> 偶 -> 奇
有一定概率走到同一格中, 换成13-14两蛇的坐标分别是(11, 1)、(1, 12)之后的路径中不可能走到同一个格子
- 13-13地图两条蛇的坐标分别是(11, 1)、(1, 11)相加为偶数, 之后的每一步可以确认
- 如何保证两条蛇是联通的?
- Flood Fill算法
- 13行13列装换为13行14列?
- 随机并中心对称封路, 保证公平性
g[r][c] = g[this.rows - 1 - r][this.cols - 1 - c] = true;
const copy_g = JSON.parse(JSON.stringify(g)) 深拷贝不影响原地图
- 画蛇
- 组成 : 多个圆圈 + 填充中间矩形
- Cell
- 传入r,c
- 坐标转换, canvas坐标系向右为X轴、向下为Y轴
thix.r = r; thix.c = c; this.x = c + 0.5; this.y = r + 0.5
- 监听键盘, 判断是否运动
- canvas添加属性 tabIndex = “0”
- 去除黑框 :
canvas:focus {outline: none;}
- 去除黑框 :
- 获取键盘输入事件
this.ctx.canvas.addEventListener("keydown", e => {处理逻辑, 'WASD'左下角动, '上下左右'右上角动}
- 当两个蛇都获取到可以运动的键盘输入时才可以动
- canvas添加属性 tabIndex = “0”
- 如何运动
- 1 获取到下一个点的位置
- 2 将蛇往后移一位, 多出一个蛇头
- 让新蛇头向下一个点运动
- 重合时, 将下一个点赋给蛇头
- 3 蛇尾的处理
- 1 长度变化, 不需要处理蛇尾
- 2 长度不变
- 蛇尾向前一个点运动
- 重合时删除蛇尾
- 画身体
- 方法 : 填充矩形
- 1 重合 : 跳过
- 2 竖直方向, x确定
- 起点坐标
- x = x - 0.5 => 瘦身后 : x = x - 0.4
- y = min(a.y, b.y)
宽 : L
瘦身后宽 : L * 0.8
- 高 : abs(a.y - b.y)
- 起点坐标
- 3 水平方向, y确定
- 起点坐标
- x = min(a.x, b.x)
- y = y - 0.5 => 瘦身后 : y = y - 0.4
- 宽 : abs(a.y - b.y)
高 : L
瘦身后高 : L * 0.8
- 起点坐标
- 碰撞检测
- 墙
- 蛇身
- 细节 : 如果蛇尾会前进的时候, 蛇尾不要判断
if(!snake.check_tail_increasing()) k--;
- 细节 : 如果蛇尾会前进的时候, 蛇尾不要判断
- 画眼睛
- 设置偏移量
- 确定位置
eye_x = (this.cells[0].x + this.eye_dx[this.eye_direction][i] * 0.15) * L
eye_y = (this.cells[0].y + this.eye_dy[this.eye_direction][i] * 0.15) * L
初始化项目 – 2022年8月28日
- 创建Backend
- 创建Web
- 创建acapp
- 解决跨域
orz