Full Table — vary tense and verb family between consecutive prompts
randomFullTablePrompt now takes the previous prompt's tense and verb ending and picks the next one to avoid an immediate repeat: when more than one tense is selected the just-shown tense is excluded, and when both -ar and -er/-ir verbs are available the next verb switches family. Both constraints are best-effort — if honouring one would leave no eligible fully-regular combo it is dropped, and the exhaustive "anything eligible at all" guarantee is preserved. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -95,7 +95,16 @@ struct PracticeSessionService {
|
||||
return nil
|
||||
}
|
||||
|
||||
func randomFullTablePrompt() -> FullTablePrompt? {
|
||||
/// Builds a Full Table prompt. `previousTenseId` / `previousEnding` describe
|
||||
/// the prompt just shown; when possible the next prompt avoids repeating the
|
||||
/// same tense back-to-back and switches the verb's ending family
|
||||
/// (-ar ⇄ -er/-ir) so consecutive rounds feel varied. Both are best-effort:
|
||||
/// if honouring them would leave no eligible combo, the constraint is
|
||||
/// dropped rather than dead-ending.
|
||||
func randomFullTablePrompt(
|
||||
previousTenseId: String? = nil,
|
||||
previousEnding: String? = nil
|
||||
) -> FullTablePrompt? {
|
||||
let settings = settings()
|
||||
// Full Table is testing the user's grasp of regular conjugation patterns,
|
||||
// not vocabulary recognition. Level filter is intentionally bypassed so
|
||||
@@ -111,25 +120,29 @@ struct PracticeSessionService {
|
||||
let candidateTenseIds = settings.selectionTenseIDs
|
||||
guard !candidateTenseIds.isEmpty else { return nil }
|
||||
|
||||
// Cheap path: random sampling. With ~1750 verbs and several hundred
|
||||
// fully-regular combos this almost always succeeds within a handful
|
||||
// of attempts.
|
||||
for _ in 0..<40 {
|
||||
guard let verb = verbs.randomElement(),
|
||||
let tenseId = candidateTenseIds.randomElement(),
|
||||
let tenseInfo = TenseInfo.find(tenseId) else { continue }
|
||||
if let prompt = makePromptIfFullyRegular(verb: verb, tenseId: tenseId, tenseInfo: tenseInfo) {
|
||||
let tenseChoices = tenseChoicesAvoidingRepeat(candidateTenseIds, previous: previousTenseId)
|
||||
let verbChoices = verbChoicesSwitchingFamily(verbs, previousEnding: previousEnding)
|
||||
let isConstrained = tenseChoices.count != candidateTenseIds.count
|
||||
|| verbChoices.count != verbs.count
|
||||
|
||||
// Best-effort: random-sample the constrained pool first so consecutive
|
||||
// prompts vary the tense and the -ar/-er-ir family.
|
||||
if isConstrained,
|
||||
let prompt = sampleFullTablePrompt(verbs: verbChoices, tenseIds: tenseChoices) {
|
||||
return prompt
|
||||
}
|
||||
|
||||
// No constraint, or honouring it found nothing quickly — sample the
|
||||
// full pool.
|
||||
if let prompt = sampleFullTablePrompt(verbs: verbs, tenseIds: candidateTenseIds) {
|
||||
return prompt
|
||||
}
|
||||
|
||||
// Guarantee: if any eligible (verb, tense) combo exists in the data we
|
||||
// return one. Only return nil when the user's settings genuinely produce
|
||||
// an empty pool (so the UI can show an error state instead of a blank).
|
||||
let shuffledVerbs = verbs.shuffled()
|
||||
let shuffledTenseIds = candidateTenseIds.shuffled()
|
||||
for verb in shuffledVerbs {
|
||||
for tenseId in shuffledTenseIds {
|
||||
for verb in verbs.shuffled() {
|
||||
for tenseId in candidateTenseIds.shuffled() {
|
||||
guard let tenseInfo = TenseInfo.find(tenseId) else { continue }
|
||||
if let prompt = makePromptIfFullyRegular(verb: verb, tenseId: tenseId, tenseInfo: tenseInfo) {
|
||||
return prompt
|
||||
@@ -140,6 +153,39 @@ struct PracticeSessionService {
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Random-sample up to 40 (verb, tense) pairs for a fully-regular combo.
|
||||
/// With ~1750 verbs and several hundred eligible combos this almost always
|
||||
/// succeeds within a handful of attempts.
|
||||
private func sampleFullTablePrompt(verbs: [Verb], tenseIds: [String]) -> FullTablePrompt? {
|
||||
guard !verbs.isEmpty, !tenseIds.isEmpty else { return nil }
|
||||
for _ in 0..<40 {
|
||||
guard let verb = verbs.randomElement(),
|
||||
let tenseId = tenseIds.randomElement(),
|
||||
let tenseInfo = TenseInfo.find(tenseId) else { continue }
|
||||
if let prompt = makePromptIfFullyRegular(verb: verb, tenseId: tenseId, tenseInfo: tenseInfo) {
|
||||
return prompt
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Tense ids with the previous one removed — only when more than one tense
|
||||
/// is selected and removing it still leaves a choice.
|
||||
private func tenseChoicesAvoidingRepeat(_ ids: [String], previous: String?) -> [String] {
|
||||
guard ids.count > 1, let previous else { return ids }
|
||||
let filtered = ids.filter { $0 != previous }
|
||||
return filtered.isEmpty ? ids : filtered
|
||||
}
|
||||
|
||||
/// Verbs whose ending family (-ar vs -er/-ir) differs from the previous
|
||||
/// verb's — only when both families are actually present in the pool.
|
||||
private func verbChoicesSwitchingFamily(_ verbs: [Verb], previousEnding: String?) -> [Verb] {
|
||||
guard let previousEnding else { return verbs }
|
||||
let previousIsAr = (previousEnding == "ar")
|
||||
let switched = verbs.filter { ($0.ending == "ar") != previousIsAr }
|
||||
return switched.isEmpty ? verbs : switched
|
||||
}
|
||||
|
||||
/// Returns a `FullTablePrompt` if this verb's forms in the given tense
|
||||
/// follow the regular pattern (per `FullTableEligibility`). Nil otherwise.
|
||||
private func makePromptIfFullyRegular(
|
||||
|
||||
@@ -269,7 +269,10 @@ struct FullTableView: View {
|
||||
cloudContext: cloudModelContext,
|
||||
reflexiveBaseInfinitives: ReflexiveVerbStore.shared.baseInfinitives
|
||||
)
|
||||
guard let prompt = service.randomFullTablePrompt() else {
|
||||
guard let prompt = service.randomFullTablePrompt(
|
||||
previousTenseId: currentTense?.id,
|
||||
previousEnding: currentVerb?.ending
|
||||
) else {
|
||||
// Genuinely no eligible (verb, tense) combo. Surface a clear error
|
||||
// instead of a blank screen — the previous behaviour silently
|
||||
// rendered an empty header and inputs.
|
||||
|
||||
Reference in New Issue
Block a user