谜题后端 - API 参考
谜题后端中提供了多种工作函数,可用于复杂的谜题逻辑。
请求上下文
通过不同方式调用的后端函数都会接收一个上下文参数(通常命名为 ctx),所有上下文对象都是运行时数据的副本,运行时修改上下文字段不会产生副作用。
自定义 API
type ApiContext = {
apiName: string;
request: {
method: 'GET' | 'POST';
query: Record<string, string>;
body: unknown | null;
};
puzzle: { id: number; gameId: number; title: string };
team: { id: number; name: string };
user: { id: number; nickname: string };
};自定义评测
type JudgeContext = {
apiName: 'judge';
request: { userAnswer: string; normAnswer: string };
puzzle: { id: number; gameId: number; title: string };
team: { id: number; name: string };
user: { id: number; nickname: string };
submission: {
id: number;
createdAt: string;
};
};其中 request.userAnswer 是提交的原答案,request.normAnswer 是规范化后的答案。
函数应返回一个 JudgeResult 作为评测结果,或返回 undefined 代表不作评测。
type JudgeAction = 'fail' | 'correct' | 'milestone' | 'start_game' | 'easter_egg' | 'finish_game';
type JudgeResult = {
action?: JudgeAction | null;
result?: string | null;
answer?: string | null;
ignored?: boolean | null;
triggers?: string[];
};提示购买钩子
type HintPurchaseContext = {
apiName: string;
puzzle: { id: number; gameId: number; title: string };
team: { id: number; name: string };
user: { id: number; nickname: string };
hint: {
id: number;
title: string;
costId: number | null;
costAmount: number;
};
purchase: {
currency: null | {
id: number;
slug: string;
name: string;
prec: number;
before: number;
after: number;
delta: number;
};
};
};工具函数
工具函数在四个全局对象中提供,根据,每个全局对象承载不同的职责。
| 对象 | 适用范围 | 主要能力 |
|---|---|---|
$game | 当前游戏任意范围 | KV、Store、货币 |
$team | 当前队伍 | KV、Store、货币 |
$puzzle | 当前谜题,跨队共享 | KV、Store、谜题资产 |
$this | 当前队伍 + 当前谜题 | KV、Store、提交、解题、事件 |
货币
$team.currency 提供当前队伍货币的接口。可以使用货币 ID 或货币标识指定货币。
$team.currency.query(): Currency[];
$team.currency.query(currency: number | string): Currency | null;
$team.currency.cost(currency: number | string, amount: number, reason?: string | null): boolean;
$team.currency.add(currency: number | string, amount: number, reason?: string | null): number | null;
$team.currency.update(currency: number | string, amount: number, reason?: string | null): Currency | null;
$team.currency.update(currency: number | string, options: { amount?: number; team_growth?: number; hidden?: boolean }, reason?: string | null): Currency | null;$game.currency 提供当前比赛中任意队伍货币的接口,提供的函数同 $team.currency,但是在第一个参数前增加一个 teamId 参数用于指定队伍。例如:
$team.currency.query(teamId: number): Currency[];Currency
type Currency = {
id: number;
slug: string;
name: string;
growth: number;
base_growth: number;
team_growth: number;
init_amount: number;
prec: number;
amount: number;
current_amount: number;
max_amount: number;
hidden: boolean;
utime_at: string;
};在实际实现中,队伍货币是懒计算的。
base_growth为该货币全局增长速度,team_growth为该队伍的附加增长速度。growth为两者之和,即队伍的有效增长速度。- 每次货币更新时,将记录货币的更新时间
utime_at和当时的货币数量amount。 current_amount为本次调用时,根据growth、utime_at、amount即时计算的当前货币量,可用于参考。
在实际实现中,队伍货币只存储为整数。
prec只用于前端显示确定小数点位置。amount等数值为原始存储的整数,而不是前端显示数值。- 例如:当
prec = 2,amount = 1_00时,前端显示为1.00。
query
$team.currency.query(): Currency[];
$team.currency.query(currency: number | string): Currency | null;查询队伍货币信息。
cost
$team.currency.cost(currency: number | string, amount: number, reason?: string | null): boolean;扣除队伍货币。如果成功,写入货币变动记录,并记录原因 reason。
- 如果提供的货币无效或函数过程中数值溢出,函数返回
false,此时不会更新货币。 amount > 0时,将扣除队伍货币。当队伍货币不足以扣除时,函数返回false,此时不会实际扣除货币。amount < 0时,将增加队伍货币。当队伍货币增加后超出max_amount时,函数返回false,此时不会实际增加货币。amount === 0时,函数返回true,且仍然写入变动记录。
这个操作是原子的,推荐用于购买等场景。
add
$team.currency.add(currency: number | string, amount: number, reason?: string | null): number | null;增加或减少队伍货币,写入货币变动记录,并记录原因 reason。返回实际变动量,可能为负。
- 如果提供的货币无效,函数返回
null,此时不会更新货币。 amount >= 0时,增加队伍货币,最多增加到max_amount,超出部分将忽略。amount < 0时,减少队伍货币,最多减少到0,超出部分将忽略。- 即使实际变动量为 0,也仍然写入变动记录。
这个操作是原子的。
update
$team.currency.update(currency: number | string, amount: number, reason?: string | null): Currency | null;
$team.currency.update(currency: number | string, options: { amount?: number; team_growth?: number; hidden?: boolean }, reason?: string | null): Currency | null;修改队伍货币余额或队伍货币设置。返回新的货币状态。
- 若更新
amount,将写入货币变动记录,并记录原因reason。否则reason没有作用。 - 被标为
hidden的货币将不会显示给队伍,但仍然正常随时间增长。
提交与解题
$this 中提供了添加提交记录和标记谜题解出的接口。
$this.submission.add(input: BackendSubmissionInput): BackendSubmission;
$this.solve(submission: BackendSubmission): boolean;INFO
为了防止状态冲突,自定义判题中无法调用这些函数。
BackendSubmission
type BackendSubmissionInput = {
userAnswer: string;
normAnswer?: string | null;
action?: JudgeAction;
result?: string | null;
realAnswer?: string | null;
ignored?: boolean;
};
type BackendSubmission = {
id: number;
teamId: number;
userId: number;
puzzleId: number;
userAnswer: string;
normAnswer: string;
action: number;
result: string | null;
realAnswer: string | null;
ignored: boolean;
ctimeAt: string;
};userAnswer是用户提交答案,normAnswer是规范化的用户提交答案。realAnswer为谜题实际答案,将显示给玩家作为参考。
$this.submission.add
$this.submission.add(input: BackendSubmissionInput): BackendSubmission;添加一条提交记录,其中提交者为函数调用者。返回添加的这条记录。
action 不会像自定义评测那样实际生效。但是未 ignored 的 fail 记录仍然会被用于累计错误提交次数,并导致以后的冷却惩罚时间增加。
$this.solve
$this.solve(submission: BackendSubmission): boolean;根据某条提交记录,将当前谜题直接标记为解出。如果这条记录 action 为 finish_game,则额外标记队伍完赛。
请使用其他函数返回的 BackendSubmission 作为参数,而不是自行构造对象,否则可能无法通过后端检查。
谜题资产
$puzzle.assets 提供当前谜题资产的接口。
$puzzle.assets.list(objectKey: string): AssetFile[];
$puzzle.assets.readText(objectKey: string, relativePath: string): string;
$puzzle.assets.readJson(objectKey: string, relativePath: string): unknown;
$puzzle.assets.readBytes(objectKey: string, relativePath: string): number[];objectKey 可以在资产组信息中查看。
AssetFile
type AssetFile = {
group_id: number;
backend: string;
object_key: string;
original_name: string;
relative_path: string;
mime_type: string;
size: number;
sha256: string;
};list
$puzzle.assets.list(objectKey: string): AssetFile[];列出资产组中的资产文件。
read
$puzzle.assets.readText(objectKey: string, relativePath: string): string;
$puzzle.assets.readJson(objectKey: string, relativePath: string): unknown;
$puzzle.assets.readBytes(objectKey: string, relativePath: string): number[];读取指定资产组中资产的内容。只允许读取通过 local 和 database 方式存储的资产。读取失败将抛出异常。
推荐用于后端需要读取私有数据的场景。
实时事件
$this.event 提供通过同步服务推送自定义同步事件的接口。
$this.event.emit(event: string, payload?: unknown): void;emit
$this.event.emit(event: string, payload?: unknown): void;向当前队伍推送当前谜题的自定义同步事件。
推荐通过 Vue SFC 组件监听相关事件,可用于实现简单的状态同步。
存储
谜题后端提供 KV 和 Store 两种存储方式,可用于不同情况下的存储需求,请分别参考对应页面。
Scope
KV 和 Store 存储都有作用域的设定,可分别用于不同场景。
| Scope | 对应全局对象 | 范围 |
|---|---|---|
global | $game | 比赛全局 |
team | $team | 队伍 |
puzzle | $puzzle | 谜题 |
team_puzzle | $this | 队伍 + 谜题 |
type GlobalScope = { type: 'global' };
type TeamScope = { type: 'team'; teamId: number };
type PuzzleScope = { type: 'puzzle'; puzzleId: number };
type TeamPuzzleScope = { type: 'team_puzzle'; teamId: number; puzzleId: number };
type Scope = GlobalScope | TeamScope | PuzzleScope | TeamPuzzleScope;