chore: Halkyon Learn moves into apps/learn
Its server, client, app and curator, and the tools that write its assets (maps, Anki fixtures, icons), as they are; paths are fixed in the commits that follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
11abd9afce
commit
649499be23
4 changed files with 0 additions and 0 deletions
|
|
@ -0,0 +1,26 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/kanji/model/hangul.dart';
|
||||
|
||||
void main() {
|
||||
test('a Hangul syllable is written with its letters: the opening '
|
||||
'consonant, the vowel and the closing one or cluster', () {
|
||||
expect(hangulLetters('가'), ['ㄱ', 'ㅏ']);
|
||||
expect(hangulLetters('닭'), ['ㄷ', 'ㅏ', 'ㄺ']);
|
||||
expect(hangulLetters('쓰'), ['ㅆ', 'ㅡ']);
|
||||
expect(hangulLetters('왜'), ['ㅇ', 'ㅙ']);
|
||||
expect(hangulLetters('힣'), ['ㅎ', 'ㅣ', 'ㅎ']);
|
||||
// What is no syllable stays as it is.
|
||||
expect(hangulLetters('ㄱ'), ['ㄱ']);
|
||||
expect(hangulLetters('食'), ['食']);
|
||||
});
|
||||
|
||||
test('scripts whose strokes are drawn: Han characters, kana, Hangul; '
|
||||
'not the alphabets', () {
|
||||
expect(hasDrawnScript('食べる'), isTrue);
|
||||
expect(hasDrawnScript('学习'), isTrue);
|
||||
expect(hasDrawnScript('학교'), isTrue);
|
||||
expect(hasDrawnScript('ㄱ'), isTrue);
|
||||
expect(hasDrawnScript('das Haus'), isFalse);
|
||||
expect(hasDrawnScript('Β β'), isFalse);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/kanji/model/radical.dart';
|
||||
|
||||
void main() {
|
||||
test('the 214 Kangxi radicals in their order, by their strokes from 1 '
|
||||
'to 17', () {
|
||||
expect(
|
||||
[for (final radical in radicals) radical.number],
|
||||
[
|
||||
for (var number = 1; number <= 214; number++) number,
|
||||
],
|
||||
);
|
||||
final strokes = [for (final radical in radicals) radical.strokes];
|
||||
expect(strokes, orderedEquals([...strokes]..sort()));
|
||||
expect((strokes.first, strokes.last), (1, 17));
|
||||
// Const, as the table is: a record compares its lists by identity.
|
||||
expect(radicals[84], const (
|
||||
number: 85,
|
||||
character: '水',
|
||||
variants: ['氵'],
|
||||
strokes: 4,
|
||||
name: 'water',
|
||||
));
|
||||
});
|
||||
|
||||
test('each is one character, as is every form it takes in kanji', () {
|
||||
for (final radical in radicals) {
|
||||
for (final form in [radical.character, ...radical.variants]) {
|
||||
expect(form.runes, hasLength(1), reason: '${radical.number}');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("a radical's label is the dictionary's", () {
|
||||
expect(radicalLabel(85), 'radical:85');
|
||||
});
|
||||
|
||||
test('the forms a radical takes in each language: simplified Chinese its '
|
||||
"own, traditional Chinese and Korean without Japanese's own", () {
|
||||
final speech = radicals[148];
|
||||
final dragon = radicals[211];
|
||||
final food = radicals[183];
|
||||
expect(radicalFormsIn(speech, 'zh-Hans'), ['讠']);
|
||||
expect(radicalFormsIn(speech, 'zh-Hant'), isEmpty);
|
||||
expect(radicalFormsIn(dragon, 'ja'), ['竜']);
|
||||
expect(radicalFormsIn(dragon, 'zh'), ['龙']);
|
||||
expect(radicalFormsIn(dragon, 'ko'), isEmpty);
|
||||
// Simplified Chinese writes 食 as 饣 at a character's side, not 飠.
|
||||
expect(radicalFormsIn(food, 'zh-CN'), ['饣']);
|
||||
expect(radicalFormsIn(food, 'zh-TW'), ['飠']);
|
||||
expect(radicalsIn('zh-Hans')[148].variants, ['讠']);
|
||||
// Each form once, never the radical itself.
|
||||
expect(radicalFormsIn(radicals[198], 'zh'), ['麦']);
|
||||
expect(radicalFormsIn(radicals[202], 'zh'), isEmpty);
|
||||
expect((hasRadicals('ko'), hasRadicals('el')), (true, false));
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
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));
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
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';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('a stroke', () {
|
||||
test('as points: a line cut into steps, its ends kept', () {
|
||||
final points = strokePolyline('M10,10L20,10');
|
||||
|
||||
expect(points, hasLength(11));
|
||||
expect(points.first, (x: 10.0, y: 10.0));
|
||||
expect(points.last, (x: 20.0, y: 10.0));
|
||||
});
|
||||
|
||||
test('as points: a curve flattened, no step longer than asked, ending '
|
||||
'where it ends', () {
|
||||
final points = strokePolyline('M0,0c10,0,20,10,20,20', step: 2);
|
||||
|
||||
expect(points.last.x, closeTo(20, 1e-9));
|
||||
expect(points.last.y, closeTo(20, 1e-9));
|
||||
for (var i = 1; i < points.length; i++) {
|
||||
final (:x, :y) = points[i];
|
||||
final before = points[i - 1];
|
||||
final dx = x - before.x;
|
||||
final dy = y - before.y;
|
||||
expect(dx * dx + dy * dy, lessThanOrEqualTo(4 + 1e-9));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group('the bundled strokes', () {
|
||||
test("a character's strokes in order, and its components", () async {
|
||||
final strokes = StrokeRepository();
|
||||
|
||||
final eat = await strokes.strokesOf('食');
|
||||
expect(eat!.paths, hasLength(9));
|
||||
expect(eat.paths.first, startsWith('M52.75,10.5'));
|
||||
expect((await strokes.strokesOf('語'))!.components, ['言', '吾']);
|
||||
expect((await strokes.strokesOf('あ'))!.paths, hasLength(3));
|
||||
expect(strokes.read('食'), eat);
|
||||
});
|
||||
|
||||
test('none for a character KanjiVG has no file for, or for more than '
|
||||
'one character', () async {
|
||||
final strokes = StrokeRepository();
|
||||
|
||||
expect(await strokes.strokesOf('😀'), isNull);
|
||||
expect(await strokes.strokesOf('食べる'), isNull);
|
||||
expect(await strokes.strokesOf(''), isNull);
|
||||
});
|
||||
|
||||
test("Chinese draws its characters as Make Me a Hanzi does, KanjiVG's "
|
||||
'for what it lacks; Korean its letters with its own strokes', () async {
|
||||
final strokes = StrokeRepository();
|
||||
|
||||
// 说 is simplified Chinese's alone, which KanjiVG doesn't draw.
|
||||
expect(await strokes.strokesOf('说'), isNull);
|
||||
final say = await strokes.strokesOf('说', language: 'zh-Hans');
|
||||
expect(say!.paths, hasLength(9));
|
||||
expect(strokes.read('说', language: 'zh'), say);
|
||||
// 水 is drawn by both, each in its own shapes.
|
||||
expect(
|
||||
(await strokes.strokesOf('水', language: 'zh'))!.paths,
|
||||
isNot((await strokes.strokesOf('水'))!.paths),
|
||||
);
|
||||
// A kana Make Me a Hanzi doesn't draw comes from KanjiVG.
|
||||
expect(await strokes.strokesOf('あ', language: 'zh'), isNotNull);
|
||||
expect(
|
||||
(await strokes.strokesOf('ㄹ', language: 'ko'))!.paths,
|
||||
hasLength(3),
|
||||
);
|
||||
expect(
|
||||
(await strokes.strokesOf('ㄺ', language: 'ko'))!.components,
|
||||
['ㄹ', 'ㄱ'],
|
||||
);
|
||||
expect(await strokes.strokesOf('ㄹ', language: 'ja'), isNull);
|
||||
});
|
||||
|
||||
test('each set by language: Japanese KanjiVG, Chinese Make Me a Hanzi '
|
||||
'first, Korean its letters first', () {
|
||||
expect(StrokeSet.of(null), [StrokeSet.kanjivg]);
|
||||
expect(StrokeSet.of('zh-Hant'), [
|
||||
StrokeSet.makemeahanzi,
|
||||
StrokeSet.kanjivg,
|
||||
]);
|
||||
expect(StrokeSet.of('ko').first, StrokeSet.hangul);
|
||||
});
|
||||
|
||||
test('the file a character is in: each 256 code points', () {
|
||||
expect(strokeFileOf('食'), '098');
|
||||
expect(strokeFileOf('あ'), '030');
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue