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>> strokes; setUpAll(() async { final repository = StrokeRepository(); strokes = { for (final character in _characters) character: (await repository.strokesOf(character))!.polylines(), }; }); /// [points] moved by [dx], [dy]. List moved(List 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 grown(List 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 wavering(List 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)); }); }