From 39996bc92cecc1a1e7d5321c32f2f35088d0869c Mon Sep 17 00:00:00 2001 From: lingdocs <71590811+lingdocs@users.noreply.github.com> Date: Wed, 13 Apr 2022 14:37:04 +0500 Subject: [PATCH] refactor and improve quiz --- package.json | 2 +- src/components/vp-explorer/TensePicker.tsx | 9 +- src/components/vp-explorer/VPDisplay.tsx | 12 +- src/components/vp-explorer/VPExplorer.tsx | 268 ++---------------- src/components/vp-explorer/VPExplorerQuiz.tsx | 264 +++++++++++++++++ src/lib/phrase-building/vp-tools.ts | 13 + 6 files changed, 309 insertions(+), 259 deletions(-) create mode 100644 src/components/vp-explorer/VPExplorerQuiz.tsx diff --git a/package.json b/package.json index 6f09a60..f131107 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lingdocs/pashto-inflector", - "version": "1.9.8", + "version": "1.9.9", "author": "lingdocs.com", "description": "A Pashto inflection and verb conjugation engine, inculding React components for displaying Pashto text, inflections, and conjugations", "homepage": "https://verbs.lingdocs.com", diff --git a/src/components/vp-explorer/TensePicker.tsx b/src/components/vp-explorer/TensePicker.tsx index 59513b2..2cbb63d 100644 --- a/src/components/vp-explorer/TensePicker.tsx +++ b/src/components/vp-explorer/TensePicker.tsx @@ -68,11 +68,10 @@ export function getRandomTense(type: "basic" | "modal" | "perfect", o?: T.Perfec return tns; } -function TensePicker({ onChange, vps, mode, locked }: { +function TensePicker({ onChange, vps, mode }: { vps: T.VPSelection, onChange: (p: T.VPSelection) => void, mode: "charts" | "phrases" | "quiz", - locked: boolean, }) { function onTenseSelect(o: { value: T.VerbTense | T.PerfectTense } | null) { const value = o?.value ? o.value : undefined; @@ -167,7 +166,7 @@ function TensePicker({ onChange, vps, mode, locked }: { label: "Modal", value: "modal", }]} - handleChange={onTenseCategorySelect} + handleChange={mode === "quiz" ? onTenseCategorySelect : () => null} /> } @@ -180,11 +179,11 @@ function TensePicker({ onChange, vps, mode, locked }: { options={tOptions} {...zIndexProps} /> - {vps.verb && !locked &&
+ {vps.verb && (mode !== "quiz") &&
- {mode !== "charts" && ({ removeKing: false, shrinkServant: false }, "abbreviationForm"); const [OSV, setOSV] = useStickyState(false, "includeOSV"); + if (!isVPSelectionComplete(VP)) { + return
+ {(() => { + const twoNPs = (VP.subject === undefined) && (VP.verb.object === undefined); + return `Choose NP${twoNPs ? "s " : ""} to make a phrase`; + })()} +
; + } const result = compileVP(renderVP(VP), { ...form, OSV }); return
{VP.verb.transitivity === "transitive" &&
diff --git a/src/components/vp-explorer/VPExplorer.tsx b/src/components/vp-explorer/VPExplorer.tsx index 08ae8f8..dae5053 100644 --- a/src/components/vp-explorer/VPExplorer.tsx +++ b/src/components/vp-explorer/VPExplorer.tsx @@ -1,9 +1,9 @@ import NPPicker from "../np-picker/NPPicker"; import VerbPicker from "./VerbPicker"; -import TensePicker, { getRandomTense } from "./TensePicker"; +import TensePicker from "./TensePicker"; import VPDisplay from "./VPDisplay"; import ButtonSelect from "../ButtonSelect"; -import { renderVP, compileVP } from "../../lib/phrase-building/index"; + import { isInvalidSubjObjCombo, } from "../../lib/phrase-building/vp-tools"; @@ -11,32 +11,15 @@ import * as T from "../../types"; import ChartDisplay from "./ChartDisplay"; import useStickyState from "../../lib/useStickyState"; import { makeVPSelectionState } from "./verb-selection"; -import { CSSProperties, useEffect, useState } from "react"; -import { randomSubjObj } from "../../library"; -import shuffleArray from "../../lib/shuffle-array"; -import InlinePs from "../InlinePs"; -import { psStringEquals } from "../../lib/p-text-helpers"; -import { randFromArray } from "../../lib/misc-helpers"; -import playAudio from "../../lib/play-audio"; -import { isVPSelectionComplete } from "../../lib/type-predicates"; +import { useEffect } from "react"; import { getKingAndServant } from "../../lib/phrase-building/render-vp"; import { isPastTense } from "../../lib/phrase-building/vp-tools"; -// import { useReward } from 'react-rewards'; +import VPExplorerQuiz from "./VPExplorerQuiz"; +import { switchSubjObj } from "../../lib/phrase-building/vp-tools" const kingEmoji = "👑"; const servantEmoji = "🙇‍♂️"; -const correctEmoji = ["✅", '🤓', "✅", '😊', "🌹", "✅", "✅", "🕺", "💃", '🥳', "👏", "✅", "💯", "😎", "✅", "👍"]; -const answerFeedback: CSSProperties = { - "fontSize": "4rem", - "transition": "opacity 0.3s ease-in", - "opacity": 0.9, - "position": "fixed", - "top": "60%", - "left": "50%", - "zIndex": 99999999, - "transform": "translate(-50%, -50%)", -} // TODO: make answerFeedback emojis appear at random translate angles a little bit // add energy drinks? @@ -54,18 +37,6 @@ const answerFeedback: CSSProperties = { // TODO: error handling on error with rendering etc -const checkDuration = 400; - -type QuizState = { - answer: { - ps: T.SingleOrLengthOpts; - e?: string[] | undefined; - }, - options: T.PsString[], - result: "waiting" | "fail", -} - -type MixType = "NPs" | "tenses" | "both"; export function VPExplorer(props: { verb: T.VerbEntry, opts: T.TextOptions, @@ -90,33 +61,16 @@ export function VPExplorer(props: { }, "verbExplorerMode", ); - const [quizState, setQuizState] = useState(undefined); - const [showCheck, setShowCheck] = useState(false); - const [currentCorrectEmoji, setCurrentCorrectEmoji] = useState(randFromArray(correctEmoji)); + useEffect(() => { - setVps(o => { + setVps(oldVps => { if (mode === "quiz") { setMode("phrases"); } - return makeVPSelectionState(props.verb, o); + return makeVPSelectionState(props.verb, oldVps); }); // eslint-disable-next-line }, [props.verb]); - function handleChangeMode(m: "charts" | "phrases" | "quiz") { - if (m === "quiz") { - handleResetQuiz(); - } - setMode(m); - } - function handleResetQuiz() { - if (!vps.verb) { - alert("Choose a verb to quiz"); - return; - } - const { VPS, qs } = makeQuizState(vps); - setVps(VPS); - setQuizState(qs); - } function handleSubjectChange(subject: T.NPSelection | undefined, skipPronounConflictCheck?: boolean) { if (!skipPronounConflictCheck && hasPronounConflict(subject, vps.verb?.object)) { alert("That combination of pronouns is not allowed"); @@ -153,31 +107,6 @@ export function VPExplorer(props: { } return f; } - function checkQuizAnswer(a: T.PsString) { - if (!quizState) return; - if (isInAnswer(a, quizState.answer)) { - const toPlay = randFromArray([true, false, false]); - if (toPlay) playAudio(`correct-${randFromArray([1,2,3])}`); - setShowCheck(true); - setTimeout(() => { - handleResetQuiz(); - }, checkDuration / 2); - setTimeout(() => { - setShowCheck(false); - }, checkDuration); - // this sucks, have to do this so the emoji doesn't change in the middle of animation - setTimeout(() => { - setCurrentCorrectEmoji(randFromArray(correctEmoji)); - }, checkDuration * 2); - } else { - playAudio(`wrong-${randFromArray([1,2])}`); - navigator.vibrate(250); - setQuizState({ - ...quizState, - result: "fail", - }); - } - } return
{(vps.verb && (typeof vps.verb.object === "object") && (vps.verb.isCompound !== "dynamic") && (mode === "phrases")) && @@ -207,8 +136,8 @@ export function VPExplorer(props: { subj/obj
} -
- {mode !== "charts" && <> + {mode !== "quiz" &&
+ {mode === "phrases" && <>
Subject {showRole(vps, "subject")}
{vps.verb && (vps.verb.object !== "none") &&
@@ -245,9 +173,8 @@ export function VPExplorer(props: { asObject np={vps.verb.object} counterPart={vps.subject} - onChange={quizLock(handleObjectChange)} + onChange={handleObjectChange} opts={props.opts} - cantClear={mode === "quiz"} />}
} } @@ -256,40 +183,12 @@ export function VPExplorer(props: { vps={vps} onChange={quizLock(setVps)} mode={mode} - locked={!!(mode === "quiz" && quizState)} />
-
- {(isVPSelectionComplete(vps) && (mode === "phrases")) && - - } - {(vps.verb && (mode === "charts")) && } - {(mode === "quiz" && quizState) &&
-
- {currentCorrectEmoji} -
- {quizState.result === "waiting" ? <> -
Choose a correct answer:
- {quizState.options.map(o =>
-
{ - checkQuizAnswer(o); - }}> - {o} -
-
)} - :
-
❌ Wrong 😭
-
The correct answer was:
- - {quizState.options.find(x => isInAnswer(x, quizState.answer)) as T.PsString} - -
- -
-
}
} + {mode === "phrases" && } + {mode === "charts" && } + {mode === "quiz" && }
} @@ -313,138 +212,3 @@ function showRole(VP: T.VPSelection, member: "subject" | "object") { : ""; } - -function switchSubjObj({ subject, verb }: T.VPSelection): T.VPSelection { - if (!subject|| !verb || !verb.object || !(typeof verb.object === "object")) { - return { subject, verb }; - } - return { - subject: verb.object, - verb: { - ...verb, - object: subject, - } - }; -} - -function makeQuizState(oldVps: T.VPSelection): { VPS: T.VPSelectionComplete, qs: QuizState } { - function makeRes(x: T.VPSelectionComplete) { - return compileVP(renderVP(x), { removeKing: false, shrinkServant: false }); - } - const vps = getRandomVPSelection("both")(oldVps); - const wrongStates: T.VPSelectionComplete[] = []; - // don't do the SO switches every time - const wholeTimeSOSwitch = randFromArray([true, false]); - [1, 2, 3].forEach(() => { - let v: T.VPSelectionComplete; - do { - const SOSwitch = wholeTimeSOSwitch && randFromArray([true, false]); - // TODO: if switich subj and obj, include the tense being correct maybe - v = getRandomVPSelection("tenses")( - SOSwitch ? switchSubjObj(vps) : vps, - ); - // eslint-disable-next-line - } while (wrongStates.find(x => x.verb.tense === v.verb.tense)); - wrongStates.push(v); - }); - const answer = makeRes(vps); - const wrongAnswers = wrongStates.map(makeRes); - const allAnswers = shuffleArray([...wrongAnswers, answer]); - const options = allAnswers.map(getOptionFromResult); - return { - VPS: vps, - qs: { - answer, - options, - result: "waiting", - }, - }; -} - -function isInAnswer(a: T.PsString, answer: { - ps: T.SingleOrLengthOpts; - e?: string[] | undefined; -}): boolean { - if ("long" in answer.ps) { - return isInAnswer(a, { ...answer, ps: answer.ps.long }) || - isInAnswer(a, { ...answer, ps: answer.ps.short }) || - !!(answer.ps.mini && isInAnswer(a, { ...answer, ps: answer.ps.mini })); - } - return answer.ps.some((x) => psStringEquals(x, a)); -} - -function getOptionFromResult(r: { - ps: T.SingleOrLengthOpts; - e?: string[] | undefined; -}): T.PsString { - const ps = "long" in r.ps - ? r.ps[randFromArray(["short", "long"] as ("short" | "long")[])] - : r.ps; - // not randomizing version pick (for now) - return ps[0]; -} - -function getRandomVPSelection(mix: MixType = "both") { - // TODO: Type safety to make sure it's safe? - return ({ subject, verb }: T.VPSelection): T.VPSelectionComplete => { - const oldSubj = (subject?.type === "pronoun") - ? subject.person - : undefined; - const oldObj = (typeof verb?.object === "object" && verb.object.type === "pronoun") - ? verb.object.person - : undefined; - const { subj, obj } = randomSubjObj( - oldSubj !== undefined ? { subj: oldSubj, obj: oldObj } : undefined - ); - const randSubj: T.PronounSelection = subject?.type === "pronoun" ? { - ...subject, - person: subj, - } : { - type: "pronoun", - distance: "far", - person: subj, - }; - const randObj: T.PronounSelection = typeof verb?.object === "object" && verb.object.type === "pronoun" ? { - ...verb.object, - person: obj, - } : { - type: "pronoun", - distance: "far", - person: obj, - }; - const s = randSubj; - // ensure that the verb selection is complete - const v: T.VerbSelectionComplete = { - ...verb, - object: ( - (typeof verb.object === "object" && !(verb.object.type === "noun" && verb.object.dynamicComplement)) - || - verb.object === undefined - ) - ? randObj - : verb.object, - }; - if (mix === "tenses") { - return { - subject: subject !== undefined ? subject : randSubj, - verb: randomizeTense(v, true), - } - } - return { - subject: s, - verb: randomizeTense(v, true), - }; - }; -}; - -function randomizeTense(verb: T.VerbSelectionComplete, dontRepeatTense: boolean): T.VerbSelectionComplete { - return { - ...verb, - tense: getRandomTense( - // TODO: WHY ISN'T THE OVERLOADING ON THIS - // @ts-ignore - verb.tenseCategory, - dontRepeatTense ? verb.tense : undefined, - ), - }; -} \ No newline at end of file diff --git a/src/components/vp-explorer/VPExplorerQuiz.tsx b/src/components/vp-explorer/VPExplorerQuiz.tsx new file mode 100644 index 0000000..f4429ed --- /dev/null +++ b/src/components/vp-explorer/VPExplorerQuiz.tsx @@ -0,0 +1,264 @@ +import { CSSProperties, useState } from "react"; +import * as T from "../../types"; +import { randFromArray } from "../../lib/misc-helpers"; +import { randomSubjObj } from "../../library"; +import shuffleArray from "../../lib/shuffle-array"; +import InlinePs from "../InlinePs"; +import { psStringEquals } from "../../lib/p-text-helpers"; +import { renderVP, compileVP } from "../../lib/phrase-building/index"; +import { getRandomTense } from "./TensePicker"; +import { switchSubjObj } from "../../lib/phrase-building/vp-tools"; +import playAudio from "../../lib/play-audio"; +import TensePicker from "./TensePicker"; + +const correctEmoji = ["✅", '🤓', "✅", '😊', "🌹", "✅", "✅", "🕺", "💃", '🥳', "👏", "✅", "💯", "😎", "✅", "👍"]; + +const answerFeedback: CSSProperties = { + "fontSize": "4rem", + "transition": "opacity 0.3s ease-in", + "opacity": 0.9, + "position": "fixed", + "top": "60%", + "left": "50%", + "zIndex": 99999999, + "transform": "translate(-50%, -50%)", +} + +const checkDuration = 400; + +type QuizState = { + answer: { + ps: T.SingleOrLengthOpts; + e?: string[] | undefined; + }, + options: T.PsString[], + result: "waiting" | "fail", +} +type MixType = "NPs" | "tenses" | "both"; + +function VPExplorerQuiz(props: { + opts: T.TextOptions, + vps: T.VPSelection, +}) { + const startingQs = makeQuizState(props.vps); + const [vps, setVps] = useState(startingQs.VPS) + const [quizState, setQuizState] = useState(startingQs.qs); + const [showCheck, setShowCheck] = useState(false); + const [currentCorrectEmoji, setCurrentCorrectEmoji] = useState(randFromArray(correctEmoji)); + function handleResetQuiz() { + const { VPS, qs } = makeQuizState(vps); + setVps(VPS); + setQuizState(qs); + } + function checkQuizAnswer(a: T.PsString) { + if (!quizState) return; + if (isInAnswer(a, quizState.answer)) { + const toPlay = randFromArray([true, false, false]); + if (toPlay) playAudio(`correct-${randFromArray([1,2,3])}`); + setShowCheck(true); + setTimeout(() => { + handleResetQuiz(); + }, checkDuration / 2); + setTimeout(() => { + setShowCheck(false); + }, checkDuration); + // this sucks, have to do this so the emoji doesn't change in the middle of animation + setTimeout(() => { + setCurrentCorrectEmoji(randFromArray(correctEmoji)); + }, checkDuration * 2); + } else { + playAudio(`wrong-${randFromArray([1,2])}`); + navigator.vibrate(250); + setQuizState({ + ...quizState, + result: "fail", + }); + } + } + const rendered = renderVP(vps); + const { subject, object } = rendered; + const { e } = compileVP(rendered, { removeKing: false, shrinkServant: false }); + return
+
+
+
Subject
+ {subject} +
+ {(object !== "none") &&
+
Object
+ {object} +
} +
+ null} + mode={"quiz"} + /> +
+
+ {e &&
+ {e.map(eLine =>
{eLine}
)} +
} +
+
+ {currentCorrectEmoji} +
+ {quizState.result === "waiting" ? <> +
Choose a correct answer:
+ {quizState.options.map(o =>
+
{ + checkQuizAnswer(o); + }}> + {o} +
+
)} + :
+
❌ Wrong 😭
+
The correct answer was:
+ + {quizState.options.find(x => isInAnswer(x, quizState.answer)) as T.PsString} + +
+ +
+
} +
+
; +} + +function QuizNPDisplay({ children }: { children: T.Rendered | T.Person.ThirdPlurMale }) { + return
+ {(typeof children === "number") + ?
Unspoken 3rd Pers. Masc. Plur.
+ :
{children.e}
} +
; +} + + +function makeQuizState(oldVps: T.VPSelection): { VPS: T.VPSelectionComplete, qs: QuizState } { + function makeRes(x: T.VPSelectionComplete) { + return compileVP(renderVP(x), { removeKing: false, shrinkServant: false }); + } + // for now, always inforce positive + const vps = getRandomVPSelection("both")({ ...oldVps, verb: { ...oldVps.verb, negative: false }}); + const wrongStates: T.VPSelectionComplete[] = []; + // don't do the SO switches every time + const wholeTimeSOSwitch = randFromArray([true, false]); + [1, 2, 3].forEach(() => { + let v: T.VPSelectionComplete; + do { + const SOSwitch = wholeTimeSOSwitch && randFromArray([true, false]); + // TODO: if switich subj and obj, include the tense being correct maybe + v = getRandomVPSelection("tenses")( + SOSwitch ? switchSubjObj(vps) : vps, + ); + // eslint-disable-next-line + } while (wrongStates.find(x => x.verb.tense === v.verb.tense)); + wrongStates.push(v); + }); + const answer = makeRes(vps); + const wrongAnswers = wrongStates.map(makeRes); + const allAnswers = shuffleArray([...wrongAnswers, answer]); + const options = allAnswers.map(getOptionFromResult); + return { + VPS: vps, + qs: { + answer, + options, + result: "waiting", + }, + }; +} + +function isInAnswer(a: T.PsString, answer: { + ps: T.SingleOrLengthOpts; + e?: string[] | undefined; +}): boolean { + if ("long" in answer.ps) { + return isInAnswer(a, { ...answer, ps: answer.ps.long }) || + isInAnswer(a, { ...answer, ps: answer.ps.short }) || + !!(answer.ps.mini && isInAnswer(a, { ...answer, ps: answer.ps.mini })); + } + return answer.ps.some((x) => psStringEquals(x, a)); +} + + +function getOptionFromResult(r: { + ps: T.SingleOrLengthOpts; + e?: string[] | undefined; +}): T.PsString { + const ps = "long" in r.ps + ? r.ps[randFromArray(["short", "long"] as ("short" | "long")[])] + : r.ps; + // not randomizing version pick (for now) + return ps[0]; +} + +function getRandomVPSelection(mix: MixType = "both") { + // TODO: Type safety to make sure it's safe? + return ({ subject, verb }: T.VPSelection): T.VPSelectionComplete => { + const oldSubj = (subject?.type === "pronoun") + ? subject.person + : undefined; + const oldObj = (typeof verb?.object === "object" && verb.object.type === "pronoun") + ? verb.object.person + : undefined; + const { subj, obj } = randomSubjObj( + oldSubj !== undefined ? { subj: oldSubj, obj: oldObj } : undefined + ); + const randSubj: T.PronounSelection = subject?.type === "pronoun" ? { + ...subject, + person: subj, + } : { + type: "pronoun", + distance: "far", + person: subj, + }; + const randObj: T.PronounSelection = typeof verb?.object === "object" && verb.object.type === "pronoun" ? { + ...verb.object, + person: obj, + } : { + type: "pronoun", + distance: "far", + person: obj, + }; + const s = randSubj; + // ensure that the verb selection is complete + const v: T.VerbSelectionComplete = { + ...verb, + object: ( + (typeof verb.object === "object" && !(verb.object.type === "noun" && verb.object.dynamicComplement)) + || + verb.object === undefined + ) + ? randObj + : verb.object, + }; + if (mix === "tenses") { + return { + subject: subject !== undefined ? subject : randSubj, + verb: randomizeTense(v, true), + } + } + return { + subject: s, + verb: randomizeTense(v, true), + }; + }; +}; + +function randomizeTense(verb: T.VerbSelectionComplete, dontRepeatTense: boolean): T.VerbSelectionComplete { + return { + ...verb, + tense: getRandomTense( + // TODO: WHY ISN'T THE OVERLOADING ON THIS + // @ts-ignore + verb.tenseCategory, + dontRepeatTense ? verb.tense : undefined, + ), + }; +} + +export default VPExplorerQuiz; \ No newline at end of file diff --git a/src/lib/phrase-building/vp-tools.ts b/src/lib/phrase-building/vp-tools.ts index c4305f3..245b937 100644 --- a/src/lib/phrase-building/vp-tools.ts +++ b/src/lib/phrase-building/vp-tools.ts @@ -153,4 +153,17 @@ export function removeDuplicates(psv: T.PsString[]): T.PsString[] { psStringEquals(t, ps) )) )); +} + +export function switchSubjObj({ subject, verb }: T.VPSelection): T.VPSelection { + if (!subject|| !verb || !verb.object || !(typeof verb.object === "object")) { + return { subject, verb }; + } + return { + subject: verb.object, + verb: { + ...verb, + object: subject, + } + }; } \ No newline at end of file