Files
English/kouyu_english/lib/core/assessment_bank.dart
T

303 lines
9.9 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'models.dart';
import 'writing_feedback.dart';
class AssessmentTask {
const AssessmentTask({
required this.id,
required this.skill,
required this.prompt,
this.audio,
this.choices = const [],
this.answerIndex,
this.requiredWords = const [],
required this.abilityId,
});
final String id;
final AssessmentSkill skill;
final String prompt;
final String? audio;
final List<String> choices;
final int? answerIndex;
final List<String> requiredWords;
final String abilityId;
}
class AssessmentPack {
const AssessmentPack({required this.id, required this.tasks});
final String id;
final List<AssessmentTask> tasks;
List<AssessmentTask> forSkill(AssessmentSkill skill) =>
tasks.where((task) => task.skill == skill).toList();
}
final a0AssessmentPacks = [
AssessmentPack(
id: 'A0-E1',
tasks: _tasks(
'A',
const [
'Hello. Im Mia.|自我介绍|买东西|问时间|0',
'How do you spell your name?|问姓名拼写|问地点|问时间|0',
'Im tired today.|很好|累|来自哪里|1',
'seven|6|7|8|1',
'one-three-eight|138|183|318|0',
'Its a key.|书|钥匙|包|1',
'Im from Hong Kong.|香港|伦敦|北京|0',
'This is my mother.|妈妈|姐姐|朋友|0',
'It is Monday.|星期一|星期二|星期三|0',
'I like tea.|茶|咖啡|电影|0',
],
const [
'Mia: Im from Hong Kong.\nShen: Im good.\nMia: My number is one-three-eight.|Mia 来自哪里?|Hong Kong|London|0',
'Mia: It is Monday.\nShen: It is three oclock.|几点?|three|one|0',
'Mia: This is my brother.\nShen: Nice to meet you.|谁是家人?|brother|mother|0',
'Mia: I like music.\nShen: I like tea.|Shen 喜欢什么?|tea|music|0',
'Mia: Whats this?\nShen: Its a pen.|这是什么?|pen|bag|0',
],
const [
'用英语介绍自己。|i,m',
'用英语说明你来自哪里。|i,m,from',
'用英语说一个喜好。|i,like',
'用英语说“这是一支笔”。|it,s,pen',
'用英语说明星期或整点。|it,s',
],
const [
'介绍姓名、地点、状态/喜好并反问。|hello,i,m,from',
'完整拼读一个四字母姓名。|a,l,e,x',
'说一个三位虚拟号码。|one,three,eight',
'介绍一位家人。|this,my',
'说明星期。|it,s,monday',
'说明整点。|it,s,o,clock',
'请求对方重复或放慢。|please',
'说出一个物品。|it,s',
],
),
),
AssessmentPack(
id: 'A0-E2',
tasks: _tasks(
'B',
const [
'Hi. My name is Leo.|自我介绍|买东西|问号码|0',
'How do you spell that?|问姓名拼写|问喜好|问家人|0',
'Im okay.|很好|还可以|累|1',
'four|3|4|5|1',
'two-nine-six|296|269|926|0',
'Its a bag.|笔|包|水|1',
'Im from London.|伦敦|香港|北京|0',
'This is my sister.|姐姐|妈妈|朋友|0',
'It is Friday.|星期五|星期一|星期日|0',
'I like coffee.|茶|咖啡|音乐|1',
],
const [
'Mia: Im from London.\nLeo: Im okay.\nMia: My number is two-nine-six.|号码是多少?|296|269|0',
'Mia: It is Friday.\nLeo: It is six oclock.|几点?|six|three|0',
'Mia: This is my father.\nLeo: Hi!|谁是家人?|father|brother|0',
'Mia: I like movies.\nLeo: I like coffee.|Leo 喜欢什么?|coffee|movies|0',
'Mia: Whats this?\nLeo: Its water.|这是什么?|water|book|0',
],
const [
'用英语介绍自己。|my,name,is',
'用英语说明你来自哪里。|i,m,from',
'用英语说一个喜好。|i,like',
'用英语说“这是一个包”。|it,s,bag',
'用英语说明星期或整点。|it,s',
],
const [
'介绍姓名、地点、状态/喜好并反问。|hi,i,m,from',
'完整拼读一个四字母姓名。|l,e,o,n',
'说一个三位虚拟号码。|two,nine,six',
'介绍一位家人。|this,my',
'说明星期。|it,s,friday',
'说明整点。|it,s,o,clock',
'请求对方重复或放慢。|please',
'说出一个物品。|it,s',
],
),
),
];
/// Replacement packs use different people, places, numbers, days and likes.
/// They keep the same assessed ability but are not immediate replays.
final a0ReplacementPacks = [
_replacementPack(a0AssessmentPacks[0], 'A0-E1R', const {
'Mia': 'Nora',
'Shen': 'Kai',
'Hong Kong': 'London',
'Monday': 'Friday',
'one-three-eight': 'two-four-seven',
'138': '247',
'three oclock': 'six oclock',
'tea': 'coffee',
'key': 'bag',
'mother': 'father',
}),
_replacementPack(a0AssessmentPacks[1], 'A0-E2R', const {
'Mia': 'Ava',
'Leo': 'Bo',
'London': 'Hong Kong',
'Friday': 'Tuesday',
'two-nine-six': 'five-one-four',
'296': '514',
'six oclock': 'two oclock',
'coffee': 'music',
'bag': 'book',
'sister': 'brother',
}),
];
AssessmentPack? replacementFor(String packId) => switch (packId) {
'A0-E1' => a0ReplacementPacks[0],
'A0-E2' => a0ReplacementPacks[1],
_ => null,
};
/// Local checks for the frozen A0 exit tasks. They accept variable names and
/// places, but require the communicative information named by each task.
bool checkOpenAssessmentAnswer(AssessmentTask task, String input) {
final text = input.toLowerCase().replaceAll('', "'");
final words = RegExp(
r"[a-z]+(?:'[a-z]+)?",
).allMatches(text).map((match) => match.group(0)!).toSet();
bool has(String word) => words.contains(word);
bool phrase(String value) => text.contains(value);
bool hasAny(Iterable<String> values) => values.any(has);
final introduction = phrase("i'm") || phrase('i am') || phrase('my name is');
final itIs = phrase("it's") || phrase('it is');
final numberWords = RegExp(
r'\b(zero|one|two|three|four|five|six|seven|eight|nine|ten)\b',
).allMatches(text).length;
final position = int.tryParse(
RegExp(r'(\d+)$').firstMatch(task.id)?.group(1) ?? '',
);
if (position == null) return false;
if (task.skill == AssessmentSkill.writing) {
const lessonIds = ['a0-01', 'a0-06', 'a0-09', 'a0-05', 'a0-08'];
return position >= 1 &&
position <= lessonIds.length &&
WritingFeedback.check(lessonIds[position - 1], input).complete;
}
if (task.skill != AssessmentSkill.speaking) return false;
return switch (position) {
1 =>
hasAny(['hello', 'hi']) &&
introduction &&
has('from') &&
(has('like') || hasAny(['good', 'okay', 'tired'])) &&
hasAny(['what', 'where', 'how', 'do']),
2 => RegExp(r'[a-z](?:[ -]?[a-z]){3,}').hasMatch(text),
3 => numberWords >= 3,
4 =>
phrase('this is my') && hasAny(['mother', 'father', 'sister', 'brother']),
5 =>
itIs &&
hasAny([
'monday',
'tuesday',
'wednesday',
'thursday',
'friday',
'saturday',
'sunday',
]),
6 =>
itIs &&
(has('clock') ||
has("o'clock") ||
has('oclock') ||
phrase("o'clock")),
7 => has('please') && hasAny(['say', 'speak', 'repeat', 'slow', 'slowly']),
8 => itIs && hasAny(['book', 'pen', 'bag', 'key', 'water']),
_ => false,
};
}
AssessmentPack _replacementPack(
AssessmentPack source,
String id,
Map<String, String> values,
) {
String replace(String value) {
var result = value;
for (final entry in values.entries) {
result = result.replaceAll(entry.key, entry.value);
}
return result;
}
return AssessmentPack(
id: id,
tasks: source.tasks
.map(
(task) => AssessmentTask(
id: task.id.replaceFirst(
source.id.split('-').last,
id.split('-').last,
),
skill: task.skill,
prompt: replace(task.prompt),
audio: task.audio == null ? null : replace(task.audio!),
choices: task.choices.map(replace).toList(),
answerIndex: task.answerIndex,
requiredWords: task.requiredWords.map(replace).toList(),
abilityId: task.abilityId,
),
)
.toList(),
);
}
List<AssessmentTask> _tasks(
String version,
List<String> listening,
List<String> reading,
List<String> writing,
List<String> speaking,
) {
AssessmentTask choice(String raw, AssessmentSkill skill, int index) {
final fields = raw.split('|');
return AssessmentTask(
id: 'A0-$version-${skill.name[0].toUpperCase()}${index + 1}',
skill: skill,
audio: skill == AssessmentSkill.listening ? fields[0] : null,
prompt: skill == AssessmentSkill.listening ? '选择正确答案' : fields[0],
choices: skill == AssessmentSkill.listening
? fields.sublist(1, 4)
: fields.sublist(1, 3),
answerIndex: int.parse(fields.last),
abilityId: 'A0-C${(index % 10 + 1).toString().padLeft(2, '0')}',
);
}
AssessmentTask open(String raw, AssessmentSkill skill, int index) {
final fields = raw.split('|');
return AssessmentTask(
id: 'A0-$version-${skill.name[0].toUpperCase()}${index + 1}',
skill: skill,
prompt: fields[0],
requiredWords: fields[1].split(','),
abilityId: 'A0-C${(index % 10 + 1).toString().padLeft(2, '0')}',
);
}
return [
...List.generate(
listening.length,
(i) => choice(listening[i], AssessmentSkill.listening, i),
),
...List.generate(
reading.length,
(i) => choice(reading[i], AssessmentSkill.reading, i),
),
...List.generate(
writing.length,
(i) => open(writing[i], AssessmentSkill.writing, i),
),
...List.generate(
speaking.length,
(i) => open(speaking[i], AssessmentSkill.speaking, i),
),
];
}