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
+
+
+ {(object !== "none") &&
}
+
+ 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