halkyon/halkyon_learn_flutter/test/feature/kanji/stroke_check_test.dart

128 lines
4.4 KiB
Dart
Raw Normal View History

import 'dart:math' as math;
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/src/feature/kanji/data/stroke_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/kanji/model/character_strokes.dart';
import 'package:halkyon_learn_flutter/src/feature/kanji/model/stroke_check.dart';
import 'package:halkyon_learn_flutter/src/feature/study/model/rating.dart';
/// Characters whose strokes the checks are tried on: kanji of few and many
/// strokes, parallel ones whose order matters, crossing ones, and kana.
const _characters = ['三', '十', '日', '木', '食', '語', 'あ', 'ア'];
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late Map<String, List<List<StrokePoint>>> strokes;
setUpAll(() async {
final repository = StrokeRepository();
strokes = {
for (final character in _characters)
character: (await repository.strokesOf(character))!.polylines(),
};
});
/// [points] moved by [dx], [dy].
List<StrokePoint> moved(List<StrokePoint> points, double dx, double dy) => [
for (final (:x, :y) in points) (x: x + dx, y: y + dy),
];
/// [points] grown by [factor] around their middle.
List<StrokePoint> grown(List<StrokePoint> points, double factor) {
final cx = points.map((p) => p.x).reduce((a, b) => a + b) / points.length;
final cy = points.map((p) => p.y).reduce((a, b) => a + b) / points.length;
return [
for (final (:x, :y) in points)
(x: cx + (x - cx) * factor, y: cy + (y - cy) * factor),
];
}
/// [points] as a hand wavers: off the line by up to [amplitude], twice
/// along it.
List<StrokePoint> wavering(List<StrokePoint> points, double amplitude) => [
for (final (i, (:x, :y)) in points.indexed)
(
x: x + amplitude * math.sin(i / points.length * 4 * math.pi),
y: y + amplitude * math.cos(i / points.length * 4 * math.pi),
),
];
test('a stroke written as it should be counts, also moved a little, '
'wavering, or a size larger or smaller', () {
for (final MapEntry(key: character, value: lines) in strokes.entries) {
for (final (i, line) in lines.indexed) {
for (final (how, written) in [
('as drawn', line),
('moved', moved(line, 5, -5)),
('wavering', wavering(line, 1.5)),
('larger', grown(line, 1.2)),
('smaller', grown(line, 0.85)),
]) {
expect(
checkStroke(written, lines, i),
StrokeVerdict.right,
reason: '$character, stroke ${i + 1}, $how',
);
}
}
}
});
test('a stroke written the other way round does not count, but for a '
'dot, which goes no way to speak of', () {
for (final MapEntry(key: character, value: lines) in strokes.entries) {
for (final (i, line) in lines.indexed) {
if (lengthOf(line) < 12) {
continue;
}
expect(
checkStroke(line.reversed.toList(), lines, i),
isNot(StrokeVerdict.right),
reason: '$character, stroke ${i + 1}',
);
}
}
});
test('a later stroke written before its turn is out of order', () {
for (final MapEntry(key: character, value: lines) in strokes.entries) {
for (var i = 0; i < lines.length; i++) {
for (var j = i + 1; j < lines.length; j++) {
expect(
checkStroke(lines[j], lines, i),
StrokeVerdict.wrongOrder,
reason: '$character: stroke ${j + 1} for ${i + 1}',
);
}
}
}
});
test('a stroke nowhere near any to come is wrong', () {
final lines = strokes['十']!;
// A diagonal through the box.
final slash = [
for (var t = 0.0; t <= 1; t += 0.02) (x: 10 + 80 * t, y: 10 + 80 * t),
];
expect(checkStroke(slash, lines, 0), StrokeVerdict.wrong);
expect(checkStroke(const [(x: 50, y: 50)], lines, 0), StrokeVerdict.wrong);
});
test('the rating suggested: none wrong Good, one or two Hard, more '
'Again', () {
expect(
[
for (final mistakes in [0, 1, 2, 3, 7]) suggestedRating(mistakes),
],
[Rating.good, Rating.hard, Rating.hard, Rating.again, Rating.again],
);
});
test('resampled: evenly along the line, the ends kept', () {
final points = resampled(const [(x: 0, y: 0), (x: 10, y: 0)], 11);
expect(points, hasLength(11));
expect(points.first, (x: 0.0, y: 0.0));
expect(points[5].x, closeTo(5, 1e-9));
expect(points.last, (x: 10.0, y: 0.0));
});
}