Skip to content

Commit

Permalink
chore(docs): update contest.md (#1700)
Browse files Browse the repository at this point in the history
* chore(docs): update contest.md

* refactor(docs): typo in contest.md

* refactor(docs)

* refactor(docs): update contest.md
  • Loading branch information
FranGuam authored Apr 2, 2024
1 parent ff92570 commit 41901b4
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 39 deletions.
2 changes: 1 addition & 1 deletion docs/_config.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
title: EESAST
description: 网站开发接口文档
description: 网站开发文档
theme: jekyll-theme-cayman
111 changes: 73 additions & 38 deletions docs/contest.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,45 @@ description: 网站开发接口文档
permalink: /contest
---

> 在开启队式之旅前,请先完成队伍创建、代码上传和角色分配流程。
>
> 目标:
>
> - 数据库`room` 管理移交给后端执行,避免让前端鉴权
> - 数据库`room`管理移交给后端执行,避免让前端鉴权
> - 一场比赛分为多个`docker`并行,每个队伍对应一个`docker`
> - 架构的可扩展性和动态性,支持比赛队伍数可变、每个队伍的角色数可变、每支队伍的代码数可变、每个角色可选的技能可变、比赛的地图可变
## 天梯逻辑

### 流程描述

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

### 接口描述

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

- `/arena/create`:创建比赛。数据库中插入`room`,并将比赛加入队列中。
- 请求方法:`POST`
- 请求:`body`中有`{contest_name: string, team_label_bind: TeamLabelBind[], map_id: uuid}`,其中`contest_name`是数据库中的`name`用于确定用于编译的镜像,`team_ids`为参加比赛的队伍 id,`labels`表示队伍的标签(例如Student Team/Tricker Team),`map_id`代表选择的地图id。`TOKEN`中包含用户的`uuid`
- 响应:`200`: `Arena created!`
- 请求:`body`中有`{contest_name: string, team_labels: TeamLabelBind[], map_id: uuid}`,其中`contest_name`是数据库中的`name`用于确定用于执行比赛的镜像,`team_labels`的类型定义见下方附录、包含了参加比赛的队伍`uuid`和队伍执方(如`Student``Tricker`,见`contest_player`),`map_id`代表选择的地图`uuid`(请求同时携带了包含用户信息的`token`
- 响应:`200`: `Arena created!`
- 工作过程:
1. 鉴权。检查登录状态,及用户是否在队伍中。
2. 限制开战频率。同一队伍不能同时打多于6场的比赛。
Expand All @@ -47,67 +54,95 @@ permalink: /contest
- `401``401 Unauthorized: Missing token`(未登录)
- `403``403 Forbidden: User not in team`(用户不在队伍中)
- `403``403 Forbidden: Team player not assigned `(队伍角色未分配代码)
- `403``403 Forbidden: Team code not compiled`代码未编译
- `403``403 Forbidden: Team code not compiled`代码未通过编译
- `404``404 Not Found: Team code not found in cos ``cos`上找不到文件)
- `422``422 Unprocessable Entity: Missing credentials`(请求缺失参数)
- `423``423 Locked: Request arena too frequently`(比赛次数过多)
- `500``undefined`(其他内部错误)
- `/arena/finish``docker`服务器比赛结束的`hook`。更新比赛结果,更新天梯分数。
- `/arena/get-score``docker`服务器比赛结束后,用于查询参战队伍现有天梯分数的路由,拿来计算本场对战的得分。后端查询数据库即可。
- 请求方法:`POST`
- 请求:`{team_id: uuid}`。同时在`headers`里传回创建`docker`时设置的`TOKEN`
- 响应:`{score: number}`
- 错误:`500``undefined`,返回报错信息
- `/arena/finish``docker`服务器比赛结束的`hook`。更新比赛结果,更新天梯分数,将比赛回放和日志文件上传至`COS`
- 请求方法:`POST`
- 请求:`{result: ContestResult[]}``TOKEN`包含的信息:`{room_id: uuid}`
- 请求:`{result: ContestResult[]}`,类型定义见下方附录。同时在`headers`里传回创建`docker`时设置的`TOKEN`
- 响应:`200``Update OK!`
- 错误:`500``undefined`,返回报错信息

注:此外,后端需要在`docker_cron`中更新数据库比赛状态和端口信息。
- `注意`:除此之外,后端需要在`docker_cron`中更新数据库比赛状态和端口信息(异步、非请求内)。

## 比赛逻辑

### 流程描述

比赛的流程就简洁多了。
比赛的流程与天梯非常相近,几大区别在于:

- 比赛由前端先写入数据库的`contest_round`表,用于记录这轮比赛的一些基本设置和`uuid`。一轮比赛(round)指的是所有队伍全循环一次的比赛之总和,一轮比赛包含多场对战。
- 后端需要对比赛队伍、队伍执方、地图进行全循环,每个循环体发起一场对战,流程和天梯中的一场对战大致相同。
- 后端需要在插入`contest_room`表时额外写入`round_id`从而与天梯区分。
- 比赛暂时默认不暴露端口,不需要更新`port`字段。
- 比赛结束时不向选手发送`Web Push`订阅通知。

具体流程如下:

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

### 接口描述

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

- `/competition/create`:管理员专用。后端可以按`contest_round`表中的信息设置所有队伍之间的完整比赛,全部队伍的比赛合起来称为一个`round`,对应一个`round_id`。设置`room`发起对战的流程跟天梯逻辑一致,只需要在`contest_room`里额外加`round_id`标识即可。
- `/competition/start-all`:管理员专用。后端可以按`contest_round`表中的信息设置所有队伍之间的完整比赛,全部队伍的比赛合起来称为一个`round`,对应一个`round_id`。设置`room`发起对战的流程跟天梯逻辑一致,只需要在`contest_room`里额外加`round_id`标识即可。
- 请求方法:`POST`
- 请求:`{round_id: uuid}``TOKEN`包含`user_uuid`
- 请求:`{round_id: uuid}`(请求同时携带了包含用户信息的`token`
- 响应:`200``Competition Created!`
- 错误:
- `422``422 Unprocessable Entity: Missing credentials`(请求缺失参数)
- `500``undefined`,返回报错信息
- `/competition/assign`:管理员专用。发起`round`中某一场特定的比赛,然后将比赛加入队列中。设置`room`发起对战的过程跟天梯逻辑一致,只需要在`contest_room`里额外加`round_id`标识即可。
- `/competition/start-one`:管理员专用,用于重新发起`round`中某一场特定的比赛。后端需要先删除这场比赛的已有记录(包括数据库中、`cos`中),然后将比赛加入队列中。设置`room`发起对战的过程跟天梯逻辑一致,只需要在`contest_room`里额外加`round_id`标识即可。
- 请求方法:`POST`
- 请求:`{team_label_bind: TeamLabelBind[], map_id: uuid, round_id: uuid}``TOKEN`包含`user_uuid`
- 响应:`200``Competition Created!`
- 请求:`{team_labels: TeamLabelBind[], map_id: uuid, round_id: uuid}`(请求同时携带了包含用户信息的`token`
- 响应:`200``Room Created!`
- 错误:
- `422``422 Unprocessable Entity: Missing credentials`(请求缺失参数)
- `500``undefined`,返回报错信息
- `/competition/finish``docker`服务器比赛结束的`hook`。更新比赛结果。
- `/competition/get-score``docker`服务器比赛结束后,用于查询参战队伍现有比赛分数的路由,拿来计算本场对战的得分。后端查询数据库即可。
- 请求方法:`POST`
- 请求:`{team_id: uuid}`。同时在`headers`里传回创建`docker`时设置的`TOKEN`(内部包含`round_id`)。
- 响应:`{score: number}`
- 错误:`500``undefined`,返回报错信息
- `/competition/finish-one``docker`服务器比赛结束的`hook`。更新比赛结果,更新比赛分数,将比赛回放和日志文件上传至`COS`
- 请求方法:`POST`
- 请求:`{result: ContestResult[]}` `TOKEN`包含的信息:`{room_id: uuid}`
- 请求:`{result: ContestResult[]}` ,类型定义见下方附录。同时在`headers`里传回创建`docker`时设置的`TOKEN`
- 响应:`200``Update OK!`
- 错误:`500``undefined`,返回报错信息

## 与赛事组的约定

1. 一场比赛对应一个`docker`容器。
2. 队式应当关注上面的`/arena/finish``/competition/finish`路由参数信息。`docker`镜像启动时会设置环境变量`URL`(对应`/arena/finish``/competition/finish`)和`TOKEN`,编译完成后需要请求`URL`,请求时需要在header中加上`TOKEN`,在body中加上每个队的分数`result`
3. `docker`目录绑定:回放文件在`/usr/local/playback`下,地图文件在`/usr/local/map`下,队伍代码应在`/usr/local/team<xxx>`下,具体格式根据比赛规则商定。
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``TEAM_LABELS``json`格式,类型为`TeamLabelBind[]`,定义见下方附录)。
- 比赛结束后先请求`SCORE_URL`,获取参战队伍在天梯/比赛中的现有分数,请求时需要在`headers`中加上`TOKEN`
- 获得现有分数后,`docker`应当据此计算出本场对战的得分(增量,而非更新后的总分)
- 完成后再请求`FINISH_URL`,在请求的`body`中传回`result`(即上面计算出的得分),请求时需要在`headers`中加上`TOKEN`
- `client`镜像启动时会设置环境变量`TEAM_LABEL`,供容器得知该队比赛执方,类型定义见下方附录`TeamLabelBind`

3. `docker`目录绑定。
- 对于`server`镜像,地图文件在`/usr/local/map`下,命名为`${map_id}.txt`,回放文件请放在在`/usr/local/playback`下,命名为`playback.thuaipb`
- 对于`client`镜像,队伍代码在`/usr/local/code`下,命名为`${player_label}.${suffix}``player_label`为在数据库存储的字符串标签,可供赛事组预先定义,如`Student1`)。

## 附录

数据结构定义
#### 数据结构定义

```javascript
interface ContestResult {
team_id: number;
team_id: uuid;
score: number;
};

Expand Down

0 comments on commit 41901b4

Please sign in to comment.