smooth first stage

This commit is contained in:
lingdocs 2022-04-13 16:07:35 +05:00
parent 39996bc92c
commit 352d30e80c
4 changed files with 145 additions and 56 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "@lingdocs/pashto-inflector", "name": "@lingdocs/pashto-inflector",
"version": "1.9.9", "version": "2.0.0",
"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",

View File

@ -0,0 +1,35 @@
import { CSSProperties } from "react";
interface IProps {
name: string;
[key: string]: CSSProperties | string;
}
const Keyframes = (props: IProps) => {
const toCss = (cssObject: CSSProperties | string) =>
typeof cssObject === "string"
? cssObject
: Object.keys(cssObject).reduce((accumulator, key) => {
const cssKey = key.replace(/[A-Z]/g, v => `-${v.toLowerCase()}`);
const cssValue = (cssObject as any)[key].toString().replace("'", "");
return `${accumulator}${cssKey}:${cssValue};`;
}, "");
return (
<style>
{`@keyframes ${props.name} {
${Object.keys(props)
.map(key => {
return ["from", "to"].includes(key)
? `${key} { ${toCss(props[key])} }`
: /^_[0-9]+$/.test(key)
? `${key.replace("_", "")}% { ${toCss(props[key])} }`
: "";
})
.join(" ")}
}`}
</style>
);
};
export default Keyframes;

View File

@ -10,6 +10,8 @@ import { getRandomTense } from "./TensePicker";
import { switchSubjObj } from "../../lib/phrase-building/vp-tools"; import { switchSubjObj } from "../../lib/phrase-building/vp-tools";
import playAudio from "../../lib/play-audio"; import playAudio from "../../lib/play-audio";
import TensePicker from "./TensePicker"; import TensePicker from "./TensePicker";
import Keyframes from "../Keyframes";
import energyDrink from "./energy-drink.jpeg";
const correctEmoji = ["✅", '🤓', "✅", '😊', "🌹", "✅", "✅", "🕺", "💃", '🥳', "👏", "✅", "💯", "😎", "✅", "👍"]; const correctEmoji = ["✅", '🤓', "✅", '😊', "🌹", "✅", "✅", "🕺", "💃", '🥳', "👏", "✅", "💯", "😎", "✅", "👍"];
@ -25,8 +27,12 @@ const answerFeedback: CSSProperties = {
} }
const checkDuration = 400; const checkDuration = 400;
const stageLength = 7;
type QuizState = { type QuizState = {
stage: "multiple choice",
qNumber: number,
vps: T.VPSelectionComplete,
answer: { answer: {
ps: T.SingleOrLengthOpts<T.PsString[]>; ps: T.SingleOrLengthOpts<T.PsString[]>;
e?: string[] | undefined; e?: string[] | undefined;
@ -40,16 +46,10 @@ function VPExplorerQuiz(props: {
opts: T.TextOptions, opts: T.TextOptions,
vps: T.VPSelection, vps: T.VPSelection,
}) { }) {
const startingQs = makeQuizState(props.vps); const startingQs = tickQuizState(props.vps);
const [vps, setVps] = useState<T.VPSelectionComplete>(startingQs.VPS) const [quizState, setQuizState] = useState<QuizState>(startingQs);
const [quizState, setQuizState] = useState<QuizState>(startingQs.qs);
const [showCheck, setShowCheck] = useState<boolean>(false); const [showCheck, setShowCheck] = useState<boolean>(false);
const [currentCorrectEmoji, setCurrentCorrectEmoji] = useState<string>(randFromArray(correctEmoji)); const [currentCorrectEmoji, setCurrentCorrectEmoji] = useState<string>(randFromArray(correctEmoji));
function handleResetQuiz() {
const { VPS, qs } = makeQuizState(vps);
setVps(VPS);
setQuizState(qs);
}
function checkQuizAnswer(a: T.PsString) { function checkQuizAnswer(a: T.PsString) {
if (!quizState) return; if (!quizState) return;
if (isInAnswer(a, quizState.answer)) { if (isInAnswer(a, quizState.answer)) {
@ -57,7 +57,7 @@ function VPExplorerQuiz(props: {
if (toPlay) playAudio(`correct-${randFromArray([1,2,3])}`); if (toPlay) playAudio(`correct-${randFromArray([1,2,3])}`);
setShowCheck(true); setShowCheck(true);
setTimeout(() => { setTimeout(() => {
handleResetQuiz(); setQuizState(tickQuizState);
}, checkDuration / 2); }, checkDuration / 2);
setTimeout(() => { setTimeout(() => {
setShowCheck(false); setShowCheck(false);
@ -75,11 +75,15 @@ function VPExplorerQuiz(props: {
}); });
} }
} }
const rendered = renderVP(vps); const rendered = renderVP(quizState.vps);
const { subject, object } = rendered; const { subject, object } = rendered;
const { e } = compileVP(rendered, { removeKing: false, shrinkServant: false }); const { e } = compileVP(rendered, { removeKing: false, shrinkServant: false });
return <div> function handleRestart() {
<div className="d-flex flex-row justify-content-around flex-wrap" style={{ marginLeft: "-0.5rem", marginRight: "-0.5rem" }}> setQuizState(tickQuizState(quizState.vps));
}
return <div className="mt-4">
<ProgressBar quizState={quizState} />
<div className="d-flex flex-row justify-content-around flex-wrap">
<div className="my-2"> <div className="my-2">
<div className="h5 text-center">Subject</div> <div className="h5 text-center">Subject</div>
<QuizNPDisplay>{subject}</QuizNPDisplay> <QuizNPDisplay>{subject}</QuizNPDisplay>
@ -90,44 +94,86 @@ function VPExplorerQuiz(props: {
</div>} </div>}
<div className="my-2"> <div className="my-2">
<TensePicker <TensePicker
vps={vps} vps={quizState.vps}
onChange={() => null} onChange={() => null}
mode={"quiz"} mode={"quiz"}
/> />
</div> </div>
</div> </div>
{e && <div className="text-center text-muted text-small"> {e && <div className="text-center text-muted">
{e.map(eLine => <div key={eLine}>{eLine}</div>)} {e.map(eLine => <div key={eLine}>{eLine}</div>)}
</div>} </div>}
<div className="text-center"> <div className="text-center">
<div style={showCheck ? answerFeedback : { ...answerFeedback, opacity: 0 }}> <div style={showCheck ? answerFeedback : { ...answerFeedback, opacity: 0 }}>
{currentCorrectEmoji} {currentCorrectEmoji}
</div> </div>
{quizState.result === "waiting" ? <> {quizState.qNumber === stageLength ?
<div className="mt-4" style={{ animation: "fade-in 0.5s" }}>
<h4>👏 Congratulations</h4>
<p className="lead">You finished the first level!</p>
<p>The <strong>other levels are still in development</strong>... In the meantime have an energy drink.</p>
<div className="mb-4">
<img src={energyDrink} alt="energy-dring" className="img-fluid" />
</div>
<button type="button" className="btn btn-primary" onClick={handleRestart}>
Restart
</button>
</div>
: (quizState.result === "waiting"
? <>
<div className="text-muted my-3">Choose a correct answer:</div> <div className="text-muted my-3">Choose a correct answer:</div>
{quizState.options.map(o => <div className="pb-3" key={o.f}> {quizState.options.map(o => <div className="pb-3" key={o.f} style={{ animation: "fade-in 0.5s" }}>
<div className="btn btn-answer btn-outline-secondary" onClick={() => { <button className="btn btn-answer btn-outline-secondary" onClick={() => {
checkQuizAnswer(o); checkQuizAnswer(o);
}}> }}>
<InlinePs opts={props.opts}>{o}</InlinePs> <InlinePs opts={props.opts}>{o}</InlinePs>
</div> </button>
</div>)} </div>)}
</> : <div> </>
<div className="h5 mt-4"> Wrong 😭</div> : <div style={{ animation: "fade-in 0.5s" }}>
<div className="my-4">The correct answer was:</div> <div className="h4 mt-4"> Wrong 😭</div>
<div className="my-4 lead">The correct answer was:</div>
<InlinePs opts={props.opts}> <InlinePs opts={props.opts}>
{quizState.options.find(x => isInAnswer(x, quizState.answer)) as T.PsString} {quizState.options.find(x => isInAnswer(x, quizState.answer)) as T.PsString}
</InlinePs> </InlinePs>
<div className="my-4"> <div className="my-4">
<button type="button" className="btn btn-primary" onClick={handleResetQuiz}> <button type="button" className="btn btn-primary" onClick={handleRestart}>
Try Again Try Again
</button> </button>
</div> </div>
</div>} </div>)
}
<Keyframes name="fade-in" from={{ opacity: 0 }} to={{ opacity: 1 }} />
</div> </div>
</div>; </div>;
} }
function ProgressBar({ quizState }: { quizState: QuizState }) {
function getProgressWidth(): string {
const num = getPercentageDone({ current: quizState.qNumber, total: stageLength });
return `${num}%`;
}
return <div className="mb-3">
<div className="progress mb-1" style={{ height: "3px" }}>
<div
className={`progress-bar bg-${quizState.result === "fail" ? "danger" : "primary"}`}
role="progressbar"
style={{ width: getProgressWidth() }}
/>
</div>
<div>
Level 1: Multiple Choice
</div>
</div>;
}
function getPercentageDone(progress: { current: number, total: number }): number {
return Math.round(
(progress.current / (progress.total + 1)) * 100
);
}
function QuizNPDisplay({ children }: { children: T.Rendered<T.NPSelection> | T.Person.ThirdPlurMale }) { function QuizNPDisplay({ children }: { children: T.Rendered<T.NPSelection> | T.Person.ThirdPlurMale }) {
return <div className="mb-3"> return <div className="mb-3">
{(typeof children === "number") {(typeof children === "number")
@ -136,14 +182,21 @@ function QuizNPDisplay({ children }: { children: T.Rendered<T.NPSelection> | T.P
</div>; </div>;
} }
/**
function makeQuizState(oldVps: T.VPSelection): { VPS: T.VPSelectionComplete, qs: QuizState } { * creates a fresh QuizState when a VPSelection is passed
* advances a QuizState when a QuizState is passed
*
* @param startingWith
* @returns
*/
function tickQuizState(startingWith: T.VPSelection | QuizState): QuizState {
function makeRes(x: T.VPSelectionComplete) { function makeRes(x: T.VPSelectionComplete) {
return compileVP(renderVP(x), { removeKing: false, shrinkServant: false }); return compileVP(renderVP(x), { removeKing: false, shrinkServant: false });
} }
const oldVps = "stage" in startingWith ? startingWith.vps : startingWith;
// for now, always inforce positive // for now, always inforce positive
const vps = getRandomVPSelection("both")({ ...oldVps, verb: { ...oldVps.verb, negative: false }}); const newVps = getRandomVPSelection("both")({ ...oldVps, verb: { ...oldVps.verb, negative: false }});
const wrongStates: T.VPSelectionComplete[] = []; const wrongVpsS: T.VPSelectionComplete[] = [];
// don't do the SO switches every time // don't do the SO switches every time
const wholeTimeSOSwitch = randFromArray([true, false]); const wholeTimeSOSwitch = randFromArray([true, false]);
[1, 2, 3].forEach(() => { [1, 2, 3].forEach(() => {
@ -152,23 +205,24 @@ function makeQuizState(oldVps: T.VPSelection): { VPS: T.VPSelectionComplete, qs:
const SOSwitch = wholeTimeSOSwitch && randFromArray([true, false]); const SOSwitch = wholeTimeSOSwitch && randFromArray([true, false]);
// TODO: if switich subj and obj, include the tense being correct maybe // TODO: if switich subj and obj, include the tense being correct maybe
v = getRandomVPSelection("tenses")( v = getRandomVPSelection("tenses")(
SOSwitch ? switchSubjObj(vps) : vps, SOSwitch ? switchSubjObj(newVps) : newVps,
); );
// eslint-disable-next-line // eslint-disable-next-line
} while (wrongStates.find(x => x.verb.tense === v.verb.tense)); } while (wrongVpsS.find(x => x.verb.tense === v.verb.tense));
wrongStates.push(v); wrongVpsS.push(v);
}); });
const answer = makeRes(vps); const answer = makeRes(newVps);
const wrongAnswers = wrongStates.map(makeRes); const wrongAnswers = wrongVpsS.map(makeRes);
const allAnswers = shuffleArray([...wrongAnswers, answer]); const allAnswers = shuffleArray([...wrongAnswers, answer]);
const options = allAnswers.map(getOptionFromResult); const options = allAnswers.map(getOptionFromResult);
const qNumber = ("stage" in startingWith) ? (startingWith.qNumber + 1) : 0;
return { return {
VPS: vps, stage: "multiple choice",
qs: { qNumber,
vps: newVps,
answer, answer,
options, options,
result: "waiting", result: "waiting",
},
}; };
} }
@ -224,8 +278,14 @@ function getRandomVPSelection(mix: MixType = "both") {
distance: "far", distance: "far",
person: obj, person: obj,
}; };
const s = randSubj;
// ensure that the verb selection is complete // ensure that the verb selection is complete
if (mix === "tenses") {
return {
subject: subject !== undefined ? subject : randSubj,
// @ts-ignore
verb: randomizeTense(verb, true),
}
}
const v: T.VerbSelectionComplete = { const v: T.VerbSelectionComplete = {
...verb, ...verb,
object: ( object: (
@ -236,14 +296,8 @@ function getRandomVPSelection(mix: MixType = "both") {
? randObj ? randObj
: verb.object, : verb.object,
}; };
if (mix === "tenses") {
return { return {
subject: subject !== undefined ? subject : randSubj, subject: randSubj,
verb: randomizeTense(v, true),
}
}
return {
subject: s,
verb: randomizeTense(v, true), verb: randomizeTense(v, true),
}; };
}; };

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB