项目实战——对战回放和排行榜

news2025/7/20 17:22:52

目录

一、天梯积分更新

二、实现对局列表页面

三、前端测试

四、实现查看录像功能

五、实现分页功能

六、后端实现查询排行耪

七、前端展示

八、限制Bot数量


一、天梯积分更新

可以自己定义一下规则

存之前算一下两名玩家的天梯积分

实现更新,实现后重启看一下能不能实现

标记自己是哪条蛇,在前端可以判断左下角是A右上角是B

 

 

二、实现对局列表页面

先写一个API从后端返回对局列表的List

Service Impl Controller

加上分页功能MybatisConfig——配置在讲义里

一路Alt+Enter回车、每页十条超出返回空

需要一个参数传入分页编号

告诉前端一共有多少页

如果在Service、Controller里面注入的话就不需要写set了

不需要定义成静态变量在写set如果是第三方类的话就需要写成

static类型后面写一个set在set方法上去写一个Autowired

老样子,要实现Service层,Controller层

先写service接口,写完写service接口的实现ServiceImpl,在重写方法之前,先创建对应的Controller,@Autowired注入刚刚写的service接口,完善Controller,最后再完善Impl里的内容。

由于我们的对局情况可能会有很多,我们把所有对局情况都展示出来显然是不合适的,因此我们需要添加分页功能。

添加分页配置
在Config.MybatisConfig中添加分页配置:

@Configuration
public class MybatisConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.SQL_SERVER));
        return interceptor;
    }
}

 添加分页配置
在Config.MybatisConfig中添加分页配置

实现分页功能
由于我们不可能一页展示所有对局信息,因此我们要把所有信息分成多个页展示,这里就需要直到当前的页是第几页的。
要传入参数:当前页的编号;

Mybatis里有api可以实现以下页面展示效果,超出范围的话返回空即可。

1: 0~9
2: 10~19
3: 20~29

api:Ipage:IPage<Record> recordIPage = new Page<>(page,10); (当前是第几页,每一页展示多少个)

Mybatis-Plus 关于分页查询的api:

getRecords():获取查询数据
getCurrent():获取当前页
getSize():获取当前分页大小
backend\service\impl\record\GetRecordListServiceImpl.java:

@Service

public class GetRecordListServiceImpl implements GetRecordListService {

    @Autowired
    private RecordMapper recordMapper;
    @Autowired
    private UsersMapper usersMapper;

    @Override
    public JSONObject getList(Integer page) {
        IPage<Record> recordIPage = new Page<>(page, 10); //(当前页码,每页展示数目)

        //排序展示最新的条目
        QueryWrapper<Record> queryWrapper = new QueryWrapper<>();
        //若设置成只能看自己则在后面.eq(自己的id)
        queryWrapper.orderByDesc("id");//降序排序
        List<Record> records = recordMapper.selectPage(recordIPage, queryWrapper).getRecords();
        JSONObject resp = new JSONObject();
        List<JSONObject> items = new LinkedList<>();
        for (Record record : records) {
            Users userA = usersMapper.selectById(record.getAId());
            Users userB = usersMapper.selectById(record.getBId());
            JSONObject item = new JSONObject();
            item.put("a_photo", userA.getPhoto());
            item.put("a_username", userA.getUsername());
            item.put("b_photo", userB.getPhoto());
            item.put("b_username", userB.getUsername());
            item.put("record", record);
            items.add(item);
        }
        resp.put("records", items);
        resp.put("records_count", recordMapper.selectCount(null));
        return resp;
    }
}


 

三、前端测试

查询一下后端数据

将列表显示出来

展示table,把之前的table复制过来即可

 前端实现相对比较简单,用$ajax接收后端的数据后,用表格把对战双方的头像、用户名、对战结果、对战时间、录像回放选项用一个表格展示出来了
具体表格样式下面给出参考:

  <table class="table table-striped table-hover">
            <thead>
                <tr class="table-dark">
                    <th>A</th>
                    <th>B</th>
                    <th>PK Result</th>
                    <th>PK Time</th>
                    <th>Operation</th>
                </tr>
            </thead>

            <tbody>
                <tr v-for="record in records" :key="record.record.id">
                    <td>
                        <img :src="record.a_photo" alt="" class="record-user-photo">
                        &nbsp;
                        <span class="record-user-username"> {{ record.a_username }} </span>
                    </td>
                    <td>
                        <img :src="record.b_photo" alt="" class="record-user-photo">
                        &nbsp;
                        <span class="record-user-username"> {{ record.b_username }} </span>
                    </td>

                    <td> {{ record.result }}</td>
                    <td> {{ record.record.createTime }}</td>
                    <td>
                        <button type="button" class="btn btn-secondary">Watch the Record </button>
                    </td>


                </tr>
            </tbody>
        </table>

 

四、实现查看录像功能

点击后跳转页面,需要写一个新的View

直接将pk界面复制过来,需要判断是录像还是对战

只需要一个PlayGround组件就可以了

把页面加到路由里面点开Router加一个路由

一定要用localhost因为有些只对localhost放行

首先我们要写一个record的store作为全局变量存储我们有关录像页面的信息,存储的信息包括:

是否为录像页面 is_record
玩家A的步骤:a_steps
玩家B的步骤:b_steps
其次,写一个录像展示页面RecordContent.vue

在RecordIndex.vue界面点击展示录像按钮就要弹出录像展示页面,因此要绑定事件,用@click="open_record_content(record.record.id)",record.record.id是当前录像的id。

在此函数,我们还要更新对局信息,可以用console.log(record)看看我们传进来的record数据格式是怎么样的,方便我们在后面写update函数。

这里建议不要想当然地觉得后端自己写了什么格式的参数就一定是传你这名字的参数,可能传送过程中会出现把大写压成小写等各种奇奇怪怪的转化,保险一点,还是console输出一下看看里面的参数是什么!

注意:我们传入的map是以一维数组的存储格式存储的,我们下面更新的时候需要把它转化为二维的格式,详情见下面的stringTo2D函数!

 const  stringTo2D = map => {
            let g = [];
            for (let i = 0,k = 0;i < 13;i ++) {
                let line = [];
                for (let j = 0;j < 14;j ++,k ++) {
                    if (map[k] === '0') line.push(0);
                    else line.push(1);
                }
                g.push(line);
            }
            return g;
        }

        const open_record_content = recordId => {
            for (let record of records.value) {
                if (record.record.id === recordId) {
                    store.commit("updateIsRecord",true); //标记成是录像页面
                    console.log(record);
                    store.commit("updateGame",{
                         map: stringTo2D(record.record.map),
                         a_id: record.record.aid,
                         a_sx: record.record.asx,
                         a_sy: record.record.asy,
                         b_id: record.record.bid,
                         b_sx: record.record.bsx,
                         b_sy: record.record.bsy,
                     })
                    router.push({
                        name: "record_content",
                        params: {
                            recordId: recordId, //可以简写成一个recordId
                        }
                    });
                    break;
                }
            }

        }

 别忘了要添加路由
router\index.js

 {
    path: "/record/:recordId/", /*路由添加参数用:*/
    name: "record_content",
    component: RecordContent,
    meta: {
      requestAuth: true,
    }
    //requestAuth: true,
  },

在GameMap.js里的监听事件函数里稍微修改一下:
如果是放录像的话我们就放录像:根据玩家历史的操作步骤将步骤回放出来;
如果不是放录像的话,我们就接收用户的输入。

回放录像
因为我们的蛇是每秒中走5个格子,所以相当于200ms一格,我们放录像的时候可以设每300ms走一格,并且确定下一步的方向。

setInterval();可以帮我们实现计时函数。

scripts\GameMap.js:

...
 add_events() {
        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;
            console.log(this.store.state.record);
            const interval_id = setInterval(() => {
                if (k >= a_steps.length - 1) { //最后一步是撞墙的一步,不需要复现,直接在后面把state设为dead即可
                    if (loser === "all" || loser === "A") {
                        snake0.status = "dead";
                    }
                    if (loser === "all" || loser === "B") {
                        snake1.status = "dead";
                    }
                    clearInterval(interval_id); //结束id为interval_id的setInterval()函数

                } else {
                    snake0.set_direction(parseInt(a_steps[k]));
                    snake1.set_direction(parseInt(b_steps[k]));
                }
                k ++;

            },300); //计时函数,每300ms做一次函数


        } else {
            this.ctx.canvas.focus();
            this.ctx.canvas.addEventListener("keydown", e => {
                let d = - 1;
                if (e.key === 'w') d = 0; //上
                else if (e.key === 'd') d = 1; //右
                else if (e.key === 's') d = 2;//下
                else if (e.key === 'a') d = 3;//左

                if (d >= 0) { //一个合法的操作
                    //前端向后端发消息: 前端 -> 后端
                    this.store.state.pk.socket.send(JSON.stringify({
                        event: "move",
                        direction: d,
                    }));
                }

            });

        }
    }
    ...

 

五、实现分页功能

把前端功能加上一个跳转的样式

Bootstrap组件Pagination

放到table外面就可以了

加上Page条组件,我们设定一次展示出来的分页有五页
click_page 实现点击页面就跳转到目标页面,-2表示上一页,-1表示下一页
我们用循环展示数据库里的所有页面
为了方便实现高亮、循环等功能,pages作为一个队列存储的是对象,
包括页面id:number、是否高亮:is_active。

<nav aria-label="...">
            <ul class="pagination" style="float:right">
                <li class="page-item">
                    <a class="page-link" @click="click_page(-2)" href="#">Previous</a>
                </li>
                <li :class="'page-item ' + page.is_active" v-for="page in pages" :key="page.number" @click="click_page(page.number)">
                    <a class="page-link"  href="#">{{page.number}} </a>
                </li>

                <li class="page-item" @click="click_page(-1)">
                    <a class="page-link" href="#">Next</a>
                </li>
            </ul>
        </nav>

实现按哪一页就返回哪一页的内容:

看看当前页面的前后两页是否存在:

  const update_pages = () => {
            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;
        }

 

六、后端实现查询排行耪

注入查询用户表的Mapper

查询所有的用户+分页

user里面包含密码,记得返回前清空密码

 

七、前端展示

和对战列表的差不多直接复制过来即可

玩家天梯分一共两项

链接也要记得修改一下

八、限制Bot数量

consume里面开一个新的线程
每个用户最多只能创建十个Bot

多了报错

由于bot列表设置分页比较麻烦,我们可以给用户添加限制,省去这些麻烦。。。

 QueryWrapper<Bots> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("user_id", user.getId());
        if (botsMapper.selectCount(queryWrapper) > 10) {
            map.put("message", "bot数量不能超过10个");
            return map;
        }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/37733.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

挂耳式蓝牙耳机性价比推荐,盘点五款性能高的耳机分享

众所周知&#xff0c;骨传导耳机之所以能够受到人们的喜欢&#xff0c;是因为其佩戴不需入耳&#xff0c;尤其是针对于运动爱好者来说&#xff0c;在户外运动的时候不但可以听见音乐&#xff0c;还可以听见外界的声音&#xff0c;进一步的将危险系数拉低&#xff0c;其次也是因…

音乐信息提取-1-音频表示

音频信号是声音的一种表示&#xff0c;它表示由振动引起的气压随时间的波动&#xff08;数字信号处理-1-关于声音与波&#xff09;。 1 波形与时域 音频信号在时域上的表示就是波形随时间的变化&#xff0c;可以将波形的幅度值理解为声压。 声音是连续的&#xff0c;但数字记…

Spring创建、Bean对象的存储和读取

文章目录1、创建Spring项目1.1 创建Maven项目1.2 添加 Spring 框架支持1.3 添加启动类并添加main&#xff08;非必要步骤&#xff09;2、存储Bean对象2.1 添加Spring配置文件&#xff08;第一次添加&#xff09;2.2 创建Bean对象2.3 将Bean对象注册到Spring容器中3、读取并使用…

秋招挂麻了,就差去送外卖了,10w字Java八股啃完,春招必拿下

最近看到一名前腾讯员工发的帖子&#xff0c;总结的近期面试结果&#xff0c;真的就是那三个字&#xff1a;挂麻了…… 一个毕业后就在腾讯的高级程序员&#xff0c;由于种种原因&#xff0c;离职出来了。趁着金九银十求职季&#xff0c;互联网大厂小厂面试了一圈&#xff0c;感…

AcWing 搜素与图论

搜索 DFS 全排列 代码 #include<iostream> using namespace std;int vis[10], a[10];void dfs(int step, int n) {if (step n 1){for (int i 1; i < n; i)printf("%d ", a[i]);printf("\n");return;}for (int i 1; i < n; i){if (!vis[i…

计算机网络笔记5 传输层

文章目录前言一、运输层概述二、运输层的端口与复用、分用的概念三、UDP协议 和 TCP协议 对比用户数据报协议 UDP&#x1f4a6;&#xff08;User Datagram Protocol&#xff09;传输控制协议 TCP&#x1f4a6;&#xff08;Transmission Control Protocol&#xff09;四、TCP协议…

作用域和作用域链

文章目录1.作用域&#xff08;Scope&#xff09;1.1 什么是作用域1.2 全局作用域1.3 函数作用域1.3 块级作用域2. 作用域链2.1 自由变量2.2 作用域链2.3 *自由变量的取值2.4 作用域与执行上下文的区别3.总结1.作用域&#xff08;Scope&#xff09; 1.1 什么是作用域 当前的执…

easy-rules规则引擎最佳落地实践

写作目的 这是一个头部互联网公司中的一个问题。因为有很多业务产品线&#xff0c;作为一个新人或者团队外的人员是很难区分不同的产品线之间的区别的&#xff0c;因此需要给某个产品线一个描述。但是随着业务的发展&#xff0c;产品线下可能又根据某个字段进一步划分&#xf…

招生CRM系统|基于Springboot实现培训机构招生CRM管理系统

作者主页&#xff1a;编程指南针 作者简介&#xff1a;Java领域优质创作者、CSDN博客专家 、掘金特邀作者、多年架构师设计经验、腾讯课堂常驻讲师 主要内容&#xff1a;Java项目、毕业设计、简历模板、学习资料、面试题库、技术互助 收藏点赞不迷路 关注作者有好处 文末获取源…

Mongodb操作基础 分片

Mongodb分片 MongoDB分片是MongoDB支持的另一种集群形式&#xff0c;它可以满足MongoDB数据量呈爆发式增长的需求。当MongoDB存储海量的数据时&#xff0c;一台机器可能无法满足数据存储的需求&#xff0c;也可能无法提供可接受的读写吞吐量&#xff0c;这时&#xff0c;我们就…

基于内容的个性化推荐算法

一、什么是推荐算法 随着移动互联网的高速发展与智能手机的普及&#xff0c;海量的有用信息虽然为人们提供了更多的价值&#xff0c;然而信息的泛滥也意味着为了寻找合适的信息必须付出更多的时间成本。事实上&#xff0c;有时候仅仅是浏览和简单的查询来寻找有用的信息变得相…

「强烈收藏」Python第三方库资源大全,1000+工具包

前言 awesome-python 是 vinta 发起维护的 Python 资源列表&#xff0c;内容包括&#xff1a;Web 框架、网络爬虫、网络内容提取、模板引擎、数据库、数据可视化、图片处理、文本处理、自然语言处理、机器学习、日志、代码分析等。 &#xff08;文末送读者福利&#xff09; …

超全!程序员必备的20个学习网站,看这一篇就够了!

之前一直想出个程序员学习清单&#xff0c;终于腾出时间弄出来了&#xff0c;也趁此机会整理了收藏夹。 此篇对于新手程序员比较有用&#xff0c;技术老鸟们也可以查缺补漏。 话不多说&#xff0c;纯纯干货呈上&#xff0c;赶紧点个赞收藏&#xff0c;以后会用得上&#xff01;…

CMake中include_directories的使用

CMake中include_directories命令用于在构建(build)中添加包含目录,其格式如下: include_directories([AFTER|BEFORE] [SYSTEM] dir1 [dir2 ...]) 将给定的目录添加到编译器(compiler)用于搜索包含文件的目录。相对路径被解释为相对于当前源目录。 包含目录被添加到当前C…

【网络】tcpdump、Wireshark 案例超详细介绍

文章目录网络分层应用层找到服务器的 IP查接口、对象的耗时删除指定网站的Cookie表示层、会话层tcpdump、wireshard传输层telnet: 路径可达性测试nc: 路径可达性测试netstat&#xff1a;查看当前连接状态iftop&#xff1a;查看当前连接的传输速率netstat -s: 查看丢包和乱序的统…

万字 HashMap 详解,基础(优雅)永不过时

本文已收录到 AndroidFamily&#xff0c;技术和职场问题&#xff0c;请关注公众号 [彭旭锐] 提问。 前言 大家好&#xff0c;我是小彭。 在上一篇文章里&#xff0c;我们聊到了散列表的整体设计思想&#xff0c;在后续几篇文章里&#xff0c;我们将以 Java 语言为例&#xff…

【王道计算机组成原理Note】5.5 指令流水线

5 指令流水线 5.1 指令流水的定义 一条指令的执行过程可以分成多个阶段(或过程)。根据计算机的不同&#xff0c;具体的分法也不同。 取指&#xff1a;根据Pc内容访问主存储器&#xff0c;取出一条指令送到IR中。分析&#xff1a;对指令操作码进行译码&#xff0c;按照给定的寻…

Visio 安装暴雷记录

Visio 安装记录起因&#xff1a; office2016家庭学生版中&#xff0c;安装visio2021后&#xff0c;插入word的vsdx图形右键显示unkown类型&#xff0c;无法识别&#xff0c;给学习工作带来很多麻烦&#xff01;   搜查一圈没找到对应可用的方法&#xff0c;想着可能是visio20…

MobPush for Flutter

集成准备 这是一个基于 MobPush 功能的扩展的 Flutter 插件。使用此插件能够帮助您在使用 Flutter 开发应用时&#xff0c;快速地实现推送功能。 在pubspec.yaml文件中加入下面依赖 dependencies:mobcommonlib:mobpush_plugin: 然后执行&#xff1a;flutter packages get 导…

倍增(小试牛刀)

二分每次折半&#xff0c;倍增每次2的倍数 原理先存储每个小区间的最值也就是初始化&#xff0c;之后直接查询 1. 把数列按倍增分成小区间 对数列的每个元素&#xff0c;把从它开始的数列分成长度为1、2、4、8、…的小区间。下图给出了一个分区的例子&#xff0c;它按小区间…