Skip to the content.

目标:

天梯逻辑

流程描述

  1. 选手在完成队伍和代码准备后,打开天梯试炼,向心仪的队伍开战!
    • 谨防战争狂。同一队伍不能同时打多于6场的比赛。前端需要从数据库上查询比赛状态,若有相同配置的比赛已经在开战,则给用户发出提示信息:已有一场相同的比赛正在开战。是否继续?
    • 后端也要检查,限制一支队伍的开战频率。
  2. 前端请求后端/arena/create路由
    • 前端需要检查数据库上的代码编译状态和角色代码分配状态,若队伍角色未分配代码,或代码未编译,则在页面报错而不请求路由。
    • 后端也要检查数据库上的代码编译状态和角色代码分配状态,还要检查contest表中的arena_switch是否为true,都正常的情况下再继续下一步。
  3. 后端在数据表contest_room中创建 room,更新statusWaiting,并在contest_room_team中绑定roomteam
  4. 选手代码的编译文件在cos中,后端需要从cos上临时下载队伍的代码(如 python 代码)或编译文件(如 c++ 编译后的可执行文件)到后端服务器上。
    • 后端服务器存储空间有限,需要定期清理下载的队伍代码和文件。
    • 后端服务器与docker服务器之间通过NFS进行文件共享,因此docker服务器自动同步了队伍文件。(备注:建议提前服务器之间组内网减少流量费。)
  5. 前两步都执行成功的前提下,后端创建docker并入队docker_queue,向前端返回创建是否成功的结果。
    • 对于一场比赛(两队参与为例),后端需要先后创建4个docker:一个server镜像对应的比赛逻辑服务器、两个client镜像对应的选手代码执行客户端(每队共用一个),一个envoy镜像对应的grpc-webgrpc转发服务器(用于前端直播,暂不急于实现)。
    • 比赛状态显示。后端创建 docker 分为两步:【第一步】是将比赛放入队列docker_queue尾,此时room -> statusWaiting;【第二步】是docker_cron 定时程序从队列中抽取队首的比赛进行,如果比赛启动成功,此时room -> statusRunning
    • 比赛期间,用户可通过特定端口观看直播。后端在上面所述启动比赛的【第二步】时分配好一个端口。如果端口数量不足,则不启动比赛。如果成功分配端口并启动比赛,则应同时更新数据库contest_room表中的port字段。
    • 前端应当使用subscription实时更新比赛状态和直播观看端口。
  6. docker 服务器结束比赛后请求后端/arena/finish路由。
    • 后端更新数据库,更新contest_room表中的statusFinishedTimeoutCrashed 、更新portNULL;更新contest_room_team表中的score字段,为这场比赛的每个队伍记录分数
    • 后端将比赛回放文件以及日志文件(如有)上传至 cos,具体路径参考COS存储桶访问路径
    • 后端向参与这场比赛的队伍队员发送Web Push订阅通知(暂不急于实现)。
    • 后端会设置最大运行时间(位于数据库内,每届比赛有不同的运行时间),若超过最大运行时间,则后端会强制停止所有相关容器的运行,并更新 room 状态为 Timeout,释放 port
  7. 比赛结束后,前端提供下载和在线观看回放的功能,直接按照COS存储桶访问路径中约定的路径从cos下载对应的文件即可。

接口描述

新版天梯接口的前缀为/arena

比赛逻辑

流程描述

比赛的流程与天梯非常相近,几大区别在于:

具体流程如下:

  1. 比赛管理员在前端页面上发起比赛。
  2. 前端在数据库contest_round里插入一行,包含了这轮比赛的名称(仅展示用)和使用的地图map_id
  3. 前端请求后端路由/competition/start-all
  4. 后端获取所有队伍数据。检查队伍代码是否完整,角色是否分配,如果不完整则跳过此队伍。
  5. 后端对于正常队伍开启循环赛,将比赛全部加入docker_queue中。
  6. docker比赛结束后向后端通信,后端在数据表上更新比赛分数。

接口描述

新版比赛接口的前缀为/competition

与赛事组的约定

  1. 一场比赛对应两个docker镜像、多个docker并行。其中server镜像为比赛逻辑服务器,client镜像为选手代码执行客户端(一队共用)。
  2. 队式应当关注上面的/arena/finish/arena/get-score/competition/finish-one/competition/get-score路由参数信息。
    • server镜像启动时会设置环境变量SCORE_URL(即/arena/get-score/competition/get-score)、FINISH_URL(即/arena/finish/competition/finish-one)、TOKEN
      • 比赛结束后先请求SCORE_URL,获取参战队伍在天梯/比赛中的现有分数,请求时需要在headers中加上TOKEN
      • 获得现有分数后,docker 应当据此计算出本场对战的得分(增量,而非更新后的总分)
      • 完成后再请求FINISH_URL,在请求的body中传回ContestResult(即上面计算出的增量得分),请求时需要在headers中加上TOKEN
    • client镜像启动时会设置环境变量TEAM_LABELTEAM_SEQ_ID,供容器得知该队比赛执方和序号。
    • 队式 docker 不需要关注 team_uuid,这对于队式而言是不可见的,队式 docker 可见的只有 TEAM_LABELSTEAM_SEQ_ID,并且分数信息须与传入的 TEAM_SEQ_ID 的顺序相同。
  3. 后端提供的环境变量说明。
    • 客户端:
      • TERMINAL: 取值为 SERVER 或者 CLIENT,表明当前比赛 docker 是客户端还是服务器。
      • TEAM_LABEL: 客户端队伍标签。对应 TeamLabelBind 中的 label 字段。
      • TEAM_SEQ_ID: 客户端使用,当前客户端的队伍序号,所有队伍从 0 开始顺序编号,与服务端 TEAM_LABELS 的顺序对应。
      • PORT: 服务器开放的端口,客户端需通过此端口加入服务器的比赛。
    • 服务端:
      • TERMINAL: 取值为 SERVER 或者 CLIENT,表明当前比赛 docker 是客户端还是服务器。
      • MODE: 判断当前比赛是天梯还是最终比赛。取值为 ARENA (天梯)或者 COMPETITION(决赛)。
      • TOKEN: 服务端验证身份的 token。发送请求时需带上。
      • TEAM_LABELS: 全局信息。本场比赛的所有队伍标签。用 : 分隔,其中的每个元素对应 TeamLabelBind 中的 label 字段,位序对应客户端 TEAM_SEQ_ID 的顺序,也对应 SCORE_URLFINISH_URL 的分数信息 scores 的顺序。
      • MAX_GAME_TIME: 比赛持续的时间,单位为秒。若超过这个时间,后端会强制停止所有相关容器。
      • MAP_ID: 地图 id。
      • SCORE_URL: 获取当前天梯分数的 url 路径。请求时需带上 TOKEN
      • FINISH_URL: 结束比赛时更新分数的 url 路径。请求时需带上 TOKEN
      • EXPOSED: 决定是否开放端口的环境变量,1 表示开放,0 表示不开放。
  4. docker目录绑定。
    • 对于server镜像,地图文件在/usr/local/map下,命名为${map_id}.txt,回放文件请放在在/usr/local/output下,命名为playback.thuaipb。如果需要上传日志文件,同样放在此目录下,命名为 xxx.logTEAM_LABELS中传入了所有队伍的label
    • 对于client镜像,队伍代码在/usr/local/code下,命名为${player_label}.${suffix}player_label为在数据库存储的字符串标签,可供赛事组预先定义,如Student1)。对于 suffix 的说明:对于 python 代码,suffixpy;对于 cpp 代码,没有 suffix,文件命名就是 ${player_label}。日志文件可以放在/usr/local/output下,命名为 xxx.logTEAM_LABEL 中传入了当前队伍的labelTEAM_SEQ_ID是当前队伍的序号,编号从 0 开始。

附录

数据结构定义

interface ContestResult {
  status: string; // `Finished`、`Timeout` 或 `Crashed`。
  scores: number[]; // 每个队伍的分数,顺序与 TEAM_LABELS 一致。
};

interface TeamLabelBind {
  team_id: uuid;
  label: string;
}

interface ServerToken {
  contest_id: string;
  round_id: string?;
  room_id: string;
  team_label_binds: TeamLabelBind[];
}