parent
e3ffc4d556
commit
cad489b678
@ -1 +0,0 @@
|
|||||||
Subproject commit 3735b434dc2160c1f323e2a925f9e87b3b9dfee8
|
|
||||||
@ -0,0 +1,284 @@
|
|||||||
|
SF:/Users/znup/projects/decky/packages/practice_engine/lib/models/deck.dart
|
||||||
|
DA:24,4
|
||||||
|
DA:34,4
|
||||||
|
DA:42,4
|
||||||
|
DA:43,4
|
||||||
|
DA:44,3
|
||||||
|
DA:45,4
|
||||||
|
DA:46,1
|
||||||
|
DA:47,4
|
||||||
|
DA:48,3
|
||||||
|
DA:53,3
|
||||||
|
DA:56,6
|
||||||
|
DA:59,1
|
||||||
|
DA:60,2
|
||||||
|
DA:61,5
|
||||||
|
DA:64,0
|
||||||
|
DA:67,0
|
||||||
|
DA:68,0
|
||||||
|
DA:69,0
|
||||||
|
DA:70,0
|
||||||
|
DA:71,0
|
||||||
|
DA:72,0
|
||||||
|
DA:73,0
|
||||||
|
DA:74,0
|
||||||
|
DA:76,0
|
||||||
|
DA:78,0
|
||||||
|
DA:79,0
|
||||||
|
DA:80,0
|
||||||
|
DA:81,0
|
||||||
|
DA:82,0
|
||||||
|
DA:83,0
|
||||||
|
LF:30
|
||||||
|
LH:14
|
||||||
|
end_of_record
|
||||||
|
SF:/Users/znup/projects/decky/packages/practice_engine/lib/models/deck_config.dart
|
||||||
|
DA:18,7
|
||||||
|
DA:27,1
|
||||||
|
DA:34,1
|
||||||
|
DA:36,0
|
||||||
|
DA:37,1
|
||||||
|
DA:39,1
|
||||||
|
DA:41,1
|
||||||
|
DA:43,0
|
||||||
|
DA:47,2
|
||||||
|
DA:50,1
|
||||||
|
DA:51,3
|
||||||
|
DA:52,3
|
||||||
|
DA:53,0
|
||||||
|
DA:54,0
|
||||||
|
DA:55,0
|
||||||
|
DA:56,0
|
||||||
|
DA:58,0
|
||||||
|
DA:60,0
|
||||||
|
DA:61,0
|
||||||
|
DA:62,0
|
||||||
|
DA:63,0
|
||||||
|
DA:64,0
|
||||||
|
LF:22
|
||||||
|
LH:10
|
||||||
|
end_of_record
|
||||||
|
SF:/Users/znup/projects/decky/packages/practice_engine/lib/models/question.dart
|
||||||
|
DA:33,9
|
||||||
|
DA:47,6
|
||||||
|
DA:59,6
|
||||||
|
DA:60,6
|
||||||
|
DA:61,6
|
||||||
|
DA:62,6
|
||||||
|
DA:63,6
|
||||||
|
DA:64,5
|
||||||
|
DA:65,4
|
||||||
|
DA:66,4
|
||||||
|
DA:67,5
|
||||||
|
DA:68,5
|
||||||
|
DA:69,5
|
||||||
|
DA:74,4
|
||||||
|
DA:75,8
|
||||||
|
DA:78,3
|
||||||
|
DA:81,2
|
||||||
|
DA:82,6
|
||||||
|
DA:83,6
|
||||||
|
DA:84,0
|
||||||
|
DA:85,0
|
||||||
|
DA:86,0
|
||||||
|
DA:87,0
|
||||||
|
DA:88,0
|
||||||
|
DA:89,0
|
||||||
|
DA:90,0
|
||||||
|
DA:91,0
|
||||||
|
DA:92,0
|
||||||
|
DA:94,0
|
||||||
|
DA:96,0
|
||||||
|
DA:97,0
|
||||||
|
DA:98,0
|
||||||
|
DA:99,0
|
||||||
|
DA:100,0
|
||||||
|
DA:101,0
|
||||||
|
DA:102,0
|
||||||
|
DA:103,0
|
||||||
|
DA:104,0
|
||||||
|
DA:105,0
|
||||||
|
LF:39
|
||||||
|
LH:19
|
||||||
|
end_of_record
|
||||||
|
SF:/Users/znup/projects/decky/packages/practice_engine/lib/logic/deck_service.dart
|
||||||
|
DA:11,1
|
||||||
|
DA:15,3
|
||||||
|
DA:16,2
|
||||||
|
DA:17,1
|
||||||
|
DA:18,2
|
||||||
|
DA:23,1
|
||||||
|
DA:25,1
|
||||||
|
DA:31,1
|
||||||
|
DA:35,3
|
||||||
|
DA:36,2
|
||||||
|
DA:37,1
|
||||||
|
DA:44,1
|
||||||
|
DA:46,1
|
||||||
|
DA:53,1
|
||||||
|
DA:57,3
|
||||||
|
DA:58,1
|
||||||
|
DA:62,1
|
||||||
|
DA:63,1
|
||||||
|
DA:64,1
|
||||||
|
DA:66,1
|
||||||
|
DA:68,1
|
||||||
|
DA:77,3
|
||||||
|
DA:87,6
|
||||||
|
DA:88,3
|
||||||
|
DA:91,6
|
||||||
|
DA:92,2
|
||||||
|
DA:96,3
|
||||||
|
DA:103,3
|
||||||
|
DA:104,6
|
||||||
|
DA:105,6
|
||||||
|
DA:109,2
|
||||||
|
DA:115,2
|
||||||
|
DA:122,2
|
||||||
|
DA:123,4
|
||||||
|
DA:128,3
|
||||||
|
DA:137,1
|
||||||
|
DA:148,1
|
||||||
|
DA:149,2
|
||||||
|
DA:154,1
|
||||||
|
LF:39
|
||||||
|
LH:39
|
||||||
|
end_of_record
|
||||||
|
SF:/Users/znup/projects/decky/packages/practice_engine/lib/models/attempt_result.dart
|
||||||
|
DA:24,2
|
||||||
|
DA:52,2
|
||||||
|
DA:62,2
|
||||||
|
DA:66,8
|
||||||
|
DA:67,2
|
||||||
|
DA:68,2
|
||||||
|
DA:69,4
|
||||||
|
DA:72,8
|
||||||
|
DA:74,2
|
||||||
|
LF:9
|
||||||
|
LH:9
|
||||||
|
end_of_record
|
||||||
|
SF:/Users/znup/projects/decky/packages/practice_engine/lib/algorithms/priority_manager.dart
|
||||||
|
DA:7,4
|
||||||
|
DA:13,8
|
||||||
|
DA:14,4
|
||||||
|
DA:15,4
|
||||||
|
DA:16,4
|
||||||
|
DA:17,4
|
||||||
|
DA:19,3
|
||||||
|
DA:20,6
|
||||||
|
DA:21,3
|
||||||
|
DA:27,1
|
||||||
|
DA:28,1
|
||||||
|
LF:11
|
||||||
|
LH:11
|
||||||
|
end_of_record
|
||||||
|
SF:/Users/znup/projects/decky/packages/practice_engine/lib/algorithms/weighted_selector.dart
|
||||||
|
DA:10,4
|
||||||
|
DA:16,2
|
||||||
|
DA:21,4
|
||||||
|
DA:22,1
|
||||||
|
DA:25,4
|
||||||
|
DA:26,2
|
||||||
|
DA:29,2
|
||||||
|
DA:30,2
|
||||||
|
DA:32,6
|
||||||
|
DA:33,2
|
||||||
|
DA:37,2
|
||||||
|
DA:38,2
|
||||||
|
DA:45,2
|
||||||
|
DA:49,4
|
||||||
|
DA:50,0
|
||||||
|
DA:54,4
|
||||||
|
DA:55,2
|
||||||
|
DA:56,0
|
||||||
|
DA:57,0
|
||||||
|
DA:62,6
|
||||||
|
DA:64,2
|
||||||
|
DA:67,2
|
||||||
|
DA:69,4
|
||||||
|
DA:70,2
|
||||||
|
DA:71,2
|
||||||
|
DA:75,6
|
||||||
|
DA:78,6
|
||||||
|
DA:79,4
|
||||||
|
DA:80,2
|
||||||
|
DA:85,0
|
||||||
|
LF:30
|
||||||
|
LH:26
|
||||||
|
end_of_record
|
||||||
|
SF:/Users/znup/projects/decky/packages/practice_engine/lib/algorithms/spaced_repetition.dart
|
||||||
|
DA:16,1
|
||||||
|
DA:20,1
|
||||||
|
DA:25,1
|
||||||
|
DA:29,2
|
||||||
|
DA:30,1
|
||||||
|
DA:31,2
|
||||||
|
DA:38,1
|
||||||
|
DA:43,1
|
||||||
|
DA:45,1
|
||||||
|
DA:46,1
|
||||||
|
DA:48,1
|
||||||
|
DA:52,3
|
||||||
|
LF:12
|
||||||
|
LH:12
|
||||||
|
end_of_record
|
||||||
|
SF:/Users/znup/projects/decky/packages/practice_engine/lib/logic/attempt_service.dart
|
||||||
|
DA:14,2
|
||||||
|
DA:20,1
|
||||||
|
DA:24,2
|
||||||
|
DA:25,2
|
||||||
|
DA:26,1
|
||||||
|
DA:28,1
|
||||||
|
DA:31,1
|
||||||
|
DA:32,1
|
||||||
|
DA:34,2
|
||||||
|
DA:43,1
|
||||||
|
DA:53,1
|
||||||
|
DA:54,2
|
||||||
|
DA:55,2
|
||||||
|
DA:57,1
|
||||||
|
DA:58,1
|
||||||
|
DA:61,2
|
||||||
|
DA:62,2
|
||||||
|
DA:68,2
|
||||||
|
DA:69,2
|
||||||
|
DA:72,1
|
||||||
|
DA:73,1
|
||||||
|
DA:77,0
|
||||||
|
DA:81,0
|
||||||
|
DA:82,0
|
||||||
|
DA:84,1
|
||||||
|
DA:87,1
|
||||||
|
DA:88,1
|
||||||
|
DA:96,1
|
||||||
|
DA:98,2
|
||||||
|
DA:104,0
|
||||||
|
DA:106,1
|
||||||
|
DA:113,2
|
||||||
|
DA:120,1
|
||||||
|
DA:124,1
|
||||||
|
DA:125,5
|
||||||
|
DA:128,2
|
||||||
|
DA:129,2
|
||||||
|
DA:132,3
|
||||||
|
DA:133,2
|
||||||
|
DA:134,1
|
||||||
|
DA:136,1
|
||||||
|
DA:138,2
|
||||||
|
DA:141,1
|
||||||
|
DA:153,1
|
||||||
|
DA:154,5
|
||||||
|
LF:45
|
||||||
|
LH:41
|
||||||
|
end_of_record
|
||||||
|
SF:/Users/znup/projects/decky/packages/practice_engine/lib/models/attempt.dart
|
||||||
|
DA:14,1
|
||||||
|
DA:21,0
|
||||||
|
DA:26,0
|
||||||
|
DA:27,0
|
||||||
|
DA:28,0
|
||||||
|
DA:29,0
|
||||||
|
DA:34,0
|
||||||
|
LF:7
|
||||||
|
LH:1
|
||||||
|
end_of_record
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"type":"CodeCoverage","coverage":[{"source":"package:practice_engine/models/deck.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fdeck.dart","uri":"package:practice_engine/models/deck.dart","_kind":"library"},"hits":[24,1,34,1,42,1,43,1,44,1,45,1,47,1,48,1,46,0,53,0,56,0,59,0,60,0,61,0,64,0,67,0,68,0,69,0,70,0,71,0,72,0,73,0,74,0,76,0,78,0,79,0,80,0,81,0,82,0,83,0]},{"source":"package:practice_engine/models/deck_config.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fdeck_config.dart","uri":"package:practice_engine/models/deck_config.dart","_kind":"library"},"hits":[18,1,27,0,34,0,36,0,37,0,39,0,41,0,43,0,47,0,50,0,51,0,52,0,53,0,54,0,55,0,56,0,58,0,60,0,61,0,62,0,63,0,64,0]},{"source":"package:practice_engine/models/question.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fquestion.dart","uri":"package:practice_engine/models/question.dart","_kind":"library"},"hits":[33,1,47,1,59,1,60,1,61,1,62,1,63,1,64,1,65,1,66,1,67,1,68,1,69,1,74,1,75,2,78,1,81,1,82,3,83,3,84,0,85,0,86,0,87,0,88,0,89,0,90,0,91,0,92,0,94,0,96,0,97,0,98,0,99,0,100,0,101,0,102,0,103,0,104,0,105,0]},{"source":"package:practice_engine/logic/attempt_service.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Flogic%2Fattempt_service.dart","uri":"package:practice_engine/logic/attempt_service.dart","_kind":"library"},"hits":[14,2,20,1,24,2,25,2,26,1,28,1,31,1,32,1,34,2,43,1,53,1,54,2,55,2,57,1,58,1,61,2,62,2,68,2,69,2,72,1,73,1,84,1,87,1,88,1,96,1,98,2,106,1,113,2,120,1,124,1,125,5,128,2,129,2,132,3,134,1,136,1,138,2,141,1,77,0,81,0,82,0,104,0,153,1,154,5,133,2]},{"source":"package:practice_engine/models/attempt_result.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fattempt_result.dart","uri":"package:practice_engine/models/attempt_result.dart","_kind":"library"},"hits":[24,1,52,1,62,1,66,4,67,1,68,1,69,2,72,4,74,1]},{"source":"package:practice_engine/algorithms/spaced_repetition.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Falgorithms%2Fspaced_repetition.dart","uri":"package:practice_engine/algorithms/spaced_repetition.dart","_kind":"library"},"hits":[16,0,20,0,25,0,29,0,30,0,31,0,38,0,43,0,45,0,46,0,48,0,52,0]},{"source":"package:practice_engine/algorithms/weighted_selector.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Falgorithms%2Fweighted_selector.dart","uri":"package:practice_engine/algorithms/weighted_selector.dart","_kind":"library"},"hits":[10,2,16,1,21,2,25,2,26,1,29,1,30,1,32,3,33,1,37,1,38,1,22,0,45,1,49,2,54,2,64,1,67,1,69,2,70,1,71,1,75,3,78,3,79,2,80,1,50,0,85,0,55,1,62,3,56,0,57,0]},{"source":"package:practice_engine/models/attempt.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fattempt.dart","uri":"package:practice_engine/models/attempt.dart","_kind":"library"},"hits":[14,1,21,0,26,0,27,0,28,0,29,0,34,0]},{"source":"package:practice_engine/logic/deck_service.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Flogic%2Fdeck_service.dart","uri":"package:practice_engine/logic/deck_service.dart","_kind":"library"},"hits":[11,0,15,0,23,0,25,0,31,0,35,0,44,0,46,0,53,0,57,0,66,0,68,0,77,1,87,2,88,1,91,2,92,1,96,1,103,1,104,2,105,2,109,1,115,1,122,1,123,2,128,1,137,0,148,0,149,0,154,0,16,0,17,0,18,0,36,0,37,0,58,0,62,0,63,0,64,0]},{"source":"package:practice_engine/algorithms/priority_manager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Falgorithms%2Fpriority_manager.dart","uri":"package:practice_engine/algorithms/priority_manager.dart","_kind":"library"},"hits":[7,1,13,2,14,1,15,1,16,1,17,1,19,1,20,2,21,1,27,0,28,0]}]}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"type":"CodeCoverage","coverage":[{"source":"package:practice_engine/models/question.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fquestion.dart","uri":"package:practice_engine/models/question.dart","_kind":"library"},"hits":[33,1,47,0,59,0,60,0,61,0,62,0,63,0,64,0,65,0,66,0,67,0,68,0,69,0,74,0,75,0,78,1,81,0,82,0,83,0,84,0,85,0,86,0,87,0,88,0,89,0,90,0,91,0,92,0,94,0,96,0,97,0,98,0,99,0,100,0,101,0,102,0,103,0,104,0,105,0]},{"source":"package:practice_engine/models/attempt_result.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fattempt_result.dart","uri":"package:practice_engine/models/attempt_result.dart","_kind":"library"},"hits":[24,1,52,1,62,1,66,4,67,1,68,1,69,2,72,4,74,1]}]}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"type":"CodeCoverage","coverage":[{"source":"package:practice_engine/models/deck_config.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fdeck_config.dart","uri":"package:practice_engine/models/deck_config.dart","_kind":"library"},"hits":[18,1,27,1,34,1,37,1,39,1,41,1,36,0,43,0,47,1,50,1,51,3,52,3,53,0,54,0,55,0,56,0,58,0,60,0,61,0,62,0,63,0,64,0]},{"source":"package:practice_engine/models/attempt_result.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fattempt_result.dart","uri":"package:practice_engine/models/attempt_result.dart","_kind":"library"},"hits":[24,0,52,0,62,0,66,0,67,0,68,0,69,0,72,0,74,0]},{"source":"package:practice_engine/models/question.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fquestion.dart","uri":"package:practice_engine/models/question.dart","_kind":"library"},"hits":[33,0,47,0,59,0,60,0,61,0,62,0,63,0,64,0,65,0,66,0,67,0,68,0,69,0,74,0,75,0,78,0,81,0,82,0,83,0,84,0,85,0,86,0,87,0,88,0,89,0,90,0,91,0,92,0,94,0,96,0,97,0,98,0,99,0,100,0,101,0,102,0,103,0,104,0,105,0]},{"source":"package:practice_engine/algorithms/priority_manager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Falgorithms%2Fpriority_manager.dart","uri":"package:practice_engine/algorithms/priority_manager.dart","_kind":"library"},"hits":[7,0,13,0,14,0,15,0,16,0,17,0,19,0,20,0,21,0,27,0,28,0]},{"source":"package:practice_engine/logic/deck_service.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Flogic%2Fdeck_service.dart","uri":"package:practice_engine/logic/deck_service.dart","_kind":"library"},"hits":[11,0,15,0,23,0,25,0,31,0,35,0,44,0,46,0,53,0,57,0,66,0,68,0,77,0,87,0,88,0,91,0,92,0,96,0,103,0,104,0,105,0,109,0,115,0,122,0,123,0,128,0,137,0,148,0,149,0,154,0,16,0,17,0,18,0,36,0,37,0,58,0,62,0,63,0,64,0]},{"source":"package:practice_engine/models/deck.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fdeck.dart","uri":"package:practice_engine/models/deck.dart","_kind":"library"},"hits":[24,0,34,0,42,0,43,0,44,0,45,0,46,0,47,0,48,0,53,0,56,0,59,0,60,0,61,0,64,0,67,0,68,0,69,0,70,0,71,0,72,0,73,0,74,0,76,0,78,0,79,0,80,0,81,0,82,0,83,0]}]}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"type":"CodeCoverage","coverage":[{"source":"package:practice_engine/models/deck.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fdeck.dart","uri":"package:practice_engine/models/deck.dart","_kind":"library"},"hits":[24,1,34,1,42,1,43,1,45,1,46,1,47,1,48,1,44,0,53,3,56,6,59,1,60,2,61,5,64,0,67,0,68,0,69,0,70,0,71,0,72,0,73,0,74,0,76,0,78,0,79,0,80,0,81,0,82,0,83,0]},{"source":"package:practice_engine/models/deck_config.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fdeck_config.dart","uri":"package:practice_engine/models/deck_config.dart","_kind":"library"},"hits":[18,1,27,0,34,0,36,0,37,0,39,0,41,0,43,0,47,0,50,0,51,0,52,0,53,0,54,0,55,0,56,0,58,0,60,0,61,0,62,0,63,0,64,0]},{"source":"package:practice_engine/models/question.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fquestion.dart","uri":"package:practice_engine/models/question.dart","_kind":"library"},"hits":[33,1,47,1,59,1,60,1,61,1,62,1,63,1,64,1,66,1,67,1,68,1,69,1,65,0,74,0,75,0,78,0,81,0,82,0,83,0,84,0,85,0,86,0,87,0,88,0,89,0,90,0,91,0,92,0,94,0,96,0,97,0,98,0,99,0,100,0,101,0,102,0,103,0,104,0,105,0]},{"source":"package:practice_engine/models/attempt_result.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fattempt_result.dart","uri":"package:practice_engine/models/attempt_result.dart","_kind":"library"},"hits":[24,0,52,0,62,0,66,0,67,0,68,0,69,0,72,0,74,0]},{"source":"package:practice_engine/algorithms/priority_manager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Falgorithms%2Fpriority_manager.dart","uri":"package:practice_engine/algorithms/priority_manager.dart","_kind":"library"},"hits":[7,0,13,0,14,0,15,0,16,0,17,0,19,0,20,0,21,0,27,0,28,0]},{"source":"package:practice_engine/logic/deck_service.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Flogic%2Fdeck_service.dart","uri":"package:practice_engine/logic/deck_service.dart","_kind":"library"},"hits":[11,0,15,0,23,0,25,0,31,0,35,0,44,0,46,0,53,0,57,0,66,0,68,0,77,0,87,0,88,0,91,0,92,0,96,0,103,0,104,0,105,0,109,0,115,0,122,0,123,0,128,0,137,0,148,0,149,0,154,0,16,0,17,0,18,0,36,0,37,0,58,0,62,0,63,0,64,0]}]}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"type":"CodeCoverage","coverage":[{"source":"package:practice_engine/models/deck.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fdeck.dart","uri":"package:practice_engine/models/deck.dart","_kind":"library"},"hits":[24,1,34,1,42,1,43,1,44,1,45,1,47,1,48,1,46,0,53,0,56,0,59,0,60,0,61,0,64,0,67,0,68,0,69,0,70,0,71,0,72,0,73,0,74,0,76,0,78,0,79,0,80,0,81,0,82,0,83,0]},{"source":"package:practice_engine/models/deck_config.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fdeck_config.dart","uri":"package:practice_engine/models/deck_config.dart","_kind":"library"},"hits":[18,1,27,0,34,0,36,0,37,0,39,0,41,0,43,0,47,0,50,0,51,0,52,0,53,0,54,0,55,0,56,0,58,0,60,0,61,0,62,0,63,0,64,0]},{"source":"package:practice_engine/models/question.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fquestion.dart","uri":"package:practice_engine/models/question.dart","_kind":"library"},"hits":[33,1,47,1,59,1,60,1,61,1,62,1,63,1,64,1,65,1,66,1,67,1,68,1,69,1,74,1,75,2,78,0,81,0,82,0,83,0,84,0,85,0,86,0,87,0,88,0,89,0,90,0,91,0,92,0,94,0,96,0,97,0,98,0,99,0,100,0,101,0,102,0,103,0,104,0,105,0]},{"source":"package:practice_engine/logic/deck_service.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Flogic%2Fdeck_service.dart","uri":"package:practice_engine/logic/deck_service.dart","_kind":"library"},"hits":[11,1,15,3,23,1,25,1,31,1,35,3,44,1,46,1,53,0,57,0,66,0,68,0,77,1,87,2,88,1,91,2,96,1,103,1,104,2,105,2,128,1,92,0,109,0,115,0,122,0,123,0,137,1,148,1,149,2,154,1,16,2,17,1,18,2,36,2,37,1,58,0,62,0,63,0,64,0]},{"source":"package:practice_engine/models/attempt_result.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fattempt_result.dart","uri":"package:practice_engine/models/attempt_result.dart","_kind":"library"},"hits":[24,0,52,0,62,0,66,0,67,0,68,0,69,0,72,0,74,0]},{"source":"package:practice_engine/algorithms/priority_manager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Falgorithms%2Fpriority_manager.dart","uri":"package:practice_engine/algorithms/priority_manager.dart","_kind":"library"},"hits":[7,1,13,2,14,1,15,1,16,1,17,1,19,0,20,0,21,0,27,0,28,0]}]}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"type":"CodeCoverage","coverage":[{"source":"package:practice_engine/models/question.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fquestion.dart","uri":"package:practice_engine/models/question.dart","_kind":"library"},"hits":[33,1,47,1,59,1,60,1,61,1,62,1,63,1,64,1,65,1,67,1,68,1,69,1,66,0,74,1,75,2,78,0,81,0,82,0,83,0,84,0,85,0,86,0,87,0,88,0,89,0,90,0,91,0,92,0,94,0,96,0,97,0,98,0,99,0,100,0,101,0,102,0,103,0,104,0,105,0]},{"source":"package:practice_engine/models/deck_config.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fdeck_config.dart","uri":"package:practice_engine/models/deck_config.dart","_kind":"library"},"hits":[18,1,27,0,34,0,36,0,37,0,39,0,41,0,43,0,47,0,50,0,51,0,52,0,53,0,54,0,55,0,56,0,58,0,60,0,61,0,62,0,63,0,64,0]},{"source":"package:practice_engine/algorithms/priority_manager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Falgorithms%2Fpriority_manager.dart","uri":"package:practice_engine/algorithms/priority_manager.dart","_kind":"library"},"hits":[7,1,13,2,14,1,15,1,16,1,17,1,19,1,20,2,21,1,27,1,28,1]},{"source":"package:practice_engine/models/attempt_result.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fattempt_result.dart","uri":"package:practice_engine/models/attempt_result.dart","_kind":"library"},"hits":[24,0,52,0,62,0,66,0,67,0,68,0,69,0,72,0,74,0]},{"source":"package:practice_engine/logic/deck_service.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Flogic%2Fdeck_service.dart","uri":"package:practice_engine/logic/deck_service.dart","_kind":"library"},"hits":[11,0,15,0,23,0,25,0,31,0,35,0,44,0,46,0,53,0,57,0,66,0,68,0,77,0,87,0,88,0,91,0,92,0,96,0,103,0,104,0,105,0,109,0,115,0,122,0,123,0,128,0,137,0,148,0,149,0,154,0,16,0,17,0,18,0,36,0,37,0,58,0,62,0,63,0,64,0]},{"source":"package:practice_engine/models/deck.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fdeck.dart","uri":"package:practice_engine/models/deck.dart","_kind":"library"},"hits":[24,0,34,0,42,0,43,0,44,0,45,0,46,0,47,0,48,0,53,0,56,0,59,0,60,0,61,0,64,0,67,0,68,0,69,0,70,0,71,0,72,0,73,0,74,0,76,0,78,0,79,0,80,0,81,0,82,0,83,0]}]}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"type":"CodeCoverage","coverage":[{"source":"package:practice_engine/models/question.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fquestion.dart","uri":"package:practice_engine/models/question.dart","_kind":"library"},"hits":[33,1,47,1,59,1,60,1,61,1,62,1,63,1,64,1,65,1,66,1,67,1,68,1,69,1,74,1,75,2,78,0,81,0,82,0,83,0,84,0,85,0,86,0,87,0,88,0,89,0,90,0,91,0,92,0,94,0,96,0,97,0,98,0,99,0,100,0,101,0,102,0,103,0,104,0,105,0]},{"source":"package:practice_engine/models/deck_config.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fdeck_config.dart","uri":"package:practice_engine/models/deck_config.dart","_kind":"library"},"hits":[18,1,27,0,34,0,36,0,37,0,39,0,41,0,43,0,47,0,50,0,51,0,52,0,53,0,54,0,55,0,56,0,58,0,60,0,61,0,62,0,63,0,64,0]},{"source":"package:practice_engine/logic/deck_service.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Flogic%2Fdeck_service.dart","uri":"package:practice_engine/logic/deck_service.dart","_kind":"library"},"hits":[11,0,15,0,23,0,25,0,31,0,35,0,44,0,46,0,53,0,57,0,66,0,68,0,77,1,87,2,88,1,91,2,92,1,96,1,103,1,104,2,105,2,109,1,115,1,122,1,123,2,128,1,137,0,148,0,149,0,154,0,16,0,17,0,18,0,36,0,37,0,58,0,62,0,63,0,64,0]},{"source":"package:practice_engine/models/attempt_result.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fattempt_result.dart","uri":"package:practice_engine/models/attempt_result.dart","_kind":"library"},"hits":[24,0,52,0,62,0,66,0,67,0,68,0,69,0,72,0,74,0]},{"source":"package:practice_engine/algorithms/priority_manager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Falgorithms%2Fpriority_manager.dart","uri":"package:practice_engine/algorithms/priority_manager.dart","_kind":"library"},"hits":[7,1,13,2,14,1,15,1,16,1,17,1,19,1,20,2,21,1,27,0,28,0]},{"source":"package:practice_engine/models/deck.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fdeck.dart","uri":"package:practice_engine/models/deck.dart","_kind":"library"},"hits":[24,0,34,0,42,0,43,0,44,0,45,0,46,0,47,0,48,0,53,0,56,0,59,0,60,0,61,0,64,0,67,0,68,0,69,0,70,0,71,0,72,0,73,0,74,0,76,0,78,0,79,0,80,0,81,0,82,0,83,0]}]}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"type":"CodeCoverage","coverage":[{"source":"package:practice_engine/models/deck.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fdeck.dart","uri":"package:practice_engine/models/deck.dart","_kind":"library"},"hits":[24,1,34,1,42,1,43,1,44,1,45,1,47,1,46,0,48,0,53,0,56,0,59,0,60,0,61,0,64,0,67,0,68,0,69,0,70,0,71,0,72,0,73,0,74,0,76,0,78,0,79,0,80,0,81,0,82,0,83,0]},{"source":"package:practice_engine/models/deck_config.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fdeck_config.dart","uri":"package:practice_engine/models/deck_config.dart","_kind":"library"},"hits":[18,1,27,0,34,0,36,0,37,0,39,0,41,0,43,0,47,1,50,0,51,0,52,0,53,0,54,0,55,0,56,0,58,0,60,0,61,0,62,0,63,0,64,0]},{"source":"package:practice_engine/models/question.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fquestion.dart","uri":"package:practice_engine/models/question.dart","_kind":"library"},"hits":[33,1,47,1,59,1,60,1,61,1,62,1,63,1,64,0,65,0,66,0,67,0,68,0,69,0,74,0,75,0,78,0,81,0,82,0,83,0,84,0,85,0,86,0,87,0,88,0,89,0,90,0,91,0,92,0,94,0,96,0,97,0,98,0,99,0,100,0,101,0,102,0,103,0,104,0,105,0]},{"source":"package:practice_engine/logic/deck_service.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Flogic%2Fdeck_service.dart","uri":"package:practice_engine/logic/deck_service.dart","_kind":"library"},"hits":[11,0,15,0,23,0,25,0,31,0,35,0,44,0,46,0,53,1,57,3,66,1,68,1,77,0,87,0,88,0,91,0,92,0,96,0,103,0,104,0,105,0,109,0,115,0,122,0,123,0,128,0,137,0,148,0,149,0,154,0,58,1,62,1,63,1,64,1,16,0,17,0,18,0,36,0,37,0]},{"source":"package:practice_engine/models/attempt_result.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fattempt_result.dart","uri":"package:practice_engine/models/attempt_result.dart","_kind":"library"},"hits":[24,0,52,0,62,0,66,0,67,0,68,0,69,0,72,0,74,0]},{"source":"package:practice_engine/algorithms/priority_manager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Falgorithms%2Fpriority_manager.dart","uri":"package:practice_engine/algorithms/priority_manager.dart","_kind":"library"},"hits":[7,0,13,0,14,0,15,0,16,0,17,0,19,0,20,0,21,0,27,0,28,0]}]}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"type":"CodeCoverage","coverage":[{"source":"package:practice_engine/models/question.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fquestion.dart","uri":"package:practice_engine/models/question.dart","_kind":"library"},"hits":[33,1,47,0,59,0,60,0,61,0,62,0,63,0,64,0,65,0,66,0,67,0,68,0,69,0,74,0,75,0,78,0,81,0,82,0,83,0,84,0,85,0,86,0,87,0,88,0,89,0,90,0,91,0,92,0,94,0,96,0,97,0,98,0,99,0,100,0,101,0,102,0,103,0,104,0,105,0]},{"source":"package:practice_engine/algorithms/spaced_repetition.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Falgorithms%2Fspaced_repetition.dart","uri":"package:practice_engine/algorithms/spaced_repetition.dart","_kind":"library"},"hits":[16,1,20,1,25,1,29,2,30,1,31,2,38,1,43,1,45,1,46,1,48,1,52,3]},{"source":"package:practice_engine/algorithms/priority_manager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Falgorithms%2Fpriority_manager.dart","uri":"package:practice_engine/algorithms/priority_manager.dart","_kind":"library"},"hits":[7,0,13,0,14,0,15,0,16,0,17,0,19,0,20,0,21,0,27,0,28,0]},{"source":"package:practice_engine/models/deck_config.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fdeck_config.dart","uri":"package:practice_engine/models/deck_config.dart","_kind":"library"},"hits":[18,0,27,0,34,0,36,0,37,0,39,0,41,0,43,0,47,0,50,0,51,0,52,0,53,0,54,0,55,0,56,0,58,0,60,0,61,0,62,0,63,0,64,0]},{"source":"package:practice_engine/logic/deck_service.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Flogic%2Fdeck_service.dart","uri":"package:practice_engine/logic/deck_service.dart","_kind":"library"},"hits":[11,0,15,0,23,0,25,0,31,0,35,0,44,0,46,0,53,0,57,0,66,0,68,0,77,0,87,0,88,0,91,0,92,0,96,0,103,0,104,0,105,0,109,0,115,0,122,0,123,0,128,0,137,0,148,0,149,0,154,0,16,0,17,0,18,0,36,0,37,0,58,0,62,0,63,0,64,0]},{"source":"package:practice_engine/models/deck.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fdeck.dart","uri":"package:practice_engine/models/deck.dart","_kind":"library"},"hits":[24,0,34,0,42,0,43,0,44,0,45,0,46,0,47,0,48,0,53,0,56,0,59,0,60,0,61,0,64,0,67,0,68,0,69,0,70,0,71,0,72,0,73,0,74,0,76,0,78,0,79,0,80,0,81,0,82,0,83,0]},{"source":"package:practice_engine/models/attempt_result.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fattempt_result.dart","uri":"package:practice_engine/models/attempt_result.dart","_kind":"library"},"hits":[24,0,52,0,62,0,66,0,67,0,68,0,69,0,72,0,74,0]},{"source":"package:practice_engine/algorithms/weighted_selector.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Falgorithms%2Fweighted_selector.dart","uri":"package:practice_engine/algorithms/weighted_selector.dart","_kind":"library"},"hits":[10,0,16,0,21,0,22,0,25,0,26,0,29,0,30,0,32,0,33,0,37,0,38,0,45,0,49,0,50,0,54,0,64,0,67,0,69,0,70,0,71,0,75,0,78,0,79,0,80,0,85,0,55,0,56,0,57,0,62,0]},{"source":"package:practice_engine/logic/attempt_service.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Flogic%2Fattempt_service.dart","uri":"package:practice_engine/logic/attempt_service.dart","_kind":"library"},"hits":[14,0,20,0,24,0,25,0,26,0,28,0,31,0,32,0,34,0,43,0,53,0,54,0,55,0,57,0,58,0,61,0,62,0,68,0,69,0,72,0,73,0,77,0,81,0,82,0,84,0,87,0,88,0,96,0,98,0,104,0,106,0,113,0,120,0,124,0,125,0,128,0,129,0,132,0,134,0,136,0,138,0,141,0,153,0,154,0,133,0]},{"source":"package:practice_engine/models/attempt.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fattempt.dart","uri":"package:practice_engine/models/attempt.dart","_kind":"library"},"hits":[14,0,21,0,26,0,27,0,28,0,29,0,34,0]}]}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"type":"CodeCoverage","coverage":[{"source":"package:practice_engine/models/question.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fquestion.dart","uri":"package:practice_engine/models/question.dart","_kind":"library"},"hits":[33,1,47,0,59,0,60,0,61,0,62,0,63,0,64,0,65,0,66,0,67,0,68,0,69,0,74,0,75,0,78,1,81,1,82,3,83,3,84,0,85,0,86,0,87,0,88,0,89,0,90,0,91,0,92,0,94,0,96,0,97,0,98,0,99,0,100,0,101,0,102,0,103,0,104,0,105,0]},{"source":"package:practice_engine/algorithms/weighted_selector.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Falgorithms%2Fweighted_selector.dart","uri":"package:practice_engine/algorithms/weighted_selector.dart","_kind":"library"},"hits":[10,2,16,1,21,2,22,1,25,2,26,1,29,1,30,1,32,3,33,1,37,1,38,1,45,1,49,2,54,2,64,1,67,1,69,2,70,1,71,1,75,3,78,3,79,2,80,1,50,0,85,0,55,1,62,3,56,0,57,0]},{"source":"package:practice_engine/algorithms/priority_manager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Falgorithms%2Fpriority_manager.dart","uri":"package:practice_engine/algorithms/priority_manager.dart","_kind":"library"},"hits":[7,0,13,0,14,0,15,0,16,0,17,0,19,0,20,0,21,0,27,0,28,0]},{"source":"package:practice_engine/models/deck_config.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fdeck_config.dart","uri":"package:practice_engine/models/deck_config.dart","_kind":"library"},"hits":[18,0,27,0,34,0,36,0,37,0,39,0,41,0,43,0,47,0,50,0,51,0,52,0,53,0,54,0,55,0,56,0,58,0,60,0,61,0,62,0,63,0,64,0]},{"source":"package:practice_engine/logic/deck_service.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Flogic%2Fdeck_service.dart","uri":"package:practice_engine/logic/deck_service.dart","_kind":"library"},"hits":[11,0,15,0,23,0,25,0,31,0,35,0,44,0,46,0,53,0,57,0,66,0,68,0,77,0,87,0,88,0,91,0,92,0,96,0,103,0,104,0,105,0,109,0,115,0,122,0,123,0,128,0,137,0,148,0,149,0,154,0,16,0,17,0,18,0,36,0,37,0,58,0,62,0,63,0,64,0]},{"source":"package:practice_engine/models/deck.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fdeck.dart","uri":"package:practice_engine/models/deck.dart","_kind":"library"},"hits":[24,0,34,0,42,0,43,0,44,0,45,0,46,0,47,0,48,0,53,0,56,0,59,0,60,0,61,0,64,0,67,0,68,0,69,0,70,0,71,0,72,0,73,0,74,0,76,0,78,0,79,0,80,0,81,0,82,0,83,0]},{"source":"package:practice_engine/models/attempt_result.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fattempt_result.dart","uri":"package:practice_engine/models/attempt_result.dart","_kind":"library"},"hits":[24,0,52,0,62,0,66,0,67,0,68,0,69,0,72,0,74,0]},{"source":"package:practice_engine/algorithms/spaced_repetition.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Falgorithms%2Fspaced_repetition.dart","uri":"package:practice_engine/algorithms/spaced_repetition.dart","_kind":"library"},"hits":[16,0,20,0,25,0,29,0,30,0,31,0,38,0,43,0,45,0,46,0,48,0,52,0]},{"source":"package:practice_engine/logic/attempt_service.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Flogic%2Fattempt_service.dart","uri":"package:practice_engine/logic/attempt_service.dart","_kind":"library"},"hits":[14,0,20,0,24,0,25,0,26,0,28,0,31,0,32,0,34,0,43,0,53,0,54,0,55,0,57,0,58,0,61,0,62,0,68,0,69,0,72,0,73,0,77,0,81,0,82,0,84,0,87,0,88,0,96,0,98,0,104,0,106,0,113,0,120,0,124,0,125,0,128,0,129,0,132,0,134,0,136,0,138,0,141,0,153,0,154,0,133,0]},{"source":"package:practice_engine/models/attempt.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/package%3Apractice_engine%2Fmodels%2Fattempt.dart","uri":"package:practice_engine/models/attempt.dart","_kind":"library"},"hits":[14,0,21,0,26,0,27,0,28,0,29,0,34,0]}]}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
import '../models/question.dart';
|
||||||
|
import '../models/deck_config.dart';
|
||||||
|
|
||||||
|
/// Manages priority point calculations for questions.
|
||||||
|
class PriorityManager {
|
||||||
|
/// Applies priority changes based on answer correctness.
|
||||||
|
static Question applyAnswerResult({
|
||||||
|
required Question question,
|
||||||
|
required bool isCorrect,
|
||||||
|
required DeckConfig config,
|
||||||
|
}) {
|
||||||
|
if (isCorrect) {
|
||||||
|
final newPriority = (question.priorityPoints -
|
||||||
|
config.priorityDecreaseOnCorrect)
|
||||||
|
.clamp(0, double.infinity)
|
||||||
|
.toInt();
|
||||||
|
return question.withPriorityPoints(newPriority);
|
||||||
|
} else {
|
||||||
|
return question.copyWith(
|
||||||
|
priorityPoints: question.priorityPoints +
|
||||||
|
config.priorityIncreaseOnIncorrect,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resets priority to 0.
|
||||||
|
static Question resetPriority(Question question) {
|
||||||
|
return question.withPriorityPoints(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
import '../models/question.dart';
|
||||||
|
|
||||||
|
/// Handles spaced repetition logic for known questions.
|
||||||
|
class SpacedRepetition {
|
||||||
|
/// Base probability for known questions (very low).
|
||||||
|
static const double baseKnownProbability = 0.01;
|
||||||
|
|
||||||
|
/// Maximum probability for known questions.
|
||||||
|
static const double maxKnownProbability = 0.15;
|
||||||
|
|
||||||
|
/// Calculates the probability weight for a known question based on
|
||||||
|
/// how long it's been since it was last seen.
|
||||||
|
///
|
||||||
|
/// [lastAttemptIndex] is the last attempt where the question was seen.
|
||||||
|
/// [currentAttemptIndex] is the current global attempt index.
|
||||||
|
static double calculateKnownQuestionWeight({
|
||||||
|
required int lastAttemptIndex,
|
||||||
|
required int currentAttemptIndex,
|
||||||
|
}) {
|
||||||
|
if (lastAttemptIndex < 0) {
|
||||||
|
// Never seen before, use base probability
|
||||||
|
return baseKnownProbability;
|
||||||
|
}
|
||||||
|
|
||||||
|
final attemptsSinceLastSeen = currentAttemptIndex - lastAttemptIndex;
|
||||||
|
|
||||||
|
// Probability increases with attempts since last seen
|
||||||
|
// Formula: base + (min(attemptsSinceLastSeen / 20, 1) * (max - base))
|
||||||
|
final progress = (attemptsSinceLastSeen / 20.0).clamp(0.0, 1.0);
|
||||||
|
final probability = baseKnownProbability +
|
||||||
|
(progress * (maxKnownProbability - baseKnownProbability));
|
||||||
|
|
||||||
|
return probability;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the weight for a question considering its known status and
|
||||||
|
/// last attempt index.
|
||||||
|
static double getQuestionWeight({
|
||||||
|
required Question question,
|
||||||
|
required int currentAttemptIndex,
|
||||||
|
required double basePriorityWeight,
|
||||||
|
}) {
|
||||||
|
if (question.isKnown) {
|
||||||
|
// Known questions use spaced repetition probability
|
||||||
|
return calculateKnownQuestionWeight(
|
||||||
|
lastAttemptIndex: question.lastAttemptIndex,
|
||||||
|
currentAttemptIndex: currentAttemptIndex,
|
||||||
|
) * basePriorityWeight;
|
||||||
|
} else {
|
||||||
|
// Unknown questions use priority-based weight
|
||||||
|
// Priority points + 1 to ensure non-zero weight
|
||||||
|
return (question.priorityPoints + 1).toDouble();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,88 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
import '../models/question.dart';
|
||||||
|
import 'spaced_repetition.dart';
|
||||||
|
|
||||||
|
/// Selects questions using weighted random selection.
|
||||||
|
class WeightedSelector {
|
||||||
|
final Random _random;
|
||||||
|
|
||||||
|
/// Creates a weighted selector with an optional random seed.
|
||||||
|
WeightedSelector({int? seed}) : _random = Random(seed);
|
||||||
|
|
||||||
|
/// Selects [count] questions from [candidates] using weighted random selection.
|
||||||
|
///
|
||||||
|
/// [currentAttemptIndex] is used for spaced repetition calculations.
|
||||||
|
/// Returns a list of selected questions (no duplicates).
|
||||||
|
List<Question> selectQuestions({
|
||||||
|
required List<Question> candidates,
|
||||||
|
required int count,
|
||||||
|
required int currentAttemptIndex,
|
||||||
|
}) {
|
||||||
|
if (candidates.isEmpty || count <= 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count >= candidates.length) {
|
||||||
|
return List.from(candidates);
|
||||||
|
}
|
||||||
|
|
||||||
|
final selected = <Question>[];
|
||||||
|
final available = List<Question>.from(candidates);
|
||||||
|
|
||||||
|
while (selected.length < count && available.isNotEmpty) {
|
||||||
|
final question = _selectOne(
|
||||||
|
candidates: available,
|
||||||
|
currentAttemptIndex: currentAttemptIndex,
|
||||||
|
);
|
||||||
|
selected.add(question);
|
||||||
|
available.remove(question);
|
||||||
|
}
|
||||||
|
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Selects a single question using weighted random selection.
|
||||||
|
Question _selectOne({
|
||||||
|
required List<Question> candidates,
|
||||||
|
required int currentAttemptIndex,
|
||||||
|
}) {
|
||||||
|
if (candidates.length == 1) {
|
||||||
|
return candidates.first;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate weights for all candidates
|
||||||
|
final weights = candidates.map((q) {
|
||||||
|
if (q.isKnown) {
|
||||||
|
return SpacedRepetition.calculateKnownQuestionWeight(
|
||||||
|
lastAttemptIndex: q.lastAttemptIndex,
|
||||||
|
currentAttemptIndex: currentAttemptIndex,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Unknown questions: priority + 1 to ensure non-zero weight
|
||||||
|
return (q.priorityPoints + 1).toDouble();
|
||||||
|
}
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
// Calculate cumulative weights
|
||||||
|
final cumulativeWeights = <double>[];
|
||||||
|
double sum = 0.0;
|
||||||
|
for (final weight in weights) {
|
||||||
|
sum += weight;
|
||||||
|
cumulativeWeights.add(sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select random value in range [0, sum)
|
||||||
|
final randomValue = _random.nextDouble() * sum;
|
||||||
|
|
||||||
|
// Find the index corresponding to the random value
|
||||||
|
for (int i = 0; i < cumulativeWeights.length; i++) {
|
||||||
|
if (randomValue < cumulativeWeights[i]) {
|
||||||
|
return candidates[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to last item (shouldn't happen, but safety)
|
||||||
|
return candidates.last;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,185 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
import '../models/deck.dart';
|
||||||
|
import '../models/question.dart';
|
||||||
|
import '../models/attempt.dart';
|
||||||
|
import '../models/attempt_result.dart';
|
||||||
|
import '../algorithms/weighted_selector.dart';
|
||||||
|
import 'deck_service.dart';
|
||||||
|
|
||||||
|
/// Service for managing attempt/quiz operations.
|
||||||
|
class AttemptService {
|
||||||
|
final WeightedSelector _selector;
|
||||||
|
|
||||||
|
/// Creates an attempt service with an optional random seed.
|
||||||
|
AttemptService({int? seed}) : _selector = WeightedSelector(seed: seed);
|
||||||
|
|
||||||
|
/// Creates a new attempt by selecting questions from the deck.
|
||||||
|
///
|
||||||
|
/// Selects [attemptSize] questions (defaults to deck config default).
|
||||||
|
/// Uses weighted random selection with no duplicates.
|
||||||
|
/// Excludes known questions unless [DeckConfig.includeKnownInAttempts] is true.
|
||||||
|
/// [includeKnown] can override the config setting for this attempt.
|
||||||
|
Attempt createAttempt({
|
||||||
|
required Deck deck,
|
||||||
|
int? attemptSize,
|
||||||
|
bool? includeKnown,
|
||||||
|
}) {
|
||||||
|
final size = attemptSize ?? deck.config.defaultAttemptSize;
|
||||||
|
|
||||||
|
// Filter candidates based on includeKnownInAttempts setting
|
||||||
|
// Use override parameter if provided, otherwise use config
|
||||||
|
final shouldIncludeKnown = includeKnown ?? deck.config.includeKnownInAttempts;
|
||||||
|
final candidates = shouldIncludeKnown
|
||||||
|
? deck.questions
|
||||||
|
: deck.questions.where((q) => !q.isKnown).toList();
|
||||||
|
|
||||||
|
final selected = _selector.selectQuestions(
|
||||||
|
candidates: candidates,
|
||||||
|
count: size,
|
||||||
|
currentAttemptIndex: deck.currentAttemptIndex,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Attempt(
|
||||||
|
id: _generateAttemptId(),
|
||||||
|
questions: selected,
|
||||||
|
startTime: DateTime.now().millisecondsSinceEpoch,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Processes an attempt result and updates the deck.
|
||||||
|
///
|
||||||
|
/// [answers] is a map of questionId -> userAnswerIndex (for single answer) or questionId -> List<int> (for multiple answers).
|
||||||
|
/// [manualOverrides] is an optional map of questionId -> bool (true if marked as needs practice).
|
||||||
|
/// Returns the updated deck and attempt result.
|
||||||
|
({
|
||||||
|
Deck updatedDeck,
|
||||||
|
AttemptResult result,
|
||||||
|
}) processAttempt({
|
||||||
|
required Deck deck,
|
||||||
|
required Attempt attempt,
|
||||||
|
required Map<String, dynamic> answers,
|
||||||
|
Map<String, bool>? manualOverrides,
|
||||||
|
int? endTime,
|
||||||
|
}) {
|
||||||
|
final overrides = manualOverrides ?? {};
|
||||||
|
final finishTime = endTime ?? DateTime.now().millisecondsSinceEpoch;
|
||||||
|
final timeSpent = finishTime - attempt.startTime;
|
||||||
|
|
||||||
|
final answerResults = <AnswerResult>[];
|
||||||
|
final updatedQuestions = <Question>[];
|
||||||
|
|
||||||
|
// Process each question in the attempt
|
||||||
|
for (final question in attempt.questions) {
|
||||||
|
final userAnswer = answers[question.id];
|
||||||
|
if (userAnswer == null) {
|
||||||
|
// Question not answered, treat as incorrect
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle both single answer (int) and multiple answers (List<int>)
|
||||||
|
final List<int> userAnswerIndices;
|
||||||
|
if (userAnswer is int) {
|
||||||
|
userAnswerIndices = [userAnswer];
|
||||||
|
} else if (userAnswer is List<int>) {
|
||||||
|
userAnswerIndices = userAnswer;
|
||||||
|
} else {
|
||||||
|
// Invalid format, treat as incorrect
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if answer is correct
|
||||||
|
// For multiple correct answers: user must select all correct answers and no incorrect ones
|
||||||
|
final correctIndices = question.correctIndices;
|
||||||
|
final userSet = userAnswerIndices.toSet();
|
||||||
|
final correctSet = correctIndices.toSet();
|
||||||
|
final isCorrect = userSet.length == correctSet.length &&
|
||||||
|
userSet.every((idx) => correctSet.contains(idx));
|
||||||
|
final userMarkedNeedsPractice = overrides[question.id] ?? false;
|
||||||
|
|
||||||
|
// Determine status change
|
||||||
|
final oldIsKnown = question.isKnown;
|
||||||
|
final oldStreak = question.consecutiveCorrect;
|
||||||
|
|
||||||
|
// Update question
|
||||||
|
final updated = userMarkedNeedsPractice
|
||||||
|
? DeckService.updateQuestionWithManualOverride(
|
||||||
|
question: question,
|
||||||
|
isCorrect: isCorrect,
|
||||||
|
userMarkedNeedsPractice: true,
|
||||||
|
config: deck.config,
|
||||||
|
currentAttemptIndex: deck.currentAttemptIndex,
|
||||||
|
)
|
||||||
|
: DeckService.updateQuestionAfterAnswer(
|
||||||
|
question: question,
|
||||||
|
isCorrect: isCorrect,
|
||||||
|
config: deck.config,
|
||||||
|
currentAttemptIndex: deck.currentAttemptIndex,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Determine status change
|
||||||
|
QuestionStatusChange statusChange;
|
||||||
|
if (userMarkedNeedsPractice) {
|
||||||
|
statusChange = QuestionStatusChange.unchanged;
|
||||||
|
} else if (isCorrect) {
|
||||||
|
if (!oldIsKnown && updated.isKnown) {
|
||||||
|
statusChange = QuestionStatusChange.improved;
|
||||||
|
} else if (updated.consecutiveCorrect > oldStreak) {
|
||||||
|
statusChange = QuestionStatusChange.improved;
|
||||||
|
} else {
|
||||||
|
statusChange = QuestionStatusChange.unchanged;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (oldIsKnown && !updated.isKnown) {
|
||||||
|
statusChange = QuestionStatusChange.regressed;
|
||||||
|
} else if (oldStreak > 0 && updated.consecutiveCorrect == 0) {
|
||||||
|
statusChange = QuestionStatusChange.regressed;
|
||||||
|
} else {
|
||||||
|
statusChange = QuestionStatusChange.unchanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
answerResults.add(AnswerResult(
|
||||||
|
question: updated,
|
||||||
|
userAnswerIndices: userAnswerIndices,
|
||||||
|
isCorrect: isCorrect,
|
||||||
|
statusChange: statusChange,
|
||||||
|
));
|
||||||
|
|
||||||
|
updatedQuestions.add(updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update deck with new question states
|
||||||
|
final questionMap = Map.fromEntries(
|
||||||
|
deck.questions.map((q) => MapEntry(q.id, q)),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (final updated in updatedQuestions) {
|
||||||
|
questionMap[updated.id] = updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
final allUpdatedQuestions = deck.questions.map((q) {
|
||||||
|
return questionMap[q.id] ?? q;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
final updatedDeck = deck.copyWith(
|
||||||
|
questions: allUpdatedQuestions,
|
||||||
|
currentAttemptIndex: deck.currentAttemptIndex + 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = AttemptResult.fromAnswers(
|
||||||
|
results: answerResults,
|
||||||
|
timeSpent: timeSpent,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
updatedDeck: updatedDeck,
|
||||||
|
result: result,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates a unique attempt ID.
|
||||||
|
String _generateAttemptId() {
|
||||||
|
return 'attempt_${DateTime.now().millisecondsSinceEpoch}_${Random().nextInt(10000)}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,166 @@
|
|||||||
|
import '../models/deck.dart';
|
||||||
|
import '../models/question.dart';
|
||||||
|
import '../models/deck_config.dart';
|
||||||
|
import '../algorithms/priority_manager.dart';
|
||||||
|
|
||||||
|
/// Service for managing deck operations.
|
||||||
|
class DeckService {
|
||||||
|
/// Marks a question as known (manual override).
|
||||||
|
///
|
||||||
|
/// Sets streak to threshold and isKnown to true.
|
||||||
|
static Deck markQuestionAsKnown({
|
||||||
|
required Deck deck,
|
||||||
|
required String questionId,
|
||||||
|
}) {
|
||||||
|
final updatedQuestions = deck.questions.map((q) {
|
||||||
|
if (q.id == questionId) {
|
||||||
|
return q.copyWith(
|
||||||
|
consecutiveCorrect: deck.config.requiredConsecutiveCorrect,
|
||||||
|
isKnown: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return q;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
return deck.copyWith(questions: updatedQuestions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marks a question as needs practice (manual override).
|
||||||
|
///
|
||||||
|
/// Sets isKnown to false and streak to 0, but keeps priority unchanged.
|
||||||
|
static Deck markQuestionAsNeedsPractice({
|
||||||
|
required Deck deck,
|
||||||
|
required String questionId,
|
||||||
|
}) {
|
||||||
|
final updatedQuestions = deck.questions.map((q) {
|
||||||
|
if (q.id == questionId) {
|
||||||
|
return q.copyWith(
|
||||||
|
isKnown: false,
|
||||||
|
consecutiveCorrect: 0,
|
||||||
|
// priorityPoints remains unchanged
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return q;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
return deck.copyWith(questions: updatedQuestions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resets the entire deck.
|
||||||
|
///
|
||||||
|
/// Resets all questions: streak = 0, isKnown = false, priority = 0.
|
||||||
|
/// Optionally resets totalAttempts if [resetAttemptCounts] is true.
|
||||||
|
/// Optionally clears attempt history if [clearAttemptHistory] is true.
|
||||||
|
static Deck resetDeck({
|
||||||
|
required Deck deck,
|
||||||
|
bool resetAttemptCounts = false,
|
||||||
|
bool clearAttemptHistory = false,
|
||||||
|
}) {
|
||||||
|
final updatedQuestions = deck.questions.map((q) {
|
||||||
|
return q.copyWith(
|
||||||
|
consecutiveCorrect: 0,
|
||||||
|
isKnown: false,
|
||||||
|
priorityPoints: 0,
|
||||||
|
lastAttemptIndex: -1,
|
||||||
|
totalCorrectAttempts: resetAttemptCounts ? 0 : q.totalCorrectAttempts,
|
||||||
|
totalAttempts: resetAttemptCounts ? 0 : q.totalAttempts,
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
return deck.copyWith(
|
||||||
|
questions: updatedQuestions,
|
||||||
|
currentAttemptIndex: 0,
|
||||||
|
attemptHistory: clearAttemptHistory ? [] : deck.attemptHistory,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates a question's state after an answer.
|
||||||
|
///
|
||||||
|
/// Applies correctness rules and updates streak, priority, and isKnown.
|
||||||
|
static Question updateQuestionAfterAnswer({
|
||||||
|
required Question question,
|
||||||
|
required bool isCorrect,
|
||||||
|
required DeckConfig config,
|
||||||
|
required int currentAttemptIndex,
|
||||||
|
}) {
|
||||||
|
Question updated = question;
|
||||||
|
|
||||||
|
if (isCorrect) {
|
||||||
|
// Increment streak
|
||||||
|
final newStreak = question.consecutiveCorrect + 1;
|
||||||
|
updated = updated.copyWith(consecutiveCorrect: newStreak);
|
||||||
|
|
||||||
|
// Update isKnown if streak reaches threshold
|
||||||
|
if (newStreak >= config.requiredConsecutiveCorrect) {
|
||||||
|
updated = updated.copyWith(isKnown: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrease priority
|
||||||
|
updated = PriorityManager.applyAnswerResult(
|
||||||
|
question: updated,
|
||||||
|
isCorrect: true,
|
||||||
|
config: config,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update totals
|
||||||
|
updated = updated.copyWith(
|
||||||
|
totalCorrectAttempts: question.totalCorrectAttempts + 1,
|
||||||
|
totalAttempts: question.totalAttempts + 1,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Reset streak
|
||||||
|
updated = updated.copyWith(
|
||||||
|
consecutiveCorrect: 0,
|
||||||
|
isKnown: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Increase priority
|
||||||
|
updated = PriorityManager.applyAnswerResult(
|
||||||
|
question: updated,
|
||||||
|
isCorrect: false,
|
||||||
|
config: config,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update totals
|
||||||
|
updated = updated.copyWith(
|
||||||
|
totalAttempts: question.totalAttempts + 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update lastAttemptIndex
|
||||||
|
updated = updated.copyWith(lastAttemptIndex: currentAttemptIndex);
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates a question with manual override after an answer.
|
||||||
|
///
|
||||||
|
/// If user marks as "Needs Practice" even after correct answer,
|
||||||
|
/// ignore correctness and don't increment streak or decrease priority.
|
||||||
|
static Question updateQuestionWithManualOverride({
|
||||||
|
required Question question,
|
||||||
|
required bool isCorrect,
|
||||||
|
required bool userMarkedNeedsPractice,
|
||||||
|
required DeckConfig config,
|
||||||
|
required int currentAttemptIndex,
|
||||||
|
}) {
|
||||||
|
if (userMarkedNeedsPractice) {
|
||||||
|
// User marked as needs practice - ignore correctness
|
||||||
|
// Don't increment streak, don't decrease priority
|
||||||
|
// Just update attempt counts and lastAttemptIndex
|
||||||
|
return question.copyWith(
|
||||||
|
totalAttempts: question.totalAttempts + 1,
|
||||||
|
lastAttemptIndex: currentAttemptIndex,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Normal flow
|
||||||
|
return updateQuestionAfterAnswer(
|
||||||
|
question: question,
|
||||||
|
isCorrect: isCorrect,
|
||||||
|
config: config,
|
||||||
|
currentAttemptIndex: currentAttemptIndex,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
import '../models/question.dart';
|
||||||
|
|
||||||
|
/// A single attempt/quiz session.
|
||||||
|
class Attempt {
|
||||||
|
/// Unique identifier for this attempt.
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
/// List of questions included in this attempt.
|
||||||
|
final List<Question> questions;
|
||||||
|
|
||||||
|
/// Timestamp when the attempt was started (milliseconds since epoch).
|
||||||
|
final int startTime;
|
||||||
|
|
||||||
|
const Attempt({
|
||||||
|
required this.id,
|
||||||
|
required this.questions,
|
||||||
|
required this.startTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Creates a copy of this attempt with the given fields replaced.
|
||||||
|
Attempt copyWith({
|
||||||
|
String? id,
|
||||||
|
List<Question>? questions,
|
||||||
|
int? startTime,
|
||||||
|
}) {
|
||||||
|
return Attempt(
|
||||||
|
id: id ?? this.id,
|
||||||
|
questions: questions ?? this.questions,
|
||||||
|
startTime: startTime ?? this.startTime,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Number of questions in this attempt.
|
||||||
|
int get questionCount => questions.length;
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,95 @@
|
|||||||
|
import 'attempt_result.dart';
|
||||||
|
|
||||||
|
/// A historical record of a completed attempt.
|
||||||
|
class AttemptHistoryEntry {
|
||||||
|
/// Timestamp when the attempt was completed (milliseconds since epoch).
|
||||||
|
final int timestamp;
|
||||||
|
|
||||||
|
/// Total number of questions in the attempt.
|
||||||
|
final int totalQuestions;
|
||||||
|
|
||||||
|
/// Number of correct answers.
|
||||||
|
final int correctCount;
|
||||||
|
|
||||||
|
/// Percentage of correct answers.
|
||||||
|
final double percentageCorrect;
|
||||||
|
|
||||||
|
/// Time spent on the attempt in milliseconds.
|
||||||
|
final int timeSpent;
|
||||||
|
|
||||||
|
/// Percentage of questions in the deck that are not yet known (0-100).
|
||||||
|
/// This represents progress - as more questions become known, this decreases.
|
||||||
|
final double unknownPercentage;
|
||||||
|
|
||||||
|
const AttemptHistoryEntry({
|
||||||
|
required this.timestamp,
|
||||||
|
required this.totalQuestions,
|
||||||
|
required this.correctCount,
|
||||||
|
required this.percentageCorrect,
|
||||||
|
required this.timeSpent,
|
||||||
|
this.unknownPercentage = 0.0,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Creates a history entry from an attempt result.
|
||||||
|
factory AttemptHistoryEntry.fromAttemptResult({
|
||||||
|
required AttemptResult result,
|
||||||
|
required int totalQuestionsInDeck,
|
||||||
|
required int knownCount,
|
||||||
|
int? timestamp,
|
||||||
|
}) {
|
||||||
|
final unknownCount = totalQuestionsInDeck - knownCount;
|
||||||
|
final unknownPercentage = totalQuestionsInDeck > 0
|
||||||
|
? (unknownCount / totalQuestionsInDeck * 100.0)
|
||||||
|
: 0.0;
|
||||||
|
|
||||||
|
return AttemptHistoryEntry(
|
||||||
|
timestamp: timestamp ?? DateTime.now().millisecondsSinceEpoch,
|
||||||
|
totalQuestions: result.totalQuestions,
|
||||||
|
correctCount: result.correctCount,
|
||||||
|
percentageCorrect: result.percentageCorrect,
|
||||||
|
timeSpent: result.timeSpent,
|
||||||
|
unknownPercentage: unknownPercentage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a copy of this entry with the given fields replaced.
|
||||||
|
AttemptHistoryEntry copyWith({
|
||||||
|
int? timestamp,
|
||||||
|
int? totalQuestions,
|
||||||
|
int? correctCount,
|
||||||
|
double? percentageCorrect,
|
||||||
|
int? timeSpent,
|
||||||
|
double? unknownPercentage,
|
||||||
|
}) {
|
||||||
|
return AttemptHistoryEntry(
|
||||||
|
timestamp: timestamp ?? this.timestamp,
|
||||||
|
totalQuestions: totalQuestions ?? this.totalQuestions,
|
||||||
|
correctCount: correctCount ?? this.correctCount,
|
||||||
|
percentageCorrect: percentageCorrect ?? this.percentageCorrect,
|
||||||
|
timeSpent: timeSpent ?? this.timeSpent,
|
||||||
|
unknownPercentage: unknownPercentage ?? this.unknownPercentage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is AttemptHistoryEntry &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
timestamp == other.timestamp &&
|
||||||
|
totalQuestions == other.totalQuestions &&
|
||||||
|
correctCount == other.correctCount &&
|
||||||
|
percentageCorrect == other.percentageCorrect &&
|
||||||
|
timeSpent == other.timeSpent &&
|
||||||
|
unknownPercentage == other.unknownPercentage;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
timestamp.hashCode ^
|
||||||
|
totalQuestions.hashCode ^
|
||||||
|
correctCount.hashCode ^
|
||||||
|
percentageCorrect.hashCode ^
|
||||||
|
timeSpent.hashCode ^
|
||||||
|
unknownPercentage.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,88 @@
|
|||||||
|
import 'question.dart';
|
||||||
|
|
||||||
|
/// Status change for a question after an attempt.
|
||||||
|
enum QuestionStatusChange {
|
||||||
|
improved,
|
||||||
|
regressed,
|
||||||
|
unchanged,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of a single answer in an attempt.
|
||||||
|
class AnswerResult {
|
||||||
|
/// The question that was answered.
|
||||||
|
final Question question;
|
||||||
|
|
||||||
|
/// The indices of answers the user selected.
|
||||||
|
final List<int> userAnswerIndices;
|
||||||
|
|
||||||
|
/// Deprecated: Use [userAnswerIndices] instead. Returns the first selected answer index.
|
||||||
|
@Deprecated('Use userAnswerIndices instead')
|
||||||
|
int get userAnswerIndex => userAnswerIndices.isNotEmpty ? userAnswerIndices.first : -1;
|
||||||
|
|
||||||
|
/// Whether the answer was correct.
|
||||||
|
final bool isCorrect;
|
||||||
|
|
||||||
|
/// The status change for this question.
|
||||||
|
final QuestionStatusChange statusChange;
|
||||||
|
|
||||||
|
const AnswerResult({
|
||||||
|
required this.question,
|
||||||
|
required this.userAnswerIndices,
|
||||||
|
required this.isCorrect,
|
||||||
|
required this.statusChange,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of a completed attempt.
|
||||||
|
class AttemptResult {
|
||||||
|
/// Total number of questions in the attempt.
|
||||||
|
final int totalQuestions;
|
||||||
|
|
||||||
|
/// Number of correct answers.
|
||||||
|
final int correctCount;
|
||||||
|
|
||||||
|
/// Percentage of correct answers.
|
||||||
|
final double percentageCorrect;
|
||||||
|
|
||||||
|
/// Time spent on the attempt in milliseconds.
|
||||||
|
final int timeSpent;
|
||||||
|
|
||||||
|
/// List of questions that were answered incorrectly.
|
||||||
|
final List<AnswerResult> incorrectQuestions;
|
||||||
|
|
||||||
|
/// List of all answer results.
|
||||||
|
final List<AnswerResult> allResults;
|
||||||
|
|
||||||
|
const AttemptResult({
|
||||||
|
required this.totalQuestions,
|
||||||
|
required this.correctCount,
|
||||||
|
required this.percentageCorrect,
|
||||||
|
required this.timeSpent,
|
||||||
|
required this.incorrectQuestions,
|
||||||
|
required this.allResults,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Creates an attempt result from answer results.
|
||||||
|
factory AttemptResult.fromAnswers({
|
||||||
|
required List<AnswerResult> results,
|
||||||
|
required int timeSpent,
|
||||||
|
}) {
|
||||||
|
final correctCount = results.where((r) => r.isCorrect).length;
|
||||||
|
final totalQuestions = results.length;
|
||||||
|
final percentageCorrect = totalQuestions > 0
|
||||||
|
? (correctCount / totalQuestions) * 100.0
|
||||||
|
: 0.0;
|
||||||
|
final incorrectQuestions =
|
||||||
|
results.where((r) => !r.isCorrect).toList();
|
||||||
|
|
||||||
|
return AttemptResult(
|
||||||
|
totalQuestions: totalQuestions,
|
||||||
|
correctCount: correctCount,
|
||||||
|
percentageCorrect: percentageCorrect,
|
||||||
|
timeSpent: timeSpent,
|
||||||
|
incorrectQuestions: incorrectQuestions,
|
||||||
|
allResults: results,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,125 @@
|
|||||||
|
import 'deck_config.dart';
|
||||||
|
import 'question.dart';
|
||||||
|
import 'attempt_history_entry.dart';
|
||||||
|
import 'incomplete_attempt.dart';
|
||||||
|
|
||||||
|
/// A practice deck containing questions and configuration.
|
||||||
|
class Deck {
|
||||||
|
/// Unique identifier for this deck.
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
/// Title of the deck.
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
/// Description of the deck.
|
||||||
|
final String description;
|
||||||
|
|
||||||
|
/// List of questions in this deck.
|
||||||
|
final List<Question> questions;
|
||||||
|
|
||||||
|
/// Configuration for this deck.
|
||||||
|
final DeckConfig config;
|
||||||
|
|
||||||
|
/// Current global attempt index (incremented with each attempt).
|
||||||
|
final int currentAttemptIndex;
|
||||||
|
|
||||||
|
/// History of completed attempts.
|
||||||
|
final List<AttemptHistoryEntry> attemptHistory;
|
||||||
|
|
||||||
|
/// Incomplete attempt that can be resumed.
|
||||||
|
final IncompleteAttempt? incompleteAttempt;
|
||||||
|
|
||||||
|
const Deck({
|
||||||
|
required this.id,
|
||||||
|
required this.title,
|
||||||
|
required this.description,
|
||||||
|
required this.questions,
|
||||||
|
required this.config,
|
||||||
|
this.currentAttemptIndex = 0,
|
||||||
|
this.attemptHistory = const [],
|
||||||
|
this.incompleteAttempt,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Creates a copy of this deck with the given fields replaced.
|
||||||
|
Deck copyWith({
|
||||||
|
String? id,
|
||||||
|
String? title,
|
||||||
|
String? description,
|
||||||
|
List<Question>? questions,
|
||||||
|
DeckConfig? config,
|
||||||
|
int? currentAttemptIndex,
|
||||||
|
List<AttemptHistoryEntry>? attemptHistory,
|
||||||
|
IncompleteAttempt? incompleteAttempt,
|
||||||
|
bool clearIncompleteAttempt = false,
|
||||||
|
}) {
|
||||||
|
return Deck(
|
||||||
|
id: id ?? this.id,
|
||||||
|
title: title ?? this.title,
|
||||||
|
description: description ?? this.description,
|
||||||
|
questions: questions ?? this.questions,
|
||||||
|
config: config ?? this.config,
|
||||||
|
currentAttemptIndex: currentAttemptIndex ?? this.currentAttemptIndex,
|
||||||
|
attemptHistory: attemptHistory ?? this.attemptHistory,
|
||||||
|
incompleteAttempt: clearIncompleteAttempt ? null : (incompleteAttempt ?? this.incompleteAttempt),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Total number of questions in the deck.
|
||||||
|
int get numberOfQuestions => questions.length;
|
||||||
|
|
||||||
|
/// Number of questions marked as known.
|
||||||
|
int get knownCount => questions.where((q) => q.isKnown).length;
|
||||||
|
|
||||||
|
/// Practice percentage: (known / total) * 100
|
||||||
|
double get practicePercentage {
|
||||||
|
if (questions.isEmpty) return 0.0;
|
||||||
|
return (knownCount / questions.length) * 100.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Number of completed attempts.
|
||||||
|
int get attemptCount => attemptHistory.length;
|
||||||
|
|
||||||
|
/// Average percentage correct across all attempts.
|
||||||
|
double get averagePercentageCorrect {
|
||||||
|
if (attemptHistory.isEmpty) return 0.0;
|
||||||
|
final sum = attemptHistory.fold<double>(
|
||||||
|
0.0,
|
||||||
|
(sum, entry) => sum + entry.percentageCorrect,
|
||||||
|
);
|
||||||
|
return sum / attemptHistory.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Total time spent on all attempts in milliseconds.
|
||||||
|
int get totalTimeSpent {
|
||||||
|
return attemptHistory.fold<int>(
|
||||||
|
0,
|
||||||
|
(sum, entry) => sum + entry.timeSpent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is Deck &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
id == other.id &&
|
||||||
|
title == other.title &&
|
||||||
|
description == other.description &&
|
||||||
|
questions.toString() == other.questions.toString() &&
|
||||||
|
config == other.config &&
|
||||||
|
currentAttemptIndex == other.currentAttemptIndex &&
|
||||||
|
attemptHistory.toString() == other.attemptHistory.toString() &&
|
||||||
|
incompleteAttempt == other.incompleteAttempt;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
id.hashCode ^
|
||||||
|
title.hashCode ^
|
||||||
|
description.hashCode ^
|
||||||
|
questions.hashCode ^
|
||||||
|
config.hashCode ^
|
||||||
|
currentAttemptIndex.hashCode ^
|
||||||
|
attemptHistory.hashCode ^
|
||||||
|
(incompleteAttempt?.hashCode ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,85 @@
|
|||||||
|
/// Configuration for a practice deck.
|
||||||
|
class DeckConfig {
|
||||||
|
/// Number of consecutive correct answers required to mark a question as known.
|
||||||
|
final int requiredConsecutiveCorrect;
|
||||||
|
|
||||||
|
/// Default number of questions to include in an attempt.
|
||||||
|
final int defaultAttemptSize;
|
||||||
|
|
||||||
|
/// Priority points to add when a question is answered incorrectly.
|
||||||
|
final int priorityIncreaseOnIncorrect;
|
||||||
|
|
||||||
|
/// Priority points to subtract when a question is answered correctly.
|
||||||
|
final int priorityDecreaseOnCorrect;
|
||||||
|
|
||||||
|
/// Whether to provide immediate feedback after each answer.
|
||||||
|
final bool immediateFeedbackEnabled;
|
||||||
|
|
||||||
|
/// Whether to include known questions in attempts.
|
||||||
|
/// If false, known questions will be excluded from attempts.
|
||||||
|
final bool includeKnownInAttempts;
|
||||||
|
|
||||||
|
/// Optional time limit for attempts in seconds.
|
||||||
|
/// If null, no time limit is enforced.
|
||||||
|
final int? timeLimitSeconds;
|
||||||
|
|
||||||
|
const DeckConfig({
|
||||||
|
this.requiredConsecutiveCorrect = 3,
|
||||||
|
this.defaultAttemptSize = 10,
|
||||||
|
this.priorityIncreaseOnIncorrect = 5,
|
||||||
|
this.priorityDecreaseOnCorrect = 2,
|
||||||
|
this.immediateFeedbackEnabled = true,
|
||||||
|
this.includeKnownInAttempts = false,
|
||||||
|
this.timeLimitSeconds,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Creates a copy of this config with the given fields replaced.
|
||||||
|
DeckConfig copyWith({
|
||||||
|
int? requiredConsecutiveCorrect,
|
||||||
|
int? defaultAttemptSize,
|
||||||
|
int? priorityIncreaseOnIncorrect,
|
||||||
|
int? priorityDecreaseOnCorrect,
|
||||||
|
bool? immediateFeedbackEnabled,
|
||||||
|
bool? includeKnownInAttempts,
|
||||||
|
int? timeLimitSeconds,
|
||||||
|
}) {
|
||||||
|
return DeckConfig(
|
||||||
|
requiredConsecutiveCorrect:
|
||||||
|
requiredConsecutiveCorrect ?? this.requiredConsecutiveCorrect,
|
||||||
|
defaultAttemptSize: defaultAttemptSize ?? this.defaultAttemptSize,
|
||||||
|
priorityIncreaseOnIncorrect:
|
||||||
|
priorityIncreaseOnIncorrect ?? this.priorityIncreaseOnIncorrect,
|
||||||
|
priorityDecreaseOnCorrect:
|
||||||
|
priorityDecreaseOnCorrect ?? this.priorityDecreaseOnCorrect,
|
||||||
|
immediateFeedbackEnabled:
|
||||||
|
immediateFeedbackEnabled ?? this.immediateFeedbackEnabled,
|
||||||
|
includeKnownInAttempts:
|
||||||
|
includeKnownInAttempts ?? this.includeKnownInAttempts,
|
||||||
|
timeLimitSeconds: timeLimitSeconds ?? this.timeLimitSeconds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is DeckConfig &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
requiredConsecutiveCorrect == other.requiredConsecutiveCorrect &&
|
||||||
|
defaultAttemptSize == other.defaultAttemptSize &&
|
||||||
|
priorityIncreaseOnIncorrect == other.priorityIncreaseOnIncorrect &&
|
||||||
|
priorityDecreaseOnCorrect == other.priorityDecreaseOnCorrect &&
|
||||||
|
immediateFeedbackEnabled == other.immediateFeedbackEnabled &&
|
||||||
|
includeKnownInAttempts == other.includeKnownInAttempts &&
|
||||||
|
timeLimitSeconds == other.timeLimitSeconds;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
requiredConsecutiveCorrect.hashCode ^
|
||||||
|
defaultAttemptSize.hashCode ^
|
||||||
|
priorityIncreaseOnIncorrect.hashCode ^
|
||||||
|
priorityDecreaseOnCorrect.hashCode ^
|
||||||
|
immediateFeedbackEnabled.hashCode ^
|
||||||
|
includeKnownInAttempts.hashCode ^
|
||||||
|
(timeLimitSeconds?.hashCode ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,134 @@
|
|||||||
|
import 'attempt.dart';
|
||||||
|
import 'question.dart';
|
||||||
|
|
||||||
|
/// Represents an incomplete attempt that can be resumed later.
|
||||||
|
class IncompleteAttempt {
|
||||||
|
/// The attempt ID.
|
||||||
|
final String attemptId;
|
||||||
|
|
||||||
|
/// List of question IDs in the attempt (in order).
|
||||||
|
final List<String> questionIds;
|
||||||
|
|
||||||
|
/// Timestamp when the attempt was started.
|
||||||
|
final int startTime;
|
||||||
|
|
||||||
|
/// Current question index (0-based).
|
||||||
|
final int currentQuestionIndex;
|
||||||
|
|
||||||
|
/// Map of questionId -> answer (int for single, List<int> for multiple).
|
||||||
|
final Map<String, dynamic> answers;
|
||||||
|
|
||||||
|
/// Map of questionId -> manual override (needs practice).
|
||||||
|
final Map<String, bool> manualOverrides;
|
||||||
|
|
||||||
|
/// Timestamp when the attempt was paused.
|
||||||
|
final int pausedAt;
|
||||||
|
|
||||||
|
/// Remaining time in seconds (if time limit was set).
|
||||||
|
final int? remainingSeconds;
|
||||||
|
|
||||||
|
const IncompleteAttempt({
|
||||||
|
required this.attemptId,
|
||||||
|
required this.questionIds,
|
||||||
|
required this.startTime,
|
||||||
|
required this.currentQuestionIndex,
|
||||||
|
required this.answers,
|
||||||
|
required this.manualOverrides,
|
||||||
|
required this.pausedAt,
|
||||||
|
this.remainingSeconds,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Creates an Attempt from this incomplete attempt using questions from the deck.
|
||||||
|
Attempt toAttempt(List<Question> deckQuestions) {
|
||||||
|
final questionMap = {for (var q in deckQuestions) q.id: q};
|
||||||
|
final questions = questionIds
|
||||||
|
.map((id) => questionMap[id])
|
||||||
|
.whereType<Question>()
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return Attempt(
|
||||||
|
id: attemptId,
|
||||||
|
questions: questions,
|
||||||
|
startTime: startTime,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an incomplete attempt from JSON.
|
||||||
|
factory IncompleteAttempt.fromJson(Map<String, dynamic> json) {
|
||||||
|
return IncompleteAttempt(
|
||||||
|
attemptId: json['attemptId'] as String,
|
||||||
|
questionIds: List<String>.from(json['questionIds'] as List),
|
||||||
|
startTime: json['startTime'] as int,
|
||||||
|
currentQuestionIndex: json['currentQuestionIndex'] as int,
|
||||||
|
answers: Map<String, dynamic>.from(json['answers'] as Map),
|
||||||
|
manualOverrides: Map<String, bool>.from(
|
||||||
|
(json['manualOverrides'] as Map?)?.map((k, v) => MapEntry(k.toString(), v as bool)) ?? {},
|
||||||
|
),
|
||||||
|
pausedAt: json['pausedAt'] as int,
|
||||||
|
remainingSeconds: json['remainingSeconds'] as int?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts to JSON for storage.
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'attemptId': attemptId,
|
||||||
|
'questionIds': questionIds,
|
||||||
|
'startTime': startTime,
|
||||||
|
'currentQuestionIndex': currentQuestionIndex,
|
||||||
|
'answers': answers,
|
||||||
|
'manualOverrides': manualOverrides,
|
||||||
|
'pausedAt': pausedAt,
|
||||||
|
if (remainingSeconds != null) 'remainingSeconds': remainingSeconds,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a copy with updated fields.
|
||||||
|
IncompleteAttempt copyWith({
|
||||||
|
String? attemptId,
|
||||||
|
List<String>? questionIds,
|
||||||
|
int? startTime,
|
||||||
|
int? currentQuestionIndex,
|
||||||
|
Map<String, dynamic>? answers,
|
||||||
|
Map<String, bool>? manualOverrides,
|
||||||
|
int? pausedAt,
|
||||||
|
int? remainingSeconds,
|
||||||
|
}) {
|
||||||
|
return IncompleteAttempt(
|
||||||
|
attemptId: attemptId ?? this.attemptId,
|
||||||
|
questionIds: questionIds ?? this.questionIds,
|
||||||
|
startTime: startTime ?? this.startTime,
|
||||||
|
currentQuestionIndex: currentQuestionIndex ?? this.currentQuestionIndex,
|
||||||
|
answers: answers ?? this.answers,
|
||||||
|
manualOverrides: manualOverrides ?? this.manualOverrides,
|
||||||
|
pausedAt: pausedAt ?? this.pausedAt,
|
||||||
|
remainingSeconds: remainingSeconds ?? this.remainingSeconds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is IncompleteAttempt &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
attemptId == other.attemptId &&
|
||||||
|
questionIds.toString() == other.questionIds.toString() &&
|
||||||
|
startTime == other.startTime &&
|
||||||
|
currentQuestionIndex == other.currentQuestionIndex &&
|
||||||
|
answers.toString() == other.answers.toString() &&
|
||||||
|
manualOverrides.toString() == other.manualOverrides.toString() &&
|
||||||
|
pausedAt == other.pausedAt &&
|
||||||
|
remainingSeconds == other.remainingSeconds;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
attemptId.hashCode ^
|
||||||
|
questionIds.hashCode ^
|
||||||
|
startTime.hashCode ^
|
||||||
|
currentQuestionIndex.hashCode ^
|
||||||
|
answers.hashCode ^
|
||||||
|
manualOverrides.hashCode ^
|
||||||
|
pausedAt.hashCode ^
|
||||||
|
(remainingSeconds?.hashCode ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,138 @@
|
|||||||
|
/// A question in a practice deck.
|
||||||
|
class Question {
|
||||||
|
/// Unique identifier for this question.
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
/// The question prompt.
|
||||||
|
final String prompt;
|
||||||
|
|
||||||
|
/// List of possible answers.
|
||||||
|
final List<String> answers;
|
||||||
|
|
||||||
|
/// Indices of correct answers in [answers].
|
||||||
|
/// For backward compatibility, if empty, falls back to [correctAnswerIndex] (deprecated).
|
||||||
|
final List<int> correctAnswerIndices;
|
||||||
|
|
||||||
|
/// Deprecated: Use [correctAnswerIndices] instead.
|
||||||
|
/// Kept for backward compatibility with existing data.
|
||||||
|
@Deprecated('Use correctAnswerIndices instead')
|
||||||
|
final int? correctAnswerIndex;
|
||||||
|
|
||||||
|
/// Number of consecutive correct answers.
|
||||||
|
final int consecutiveCorrect;
|
||||||
|
|
||||||
|
/// Whether this question is considered "known".
|
||||||
|
final bool isKnown;
|
||||||
|
|
||||||
|
/// Priority points (higher = more likely to be selected).
|
||||||
|
final int priorityPoints;
|
||||||
|
|
||||||
|
/// The last attempt index where this question was seen.
|
||||||
|
final int lastAttemptIndex;
|
||||||
|
|
||||||
|
/// Total number of correct attempts.
|
||||||
|
final int totalCorrectAttempts;
|
||||||
|
|
||||||
|
/// Total number of attempts.
|
||||||
|
final int totalAttempts;
|
||||||
|
|
||||||
|
Question({
|
||||||
|
required this.id,
|
||||||
|
required this.prompt,
|
||||||
|
required this.answers,
|
||||||
|
List<int>? correctAnswerIndices,
|
||||||
|
@Deprecated('Use correctAnswerIndices instead') int? correctAnswerIndex,
|
||||||
|
this.consecutiveCorrect = 0,
|
||||||
|
this.isKnown = false,
|
||||||
|
this.priorityPoints = 0,
|
||||||
|
this.lastAttemptIndex = -1,
|
||||||
|
this.totalCorrectAttempts = 0,
|
||||||
|
this.totalAttempts = 0,
|
||||||
|
}) : correctAnswerIndices = correctAnswerIndices ??
|
||||||
|
(correctAnswerIndex != null ? [correctAnswerIndex] : const []),
|
||||||
|
correctAnswerIndex = correctAnswerIndex;
|
||||||
|
|
||||||
|
/// Creates a copy of this question with the given fields replaced.
|
||||||
|
Question copyWith({
|
||||||
|
String? id,
|
||||||
|
String? prompt,
|
||||||
|
List<String>? answers,
|
||||||
|
List<int>? correctAnswerIndices,
|
||||||
|
@Deprecated('Use correctAnswerIndices instead') int? correctAnswerIndex,
|
||||||
|
int? consecutiveCorrect,
|
||||||
|
bool? isKnown,
|
||||||
|
int? priorityPoints,
|
||||||
|
int? lastAttemptIndex,
|
||||||
|
int? totalCorrectAttempts,
|
||||||
|
int? totalAttempts,
|
||||||
|
}) {
|
||||||
|
return Question(
|
||||||
|
id: id ?? this.id,
|
||||||
|
prompt: prompt ?? this.prompt,
|
||||||
|
answers: answers ?? this.answers,
|
||||||
|
correctAnswerIndices: correctAnswerIndices ?? this.correctAnswerIndices,
|
||||||
|
correctAnswerIndex: correctAnswerIndex ?? this.correctAnswerIndex,
|
||||||
|
consecutiveCorrect: consecutiveCorrect ?? this.consecutiveCorrect,
|
||||||
|
isKnown: isKnown ?? this.isKnown,
|
||||||
|
priorityPoints: priorityPoints ?? this.priorityPoints,
|
||||||
|
lastAttemptIndex: lastAttemptIndex ?? this.lastAttemptIndex,
|
||||||
|
totalCorrectAttempts: totalCorrectAttempts ?? this.totalCorrectAttempts,
|
||||||
|
totalAttempts: totalAttempts ?? this.totalAttempts,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the correct answer indices, with backward compatibility.
|
||||||
|
List<int> get correctIndices {
|
||||||
|
if (correctAnswerIndices.isNotEmpty) {
|
||||||
|
return correctAnswerIndices;
|
||||||
|
}
|
||||||
|
// Backward compatibility
|
||||||
|
if (correctAnswerIndex != null) {
|
||||||
|
return [correctAnswerIndex!];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if an answer index is correct.
|
||||||
|
bool isCorrectAnswer(int index) {
|
||||||
|
return correctIndices.contains(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether this question has multiple correct answers.
|
||||||
|
bool get hasMultipleCorrectAnswers => correctIndices.length > 1;
|
||||||
|
|
||||||
|
/// Validates that priorityPoints is non-negative.
|
||||||
|
Question withPriorityPoints(int points) {
|
||||||
|
return copyWith(priorityPoints: points < 0 ? 0 : points);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is Question &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
id == other.id &&
|
||||||
|
prompt == other.prompt &&
|
||||||
|
answers.toString() == other.answers.toString() &&
|
||||||
|
correctAnswerIndices.toString() == other.correctAnswerIndices.toString() &&
|
||||||
|
consecutiveCorrect == other.consecutiveCorrect &&
|
||||||
|
isKnown == other.isKnown &&
|
||||||
|
priorityPoints == other.priorityPoints &&
|
||||||
|
lastAttemptIndex == other.lastAttemptIndex &&
|
||||||
|
totalCorrectAttempts == other.totalCorrectAttempts &&
|
||||||
|
totalAttempts == other.totalAttempts;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
id.hashCode ^
|
||||||
|
prompt.hashCode ^
|
||||||
|
answers.hashCode ^
|
||||||
|
correctAnswerIndices.hashCode ^
|
||||||
|
consecutiveCorrect.hashCode ^
|
||||||
|
isKnown.hashCode ^
|
||||||
|
priorityPoints.hashCode ^
|
||||||
|
lastAttemptIndex.hashCode ^
|
||||||
|
totalCorrectAttempts.hashCode ^
|
||||||
|
totalAttempts.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
/// Practice Engine - A pure Dart package for spaced repetition and practice tracking.
|
||||||
|
library practice_engine;
|
||||||
|
|
||||||
|
// Models
|
||||||
|
export 'models/deck.dart';
|
||||||
|
export 'models/deck_config.dart';
|
||||||
|
export 'models/question.dart';
|
||||||
|
export 'models/attempt.dart';
|
||||||
|
export 'models/attempt_result.dart';
|
||||||
|
export 'models/attempt_history_entry.dart';
|
||||||
|
export 'models/incomplete_attempt.dart';
|
||||||
|
|
||||||
|
// Services
|
||||||
|
export 'logic/deck_service.dart';
|
||||||
|
export 'logic/attempt_service.dart';
|
||||||
|
|
||||||
|
// Algorithms
|
||||||
|
export 'algorithms/weighted_selector.dart';
|
||||||
|
export 'algorithms/spaced_repetition.dart';
|
||||||
|
export 'algorithms/priority_manager.dart';
|
||||||
|
|
||||||
@ -0,0 +1,381 @@
|
|||||||
|
# Generated by pub
|
||||||
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
|
packages:
|
||||||
|
_fe_analyzer_shared:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: _fe_analyzer_shared
|
||||||
|
sha256: "5b7468c326d2f8a4f630056404ca0d291ade42918f4a3c6233618e724f39da8e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "92.0.0"
|
||||||
|
analyzer:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: analyzer
|
||||||
|
sha256: "70e4b1ef8003c64793a9e268a551a82869a8a96f39deb73dea28084b0e8bf75e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.0.0"
|
||||||
|
args:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: args
|
||||||
|
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.7.0"
|
||||||
|
async:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: async
|
||||||
|
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.13.0"
|
||||||
|
boolean_selector:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: boolean_selector
|
||||||
|
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.2"
|
||||||
|
cli_config:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cli_config
|
||||||
|
sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.0"
|
||||||
|
collection:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: collection
|
||||||
|
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.19.1"
|
||||||
|
convert:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: convert
|
||||||
|
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.2"
|
||||||
|
coverage:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: coverage
|
||||||
|
sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.15.0"
|
||||||
|
crypto:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: crypto
|
||||||
|
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.7"
|
||||||
|
file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file
|
||||||
|
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.1"
|
||||||
|
frontend_server_client:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: frontend_server_client
|
||||||
|
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.0"
|
||||||
|
glob:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: glob
|
||||||
|
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.3"
|
||||||
|
http_multi_server:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http_multi_server
|
||||||
|
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.2"
|
||||||
|
http_parser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http_parser
|
||||||
|
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.2"
|
||||||
|
io:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: io
|
||||||
|
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.5"
|
||||||
|
logging:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: logging
|
||||||
|
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.0"
|
||||||
|
matcher:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: matcher
|
||||||
|
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.12.18"
|
||||||
|
meta:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: meta
|
||||||
|
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.17.0"
|
||||||
|
mime:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: mime
|
||||||
|
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
|
node_preamble:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: node_preamble
|
||||||
|
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.2"
|
||||||
|
package_config:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: package_config
|
||||||
|
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
|
path:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path
|
||||||
|
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.9.1"
|
||||||
|
pool:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pool
|
||||||
|
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.5.2"
|
||||||
|
pub_semver:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pub_semver
|
||||||
|
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
|
shelf:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shelf
|
||||||
|
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.2"
|
||||||
|
shelf_packages_handler:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shelf_packages_handler
|
||||||
|
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.2"
|
||||||
|
shelf_static:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shelf_static
|
||||||
|
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.3"
|
||||||
|
shelf_web_socket:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shelf_web_socket
|
||||||
|
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.0"
|
||||||
|
source_map_stack_trace:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_map_stack_trace
|
||||||
|
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.2"
|
||||||
|
source_maps:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_maps
|
||||||
|
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.10.13"
|
||||||
|
source_span:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_span
|
||||||
|
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.10.1"
|
||||||
|
stack_trace:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stack_trace
|
||||||
|
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.12.1"
|
||||||
|
stream_channel:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stream_channel
|
||||||
|
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.4"
|
||||||
|
string_scanner:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: string_scanner
|
||||||
|
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.1"
|
||||||
|
term_glyph:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: term_glyph
|
||||||
|
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.2"
|
||||||
|
test:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: test
|
||||||
|
sha256: "77cc98ea27006c84e71a7356cf3daf9ddbde2d91d84f77dbfe64cf0e4d9611ae"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.28.0"
|
||||||
|
test_api:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: test_api
|
||||||
|
sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.8"
|
||||||
|
test_core:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: test_core
|
||||||
|
sha256: f1072617a6657e5fc09662e721307f7fb009b4ed89b19f47175d11d5254a62d4
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.14"
|
||||||
|
typed_data:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: typed_data
|
||||||
|
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.0"
|
||||||
|
vm_service:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vm_service
|
||||||
|
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "15.0.2"
|
||||||
|
watcher:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: watcher
|
||||||
|
sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.4"
|
||||||
|
web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: web
|
||||||
|
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
|
web_socket:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: web_socket
|
||||||
|
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
|
web_socket_channel:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: web_socket_channel
|
||||||
|
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.3"
|
||||||
|
webkit_inspection_protocol:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: webkit_inspection_protocol
|
||||||
|
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.1"
|
||||||
|
yaml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: yaml
|
||||||
|
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.3"
|
||||||
|
sdks:
|
||||||
|
dart: ">=3.9.0 <4.0.0"
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
name: practice_engine
|
||||||
|
description: A pure Dart package implementing spaced repetition, weighted selection, and practice tracking for question decks.
|
||||||
|
version: 1.0.0
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: '>=3.0.0 <4.0.0'
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
test: ^1.24.0
|
||||||
|
|
||||||
@ -0,0 +1,157 @@
|
|||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:practice_engine/models/deck.dart';
|
||||||
|
import 'package:practice_engine/models/deck_config.dart';
|
||||||
|
import 'package:practice_engine/models/question.dart';
|
||||||
|
import 'package:practice_engine/logic/attempt_service.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Attempt Flow', () {
|
||||||
|
late Deck deck;
|
||||||
|
late AttemptService attemptService;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
final config = const DeckConfig(
|
||||||
|
defaultAttemptSize: 5,
|
||||||
|
requiredConsecutiveCorrect: 3,
|
||||||
|
);
|
||||||
|
|
||||||
|
final questions = List.generate(10, (i) {
|
||||||
|
return Question(
|
||||||
|
id: 'q$i',
|
||||||
|
prompt: 'Question $i',
|
||||||
|
answers: ['A', 'B', 'C'],
|
||||||
|
correctAnswerIndices: [0],
|
||||||
|
priorityPoints: i,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
deck = Deck(
|
||||||
|
id: 'deck1',
|
||||||
|
title: 'Test Deck',
|
||||||
|
description: 'Test',
|
||||||
|
questions: questions,
|
||||||
|
config: config,
|
||||||
|
);
|
||||||
|
|
||||||
|
attemptService = AttemptService(seed: 42);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('createAttempt selects correct number of questions', () {
|
||||||
|
final attempt = attemptService.createAttempt(deck: deck);
|
||||||
|
|
||||||
|
expect(attempt.questions.length, equals(deck.config.defaultAttemptSize));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('createAttempt uses custom attempt size', () {
|
||||||
|
final attempt = attemptService.createAttempt(
|
||||||
|
deck: deck,
|
||||||
|
attemptSize: 3,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(attempt.questions.length, equals(3));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('createAttempt has no duplicate questions', () {
|
||||||
|
final attempt = attemptService.createAttempt(deck: deck);
|
||||||
|
|
||||||
|
final ids = attempt.questions.map((q) => q.id).toList();
|
||||||
|
final uniqueIds = ids.toSet();
|
||||||
|
|
||||||
|
expect(uniqueIds.length, equals(ids.length));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('processAttempt updates deck with results', () {
|
||||||
|
final attempt = attemptService.createAttempt(deck: deck);
|
||||||
|
final answers = <String, int>{};
|
||||||
|
|
||||||
|
// Answer all questions correctly
|
||||||
|
for (final question in attempt.questions) {
|
||||||
|
answers[question.id] = question.correctIndices.first;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = attemptService.processAttempt(
|
||||||
|
deck: deck,
|
||||||
|
attempt: attempt,
|
||||||
|
answers: answers,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.updatedDeck.currentAttemptIndex,
|
||||||
|
equals(deck.currentAttemptIndex + 1));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('processAttempt calculates correct percentage', () {
|
||||||
|
final attempt = attemptService.createAttempt(
|
||||||
|
deck: deck,
|
||||||
|
attemptSize: 3,
|
||||||
|
);
|
||||||
|
final answers = <String, int>{};
|
||||||
|
|
||||||
|
// Answer 2 out of 3 correctly
|
||||||
|
answers[attempt.questions[0].id] =
|
||||||
|
attempt.questions[0].correctIndices.first;
|
||||||
|
answers[attempt.questions[1].id] =
|
||||||
|
attempt.questions[1].correctIndices.first;
|
||||||
|
answers[attempt.questions[2].id] = 999; // Wrong answer
|
||||||
|
|
||||||
|
final result = attemptService.processAttempt(
|
||||||
|
deck: deck,
|
||||||
|
attempt: attempt,
|
||||||
|
answers: answers,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.result.percentageCorrect, closeTo(66.67, 0.01));
|
||||||
|
expect(result.result.correctCount, equals(2));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('processAttempt tracks incorrect questions', () {
|
||||||
|
final attempt = attemptService.createAttempt(
|
||||||
|
deck: deck,
|
||||||
|
attemptSize: 3,
|
||||||
|
);
|
||||||
|
final answers = <String, int>{};
|
||||||
|
|
||||||
|
// Answer first correctly, rest incorrectly
|
||||||
|
answers[attempt.questions[0].id] =
|
||||||
|
attempt.questions[0].correctIndices.first;
|
||||||
|
answers[attempt.questions[1].id] = 999;
|
||||||
|
answers[attempt.questions[2].id] = 999;
|
||||||
|
|
||||||
|
final result = attemptService.processAttempt(
|
||||||
|
deck: deck,
|
||||||
|
attempt: attempt,
|
||||||
|
answers: answers,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.result.incorrectQuestions.length, equals(2));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('processAttempt updates question streaks correctly', () {
|
||||||
|
final question = Question(
|
||||||
|
id: 'q1',
|
||||||
|
prompt: 'Test',
|
||||||
|
answers: ['A', 'B'],
|
||||||
|
correctAnswerIndices: [0],
|
||||||
|
consecutiveCorrect: 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
final testDeck = deck.copyWith(questions: [question]);
|
||||||
|
final attempt = attemptService.createAttempt(
|
||||||
|
deck: testDeck,
|
||||||
|
attemptSize: 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
final answers = {question.id: question.correctIndices.first};
|
||||||
|
|
||||||
|
final result = attemptService.processAttempt(
|
||||||
|
deck: testDeck,
|
||||||
|
attempt: attempt,
|
||||||
|
answers: answers,
|
||||||
|
);
|
||||||
|
|
||||||
|
final updated = result.updatedDeck.questions.first;
|
||||||
|
expect(updated.consecutiveCorrect, equals(3));
|
||||||
|
expect(updated.isKnown, equals(true));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,144 @@
|
|||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:practice_engine/models/question.dart';
|
||||||
|
import 'package:practice_engine/models/attempt_result.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('AttemptResult', () {
|
||||||
|
test('fromAnswers calculates correct percentage', () {
|
||||||
|
final question = Question(
|
||||||
|
id: 'q1',
|
||||||
|
prompt: 'Test',
|
||||||
|
answers: ['A', 'B'],
|
||||||
|
correctAnswerIndices: [0],
|
||||||
|
);
|
||||||
|
|
||||||
|
final results = [
|
||||||
|
AnswerResult(
|
||||||
|
question: question,
|
||||||
|
userAnswerIndices: [0],
|
||||||
|
isCorrect: true,
|
||||||
|
statusChange: QuestionStatusChange.unchanged,
|
||||||
|
),
|
||||||
|
AnswerResult(
|
||||||
|
question: question,
|
||||||
|
userAnswerIndices: [1],
|
||||||
|
isCorrect: false,
|
||||||
|
statusChange: QuestionStatusChange.unchanged,
|
||||||
|
),
|
||||||
|
AnswerResult(
|
||||||
|
question: question,
|
||||||
|
userAnswerIndices: [0],
|
||||||
|
isCorrect: true,
|
||||||
|
statusChange: QuestionStatusChange.unchanged,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
final attemptResult = AttemptResult.fromAnswers(
|
||||||
|
results: results,
|
||||||
|
timeSpent: 1000,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(attemptResult.totalQuestions, equals(3));
|
||||||
|
expect(attemptResult.correctCount, equals(2));
|
||||||
|
expect(attemptResult.percentageCorrect, closeTo(66.67, 0.01));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fromAnswers handles empty results', () {
|
||||||
|
final attemptResult = AttemptResult.fromAnswers(
|
||||||
|
results: [],
|
||||||
|
timeSpent: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(attemptResult.totalQuestions, equals(0));
|
||||||
|
expect(attemptResult.correctCount, equals(0));
|
||||||
|
expect(attemptResult.percentageCorrect, equals(0.0));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fromAnswers filters incorrect questions', () {
|
||||||
|
final question = Question(
|
||||||
|
id: 'q1',
|
||||||
|
prompt: 'Test',
|
||||||
|
answers: ['A', 'B'],
|
||||||
|
correctAnswerIndices: [0],
|
||||||
|
);
|
||||||
|
|
||||||
|
final results = [
|
||||||
|
AnswerResult(
|
||||||
|
question: question,
|
||||||
|
userAnswerIndices: [0],
|
||||||
|
isCorrect: true,
|
||||||
|
statusChange: QuestionStatusChange.unchanged,
|
||||||
|
),
|
||||||
|
AnswerResult(
|
||||||
|
question: question,
|
||||||
|
userAnswerIndices: [1],
|
||||||
|
isCorrect: false,
|
||||||
|
statusChange: QuestionStatusChange.unchanged,
|
||||||
|
),
|
||||||
|
AnswerResult(
|
||||||
|
question: question,
|
||||||
|
userAnswerIndices: [1],
|
||||||
|
isCorrect: false,
|
||||||
|
statusChange: QuestionStatusChange.unchanged,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
final attemptResult = AttemptResult.fromAnswers(
|
||||||
|
results: results,
|
||||||
|
timeSpent: 1000,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(attemptResult.incorrectQuestions.length, equals(2));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fromAnswers includes all results', () {
|
||||||
|
final question = Question(
|
||||||
|
id: 'q1',
|
||||||
|
prompt: 'Test',
|
||||||
|
answers: ['A', 'B'],
|
||||||
|
correctAnswerIndices: [0],
|
||||||
|
);
|
||||||
|
|
||||||
|
final results = List.generate(5, (i) {
|
||||||
|
return AnswerResult(
|
||||||
|
question: question,
|
||||||
|
userAnswerIndices: [i % 2],
|
||||||
|
isCorrect: i % 2 == 0,
|
||||||
|
statusChange: QuestionStatusChange.unchanged,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
final attemptResult = AttemptResult.fromAnswers(
|
||||||
|
results: results,
|
||||||
|
timeSpent: 2000,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(attemptResult.allResults.length, equals(5));
|
||||||
|
expect(attemptResult.timeSpent, equals(2000));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('AnswerResult', () {
|
||||||
|
test('contains question and answer information', () {
|
||||||
|
final question = Question(
|
||||||
|
id: 'q1',
|
||||||
|
prompt: 'What is 2+2?',
|
||||||
|
answers: ['3', '4', '5'],
|
||||||
|
correctAnswerIndices: [1],
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = AnswerResult(
|
||||||
|
question: question,
|
||||||
|
userAnswerIndices: [1],
|
||||||
|
isCorrect: true,
|
||||||
|
statusChange: QuestionStatusChange.improved,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.question, equals(question));
|
||||||
|
expect(result.userAnswerIndices, equals([1]));
|
||||||
|
expect(result.isCorrect, equals(true));
|
||||||
|
expect(result.statusChange, equals(QuestionStatusChange.improved));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:practice_engine/models/deck_config.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('DeckConfig', () {
|
||||||
|
test('has default values', () {
|
||||||
|
const config = DeckConfig();
|
||||||
|
|
||||||
|
expect(config.requiredConsecutiveCorrect, equals(3));
|
||||||
|
expect(config.defaultAttemptSize, equals(10));
|
||||||
|
expect(config.priorityIncreaseOnIncorrect, equals(5));
|
||||||
|
expect(config.priorityDecreaseOnCorrect, equals(2));
|
||||||
|
expect(config.immediateFeedbackEnabled, equals(true));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can be created with custom values', () {
|
||||||
|
const config = DeckConfig(
|
||||||
|
requiredConsecutiveCorrect: 5,
|
||||||
|
defaultAttemptSize: 20,
|
||||||
|
priorityIncreaseOnIncorrect: 10,
|
||||||
|
priorityDecreaseOnCorrect: 3,
|
||||||
|
immediateFeedbackEnabled: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(config.requiredConsecutiveCorrect, equals(5));
|
||||||
|
expect(config.defaultAttemptSize, equals(20));
|
||||||
|
expect(config.priorityIncreaseOnIncorrect, equals(10));
|
||||||
|
expect(config.priorityDecreaseOnCorrect, equals(3));
|
||||||
|
expect(config.immediateFeedbackEnabled, equals(false));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('copyWith creates new config with updated fields', () {
|
||||||
|
const config = DeckConfig();
|
||||||
|
final updated = config.copyWith(
|
||||||
|
requiredConsecutiveCorrect: 4,
|
||||||
|
immediateFeedbackEnabled: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(updated.requiredConsecutiveCorrect, equals(4));
|
||||||
|
expect(updated.defaultAttemptSize, equals(config.defaultAttemptSize));
|
||||||
|
expect(updated.immediateFeedbackEnabled, equals(false));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('equality works correctly', () {
|
||||||
|
const config1 = DeckConfig();
|
||||||
|
const config2 = DeckConfig();
|
||||||
|
const config3 = DeckConfig(requiredConsecutiveCorrect: 5);
|
||||||
|
|
||||||
|
expect(config1, equals(config2));
|
||||||
|
expect(config1, isNot(equals(config3)));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,115 @@
|
|||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:practice_engine/models/deck.dart';
|
||||||
|
import 'package:practice_engine/models/deck_config.dart';
|
||||||
|
import 'package:practice_engine/models/question.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Deck', () {
|
||||||
|
late DeckConfig defaultConfig;
|
||||||
|
late List<Question> sampleQuestions;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
defaultConfig = const DeckConfig();
|
||||||
|
sampleQuestions = [
|
||||||
|
Question(
|
||||||
|
id: 'q1',
|
||||||
|
prompt: 'What is 2+2?',
|
||||||
|
answers: ['3', '4', '5'],
|
||||||
|
correctAnswerIndices: [1],
|
||||||
|
isKnown: true,
|
||||||
|
),
|
||||||
|
Question(
|
||||||
|
id: 'q2',
|
||||||
|
prompt: 'What is 3+3?',
|
||||||
|
answers: ['5', '6', '7'],
|
||||||
|
correctAnswerIndices: [1],
|
||||||
|
isKnown: false,
|
||||||
|
),
|
||||||
|
Question(
|
||||||
|
id: 'q3',
|
||||||
|
prompt: 'What is 4+4?',
|
||||||
|
answers: ['7', '8', '9'],
|
||||||
|
correctAnswerIndices: [1],
|
||||||
|
isKnown: true,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
test('calculates numberOfQuestions correctly', () {
|
||||||
|
final deck = Deck(
|
||||||
|
id: 'deck1',
|
||||||
|
title: 'Math Deck',
|
||||||
|
description: 'Basic math',
|
||||||
|
questions: sampleQuestions,
|
||||||
|
config: defaultConfig,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(deck.numberOfQuestions, equals(3));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('calculates knownCount correctly', () {
|
||||||
|
final deck = Deck(
|
||||||
|
id: 'deck1',
|
||||||
|
title: 'Math Deck',
|
||||||
|
description: 'Basic math',
|
||||||
|
questions: sampleQuestions,
|
||||||
|
config: defaultConfig,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(deck.knownCount, equals(2));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('calculates practicePercentage correctly', () {
|
||||||
|
final deck = Deck(
|
||||||
|
id: 'deck1',
|
||||||
|
title: 'Math Deck',
|
||||||
|
description: 'Basic math',
|
||||||
|
questions: sampleQuestions,
|
||||||
|
config: defaultConfig,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(deck.practicePercentage, closeTo(66.67, 0.01));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('practicePercentage is 0 for empty deck', () {
|
||||||
|
final deck = Deck(
|
||||||
|
id: 'deck1',
|
||||||
|
title: 'Empty Deck',
|
||||||
|
description: 'No questions',
|
||||||
|
questions: [],
|
||||||
|
config: defaultConfig,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(deck.practicePercentage, equals(0.0));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('practicePercentage is 100 when all questions are known', () {
|
||||||
|
final allKnown = sampleQuestions.map((q) => q.copyWith(isKnown: true)).toList();
|
||||||
|
final deck = Deck(
|
||||||
|
id: 'deck1',
|
||||||
|
title: 'All Known',
|
||||||
|
description: 'All known',
|
||||||
|
questions: allKnown,
|
||||||
|
config: defaultConfig,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(deck.practicePercentage, equals(100.0));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('copyWith creates new deck with updated fields', () {
|
||||||
|
final deck = Deck(
|
||||||
|
id: 'deck1',
|
||||||
|
title: 'Math Deck',
|
||||||
|
description: 'Basic math',
|
||||||
|
questions: sampleQuestions,
|
||||||
|
config: defaultConfig,
|
||||||
|
);
|
||||||
|
|
||||||
|
final updated = deck.copyWith(title: 'Updated Title');
|
||||||
|
expect(updated.title, equals('Updated Title'));
|
||||||
|
expect(updated.id, equals(deck.id));
|
||||||
|
expect(updated.questions, equals(deck.questions));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,125 @@
|
|||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:practice_engine/models/deck.dart';
|
||||||
|
import 'package:practice_engine/models/deck_config.dart';
|
||||||
|
import 'package:practice_engine/models/question.dart';
|
||||||
|
import 'package:practice_engine/logic/deck_service.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Manual Override Logic', () {
|
||||||
|
late DeckConfig config;
|
||||||
|
late Deck deck;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
config = const DeckConfig(requiredConsecutiveCorrect: 3);
|
||||||
|
deck = Deck(
|
||||||
|
id: 'deck1',
|
||||||
|
title: 'Test Deck',
|
||||||
|
description: 'Test',
|
||||||
|
questions: [
|
||||||
|
Question(
|
||||||
|
id: 'q1',
|
||||||
|
prompt: 'Question 1',
|
||||||
|
answers: ['A', 'B'],
|
||||||
|
correctAnswerIndices: [0],
|
||||||
|
consecutiveCorrect: 1,
|
||||||
|
isKnown: false,
|
||||||
|
priorityPoints: 5,
|
||||||
|
),
|
||||||
|
Question(
|
||||||
|
id: 'q2',
|
||||||
|
prompt: 'Question 2',
|
||||||
|
answers: ['A', 'B'],
|
||||||
|
correctAnswerIndices: [0],
|
||||||
|
consecutiveCorrect: 0,
|
||||||
|
isKnown: false,
|
||||||
|
priorityPoints: 10,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
config: config,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('markQuestionAsKnown sets streak to threshold and isKnown to true', () {
|
||||||
|
final updated = DeckService.markQuestionAsKnown(
|
||||||
|
deck: deck,
|
||||||
|
questionId: 'q1',
|
||||||
|
);
|
||||||
|
|
||||||
|
final question = updated.questions.firstWhere((q) => q.id == 'q1');
|
||||||
|
expect(question.consecutiveCorrect,
|
||||||
|
equals(config.requiredConsecutiveCorrect));
|
||||||
|
expect(question.isKnown, equals(true));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('markQuestionAsNeedsPractice sets isKnown to false and streak to 0', () {
|
||||||
|
// First mark as known
|
||||||
|
var updated = DeckService.markQuestionAsKnown(
|
||||||
|
deck: deck,
|
||||||
|
questionId: 'q1',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Then mark as needs practice
|
||||||
|
updated = DeckService.markQuestionAsNeedsPractice(
|
||||||
|
deck: updated,
|
||||||
|
questionId: 'q1',
|
||||||
|
);
|
||||||
|
|
||||||
|
final question = updated.questions.firstWhere((q) => q.id == 'q1');
|
||||||
|
expect(question.isKnown, equals(false));
|
||||||
|
expect(question.consecutiveCorrect, equals(0));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('markQuestionAsNeedsPractice preserves priority', () {
|
||||||
|
final updated = DeckService.markQuestionAsNeedsPractice(
|
||||||
|
deck: deck,
|
||||||
|
questionId: 'q1',
|
||||||
|
);
|
||||||
|
|
||||||
|
final question = updated.questions.firstWhere((q) => q.id == 'q1');
|
||||||
|
expect(question.priorityPoints, equals(5));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('manual override with needs practice ignores correctness', () {
|
||||||
|
final question = deck.questions.first;
|
||||||
|
|
||||||
|
// Answer correctly but mark as needs practice
|
||||||
|
final updated = DeckService.updateQuestionWithManualOverride(
|
||||||
|
question: question,
|
||||||
|
isCorrect: true,
|
||||||
|
userMarkedNeedsPractice: true,
|
||||||
|
config: config,
|
||||||
|
currentAttemptIndex: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Streak should not increment
|
||||||
|
expect(updated.consecutiveCorrect, equals(question.consecutiveCorrect));
|
||||||
|
|
||||||
|
// Priority should not decrease
|
||||||
|
expect(updated.priorityPoints, equals(question.priorityPoints));
|
||||||
|
|
||||||
|
// isKnown should not change
|
||||||
|
expect(updated.isKnown, equals(question.isKnown));
|
||||||
|
|
||||||
|
// But totalAttempts should increment
|
||||||
|
expect(updated.totalAttempts, equals(question.totalAttempts + 1));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('manual override without needs practice follows normal flow', () {
|
||||||
|
final question = deck.questions.first;
|
||||||
|
|
||||||
|
final updated = DeckService.updateQuestionWithManualOverride(
|
||||||
|
question: question,
|
||||||
|
isCorrect: true,
|
||||||
|
userMarkedNeedsPractice: false,
|
||||||
|
config: config,
|
||||||
|
currentAttemptIndex: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should behave like normal update
|
||||||
|
expect(updated.consecutiveCorrect, equals(question.consecutiveCorrect + 1));
|
||||||
|
expect(updated.priorityPoints,
|
||||||
|
equals(question.priorityPoints - config.priorityDecreaseOnCorrect));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,102 @@
|
|||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:practice_engine/models/question.dart';
|
||||||
|
import 'package:practice_engine/models/deck_config.dart';
|
||||||
|
import 'package:practice_engine/algorithms/priority_manager.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('PriorityManager', () {
|
||||||
|
late DeckConfig config;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
config = const DeckConfig(
|
||||||
|
priorityIncreaseOnIncorrect: 5,
|
||||||
|
priorityDecreaseOnCorrect: 2,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('increases priority on incorrect answer', () {
|
||||||
|
final question = Question(
|
||||||
|
id: 'q1',
|
||||||
|
prompt: 'Test',
|
||||||
|
answers: ['A', 'B'],
|
||||||
|
correctAnswerIndices: [0],
|
||||||
|
priorityPoints: 10,
|
||||||
|
);
|
||||||
|
|
||||||
|
final updated = PriorityManager.applyAnswerResult(
|
||||||
|
question: question,
|
||||||
|
isCorrect: false,
|
||||||
|
config: config,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(updated.priorityPoints,
|
||||||
|
equals(10 + config.priorityIncreaseOnIncorrect));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('decreases priority on correct answer', () {
|
||||||
|
final question = Question(
|
||||||
|
id: 'q1',
|
||||||
|
prompt: 'Test',
|
||||||
|
answers: ['A', 'B'],
|
||||||
|
correctAnswerIndices: [0],
|
||||||
|
priorityPoints: 10,
|
||||||
|
);
|
||||||
|
|
||||||
|
final updated = PriorityManager.applyAnswerResult(
|
||||||
|
question: question,
|
||||||
|
isCorrect: true,
|
||||||
|
config: config,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(updated.priorityPoints,
|
||||||
|
equals(10 - config.priorityDecreaseOnCorrect));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('priority cannot go below 0', () {
|
||||||
|
final question = Question(
|
||||||
|
id: 'q1',
|
||||||
|
prompt: 'Test',
|
||||||
|
answers: ['A', 'B'],
|
||||||
|
correctAnswerIndices: [0],
|
||||||
|
priorityPoints: 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
final updated = PriorityManager.applyAnswerResult(
|
||||||
|
question: question,
|
||||||
|
isCorrect: true,
|
||||||
|
config: config,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(updated.priorityPoints, equals(0));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resetPriority sets priority to 0', () {
|
||||||
|
final question = Question(
|
||||||
|
id: 'q1',
|
||||||
|
prompt: 'Test',
|
||||||
|
answers: ['A', 'B'],
|
||||||
|
correctAnswerIndices: [0],
|
||||||
|
priorityPoints: 100,
|
||||||
|
);
|
||||||
|
|
||||||
|
final updated = PriorityManager.resetPriority(question);
|
||||||
|
|
||||||
|
expect(updated.priorityPoints, equals(0));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('withPriorityPoints enforces non-negative priority', () {
|
||||||
|
final question = Question(
|
||||||
|
id: 'q1',
|
||||||
|
prompt: 'Test',
|
||||||
|
answers: ['A', 'B'],
|
||||||
|
correctAnswerIndices: [0],
|
||||||
|
priorityPoints: 10,
|
||||||
|
);
|
||||||
|
|
||||||
|
final updated = question.withPriorityPoints(-5);
|
||||||
|
|
||||||
|
expect(updated.priorityPoints, equals(0));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,204 @@
|
|||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:practice_engine/models/question.dart';
|
||||||
|
import 'package:practice_engine/models/deck_config.dart';
|
||||||
|
import 'package:practice_engine/logic/deck_service.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Question State Transitions', () {
|
||||||
|
late DeckConfig config;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
config = const DeckConfig(requiredConsecutiveCorrect: 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('correct answer increments streak', () {
|
||||||
|
final question = Question(
|
||||||
|
id: 'q1',
|
||||||
|
prompt: 'Test',
|
||||||
|
answers: ['A', 'B'],
|
||||||
|
correctAnswerIndices: [0],
|
||||||
|
consecutiveCorrect: 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
final updated = DeckService.updateQuestionAfterAnswer(
|
||||||
|
question: question,
|
||||||
|
isCorrect: true,
|
||||||
|
config: config,
|
||||||
|
currentAttemptIndex: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(updated.consecutiveCorrect, equals(2));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('incorrect answer resets streak', () {
|
||||||
|
final question = Question(
|
||||||
|
id: 'q1',
|
||||||
|
prompt: 'Test',
|
||||||
|
answers: ['A', 'B'],
|
||||||
|
correctAnswerIndices: [0],
|
||||||
|
consecutiveCorrect: 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
final updated = DeckService.updateQuestionAfterAnswer(
|
||||||
|
question: question,
|
||||||
|
isCorrect: false,
|
||||||
|
config: config,
|
||||||
|
currentAttemptIndex: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(updated.consecutiveCorrect, equals(0));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('question becomes known when streak reaches threshold', () {
|
||||||
|
final question = Question(
|
||||||
|
id: 'q1',
|
||||||
|
prompt: 'Test',
|
||||||
|
answers: ['A', 'B'],
|
||||||
|
correctAnswerIndices: [0],
|
||||||
|
consecutiveCorrect: 2,
|
||||||
|
isKnown: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
final updated = DeckService.updateQuestionAfterAnswer(
|
||||||
|
question: question,
|
||||||
|
isCorrect: true,
|
||||||
|
config: config,
|
||||||
|
currentAttemptIndex: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(updated.consecutiveCorrect, equals(3));
|
||||||
|
expect(updated.isKnown, equals(true));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('incorrect answer sets isKnown to false', () {
|
||||||
|
final question = Question(
|
||||||
|
id: 'q1',
|
||||||
|
prompt: 'Test',
|
||||||
|
answers: ['A', 'B'],
|
||||||
|
correctAnswerIndices: [0],
|
||||||
|
consecutiveCorrect: 3,
|
||||||
|
isKnown: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
final updated = DeckService.updateQuestionAfterAnswer(
|
||||||
|
question: question,
|
||||||
|
isCorrect: false,
|
||||||
|
config: config,
|
||||||
|
currentAttemptIndex: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(updated.isKnown, equals(false));
|
||||||
|
expect(updated.consecutiveCorrect, equals(0));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('priority increases on incorrect answer', () {
|
||||||
|
final question = Question(
|
||||||
|
id: 'q1',
|
||||||
|
prompt: 'Test',
|
||||||
|
answers: ['A', 'B'],
|
||||||
|
correctAnswerIndices: [0],
|
||||||
|
priorityPoints: 5,
|
||||||
|
);
|
||||||
|
|
||||||
|
final updated = DeckService.updateQuestionAfterAnswer(
|
||||||
|
question: question,
|
||||||
|
isCorrect: false,
|
||||||
|
config: config,
|
||||||
|
currentAttemptIndex: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(updated.priorityPoints,
|
||||||
|
equals(5 + config.priorityIncreaseOnIncorrect));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('priority decreases on correct answer', () {
|
||||||
|
final question = Question(
|
||||||
|
id: 'q1',
|
||||||
|
prompt: 'Test',
|
||||||
|
answers: ['A', 'B'],
|
||||||
|
correctAnswerIndices: [0],
|
||||||
|
priorityPoints: 10,
|
||||||
|
);
|
||||||
|
|
||||||
|
final updated = DeckService.updateQuestionAfterAnswer(
|
||||||
|
question: question,
|
||||||
|
isCorrect: true,
|
||||||
|
config: config,
|
||||||
|
currentAttemptIndex: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(updated.priorityPoints,
|
||||||
|
equals(10 - config.priorityDecreaseOnCorrect));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('priority cannot go below 0', () {
|
||||||
|
final question = Question(
|
||||||
|
id: 'q1',
|
||||||
|
prompt: 'Test',
|
||||||
|
answers: ['A', 'B'],
|
||||||
|
correctAnswerIndices: [0],
|
||||||
|
priorityPoints: 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
final updated = DeckService.updateQuestionAfterAnswer(
|
||||||
|
question: question,
|
||||||
|
isCorrect: true,
|
||||||
|
config: config,
|
||||||
|
currentAttemptIndex: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(updated.priorityPoints, equals(0));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('lastAttemptIndex is updated', () {
|
||||||
|
final question = Question(
|
||||||
|
id: 'q1',
|
||||||
|
prompt: 'Test',
|
||||||
|
answers: ['A', 'B'],
|
||||||
|
correctAnswerIndices: [0],
|
||||||
|
lastAttemptIndex: 5,
|
||||||
|
);
|
||||||
|
|
||||||
|
final updated = DeckService.updateQuestionAfterAnswer(
|
||||||
|
question: question,
|
||||||
|
isCorrect: true,
|
||||||
|
config: config,
|
||||||
|
currentAttemptIndex: 10,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(updated.lastAttemptIndex, equals(10));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('totalAttempts increments on both correct and incorrect', () {
|
||||||
|
final question = Question(
|
||||||
|
id: 'q1',
|
||||||
|
prompt: 'Test',
|
||||||
|
answers: ['A', 'B'],
|
||||||
|
correctAnswerIndices: [0],
|
||||||
|
totalAttempts: 5,
|
||||||
|
totalCorrectAttempts: 3,
|
||||||
|
);
|
||||||
|
|
||||||
|
final updatedCorrect = DeckService.updateQuestionAfterAnswer(
|
||||||
|
question: question,
|
||||||
|
isCorrect: true,
|
||||||
|
config: config,
|
||||||
|
currentAttemptIndex: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(updatedCorrect.totalAttempts, equals(6));
|
||||||
|
expect(updatedCorrect.totalCorrectAttempts, equals(4));
|
||||||
|
|
||||||
|
final updatedIncorrect = DeckService.updateQuestionAfterAnswer(
|
||||||
|
question: question,
|
||||||
|
isCorrect: false,
|
||||||
|
config: config,
|
||||||
|
currentAttemptIndex: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(updatedIncorrect.totalAttempts, equals(6));
|
||||||
|
expect(updatedIncorrect.totalCorrectAttempts, equals(3));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,99 @@
|
|||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:practice_engine/models/deck.dart';
|
||||||
|
import 'package:practice_engine/models/deck_config.dart';
|
||||||
|
import 'package:practice_engine/models/question.dart';
|
||||||
|
import 'package:practice_engine/logic/deck_service.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Reset Logic', () {
|
||||||
|
late DeckConfig config;
|
||||||
|
late Deck deck;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
config = const DeckConfig(requiredConsecutiveCorrect: 3);
|
||||||
|
deck = Deck(
|
||||||
|
id: 'deck1',
|
||||||
|
title: 'Test Deck',
|
||||||
|
description: 'Test',
|
||||||
|
questions: [
|
||||||
|
Question(
|
||||||
|
id: 'q1',
|
||||||
|
prompt: 'Question 1',
|
||||||
|
answers: ['A', 'B'],
|
||||||
|
correctAnswerIndices: [0],
|
||||||
|
consecutiveCorrect: 5,
|
||||||
|
isKnown: true,
|
||||||
|
priorityPoints: 10,
|
||||||
|
lastAttemptIndex: 20,
|
||||||
|
totalCorrectAttempts: 15,
|
||||||
|
totalAttempts: 20,
|
||||||
|
),
|
||||||
|
Question(
|
||||||
|
id: 'q2',
|
||||||
|
prompt: 'Question 2',
|
||||||
|
answers: ['A', 'B'],
|
||||||
|
correctAnswerIndices: [0],
|
||||||
|
consecutiveCorrect: 2,
|
||||||
|
isKnown: false,
|
||||||
|
priorityPoints: 5,
|
||||||
|
lastAttemptIndex: 15,
|
||||||
|
totalCorrectAttempts: 8,
|
||||||
|
totalAttempts: 12,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
config: config,
|
||||||
|
currentAttemptIndex: 25,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resetDeck resets all question states', () {
|
||||||
|
final reset = DeckService.resetDeck(deck: deck);
|
||||||
|
|
||||||
|
for (final question in reset.questions) {
|
||||||
|
expect(question.consecutiveCorrect, equals(0));
|
||||||
|
expect(question.isKnown, equals(false));
|
||||||
|
expect(question.priorityPoints, equals(0));
|
||||||
|
expect(question.lastAttemptIndex, equals(-1));
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(reset.currentAttemptIndex, equals(0));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resetDeck preserves attempt counts by default', () {
|
||||||
|
final reset = DeckService.resetDeck(deck: deck);
|
||||||
|
|
||||||
|
expect(reset.questions[0].totalAttempts, equals(20));
|
||||||
|
expect(reset.questions[0].totalCorrectAttempts, equals(15));
|
||||||
|
expect(reset.questions[1].totalAttempts, equals(12));
|
||||||
|
expect(reset.questions[1].totalCorrectAttempts, equals(8));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resetDeck resets attempt counts when requested', () {
|
||||||
|
final reset = DeckService.resetDeck(
|
||||||
|
deck: deck,
|
||||||
|
resetAttemptCounts: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (final question in reset.questions) {
|
||||||
|
expect(question.totalAttempts, equals(0));
|
||||||
|
expect(question.totalCorrectAttempts, equals(0));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resetDeck resets currentAttemptIndex to 0', () {
|
||||||
|
final reset = DeckService.resetDeck(deck: deck);
|
||||||
|
|
||||||
|
expect(reset.currentAttemptIndex, equals(0));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resetDeck preserves deck metadata', () {
|
||||||
|
final reset = DeckService.resetDeck(deck: deck);
|
||||||
|
|
||||||
|
expect(reset.id, equals(deck.id));
|
||||||
|
expect(reset.title, equals(deck.title));
|
||||||
|
expect(reset.description, equals(deck.description));
|
||||||
|
expect(reset.config, equals(deck.config));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,101 @@
|
|||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:practice_engine/models/question.dart';
|
||||||
|
import 'package:practice_engine/algorithms/spaced_repetition.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('SpacedRepetition', () {
|
||||||
|
test('baseKnownProbability for never-seen known question', () {
|
||||||
|
final weight = SpacedRepetition.calculateKnownQuestionWeight(
|
||||||
|
lastAttemptIndex: -1,
|
||||||
|
currentAttemptIndex: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(weight, equals(SpacedRepetition.baseKnownProbability));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('probability increases with attempts since last seen', () {
|
||||||
|
final weight1 = SpacedRepetition.calculateKnownQuestionWeight(
|
||||||
|
lastAttemptIndex: 0,
|
||||||
|
currentAttemptIndex: 10,
|
||||||
|
);
|
||||||
|
|
||||||
|
final weight2 = SpacedRepetition.calculateKnownQuestionWeight(
|
||||||
|
lastAttemptIndex: 0,
|
||||||
|
currentAttemptIndex: 30,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(weight2, greaterThan(weight1));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('probability is capped at maxKnownProbability', () {
|
||||||
|
final weight = SpacedRepetition.calculateKnownQuestionWeight(
|
||||||
|
lastAttemptIndex: 0,
|
||||||
|
currentAttemptIndex: 1000,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(weight, lessThanOrEqualTo(SpacedRepetition.maxKnownProbability));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('probability increases gradually', () {
|
||||||
|
final weights = <double>[];
|
||||||
|
for (int i = 0; i <= 30; i++) {
|
||||||
|
final weight = SpacedRepetition.calculateKnownQuestionWeight(
|
||||||
|
lastAttemptIndex: 0,
|
||||||
|
currentAttemptIndex: i,
|
||||||
|
);
|
||||||
|
weights.add(weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should be monotonically increasing
|
||||||
|
for (int i = 1; i < weights.length; i++) {
|
||||||
|
expect(weights[i], greaterThanOrEqualTo(weights[i - 1]));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getQuestionWeight uses priority for unknown questions', () {
|
||||||
|
final question = Question(
|
||||||
|
id: 'q1',
|
||||||
|
prompt: 'Test',
|
||||||
|
answers: ['A', 'B'],
|
||||||
|
correctAnswerIndices: [0],
|
||||||
|
isKnown: false,
|
||||||
|
priorityPoints: 10,
|
||||||
|
);
|
||||||
|
|
||||||
|
final weight = SpacedRepetition.getQuestionWeight(
|
||||||
|
question: question,
|
||||||
|
currentAttemptIndex: 0,
|
||||||
|
basePriorityWeight: 1.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should be priority + 1
|
||||||
|
expect(weight, equals(11.0));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getQuestionWeight uses spaced repetition for known questions', () {
|
||||||
|
final question = Question(
|
||||||
|
id: 'q1',
|
||||||
|
prompt: 'Test',
|
||||||
|
answers: ['A', 'B'],
|
||||||
|
correctAnswerIndices: [0],
|
||||||
|
isKnown: true,
|
||||||
|
lastAttemptIndex: 5,
|
||||||
|
priorityPoints: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
final weight = SpacedRepetition.getQuestionWeight(
|
||||||
|
question: question,
|
||||||
|
currentAttemptIndex: 10,
|
||||||
|
basePriorityWeight: 1.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should use spaced repetition probability
|
||||||
|
final expected = SpacedRepetition.calculateKnownQuestionWeight(
|
||||||
|
lastAttemptIndex: 5,
|
||||||
|
currentAttemptIndex: 10,
|
||||||
|
);
|
||||||
|
expect(weight, closeTo(expected, 0.001));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,138 @@
|
|||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:practice_engine/models/question.dart';
|
||||||
|
import 'package:practice_engine/algorithms/weighted_selector.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('WeightedSelector', () {
|
||||||
|
test('selects correct number of questions', () {
|
||||||
|
final selector = WeightedSelector(seed: 42);
|
||||||
|
final questions = List.generate(10, (i) {
|
||||||
|
return Question(
|
||||||
|
id: 'q$i',
|
||||||
|
prompt: 'Question $i',
|
||||||
|
answers: ['A', 'B'],
|
||||||
|
correctAnswerIndices: [0],
|
||||||
|
priorityPoints: i,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
final selected = selector.selectQuestions(
|
||||||
|
candidates: questions,
|
||||||
|
count: 5,
|
||||||
|
currentAttemptIndex: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(selected.length, equals(5));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns all questions if count >= candidates length', () {
|
||||||
|
final selector = WeightedSelector(seed: 42);
|
||||||
|
final questions = List.generate(5, (i) {
|
||||||
|
return Question(
|
||||||
|
id: 'q$i',
|
||||||
|
prompt: 'Question $i',
|
||||||
|
answers: ['A', 'B'],
|
||||||
|
correctAnswerIndices: [0],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
final selected = selector.selectQuestions(
|
||||||
|
candidates: questions,
|
||||||
|
count: 10,
|
||||||
|
currentAttemptIndex: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(selected.length, equals(5));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns empty list for empty candidates', () {
|
||||||
|
final selector = WeightedSelector(seed: 42);
|
||||||
|
final selected = selector.selectQuestions(
|
||||||
|
candidates: [],
|
||||||
|
count: 5,
|
||||||
|
currentAttemptIndex: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(selected, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns empty list for count <= 0', () {
|
||||||
|
final selector = WeightedSelector(seed: 42);
|
||||||
|
final questions = List.generate(5, (i) {
|
||||||
|
return Question(
|
||||||
|
id: 'q$i',
|
||||||
|
prompt: 'Question $i',
|
||||||
|
answers: ['A', 'B'],
|
||||||
|
correctAnswerIndices: [0],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
final selected = selector.selectQuestions(
|
||||||
|
candidates: questions,
|
||||||
|
count: 0,
|
||||||
|
currentAttemptIndex: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(selected, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('no duplicates in selection', () {
|
||||||
|
final selector = WeightedSelector(seed: 42);
|
||||||
|
final questions = List.generate(10, (i) {
|
||||||
|
return Question(
|
||||||
|
id: 'q$i',
|
||||||
|
prompt: 'Question $i',
|
||||||
|
answers: ['A', 'B'],
|
||||||
|
correctAnswerIndices: [0],
|
||||||
|
priorityPoints: 5,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
final selected = selector.selectQuestions(
|
||||||
|
candidates: questions,
|
||||||
|
count: 5,
|
||||||
|
currentAttemptIndex: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
final ids = selected.map((q) => q.id).toSet();
|
||||||
|
expect(ids.length, equals(selected.length));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('higher priority questions are more likely to be selected', () {
|
||||||
|
final selector = WeightedSelector(seed: 42);
|
||||||
|
final questions = [
|
||||||
|
Question(
|
||||||
|
id: 'low',
|
||||||
|
prompt: 'Low priority',
|
||||||
|
answers: ['A', 'B'],
|
||||||
|
correctAnswerIndices: [0],
|
||||||
|
priorityPoints: 1,
|
||||||
|
),
|
||||||
|
Question(
|
||||||
|
id: 'high',
|
||||||
|
prompt: 'High priority',
|
||||||
|
answers: ['A', 'B'],
|
||||||
|
correctAnswerIndices: [0],
|
||||||
|
priorityPoints: 100,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Run multiple selections and count occurrences
|
||||||
|
int highSelected = 0;
|
||||||
|
for (int i = 0; i < 100; i++) {
|
||||||
|
final selected = selector.selectQuestions(
|
||||||
|
candidates: questions,
|
||||||
|
count: 1,
|
||||||
|
currentAttemptIndex: 0,
|
||||||
|
);
|
||||||
|
if (selected.first.id == 'high') {
|
||||||
|
highSelected++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// High priority should be selected more often
|
||||||
|
expect(highSelected, greaterThan(50));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Reference in new issue