Skip to content

谜题后端 - API 参考

谜题后端中提供了多种工作函数,可用于复杂的谜题逻辑。

请求上下文

通过不同方式调用的后端函数都会接收一个上下文参数(通常命名为 ctx),所有上下文对象都是运行时数据的副本,运行时修改上下文字段不会产生副作用。

自定义 API

ts
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 };
};

自定义评测

ts
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 代表不作评测。

ts
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[];
};

提示购买钩子

ts
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 或货币标识指定货币。

ts
$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 参数用于指定队伍。例如:

ts
$team.currency.query(teamId: number): Currency[];

Currency

ts
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 为本次调用时,根据 growthutime_atamount 即时计算的当前货币量,可用于参考。

在实际实现中,队伍货币只存储为整数。

  • prec 只用于前端显示确定小数点位置。
  • amount 等数值为原始存储的整数,而不是前端显示数值。
  • 例如:当 prec = 2amount = 1_00 时,前端显示为 1.00

query

ts
$team.currency.query(): Currency[];
$team.currency.query(currency: number | string): Currency | null;

查询队伍货币信息。

cost

ts
$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

ts
$team.currency.add(currency: number | string, amount: number, reason?: string | null): number | null;

增加或减少队伍货币,写入货币变动记录,并记录原因 reason。返回实际变动量,可能为负。

  • 如果提供的货币无效,函数返回 null,此时不会更新货币。
  • amount >= 0 时,增加队伍货币,最多增加到 max_amount,超出部分将忽略。
  • amount < 0 时,减少队伍货币,最多减少到 0,超出部分将忽略。
  • 即使实际变动量为 0,也仍然写入变动记录。

这个操作是原子的。

update

ts
$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 中提供了添加提交记录和标记谜题解出的接口。

ts
$this.submission.add(input: BackendSubmissionInput): BackendSubmission;
$this.solve(submission: BackendSubmission): boolean;

INFO

为了防止状态冲突,自定义判题中无法调用这些函数。

BackendSubmission

ts
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

ts
$this.submission.add(input: BackendSubmissionInput): BackendSubmission;

添加一条提交记录,其中提交者为函数调用者。返回添加的这条记录。

action 不会像自定义评测那样实际生效。但是未 ignoredfail 记录仍然会被用于累计错误提交次数,并导致以后的冷却惩罚时间增加。

$this.solve

ts
$this.solve(submission: BackendSubmission): boolean;

根据某条提交记录,将当前谜题直接标记为解出。如果这条记录 actionfinish_game,则额外标记队伍完赛。

请使用其他函数返回的 BackendSubmission 作为参数,而不是自行构造对象,否则可能无法通过后端检查。

谜题资产

$puzzle.assets 提供当前谜题资产的接口。

ts
$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

ts
type AssetFile = {
  group_id: number;
  backend: string;
  object_key: string;
  original_name: string;
  relative_path: string;
  mime_type: string;
  size: number;
  sha256: string;
};

list

ts
$puzzle.assets.list(objectKey: string): AssetFile[];

列出资产组中的资产文件。

read

ts
$puzzle.assets.readText(objectKey: string, relativePath: string): string;
$puzzle.assets.readJson(objectKey: string, relativePath: string): unknown;
$puzzle.assets.readBytes(objectKey: string, relativePath: string): number[];

读取指定资产组中资产的内容。只允许读取通过 localdatabase 方式存储的资产。读取失败将抛出异常。

推荐用于后端需要读取私有数据的场景。

实时事件

$this.event 提供通过同步服务推送自定义同步事件的接口。

ts
$this.event.emit(event: string, payload?: unknown): void;

emit

ts
$this.event.emit(event: string, payload?: unknown): void;

向当前队伍推送当前谜题的自定义同步事件。

推荐通过 Vue SFC 组件监听相关事件,可用于实现简单的状态同步。

存储

谜题后端提供 KVStore 两种存储方式,可用于不同情况下的存储需求,请分别参考对应页面。

Scope

KV 和 Store 存储都有作用域的设定,可分别用于不同场景。

Scope对应全局对象范围
global$game比赛全局
team$team队伍
puzzle$puzzle谜题
team_puzzle$this队伍 + 谜题
ts
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;