feat(挑战): 做对之后可以加一条写法上的限制再做一遍,判题时记下谁做到了
借小丑牌的赌注档:同一道题加一条限制(「整首歌只用 1 次 print」)再做一遍, 给课上做得快的学生找事做(做完一节课的题,中位要空等 13 分钟)。 判定沿用 AST 规则,挑战配在 problem.challenges,做到的记在 challenge_success; 做对的提交顺手判一遍,挑战没做到也照样算做对。没过的规则带上是第几行。 0039 灌进第一批 56 道常用题的 73 条挑战,api 起来后拿历史上做对的代码补一遍谁已经做到了。 出题页能配挑战、从做对的代码里找候选;课堂看板的格子带上挑战状态。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cc1361e596
commit
0faf78ad8f
17 files changed
+9057
-37
No files matched your search
@@ -11,6 +11,10 @@ import { problemLanguageSchema, runnableLanguageSchema } from "./language"
|
||||
import {
|
||||
astRuleSchema,
|
||||
astRulesSchema,
|
||||
CHALLENGE_RULES_MAX,
|
||||
CHALLENGE_WORDING_MAX,
|
||||
CHALLENGES_PER_LANGUAGE_MAX,
|
||||
problemChallengesSchema,
|
||||
problemDifficultySchema,
|
||||
sqlConfigSchema,
|
||||
sqlDisplaySchema,
|
||||
@@ -945,6 +949,7 @@ export const adminProblemSchema = z.object({
|
||||
flowchartData: z.record(z.string(), z.unknown()),
|
||||
flowchartHint: z.string().nullable(),
|
||||
astRules: astRulesSchema.nullable(),
|
||||
challenges: problemChallengesSchema.nullable(),
|
||||
answers: z.array(problemAnswerSchema),
|
||||
prompt: z.string().nullable(),
|
||||
sqlConfig: sqlConfigSchema.nullable(),
|
||||
@@ -963,6 +968,21 @@ export const adminProblemSchema = z.object({
|
||||
*/
|
||||
export const PROBLEM_TAG_MAX_LENGTH = 32
|
||||
|
||||
/**
|
||||
* 出题页提交的一条挑战。新加的不带 id,后端发;规则改过的后端也换新 id(见契约
|
||||
* challengeSchema)。规则的语义校验(这种语言有没有这个语法)在后端 astRulesError,同 astRules
|
||||
*/
|
||||
export const challengeInputSchema = z.object({
|
||||
id: z.string().max(32).optional(),
|
||||
wording: z.string().trim().min(1).max(CHALLENGE_WORDING_MAX),
|
||||
rules: z.array(astRuleSchema).min(1).max(CHALLENGE_RULES_MAX),
|
||||
})
|
||||
|
||||
export const challengeInputsSchema = z.record(
|
||||
z.string(),
|
||||
z.array(challengeInputSchema).max(CHALLENGES_PER_LANGUAGE_MAX),
|
||||
)
|
||||
|
||||
export const createProblemRequestSchema = z.object({
|
||||
_id: z.string().trim().min(1).max(32),
|
||||
title: z.string().trim().min(1).max(1024),
|
||||
@@ -1002,6 +1022,11 @@ export const createProblemRequestSchema = z.object({
|
||||
flowchartData: z.record(z.string(), z.unknown()).nullable().default(null),
|
||||
flowchartHint: z.string().nullable().default(null),
|
||||
astRules: astRulesSchema.nullable().default(null),
|
||||
/**
|
||||
* 不带这个键 = 不动原来的挑战。上线那一刻浏览器里还开着旧版出题页,它保存时不带挑战,
|
||||
* 给个默认 null 的话一保存就把挑战清了;null 才是「清空」
|
||||
*/
|
||||
challenges: challengeInputsSchema.nullable().optional(),
|
||||
// order_sensitive 缺省补 false —— 与旧后端 SQLConfigSerializer 的
|
||||
// `BooleanField(default=False)` 一致
|
||||
sqlConfig: sqlConfigSchema
|
||||
@@ -1122,6 +1147,33 @@ export const astCheckResponseSchema = z.object({
|
||||
),
|
||||
})
|
||||
|
||||
/**
|
||||
* 出题页「挑战」的候选:拿这道题历史上做对的代码,找「一批人用另一种写法也做对了」的地方。
|
||||
* 只是给老师挑的素材 —— 机械找出来的会有坏的(拆掉 elif 换成一串 if 也算「不用 elif」),
|
||||
* 所以每条都带一份做到的人的真实代码,好不好老师看了再加
|
||||
*/
|
||||
export const challengeSuggestionsRequestSchema = z.object({
|
||||
language: runnableLanguageSchema,
|
||||
})
|
||||
|
||||
export const challengeSuggestionSchema = z.object({
|
||||
/** 默认说法,老师加上之后可以改 */
|
||||
wording: z.string(),
|
||||
rules: z.array(astRuleSchema),
|
||||
/** 这样做对过的人数 */
|
||||
did: z.number().int(),
|
||||
/** 第一次做对时就这样写的人占几成(%);「少用 print」这类没有 */
|
||||
firstShare: z.number().int().nullable(),
|
||||
/** 一份做到的人的代码 */
|
||||
example: z.string(),
|
||||
})
|
||||
|
||||
export const challengeSuggestionsResponseSchema = z.object({
|
||||
/** 这种语言做对过的人数 */
|
||||
acCount: z.number().int(),
|
||||
suggestions: z.array(challengeSuggestionSchema),
|
||||
})
|
||||
|
||||
/** AI 按题面和标准答案想几组测试输入(输出由标准答案在判题机上跑,不让 AI 编) */
|
||||
export const generateTestInputsRequestSchema = z.object({
|
||||
description: z.string().max(64 * 1024),
|
||||
@@ -1180,6 +1232,9 @@ export type GenerateSqlTestCaseResponse = z.infer<typeof generateSqlTestCaseResp
|
||||
export type TestCaseFiles = z.infer<typeof testCaseFilesSchema>
|
||||
export type AstCheckRequest = z.input<typeof astCheckRequestSchema>
|
||||
export type AstCheckResponse = z.infer<typeof astCheckResponseSchema>
|
||||
export type ChallengeInput = z.input<typeof challengeInputSchema>
|
||||
export type ChallengeSuggestion = z.infer<typeof challengeSuggestionSchema>
|
||||
export type ChallengeSuggestionsResponse = z.infer<typeof challengeSuggestionsResponseSchema>
|
||||
export type GenerateTestInputsRequest = z.input<typeof generateTestInputsRequestSchema>
|
||||
export type GenerateTestInputsResponse = z.infer<typeof generateTestInputsResponseSchema>
|
||||
|
||||
|
||||
@@ -88,6 +88,8 @@ export const classBoardProblemSchema = z.object({
|
||||
title: z.string(),
|
||||
/** SQL 题不受布置的语言限制:格子数的是 SQL 交的,点进提交列表也按 SQL 筛 */
|
||||
isSql: z.boolean(),
|
||||
/** 这道题配了挑战(见 problem.ts 的 challengeSchema):题卡片写挑战成功几人 */
|
||||
hasChallenges: z.boolean(),
|
||||
})
|
||||
|
||||
export const classBoardCellSchema = z.object({
|
||||
@@ -128,6 +130,11 @@ export const classBoardCellSchema = z.object({
|
||||
trail: z.array(z.object({ id: z.string(), result: judgeStatusSchema, createTime: z.string() })),
|
||||
/** 今天第几次代码提交做对的,1 = 一次就对;今天之前就做对了、今天没靠代码做对的为 null */
|
||||
acceptedTry: z.number().int().nullable(),
|
||||
/**
|
||||
* 挑战:done = 做到了这道题的一条挑战(不限今天);doing = 做对之后今天又交了、还没做到 ——
|
||||
* 做完了的学生是在挑战还是闲着,老师一眼看得出。没配挑战的题、没做对的格子为 null
|
||||
*/
|
||||
challenge: z.enum(["done", "doing"]).nullable(),
|
||||
})
|
||||
|
||||
export const classBoardStudentSchema = z.object({
|
||||
|
||||
@@ -355,6 +355,62 @@ export const astRequirementSchema = z.object({
|
||||
/** 按语言分组,与 astRulesSchema 同一套键 */
|
||||
export const astRequirementsSchema = z.record(z.string(), z.array(astRequirementSchema))
|
||||
|
||||
/**
|
||||
* 挑战:做对之后,同一道题加一条写法上的限制再做一遍(「整首歌只用 1 次 print」)。
|
||||
* 判定沿用 AST 规则,几条规则同时满足才算做到 —— 单写「用 while」的话学生留着原来的
|
||||
* for、再补一个空 while 就过了,所以常配成「用 while + 不用 for」。
|
||||
*
|
||||
* 和 astRules 一样按语言分组。`id` 是谁做到了(challenge_success)的记账键:
|
||||
* 规则一改就换新 id,旧的做到记录跟着旧规则作废,只改说法不换。
|
||||
* `scannedAt` 是后端记的「已经拿历史上做对的代码补过一遍记录」,见 services/challenge.ts。
|
||||
*/
|
||||
export const challengeSchema = z.object({
|
||||
id: z.string(),
|
||||
/** 给学生看的说法,例如「整首歌只用 1 次 print」 */
|
||||
wording: z.string(),
|
||||
rules: z.array(astRuleSchema),
|
||||
scannedAt: z.string().optional(),
|
||||
})
|
||||
|
||||
export const problemChallengesSchema = z.record(z.string(), z.array(challengeSchema))
|
||||
|
||||
/** 挑战说法的长度上限:要在结果页签的一行里放得下 */
|
||||
export const CHALLENGE_WORDING_MAX = 24
|
||||
|
||||
/** 一条挑战最多几条规则、一道题一种语言最多几条挑战 */
|
||||
export const CHALLENGE_RULES_MAX = 4
|
||||
export const CHALLENGES_PER_LANGUAGE_MAX = 4
|
||||
|
||||
/** 我做到了一条挑战:本班、全站各是第几个做到的 */
|
||||
export const challengeMineSchema = z.object({
|
||||
at: z.string(),
|
||||
siteRank: z.number().int(),
|
||||
/** 没有班级(老师、自己注册的号)时为 null */
|
||||
classRank: z.number().int().nullable(),
|
||||
})
|
||||
|
||||
/** 下发给学生的一条挑战:只有说法和人数,规则原文不出去(同 astRequirements) */
|
||||
export const challengeItemSchema = z.object({
|
||||
id: z.string(),
|
||||
wording: z.string(),
|
||||
/** 全站做到的人数 */
|
||||
doneCount: z.number().int(),
|
||||
mine: challengeMineSchema.nullable(),
|
||||
})
|
||||
|
||||
/** GET /challenges:几道题、某种语言下的挑战 */
|
||||
export const problemChallengeGroupSchema = z.object({
|
||||
problemId: z.string(),
|
||||
title: z.string(),
|
||||
/** 这种语言做对过这道题的人数(「642 个做对的人里 62 人做到」的 642) */
|
||||
acCount: z.number().int(),
|
||||
/** 我用这种语言做对过没有:没做对的挑战不给接 */
|
||||
solved: z.boolean(),
|
||||
challenges: z.array(challengeItemSchema),
|
||||
})
|
||||
|
||||
export const challengeListSchema = z.array(problemChallengeGroupSchema)
|
||||
|
||||
export const problemDetailSchema = z.object({
|
||||
id: z.number().int(),
|
||||
_id: z.string(),
|
||||
@@ -555,6 +611,12 @@ export type AstRule = z.infer<typeof astRuleSchema>
|
||||
export type AstRules = z.infer<typeof astRulesSchema>
|
||||
export type AstRequirement = z.infer<typeof astRequirementSchema>
|
||||
export type AstRequirements = z.infer<typeof astRequirementsSchema>
|
||||
export type Challenge = z.infer<typeof challengeSchema>
|
||||
export type ProblemChallenges = z.infer<typeof problemChallengesSchema>
|
||||
export type ChallengeMine = z.infer<typeof challengeMineSchema>
|
||||
export type ChallengeItem = z.infer<typeof challengeItemSchema>
|
||||
export type ProblemChallengeGroup = z.infer<typeof problemChallengeGroupSchema>
|
||||
export type ChallengeList = z.infer<typeof challengeListSchema>
|
||||
export type ProblemDifficulty = z.infer<typeof problemDifficultySchema>
|
||||
export type ProblemListItem = z.infer<typeof problemListItemSchema>
|
||||
export type ProblemList = z.infer<typeof problemListSchema>
|
||||
|
||||
@@ -51,6 +51,19 @@ export interface JudgeInfo {
|
||||
data: JudgeCaseResult[] | null
|
||||
}
|
||||
|
||||
/** 一条 AST 规则的判定结果,judge/ast.ts 的 checkAst 产出 */
|
||||
export const astResultSchema = z.object({
|
||||
description: z.string(),
|
||||
passed: z.boolean(),
|
||||
/** count_* 规则实际数到的次数,判题机只在这两个引擎上写 */
|
||||
actual: z.number().optional(),
|
||||
/**
|
||||
* 没过的「不能用 X」「X 最多 N 次」:X 出现在学生代码的第几行(1 起,已扣掉题目模板的
|
||||
* 前置代码)。2026-10 加的,之前的提交没有
|
||||
*/
|
||||
lines: z.array(z.number().int()).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* 判题产出的统计(`submission.statistic_info` 的 JSONB 原文)。
|
||||
*
|
||||
@@ -67,13 +80,17 @@ export const statisticInfoSchema = z.looseObject({
|
||||
err_info: z.string().optional(),
|
||||
time_cost: z.number().optional(),
|
||||
memory_cost: z.number().optional(),
|
||||
ast_results: z
|
||||
ast_results: z.array(astResultSchema).optional(),
|
||||
/**
|
||||
* 做对的提交拿这道题的每条挑战(problem.challenges 里这种语言的)各判一遍,
|
||||
* judge/run.ts 写。比赛提交不判。前端在挑战中时按 id 取它正在挑战的那条
|
||||
*/
|
||||
challenges: z
|
||||
.array(
|
||||
z.object({
|
||||
description: z.string(),
|
||||
id: z.string(),
|
||||
passed: z.boolean(),
|
||||
/** count_* 规则实际数到的次数,判题机只在这两个引擎上写 */
|
||||
actual: z.number().optional(),
|
||||
results: z.array(astResultSchema),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
@@ -616,6 +633,7 @@ export type TrialRunResponse =
|
||||
| { status: "too-much-output" }
|
||||
|
||||
export type StatisticInfo = z.infer<typeof statisticInfoSchema>
|
||||
export type AstResultInfo = z.infer<typeof astResultSchema>
|
||||
export type SubmissionTrace = z.infer<typeof submissionTraceSchema>
|
||||
export type CreateSubmissionRequest = z.input<typeof createSubmissionRequestSchema>
|
||||
export type SubmissionDetail = z.infer<typeof submissionDetailSchema>
|
||||
|
||||
Reference in new issue
Block a user