very beta quiz happening!
This commit is contained in:
parent
1c4694d5fc
commit
0fb73af58b
|
@ -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": {
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
|
@ -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,
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue