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
|
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()
|
let settings = settings()
|
||||||
// Full Table is testing the user's grasp of regular conjugation patterns,
|
// Full Table is testing the user's grasp of regular conjugation patterns,
|
||||||
// not vocabulary recognition. Level filter is intentionally bypassed so
|
// not vocabulary recognition. Level filter is intentionally bypassed so
|
||||||
@@ -111,25 +120,29 @@ struct PracticeSessionService {
|
|||||||
let candidateTenseIds = settings.selectionTenseIDs
|
let candidateTenseIds = settings.selectionTenseIDs
|
||||||
guard !candidateTenseIds.isEmpty else { return nil }
|
guard !candidateTenseIds.isEmpty else { return nil }
|
||||||
|
|
||||||
// Cheap path: random sampling. With ~1750 verbs and several hundred
|
let tenseChoices = tenseChoicesAvoidingRepeat(candidateTenseIds, previous: previousTenseId)
|
||||||
// fully-regular combos this almost always succeeds within a handful
|
let verbChoices = verbChoicesSwitchingFamily(verbs, previousEnding: previousEnding)
|
||||||
// of attempts.
|
let isConstrained = tenseChoices.count != candidateTenseIds.count
|
||||||
for _ in 0..<40 {
|
|| verbChoices.count != verbs.count
|
||||||
guard let verb = verbs.randomElement(),
|
|
||||||
let tenseId = candidateTenseIds.randomElement(),
|
// Best-effort: random-sample the constrained pool first so consecutive
|
||||||
let tenseInfo = TenseInfo.find(tenseId) else { continue }
|
// prompts vary the tense and the -ar/-er-ir family.
|
||||||
if let prompt = makePromptIfFullyRegular(verb: verb, tenseId: tenseId, tenseInfo: tenseInfo) {
|
if isConstrained,
|
||||||
return prompt
|
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
|
// Guarantee: if any eligible (verb, tense) combo exists in the data we
|
||||||
// return one. Only return nil when the user's settings genuinely produce
|
// 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).
|
// an empty pool (so the UI can show an error state instead of a blank).
|
||||||
let shuffledVerbs = verbs.shuffled()
|
for verb in verbs.shuffled() {
|
||||||
let shuffledTenseIds = candidateTenseIds.shuffled()
|
for tenseId in candidateTenseIds.shuffled() {
|
||||||
for verb in shuffledVerbs {
|
|
||||||
for tenseId in shuffledTenseIds {
|
|
||||||
guard let tenseInfo = TenseInfo.find(tenseId) else { continue }
|
guard let tenseInfo = TenseInfo.find(tenseId) else { continue }
|
||||||
if let prompt = makePromptIfFullyRegular(verb: verb, tenseId: tenseId, tenseInfo: tenseInfo) {
|
if let prompt = makePromptIfFullyRegular(verb: verb, tenseId: tenseId, tenseInfo: tenseInfo) {
|
||||||
return prompt
|
return prompt
|
||||||
@@ -140,6 +153,39 @@ struct PracticeSessionService {
|
|||||||
return nil
|
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
|
/// Returns a `FullTablePrompt` if this verb's forms in the given tense
|
||||||
/// follow the regular pattern (per `FullTableEligibility`). Nil otherwise.
|
/// follow the regular pattern (per `FullTableEligibility`). Nil otherwise.
|
||||||
private func makePromptIfFullyRegular(
|
private func makePromptIfFullyRegular(
|
||||||
|
|||||||
@@ -269,7 +269,10 @@ struct FullTableView: View {
|
|||||||
cloudContext: cloudModelContext,
|
cloudContext: cloudModelContext,
|
||||||
reflexiveBaseInfinitives: ReflexiveVerbStore.shared.baseInfinitives
|
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
|
// Genuinely no eligible (verb, tense) combo. Surface a clear error
|
||||||
// instead of a blank screen — the previous behaviour silently
|
// instead of a blank screen — the previous behaviour silently
|
||||||
// rendered an empty header and inputs.
|
// rendered an empty header and inputs.
|
||||||
|
|||||||
Reference in New Issue
Block a user