very beta quiz happening!

This commit is contained in:
lingdocs 2022-04-12 13:39:03 +05:00
parent 1c4694d5fc
commit 0fb73af58b
6 changed files with 161 additions and 37 deletions

View File

@ -26,6 +26,7 @@
"classnames": "^2.2.6", "classnames": "^2.2.6",
"pbf": "^3.2.1", "pbf": "^3.2.1",
"rambda": "^6.7.0", "rambda": "^6.7.0",
"react-rewards": "^2.0.3",
"react-select": "^5.2.2" "react-select": "^5.2.2"
}, },
"devDependencies": { "devDependencies": {

View File

@ -58,9 +58,9 @@ function NPPicker(props: {
: <div></div>; : <div></div>;
return <div> return <div>
{!npType && <div className="text-center mt-3"> {!npType && <div className="text-center mt-3">
{/* <div className="h6 mr-3"> <div className="h6 mr-3">
Choose NP Choose NP
</div> */} </div>
{npTypes.map((npt) => <div className="mb-2"> {npTypes.map((npt) => <div className="mb-2">
<button <button
key={npt} key={npt}

View File

@ -3,7 +3,7 @@ import VerbPicker from "./VerbPicker";
import TensePicker, { getRandomTense } from "./TensePicker"; import TensePicker, { getRandomTense } from "./TensePicker";
import VPDisplay from "./VPDisplay"; import VPDisplay from "./VPDisplay";
import ButtonSelect from "../ButtonSelect"; import ButtonSelect from "../ButtonSelect";
import { renderVP } from "../../lib/phrase-building/index"; import { renderVP, compileVP } from "../../lib/phrase-building/index";
import { import {
isInvalidSubjObjCombo, isInvalidSubjObjCombo,
} from "../../lib/phrase-building/vp-tools"; } from "../../lib/phrase-building/vp-tools";
@ -13,6 +13,10 @@ import useStickyState from "../../lib/useStickyState";
import { makeVPSelectionState } from "./verb-selection"; import { makeVPSelectionState } from "./verb-selection";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { randomSubjObj } from "../../library"; import { randomSubjObj } from "../../library";
import shuffleArray from "../../lib/shuffle-array";
import InlinePs from "../InlinePs";
import { psStringEquals } from "../../lib/p-text-helpers";
// import { useReward } from 'react-rewards';
const kingEmoji = "👑"; const kingEmoji = "👑";
const servantEmoji = "🙇‍♂️"; const servantEmoji = "🙇‍♂️";
@ -29,8 +33,17 @@ const servantEmoji = "🙇‍♂️";
// TODO: option to show 3 modes Phrases - Charts - Quiz // TODO: option to show 3 modes Phrases - Charts - Quiz
// TODO: error handling on error with rendering etc // TODO: error handling on error with rendering etc
type MixState = "NPs" | "tenses" | "both";
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,
@ -48,12 +61,17 @@ export function VPExplorer(props: {
"vpsState1", "vpsState1",
); );
const [mode, setMode] = useStickyState<"charts" | "phrases" | "quiz">("phrases", "verbExplorerMode"); const [mode, setMode] = useStickyState<"charts" | "phrases" | "quiz">("phrases", "verbExplorerMode");
const [mix, setMix] = useStickyState<MixState>("NPs", "mixState1"); const [quizState, setQuizState] = useState<QuizState | undefined>(undefined);
const [showAnswer, setShowAnswer] = useState<boolean>(false); // const { reward } = useReward('rewardId', "emoji", {
// emoji: ['🤓', '😊', '🥳', "👏", "💯", "😎", "👍"],
// lifetime: 50,
// elementCount: 10,
// elementSize: 30,
// });
useEffect(() => { useEffect(() => {
setVps(o => { setVps(o => {
if (mode === "quiz") { if (mode === "quiz") {
return setRandomQuizState(mix)( return getRandomVPSelection("both")(
makeVPSelectionState(props.verb, o) makeVPSelectionState(props.verb, o)
); );
} }
@ -72,8 +90,9 @@ export function VPExplorer(props: {
alert("Choose a verb to quiz"); alert("Choose a verb to quiz");
return; return;
} }
setShowAnswer(false); const { VPS, qs } = makeQuizState(vps);
setVps(setRandomQuizState(mix)); 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)) {
@ -102,6 +121,24 @@ export function VPExplorer(props: {
if (vps.verb?.isCompound === "dynamic") return; if (vps.verb?.isCompound === "dynamic") return;
setVps(switchSubjObj) setVps(switchSubjObj)
} }
function quizLock<T>(f: T) {
if (mode === "quiz") {
return () => null;
}
return f;
}
function checkQuizAnswer(a: T.PsString) {
if (!quizState) return;
if (isInAnswer(a, quizState.answer)) {
// reward();
handleResetQuiz();
} else {
setQuizState({
...quizState,
result: "fail",
});
}
}
const verbPhrase: T.VPSelectionComplete | undefined = completeVPSelection(vps); const verbPhrase: T.VPSelectionComplete | undefined = completeVPSelection(vps);
const VPRendered = verbPhrase && renderVP(verbPhrase); const VPRendered = verbPhrase && renderVP(verbPhrase);
return <div className="mt-3" style={{ maxWidth: "950px"}}> return <div className="mt-3" style={{ maxWidth: "950px"}}>
@ -127,19 +164,6 @@ export function VPExplorer(props: {
handleChange={handleChangeMode} handleChange={handleChangeMode}
/> />
</div> </div>
{mode === "quiz" && <div className="mt-2 mb-3 d-flex flex-row justify-content-center align-items-center">
<div className="mr-2">Mix:</div>
<ButtonSelect
small
value={mix}
options={[
{ label: "NPs", value: "NPs" },
{ label: "Tenses", value: "tenses" },
{ label: "Both", value: "both" },
]}
handleChange={setMix}
/>
</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")) &&
<div className="text-center mt-4"> <div className="text-center mt-4">
<button onClick={handleSubjObjSwap} className="btn btn-sm btn-light"> <button onClick={handleSubjObjSwap} className="btn btn-sm btn-light">
@ -162,7 +186,7 @@ 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={handleSubjectChange} onChange={quizLock(handleSubjectChange)}
opts={props.opts} opts={props.opts}
cantClear={mode === "quiz"} cantClear={mode === "quiz"}
/> />
@ -184,7 +208,7 @@ export function VPExplorer(props: {
asObject asObject
np={vps.verb.object} np={vps.verb.object}
counterPart={vps.subject} counterPart={vps.subject}
onChange={handleObjectChange} onChange={quizLock(handleObjectChange)}
opts={props.opts} opts={props.opts}
cantClear={mode === "quiz"} cantClear={mode === "quiz"}
/>} />}
@ -193,25 +217,37 @@ export function VPExplorer(props: {
<div className="my-2"> <div className="my-2">
<TensePicker <TensePicker
vps={vps} vps={vps}
onChange={setVps} onChange={quizLock(setVps)}
mode={mode} mode={mode}
/> />
</div> </div>
</div> </div>
{(vps.verb && (mode === "quiz")) && <div className="text-center my-2"> {(verbPhrase && (mode === "phrases")) &&
<button
className="btn btn-primary"
onClick={showAnswer ? handleResetQuiz : () => setShowAnswer(true)}
>
{showAnswer
? <>Next <i className="ml-1 fas fa-random"/></>
: <>Show Answer</>}
</button>
</div>}
{(verbPhrase && ((mode === "phrases") || (mode === "quiz" && showAnswer))) &&
<VPDisplay VP={verbPhrase} opts={props.opts} /> <VPDisplay VP={verbPhrase} opts={props.opts} />
} }
{(vps.verb && (mode === "charts")) && <ChartDisplay VS={vps.verb} opts={props.opts} />} {(vps.verb && (mode === "charts")) && <ChartDisplay VS={vps.verb} opts={props.opts} />}
<span id="rewardId" />
{(mode === "quiz" && quizState) && <div className="text-center">
{quizState.result === "waiting" ? <>
<div className="text-muted my-3">Choose a correct answer:</div>
{quizState.options.map(o => <div className="pb-3">
<div className="btn 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>}
{mode === "quiz" && <div style={{ height: "300px" }}> {mode === "quiz" && <div style={{ height: "300px" }}>
{/* spacer for blank space while quizzing */} {/* spacer for blank space while quizzing */}
</div>} </div>}
@ -260,7 +296,63 @@ function switchSubjObj({ subject, verb }: T.VPSelectionState): T.VPSelectionStat
}; };
} }
function setRandomQuizState(mix: MixState) { function makeQuizState(oldVps: T.VPSelectionState): { VPS: T.VPSelectionState, qs: QuizState } {
function makeRes(x: T.VPSelectionState) {
const y = completeVPSelection(x);
if (!y) {
throw new Error("trying to make a quiz out of an incomplete VPSelection")
}
return compileVP(renderVP(y), { removeKing: false, shrinkServant: false });
}
const vps = getRandomVPSelection("both")(oldVps);
const wrongStates: T.VPSelectionState[] = [];
[1, 2, 3, 4].forEach(() => {
// TODO: don't repeat tenses
let v: T.VPSelectionState;
do {
v = getRandomVPSelection("tenses")(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 {
// TODO: randomize length pick
const ps = "long" in r.ps ? r.ps.long : r.ps;
// TODO: randomize version pick
return ps[0];
}
function getRandomVPSelection(mix: MixType = "both") {
// TODO: Type safety to make sure it's safe?
return ({ subject, verb }: T.VPSelectionState): T.VPSelectionState => { return ({ subject, verb }: T.VPSelectionState): T.VPSelectionState => {
if (mix === "tenses") { if (mix === "tenses") {
return { return {

24
src/lib/shuffle-array.ts Normal file
View File

@ -0,0 +1,24 @@
// https://stackoverflow.com/a/2450976
function shuffleArray<T>(arr: Readonly<Array<T>>): Array<T> {
let currentIndex = arr.length, temporaryValue, randomIndex;
const array = [...arr];
// While there remain elements to shuffle...
while (0 !== currentIndex) {
// Pick a remaining element...
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
// And swap it with the current element.
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}
return array;
}
export default shuffleArray;

View File

@ -111,6 +111,7 @@ import {
splitUpSyllables, splitUpSyllables,
countSyllables, countSyllables,
} from "./lib/accent-helpers"; } from "./lib/accent-helpers";
import shuffleArray from "./lib/shuffle-array";
import defaultTextOptions from "./lib/default-text-options"; import defaultTextOptions from "./lib/default-text-options";
import * as grammarUnits from "./lib/grammar-units"; import * as grammarUnits from "./lib/grammar-units";
import genderColors from "./lib/gender-colors"; import genderColors from "./lib/gender-colors";
@ -167,6 +168,7 @@ export {
randomPerson, randomPerson,
isInvalidSubjObjCombo, isInvalidSubjObjCombo,
randomSubjObj, randomSubjObj,
shuffleArray,
// protobuf helpers // protobuf helpers
readDictionary, readDictionary,
writeDictionary, writeDictionary,

View File

@ -9523,6 +9523,11 @@ react-refresh@^0.8.3:
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
integrity sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg== integrity sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg==
react-rewards@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/react-rewards/-/react-rewards-2.0.3.tgz#685aee98654ab4f060e297849f39cf0a16cec042"
integrity sha512-qww5Jtk2HmCjR9wc7+0c2iP8oCQzENZn8pnoJGi0S6o+6dAD25+ZoagLSWdoBLGVwxfP5kFBjv3VZ6L8fPk6lg==
react-scripts@4.0.3: react-scripts@4.0.3:
version "4.0.3" version "4.0.3"
resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-4.0.3.tgz#b1cafed7c3fa603e7628ba0f187787964cb5d345" resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-4.0.3.tgz#b1cafed7c3fa603e7628ba0f187787964cb5d345"