303 lines
9.9 KiB
Dart
303 lines
9.9 KiB
Dart
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. I’m Mia.|自我介绍|买东西|问时间|0',
|
||
'How do you spell your name?|问姓名拼写|问地点|问时间|0',
|
||
'I’m tired today.|很好|累|来自哪里|1',
|
||
'seven|6|7|8|1',
|
||
'one-three-eight|138|183|318|0',
|
||
'It’s a key.|书|钥匙|包|1',
|
||
'I’m from Hong Kong.|香港|伦敦|北京|0',
|
||
'This is my mother.|妈妈|姐姐|朋友|0',
|
||
'It is Monday.|星期一|星期二|星期三|0',
|
||
'I like tea.|茶|咖啡|电影|0',
|
||
],
|
||
const [
|
||
'Mia: I’m from Hong Kong.\nShen: I’m good.\nMia: My number is one-three-eight.|Mia 来自哪里?|Hong Kong|London|0',
|
||
'Mia: It is Monday.\nShen: It is three o’clock.|几点?|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: What’s this?\nShen: It’s 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',
|
||
'I’m okay.|很好|还可以|累|1',
|
||
'four|3|4|5|1',
|
||
'two-nine-six|296|269|926|0',
|
||
'It’s a bag.|笔|包|水|1',
|
||
'I’m from London.|伦敦|香港|北京|0',
|
||
'This is my sister.|姐姐|妈妈|朋友|0',
|
||
'It is Friday.|星期五|星期一|星期日|0',
|
||
'I like coffee.|茶|咖啡|音乐|1',
|
||
],
|
||
const [
|
||
'Mia: I’m from London.\nLeo: I’m okay.\nMia: My number is two-nine-six.|号码是多少?|296|269|0',
|
||
'Mia: It is Friday.\nLeo: It is six o’clock.|几点?|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: What’s this?\nLeo: It’s 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 o’clock': 'six o’clock',
|
||
'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 o’clock': 'two o’clock',
|
||
'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),
|
||
),
|
||
];
|
||
}
|