import '../models.dart'; import 'answer_rules.dart'; /// One stage-assessment task, from the level's `assessment` JSON. Choice /// tasks carry `choices` and `answerIndex`; open tasks (writing, speaking) /// carry an `accept` rule in the language of `answer_rules.dart` and the /// `guide` shown after a wrong answer. class AssessmentTask { const AssessmentTask({ required this.id, required this.skill, required this.prompt, this.audio, this.choices = const [], this.answerIndex, this.accept = const [], this.guide = '', }); factory AssessmentTask.fromJson(Map json) => AssessmentTask( id: json['id'] as String, skill: AssessmentSkill.values.byName(json['skill'] as String), prompt: json['prompt'] as String? ?? '', audio: json['audio'] as String?, choices: [for (final choice in json['choices'] as List? ?? const []) '$choice'], answerIndex: json['answerIndex'] as int?, accept: [ for (final group in json['accept'] as List? ?? const []) [for (final term in group as List) '$term'], ], guide: json['guide'] as String? ?? '', ); final String id; final AssessmentSkill skill; final String prompt; final String? audio; final List choices; final int? answerIndex; final List> accept; final String guide; } extension AssessmentSkillLabel on AssessmentSkill { String get label => switch (this) { AssessmentSkill.listening => '听力', AssessmentSkill.speaking => '口语', AssessmentSkill.reading => '阅读', AssessmentSkill.writing => '写作', }; } /// A stage-assessment pack. A replacement pack ([replaces] set) assesses the /// same abilities as its original with different people, places and values, /// and its results count toward the original. class AssessmentPack { const AssessmentPack({ required this.id, required this.level, required this.tasks, this.replaces, }); factory AssessmentPack.fromJson(Map json) => AssessmentPack( id: json['id'] as String, level: json['level'] as String? ?? '', replaces: json['replaces'] as String?, tasks: [ for (final task in json['tasks'] as List? ?? const []) AssessmentTask.fromJson(task as Map), ], ); final String id; final String level; final String? replaces; final List tasks; List forSkill(AssessmentSkill skill) => tasks.where((task) => task.skill == skill).toList(); } /// Every loaded assessment pack, in file order; filled by `CourseRepository`. final List assessmentPacks = []; /// The A0 stage assessments, in the order they unlock. List get a0AssessmentPacks => [ for (final pack in assessmentPacks) if (pack.level == 'A0' && pack.replaces == null) pack, ]; /// The A0 replacement packs. List get a0ReplacementPacks => [ for (final pack in assessmentPacks) if (pack.level == 'A0' && pack.replaces != null) pack, ]; /// The replacement pack of [packId], if it has one. AssessmentPack? replacementFor(String packId) => assessmentPacks.where((pack) => pack.replaces == packId).firstOrNull; /// The pack whose results [packId] counts toward: the original of a /// replacement pack, otherwise [packId] itself. String canonicalAssessmentPackId(String packId) => assessmentPacks .where((pack) => pack.id == packId) .firstOrNull ?.replaces ?? packId; /// Local check of an open task with the same rule engine as the lessons: it /// accepts variable names and places, but requires the communicative /// information the task names. bool checkOpenAssessmentAnswer(AssessmentTask task, String input) => task.accept.isNotEmpty && matchesRule(input, task.accept); /// What a correct answer to [task] looks like, shown when reviewing a /// wrong answer after the assessment. [answer] is what the learner gave. String assessmentAnswerGuide(AssessmentTask task, String answer) { final index = task.answerIndex; if (index != null && index >= 0 && index < task.choices.length) { final correct = task.choices[index]; if (task.skill == AssessmentSkill.listening && answer == correct) { return '正确答案:$correct(选对了,但作答前没有播放音频,所以不计分)'; } return '正确答案:$correct'; } if (task.guide.isNotEmpty) return task.guide; return task.skill == AssessmentSkill.writing ? '需要写一个完整的英文句子。' : '需要用完整英文回答题目要求。'; }