refactor and improve quiz
This commit is contained in:
parent
68633089f9
commit
39996bc92c
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@lingdocs/pashto-inflector",
|
"name": "@lingdocs/pashto-inflector",
|
||||||
"version": "1.9.8",
|
"version": "1.9.9",
|
||||||
"author": "lingdocs.com",
|
"author": "lingdocs.com",
|
||||||
"description": "A Pashto inflection and verb conjugation engine, inculding React components for displaying Pashto text, inflections, and conjugations",
|
"description": "A Pashto inflection and verb conjugation engine, inculding React components for displaying Pashto text, inflections, and conjugations",
|
||||||
"homepage": "https://verbs.lingdocs.com",
|
"homepage": "https://verbs.lingdocs.com",
|
||||||
|
|
|
@ -68,11 +68,10 @@ export function getRandomTense(type: "basic" | "modal" | "perfect", o?: T.Perfec
|
||||||
return tns;
|
return tns;
|
||||||
}
|
}
|
||||||
|
|
||||||
function TensePicker({ onChange, vps, mode, locked }: {
|
function TensePicker({ onChange, vps, mode }: {
|
||||||
vps: T.VPSelection,
|
vps: T.VPSelection,
|
||||||
onChange: (p: T.VPSelection) => void,
|
onChange: (p: T.VPSelection) => void,
|
||||||
mode: "charts" | "phrases" | "quiz",
|
mode: "charts" | "phrases" | "quiz",
|
||||||
locked: boolean,
|
|
||||||
}) {
|
}) {
|
||||||
function onTenseSelect(o: { value: T.VerbTense | T.PerfectTense } | null) {
|
function onTenseSelect(o: { value: T.VerbTense | T.PerfectTense } | null) {
|
||||||
const value = o?.value ? o.value : undefined;
|
const value = o?.value ? o.value : undefined;
|
||||||
|
@ -167,7 +166,7 @@ function TensePicker({ onChange, vps, mode, locked }: {
|
||||||
label: "Modal",
|
label: "Modal",
|
||||||
value: "modal",
|
value: "modal",
|
||||||
}]}
|
}]}
|
||||||
handleChange={onTenseCategorySelect}
|
handleChange={mode === "quiz" ? onTenseCategorySelect : () => null}
|
||||||
/>
|
/>
|
||||||
</div>}
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
|
@ -180,11 +179,11 @@ function TensePicker({ onChange, vps, mode, locked }: {
|
||||||
options={tOptions}
|
options={tOptions}
|
||||||
{...zIndexProps}
|
{...zIndexProps}
|
||||||
/>
|
/>
|
||||||
{vps.verb && !locked && <div className="d-flex flex-row justify-content-between align-items-center mt-3 mb-1" style={{ width: "100%" }}>
|
{vps.verb && (mode !== "quiz") && <div className="d-flex flex-row justify-content-between align-items-center mt-3 mb-1" style={{ width: "100%" }}>
|
||||||
<div className="btn btn-light clickable" onClick={moveTense("back")}>
|
<div className="btn btn-light clickable" onClick={moveTense("back")}>
|
||||||
<i className="fas fa-chevron-left" />
|
<i className="fas fa-chevron-left" />
|
||||||
</div>
|
</div>
|
||||||
{mode !== "charts" && <ButtonSelect
|
{mode === "phrases" && <ButtonSelect
|
||||||
small
|
small
|
||||||
value={vps.verb.negative.toString()}
|
value={vps.verb.negative.toString()}
|
||||||
options={[{
|
options={[{
|
||||||
|
|
|
@ -4,10 +4,20 @@ import InlinePs from "../InlinePs";
|
||||||
import AbbreviationFormSelector from "./AbbreviationFormSelector";
|
import AbbreviationFormSelector from "./AbbreviationFormSelector";
|
||||||
import { isPastTense } from "../../lib/phrase-building/vp-tools";
|
import { isPastTense } from "../../lib/phrase-building/vp-tools";
|
||||||
import { useStickyState } from "../../library";
|
import { useStickyState } from "../../library";
|
||||||
|
import { isVPSelectionComplete } from "../../lib/type-predicates";
|
||||||
|
|
||||||
function VPDisplay({ VP, opts }: { VP: T.VPSelectionComplete, opts: T.TextOptions }) {
|
|
||||||
|
function VPDisplay({ VP, opts }: { VP: T.VPSelection | T.VPSelectionComplete, opts: T.TextOptions }) {
|
||||||
const [form, setForm] = useStickyState<T.FormVersion>({ removeKing: false, shrinkServant: false }, "abbreviationForm");
|
const [form, setForm] = useStickyState<T.FormVersion>({ removeKing: false, shrinkServant: false }, "abbreviationForm");
|
||||||
const [OSV, setOSV] = useStickyState<boolean>(false, "includeOSV");
|
const [OSV, setOSV] = useStickyState<boolean>(false, "includeOSV");
|
||||||
|
if (!isVPSelectionComplete(VP)) {
|
||||||
|
return <div className="lead text-muted text-center mt-4">
|
||||||
|
{(() => {
|
||||||
|
const twoNPs = (VP.subject === undefined) && (VP.verb.object === undefined);
|
||||||
|
return `Choose NP${twoNPs ? "s " : ""} to make a phrase`;
|
||||||
|
})()}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
const result = compileVP(renderVP(VP), { ...form, OSV });
|
const result = compileVP(renderVP(VP), { ...form, OSV });
|
||||||
return <div className="text-center mt-2">
|
return <div className="text-center mt-2">
|
||||||
{VP.verb.transitivity === "transitive" && <div className="form-check mb-2">
|
{VP.verb.transitivity === "transitive" && <div className="form-check mb-2">
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import NPPicker from "../np-picker/NPPicker";
|
import NPPicker from "../np-picker/NPPicker";
|
||||||
import VerbPicker from "./VerbPicker";
|
import VerbPicker from "./VerbPicker";
|
||||||
import TensePicker, { getRandomTense } from "./TensePicker";
|
import TensePicker from "./TensePicker";
|
||||||
import VPDisplay from "./VPDisplay";
|
import VPDisplay from "./VPDisplay";
|
||||||
import ButtonSelect from "../ButtonSelect";
|
import ButtonSelect from "../ButtonSelect";
|
||||||
import { renderVP, compileVP } from "../../lib/phrase-building/index";
|
|
||||||
import {
|
import {
|
||||||
isInvalidSubjObjCombo,
|
isInvalidSubjObjCombo,
|
||||||
} from "../../lib/phrase-building/vp-tools";
|
} from "../../lib/phrase-building/vp-tools";
|
||||||
|
@ -11,32 +11,15 @@ import * as T from "../../types";
|
||||||
import ChartDisplay from "./ChartDisplay";
|
import ChartDisplay from "./ChartDisplay";
|
||||||
import useStickyState from "../../lib/useStickyState";
|
import useStickyState from "../../lib/useStickyState";
|
||||||
import { makeVPSelectionState } from "./verb-selection";
|
import { makeVPSelectionState } from "./verb-selection";
|
||||||
import { CSSProperties, useEffect, useState } from "react";
|
import { useEffect } 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 { getKingAndServant } from "../../lib/phrase-building/render-vp";
|
import { getKingAndServant } from "../../lib/phrase-building/render-vp";
|
||||||
import { isPastTense } from "../../lib/phrase-building/vp-tools";
|
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 kingEmoji = "👑";
|
||||||
const servantEmoji = "🙇♂️";
|
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
|
// TODO: make answerFeedback emojis appear at random translate angles a little bit
|
||||||
// add energy drinks?
|
// add energy drinks?
|
||||||
|
@ -54,18 +37,6 @@ const answerFeedback: CSSProperties = {
|
||||||
|
|
||||||
// TODO: error handling on error with rendering etc
|
// TODO: error handling on error with rendering etc
|
||||||
|
|
||||||
const checkDuration = 400;
|
|
||||||
|
|
||||||
type QuizState = {
|
|
||||||
answer: {
|
|
||||||
ps: T.SingleOrLengthOpts<T.PsString[]>;
|
|
||||||
e?: string[] | undefined;
|
|
||||||
},
|
|
||||||
options: T.PsString[],
|
|
||||||
result: "waiting" | "fail",
|
|
||||||
}
|
|
||||||
|
|
||||||
type MixType = "NPs" | "tenses" | "both";
|
|
||||||
export function VPExplorer(props: {
|
export function VPExplorer(props: {
|
||||||
verb: T.VerbEntry,
|
verb: T.VerbEntry,
|
||||||
opts: T.TextOptions,
|
opts: T.TextOptions,
|
||||||
|
@ -90,33 +61,16 @@ export function VPExplorer(props: {
|
||||||
},
|
},
|
||||||
"verbExplorerMode",
|
"verbExplorerMode",
|
||||||
);
|
);
|
||||||
const [quizState, setQuizState] = useState<QuizState | undefined>(undefined);
|
|
||||||
const [showCheck, setShowCheck] = useState<boolean>(false);
|
|
||||||
const [currentCorrectEmoji, setCurrentCorrectEmoji] = useState<string>(randFromArray(correctEmoji));
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setVps(o => {
|
setVps(oldVps => {
|
||||||
if (mode === "quiz") {
|
if (mode === "quiz") {
|
||||||
setMode("phrases");
|
setMode("phrases");
|
||||||
}
|
}
|
||||||
return makeVPSelectionState(props.verb, o);
|
return makeVPSelectionState(props.verb, oldVps);
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
}, [props.verb]);
|
}, [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) {
|
function handleSubjectChange(subject: T.NPSelection | undefined, skipPronounConflictCheck?: boolean) {
|
||||||
if (!skipPronounConflictCheck && hasPronounConflict(subject, vps.verb?.object)) {
|
if (!skipPronounConflictCheck && hasPronounConflict(subject, vps.verb?.object)) {
|
||||||
alert("That combination of pronouns is not allowed");
|
alert("That combination of pronouns is not allowed");
|
||||||
|
@ -153,31 +107,6 @@ export function VPExplorer(props: {
|
||||||
}
|
}
|
||||||
return f;
|
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 <div className="mt-3" style={{ maxWidth: "950px"}}>
|
return <div className="mt-3" style={{ maxWidth: "950px"}}>
|
||||||
<VerbPicker
|
<VerbPicker
|
||||||
{..."getNounByTs" in props ? {
|
{..."getNounByTs" in props ? {
|
||||||
|
@ -198,7 +127,7 @@ export function VPExplorer(props: {
|
||||||
{ label: "Phrases", value: "phrases" },
|
{ label: "Phrases", value: "phrases" },
|
||||||
{ label: "Quiz", value: "quiz" },
|
{ label: "Quiz", value: "quiz" },
|
||||||
]}
|
]}
|
||||||
handleChange={handleChangeMode}
|
handleChange={setMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{(vps.verb && (typeof vps.verb.object === "object") && (vps.verb.isCompound !== "dynamic") && (mode === "phrases")) &&
|
{(vps.verb && (typeof vps.verb.object === "object") && (vps.verb.isCompound !== "dynamic") && (mode === "phrases")) &&
|
||||||
|
@ -207,8 +136,8 @@ export function VPExplorer(props: {
|
||||||
<i className="fas fa-exchange-alt mr-2" /> subj/obj
|
<i className="fas fa-exchange-alt mr-2" /> subj/obj
|
||||||
</button>
|
</button>
|
||||||
</div>}
|
</div>}
|
||||||
<div className="d-flex flex-row justify-content-around flex-wrap" style={{ marginLeft: "-0.5rem", marginRight: "-0.5rem" }}>
|
{mode !== "quiz" && <div className="d-flex flex-row justify-content-around flex-wrap" style={{ marginLeft: "-0.5rem", marginRight: "-0.5rem" }}>
|
||||||
{mode !== "charts" && <>
|
{mode === "phrases" && <>
|
||||||
<div className="my-2">
|
<div className="my-2">
|
||||||
<div className="h5 text-center">Subject {showRole(vps, "subject")}</div>
|
<div className="h5 text-center">Subject {showRole(vps, "subject")}</div>
|
||||||
<NPPicker
|
<NPPicker
|
||||||
|
@ -223,9 +152,8 @@ export function VPExplorer(props: {
|
||||||
}}
|
}}
|
||||||
np={vps.subject}
|
np={vps.subject}
|
||||||
counterPart={vps.verb ? vps.verb.object : undefined}
|
counterPart={vps.verb ? vps.verb.object : undefined}
|
||||||
onChange={quizLock(handleSubjectChange)}
|
onChange={handleSubjectChange}
|
||||||
opts={props.opts}
|
opts={props.opts}
|
||||||
cantClear={mode === "quiz"}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{vps.verb && (vps.verb.object !== "none") && <div className="my-2">
|
{vps.verb && (vps.verb.object !== "none") && <div className="my-2">
|
||||||
|
@ -245,9 +173,8 @@ export function VPExplorer(props: {
|
||||||
asObject
|
asObject
|
||||||
np={vps.verb.object}
|
np={vps.verb.object}
|
||||||
counterPart={vps.subject}
|
counterPart={vps.subject}
|
||||||
onChange={quizLock(handleObjectChange)}
|
onChange={handleObjectChange}
|
||||||
opts={props.opts}
|
opts={props.opts}
|
||||||
cantClear={mode === "quiz"}
|
|
||||||
/>}
|
/>}
|
||||||
</div>}
|
</div>}
|
||||||
</>}
|
</>}
|
||||||
|
@ -256,40 +183,12 @@ export function VPExplorer(props: {
|
||||||
vps={vps}
|
vps={vps}
|
||||||
onChange={quizLock(setVps)}
|
onChange={quizLock(setVps)}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
locked={!!(mode === "quiz" && quizState)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{(isVPSelectionComplete(vps) && (mode === "phrases")) &&
|
|
||||||
<VPDisplay VP={vps} opts={props.opts} />
|
|
||||||
}
|
|
||||||
{(vps.verb && (mode === "charts")) && <ChartDisplay VS={vps.verb} opts={props.opts} />}
|
|
||||||
{(mode === "quiz" && quizState) && <div className="text-center">
|
|
||||||
<div style={showCheck ? answerFeedback : { ...answerFeedback, opacity: 0 }}>
|
|
||||||
{currentCorrectEmoji}
|
|
||||||
</div>
|
|
||||||
{quizState.result === "waiting" ? <>
|
|
||||||
<div className="text-muted my-3">Choose a correct answer:</div>
|
|
||||||
{quizState.options.map(o => <div className="pb-3" key={o.f}>
|
|
||||||
<div className="btn btn-answer btn-outline-secondary" onClick={() => {
|
|
||||||
checkQuizAnswer(o);
|
|
||||||
}}>
|
|
||||||
<InlinePs opts={props.opts}>{o}</InlinePs>
|
|
||||||
</div>
|
|
||||||
</div>)}
|
|
||||||
</> : <div>
|
|
||||||
<div className="h5 mt-4">❌ Wrong 😭</div>
|
|
||||||
<div className="my-4">The correct answer was:</div>
|
|
||||||
<InlinePs opts={props.opts}>
|
|
||||||
{quizState.options.find(x => isInAnswer(x, quizState.answer)) as T.PsString}
|
|
||||||
</InlinePs>
|
|
||||||
<div className="my-4">
|
|
||||||
<button type="button" className="btn btn-primary" onClick={handleResetQuiz}>
|
|
||||||
Try Again
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>}
|
|
||||||
</div>}
|
</div>}
|
||||||
|
{mode === "phrases" && <VPDisplay VP={vps} opts={props.opts} />}
|
||||||
|
{mode === "charts" && <ChartDisplay VS={vps.verb} opts={props.opts} />}
|
||||||
|
{mode === "quiz" && <VPExplorerQuiz opts={props.opts} vps={vps} />}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -313,138 +212,3 @@ function showRole(VP: T.VPSelection, member: "subject" | "object") {
|
||||||
</span>
|
</span>
|
||||||
: "";
|
: "";
|
||||||
}
|
}
|
||||||
|
|
||||||
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<T.PsString[]>;
|
|
||||||
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<T.PsString[]>;
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -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<T.PsString[]>;
|
||||||
|
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<T.VPSelectionComplete>(startingQs.VPS)
|
||||||
|
const [quizState, setQuizState] = useState<QuizState>(startingQs.qs);
|
||||||
|
const [showCheck, setShowCheck] = useState<boolean>(false);
|
||||||
|
const [currentCorrectEmoji, setCurrentCorrectEmoji] = useState<string>(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 <div>
|
||||||
|
<div className="d-flex flex-row justify-content-around flex-wrap" style={{ marginLeft: "-0.5rem", marginRight: "-0.5rem" }}>
|
||||||
|
<div className="my-2">
|
||||||
|
<div className="h5 text-center">Subject</div>
|
||||||
|
<QuizNPDisplay>{subject}</QuizNPDisplay>
|
||||||
|
</div>
|
||||||
|
{(object !== "none") && <div className="my-2">
|
||||||
|
<div className="h5 text-center">Object</div>
|
||||||
|
<QuizNPDisplay>{object}</QuizNPDisplay>
|
||||||
|
</div>}
|
||||||
|
<div className="my-2">
|
||||||
|
<TensePicker
|
||||||
|
vps={vps}
|
||||||
|
onChange={() => null}
|
||||||
|
mode={"quiz"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{e && <div className="text-center text-muted text-small">
|
||||||
|
{e.map(eLine => <div key={eLine}>{eLine}</div>)}
|
||||||
|
</div>}
|
||||||
|
<div className="text-center">
|
||||||
|
<div style={showCheck ? answerFeedback : { ...answerFeedback, opacity: 0 }}>
|
||||||
|
{currentCorrectEmoji}
|
||||||
|
</div>
|
||||||
|
{quizState.result === "waiting" ? <>
|
||||||
|
<div className="text-muted my-3">Choose a correct answer:</div>
|
||||||
|
{quizState.options.map(o => <div className="pb-3" key={o.f}>
|
||||||
|
<div className="btn btn-answer btn-outline-secondary" onClick={() => {
|
||||||
|
checkQuizAnswer(o);
|
||||||
|
}}>
|
||||||
|
<InlinePs opts={props.opts}>{o}</InlinePs>
|
||||||
|
</div>
|
||||||
|
</div>)}
|
||||||
|
</> : <div>
|
||||||
|
<div className="h5 mt-4">❌ Wrong 😭</div>
|
||||||
|
<div className="my-4">The correct answer was:</div>
|
||||||
|
<InlinePs opts={props.opts}>
|
||||||
|
{quizState.options.find(x => isInAnswer(x, quizState.answer)) as T.PsString}
|
||||||
|
</InlinePs>
|
||||||
|
<div className="my-4">
|
||||||
|
<button type="button" className="btn btn-primary" onClick={handleResetQuiz}>
|
||||||
|
Try Again
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>}
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function QuizNPDisplay({ children }: { children: T.Rendered<T.NPSelection> | T.Person.ThirdPlurMale }) {
|
||||||
|
return <div className="mb-3">
|
||||||
|
{(typeof children === "number")
|
||||||
|
? <div className="text-muted">Unspoken 3rd Pers. Masc. Plur.</div>
|
||||||
|
: <div style={{ fontSize: "larger" }}>{children.e}</div>}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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<T.PsString[]>;
|
||||||
|
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<T.PsString[]>;
|
||||||
|
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;
|
|
@ -153,4 +153,17 @@ export function removeDuplicates(psv: T.PsString[]): T.PsString[] {
|
||||||
psStringEquals(t, ps)
|
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,
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
Loading…
Reference in New Issue