beta new verb engine

This commit is contained in:
adueck 2023-07-22 20:37:34 +04:00
parent 3b6d013402
commit 5edf0d1e02
3 changed files with 608 additions and 463 deletions

View File

@ -10,11 +10,14 @@ import { psStringEquals } from "../../../lib/src/p-text-helpers";
import { renderVP } from "../../../lib/src/phrase-building/render-vp";
import { compileVP } from "../../../lib/src/phrase-building/compile";
import { getRandomTense } from "./TensePicker";
import { getTenseFromVerbSelection, removeBa, switchSubjObj } from "../../../lib/src/phrase-building/vp-tools";
import {
getTenseFromVerbSelection,
removeBa,
switchSubjObj,
} from "../../../lib/src/phrase-building/vp-tools";
import playAudio from "../play-audio";
import TensePicker from "./TensePicker";
import Keyframes from "../Keyframes";
import energyDrink from "./energy-drink.jpg";
import { isImperativeTense } from "../../../lib/src/type-predicates";
import {
adjustObjectSelection,
@ -25,51 +28,71 @@ import {
getSubjectSelection,
} from "../../../lib/src/phrase-building/blocks-utils";
const correctEmoji = ["✅", '🤓', "✅", '😊', "🌹", "✅", "✅", '🥳', "👏", "✅", "💯", "😎", "✅", "👍"];
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%)",
}
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;
const stageLength = 5;
type QuizState = ({
stage: "multiple choice",
type QuizState = (
| {
stage: "multiple choice";
answer: {
ps: T.SingleOrLengthOpts<T.PsString[]>;
e?: string[] | undefined;
},
options: T.PsString[],
} | {
stage: "blanks",
};
options: T.PsString[];
}
| {
stage: "blanks";
answer: {
ps: T.PsString[],
withBa: boolean,
},
}) & {
qNumber: number,
vps: T.VPSelectionComplete,
result: "waiting" | "fail",
}
ps: T.PsString[];
withBa: boolean;
};
}
) & {
qNumber: number;
vps: T.VPSelectionComplete;
result: "waiting" | "fail";
};
type MixType = "NPs" | "tenses" | "both";
function VPExplorerQuiz(props: {
opts: T.TextOptions,
vps: T.VPSelectionState,
opts: T.TextOptions;
vps: T.VPSelectionState;
}) {
const [quizState, setQuizState] = useState<QuizState | "loading">("loading");
const [showCheck, setShowCheck] = useState<boolean>(false);
const [answerBlank, setAnswerBlank] = useState<string>("");
const [withBa, setWithBa] = useState<boolean>(false);
const [currentCorrectEmoji, setCurrentCorrectEmoji] = useState<string>(randFromArray(correctEmoji));
const [currentCorrectEmoji, setCurrentCorrectEmoji] = useState<string>(
randFromArray(correctEmoji)
);
useEffect(() => {
setQuizState(tickQuizState(completeVPs(props.vps)));
}, [props.vps]);
@ -82,21 +105,22 @@ function VPExplorerQuiz(props: {
setAnswerBlank("");
setQuizState(tickQuizState(quizState.vps));
}
function checkAnswer(a: T.PsString | { text: string, withBa: boolean }) {
function checkAnswer(a: T.PsString | { text: string; withBa: boolean }) {
if (quizState === "loading") return;
if (!quizState) return;
const correct = "p" in a
const correct =
"p" in a
? isInAnswer(a, quizState.answer)
// @ts-ignore // TODO: CLEANUP
: blanksAnswerCorrect(a, quizState.answer);
: // @ts-ignore // TODO: CLEANUP
blanksAnswerCorrect(a, quizState.answer);
setAnswerBlank("");
setWithBa(false);
if (correct) {
const toPlay = randFromArray([true, false, false]);
if (toPlay) playAudio(`correct-${randFromArray([1,2,3])}`);
if (toPlay) playAudio(`correct-${randFromArray([1, 2, 3])}`);
setShowCheck(true);
setTimeout(() => {
setQuizState(o => {
setQuizState((o) => {
if (o === "loading") return o;
return tickQuizState(o);
});
@ -109,7 +133,7 @@ function VPExplorerQuiz(props: {
setCurrentCorrectEmoji(randFromArray(correctEmoji));
}, checkDuration * 2);
} else {
playAudio(`wrong-${randFromArray([1,2])}`);
playAudio(`wrong-${randFromArray([1, 2])}`);
navigator.vibrate(300);
setQuizState({
...quizState,
@ -118,20 +142,32 @@ function VPExplorerQuiz(props: {
}
}
const rendered = renderVP(quizState.vps);
const subject: T.Rendered<T.NPSelection> = getSubjectSelectionFromBlocks(rendered.blocks).selection;
const subject: T.Rendered<T.NPSelection> = getSubjectSelectionFromBlocks(
rendered.blocks
).selection;
const object = getObjectSelectionFromBlocks(rendered.blocks).selection;
const { e } = compileVP(rendered, { removeKing: false, shrinkServant: false });
return <div className="mt-4">
const { e } = compileVP(rendered, {
removeKing: false,
shrinkServant: false,
});
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="h5 text-center">Subject</div>
<QuizNPDisplay opts={props.opts} stage={quizState.stage}>{subject}</QuizNPDisplay>
<QuizNPDisplay opts={props.opts} stage={quizState.stage}>
{subject}
</QuizNPDisplay>
</div>
{(object !== "none") && <div className="my-2">
{object !== "none" && (
<div className="my-2">
<div className="h5 text-center">Object</div>
<QuizNPDisplay opts={props.opts} stage={quizState.stage}>{object}</QuizNPDisplay>
</div>}
<QuizNPDisplay opts={props.opts} stage={quizState.stage}>
{object}
</QuizNPDisplay>
</div>
)}
<div className="my-2">
<TensePicker
vpsComplete={quizState.vps}
@ -140,52 +176,81 @@ function VPExplorerQuiz(props: {
/>
</div>
</div>
{e && <div className="text-center text-muted">
{e.map(eLine => <div key={eLine}>{eLine}</div>)}
</div>}
{e && (
<div className="text-center text-muted">
{e.map((eLine) => (
<div key={eLine}>{eLine}</div>
))}
</div>
)}
<div className="text-center">
<div style={showCheck ? answerFeedback : { ...answerFeedback, opacity: 0 } as any}>
<div
style={
showCheck
? answerFeedback
: ({ ...answerFeedback, opacity: 0 } as any)
}
>
{currentCorrectEmoji}
</div>
{quizState.qNumber === stageLength ?
{quizState.qNumber === stageLength ? (
<div className="mt-4" style={{ animation: "fade-in 0.5s" }}>
<h4>👏 Congratulations</h4>
<p className="lead">You finished the first two levels!</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}>
<p>There may be other levels in the future...</p>
<button
type="button"
className="btn btn-primary"
onClick={handleRestart}
>
Restart
</button>
</div>
: (quizState.result === "waiting"
? (quizState.stage === "multiple choice" ? <>
) : quizState.result === "waiting" ? (
quizState.stage === "multiple choice" ? (
<>
<div className="text-muted my-3">Choose a correct answer:</div>
{quizState.options.map(o => <div className="pb-3" key={o.f} style={{ animation: "fade-in 0.5s" }}>
{quizState.options.map((o) => (
<div
className="pb-3"
key={o.f}
style={{ animation: "fade-in 0.5s" }}
>
<button
className="btn btn-answer btn-outline-secondary"
onClick={() => checkAnswer(o)}>
onClick={() => checkAnswer(o)}
>
<InlinePs opts={props.opts}>{o}</InlinePs>
</button>
</div>)}
</> : <div>
<div className="text-muted my-3">Type the <strong>verb in Pashto script</strong> to finish the phrase:</div>
<form onSubmit={e => {
</div>
))}
</>
) : (
<div>
<div className="text-muted my-3">
Type the <strong>verb in Pashto script</strong> to finish the
phrase:
</div>
<form
onSubmit={(e) => {
if (!answerBlank) {
alert("Enter the verb in Pashto script");
};
}
e.preventDefault();
checkAnswer({ text: answerBlank, withBa });
}}>
<div className="mb-3" style={{ maxWidth: "250px", margin: "0 auto"}}>
}}
>
<div
className="mb-3"
style={{ maxWidth: "250px", margin: "0 auto" }}
>
<input
type="text"
dir="auto"
className="form-control"
placeholder="type verb here"
value={answerBlank}
onChange={e => setAnswerBlank(e.target.value)}
onChange={(e) => setAnswerBlank(e.target.value)}
/>
</div>
<div className="form-check mb-4" style={{ fontSize: "large" }}>
@ -193,102 +258,142 @@ function VPExplorerQuiz(props: {
className="form-check-input"
type="checkbox"
checked={withBa}
onChange={e => setWithBa(e.target.checked)}
onChange={(e) => setWithBa(e.target.checked)}
id="addBa"
/>
<label className="form-check-label text-muted" htmlFor="addBa">
add <InlinePs opts={props.opts}>{baParticle}</InlinePs> in kids' section
<label
className="form-check-label text-muted"
htmlFor="addBa"
>
add <InlinePs opts={props.opts}>{baParticle}</InlinePs> in
kids' section
</label>
</div>
<button type="submit" className="btn btn-primary">
Check
</button>
</form>
</div>)
: <div style={{ animation: "fade-in 0.5s" }}>
</div>
)
) : (
<div style={{ animation: "fade-in 0.5s" }}>
<div className="h4 mt-4"> Wrong 😭</div>
{quizState.stage === "multiple choice" ?
{quizState.stage === "multiple choice" ? (
<div>
<div className="my-4 lead">The correct answer was:</div>
<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>
</div>
:
) : (
<div>
<div className="my-4 lead">Possible correct answers were:</div>
{quizState.answer.ps.map((p, i) => <div key={i}>
{quizState.answer.ps.map((p, i) => (
<div key={i}>
<InlinePs opts={props.opts}>{p}</InlinePs>
</div>)}
</div>
))}
<div className="mt-2">
<strong>{("withBa" in quizState.answer && quizState.answer.withBa) ? "With" : "without"}</strong>
{` `}
a <InlinePs opts={props.opts}>{baParticle}</InlinePs> in the phrase
<strong>
{"withBa" in quizState.answer && quizState.answer.withBa
? "With"
: "without"}
</strong>
{` `}a <InlinePs opts={props.opts}>{baParticle}</InlinePs> in
the phrase
</div>
</div>
}
<button type="button" className="btn btn-primary mt-4" onClick={handleRestart}>
)}
<button
type="button"
className="btn btn-primary mt-4"
onClick={handleRestart}
>
Try Again
</button>
</div>)
}
</div>
)}
<Keyframes name="fade-in" from={{ opacity: 0 }} to={{ opacity: 1 }} />
</div>
</div>;
</div>
);
}
function blanksAnswerCorrect(a: { text: string, withBa: boolean }, answer: { ps: T.PsString[], withBa?: boolean }): boolean {
function blanksAnswerCorrect(
a: { text: string; withBa: boolean },
answer: { ps: T.PsString[]; withBa?: boolean }
): boolean {
const p = standardizePashto(a.text).trim();
const given = removeBa({ p, f: "" }).p;
return (
a.withBa === answer.withBa
&&
answer.ps.some(x => x.p === given)
);
return a.withBa === answer.withBa && answer.ps.some((x) => x.p === given);
}
function ProgressBar({ quizState }: { quizState: QuizState }) {
function getPercentageDone({ current, total }: { current: number, total: number }): number {
return Math.round(
(current / total) * 100
);
function getPercentageDone({
current,
total,
}: {
current: number;
total: number;
}): number {
return Math.round((current / total) * 100);
}
function getProgressWidth(): string {
const num = getPercentageDone({ current: quizState.qNumber, total: stageLength });
const num = getPercentageDone({
current: quizState.qNumber,
total: stageLength,
});
return `${num}%`;
}
return <div className="mb-3">
return (
<div className="mb-3">
<div className="progress mb-1" style={{ height: "3px" }}>
<div
className={`progress-bar bg-${quizState.result === "fail" ? "danger" : "primary"}`}
className={`progress-bar bg-${
quizState.result === "fail" ? "danger" : "primary"
}`}
role="progressbar"
style={{ width: getProgressWidth() }}
/>
</div>
<div>
{quizState.stage === "multiple choice"
? "Level 1: Multiple Choice"
: "Level 2: Type the Verb"}
</div>
</div>;
</div>
);
}
function QuizNPDisplay({ children, stage, opts }: {
stage: "blanks" | "multiple choice",
children: T.Rendered<T.NPSelection> | T.Person.ThirdPlurMale,
opts: T.TextOptions,
function QuizNPDisplay({
children,
stage,
opts,
}: {
stage: "blanks" | "multiple choice";
children: T.Rendered<T.NPSelection> | T.Person.ThirdPlurMale;
opts: T.TextOptions;
}) {
return <div className="mb-3">
{(typeof children === "number")
? <div className="text-muted">Unspoken 3rd Pers. Masc. Plur.</div>
: <div className="text-centered" style={{ fontSize: "large" }}>
{stage === "blanks" && <div>
return (
<div className="mb-3">
{typeof children === "number" ? (
<div className="text-muted">Unspoken 3rd Pers. Masc. Plur.</div>
) : (
<div className="text-centered" style={{ fontSize: "large" }}>
{stage === "blanks" && (
<div>
<InlinePs opts={opts}>{children.selection.ps[0]}</InlinePs>
</div>}
</div>
)}
<div>{children.selection.e}</div>
</div>}
</div>;
</div>
)}
</div>
);
}
/**
@ -298,13 +403,18 @@ function QuizNPDisplay({ children, stage, opts }: {
* @param startingWith
* @returns
*/
function tickQuizState(startingWith: T.VPSelectionComplete | QuizState): QuizState {
function tickQuizState(
startingWith: T.VPSelectionComplete | QuizState
): QuizState {
function makeRes(x: T.VPSelectionComplete) {
return compileVP(renderVP(x), { removeKing: false, shrinkServant: false });
}
const oldVps = "stage" in startingWith ? startingWith.vps : startingWith;
// for now, always inforce positive
const newVps = getRandomVPSelection("both")({ ...oldVps, verb: { ...oldVps.verb, negative: false }});
const newVps = getRandomVPSelection("both")({
...oldVps,
verb: { ...oldVps.verb, negative: false },
});
const wrongVpsS: T.VPSelectionComplete[] = [];
// don't do the SO switches every time
const wholeTimeSOSwitch = randFromArray([true, false]);
@ -314,17 +424,22 @@ function tickQuizState(startingWith: T.VPSelectionComplete | QuizState): QuizSta
const SOSwitch = wholeTimeSOSwitch && randFromArray([true, false]);
// TODO: if switich subj and obj, include the tense being correct maybe
v = getRandomVPSelection("tenses")(
SOSwitch ? switchSubjObj(newVps) : newVps,
SOSwitch ? switchSubjObj(newVps) : newVps
);
// eslint-disable-next-line
} while (wrongVpsS.find(x => x.verb.tense === v.verb.tense));
} while (wrongVpsS.find((x) => x.verb.tense === v.verb.tense));
wrongVpsS.push(v);
});
const qNumber = "stage" in startingWith ? (startingWith.qNumber + 1) : 0;
const beatFirstStage = "stage" in startingWith && (qNumber === stageLength) && startingWith.stage === "multiple choice";
const qNumber = "stage" in startingWith ? startingWith.qNumber + 1 : 0;
const beatFirstStage =
"stage" in startingWith &&
qNumber === stageLength &&
startingWith.stage === "multiple choice";
const stage = beatFirstStage
? "blanks"
: ("stage" in startingWith ? startingWith.stage : "multiple choice");
: "stage" in startingWith
? startingWith.stage
: "multiple choice";
const blanksAnswer = getBlanksAnswer(newVps);
if (stage === "blanks") {
return {
@ -350,7 +465,10 @@ function tickQuizState(startingWith: T.VPSelectionComplete | QuizState): QuizSta
return out;
}
function getBlanksAnswer(vps: T.VPSelectionComplete): { ps: T.PsString[], withBa: boolean } {
function getBlanksAnswer(vps: T.VPSelectionComplete): {
ps: T.PsString[];
withBa: boolean;
} {
// TODO: !!!
// const { verb, perfectiveHead } = getVerbAndHeadFromBlocks(renderVP(vps).blocks);
// const ps = flattenLengths(verb.block.ps).map(x => {
@ -363,27 +481,32 @@ function getBlanksAnswer(vps: T.VPSelectionComplete): { ps: T.PsString[], withBa
return {
ps: [{ p: "TOOD", f: "TODO" }],
withBa: false, // verb.block.hasBa,
}
};
}
function isInAnswer(a: T.PsString, answer: {
function isInAnswer(
a: T.PsString,
answer: {
ps: T.SingleOrLengthOpts<T.PsString[]>;
e?: string[] | undefined;
}): boolean {
}
): boolean {
if ("long" in answer.ps) {
return isInAnswer(a, { ...answer, ps: answer.ps.long }) ||
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 }));
!!(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
const ps =
"long" in r.ps
? r.ps[randFromArray(["short", "long"] as ("short" | "long")[])]
: r.ps;
// not randomizing version pick (for now)
@ -393,10 +516,12 @@ function getOptionFromResult(r: {
function completeVPs(vps: T.VPSelectionState): T.VPSelectionComplete {
const vpsSubj = getSubjectSelection(vps.blocks).selection;
const vpsObj = getObjectSelection(vps.blocks).selection;
const oldSubj = vpsSubj?.selection.type === "pronoun"
const oldSubj =
vpsSubj?.selection.type === "pronoun"
? vpsSubj.selection.person
: undefined;
const oldObj = (typeof vpsObj === "object" && vpsObj.selection.type === "pronoun")
const oldObj =
typeof vpsObj === "object" && vpsObj.selection.type === "pronoun"
? vpsObj.selection.person
: undefined;
const { subj, obj } = randomSubjObj(
@ -423,20 +548,22 @@ function completeVPs(vps: T.VPSelectionState): T.VPSelectionComplete {
person: subj,
},
}),
(
(typeof vpsObj === "object" && !(vpsObj.selection.type === "noun" && vpsObj.selection.dynamicComplement))
||
(typeof vpsObj === "object" &&
!(
vpsObj.selection.type === "noun" &&
(vpsObj.selection.dynamicComplement ||
vpsObj.selection.genStativeComplement)
)) ||
vpsObj === undefined
)
? {
type: "NP",
selection: {
type: "pronoun",
distance: "far",
person: obj,
},
}
}
: vpsObj,
: vpsObj
),
verb,
};
@ -448,10 +575,12 @@ function getRandomVPSelection(mix: MixType = "both") {
const subject = getSubjectSelection(VPS.blocks).selection;
const object = getObjectSelection(VPS.blocks).selection;
const verb = VPS.verb;
const oldSubj = (subject.selection.type === "pronoun")
const oldSubj =
subject.selection.type === "pronoun"
? subject.selection.person
: undefined;
const oldObj = (typeof object === "object" && object.selection.type === "pronoun")
const oldObj =
typeof object === "object" && object.selection.type === "pronoun"
? object.selection.person
: undefined;
const { subj, obj } = randomSubjObj(
@ -459,59 +588,70 @@ function getRandomVPSelection(mix: MixType = "both") {
);
const randSubj: T.NPSelection = {
type: "NP",
selection: (
subject?.selection.type === "pronoun" ? {
selection:
subject?.selection.type === "pronoun"
? {
...subject.selection,
person: subj,
} : {
}
: {
type: "pronoun",
distance: "far",
person: subj,
}
),
},
};
const randObj: T.NPSelection = {
type: "NP",
selection: (
typeof object === "object" && object.selection.type === "pronoun" ? {
selection:
typeof object === "object" && object.selection.type === "pronoun"
? {
...object.selection,
person: obj,
} : {
}
: {
type: "pronoun",
distance: "far",
person: obj,
}
),
},
};
// ensure that the verb selection is complete
if (mix === "tenses") {
return {
blocks: possibleShuffleArray(adjustObjectSelection(
adjustSubjectSelection(VPS.blocks, subject !== undefined ? subject : randSubj),
object !== undefined ? object : randObj,
)),
blocks: possibleShuffleArray(
adjustObjectSelection(
adjustSubjectSelection(
VPS.blocks,
subject !== undefined ? subject : randSubj
),
object !== undefined ? object : randObj
)
),
verb: randomizeTense(verb, true),
form: { removeKing: false, shrinkServant: false },
externalComplement: undefined,
}
};
}
return {
blocks: possibleShuffleArray(adjustObjectSelection(
blocks: possibleShuffleArray(
adjustObjectSelection(
adjustSubjectSelection(VPS.blocks, randSubj),
(
(typeof object === "object" && !(object.selection.type === "noun" && object.selection.dynamicComplement))
||
(typeof object === "object" &&
!(
object.selection.type === "noun" &&
(object.selection.dynamicComplement ||
object.selection.genStativeComplement)
)) ||
object === undefined
)
? randObj
: object,
)),
: object
)
),
verb: randomizeTense(verb, true),
form: { removeKing: false, shrinkServant: false },
externalComplement: undefined,
};
};
};
}
function possibleShuffleArray<X>(arr: X[]): X[] {
const willShuffle = randFromArray([true, false, false]);
@ -521,12 +661,13 @@ function possibleShuffleArray<X>(arr: X[]): X[] {
return arr;
}
function randomizeTense(verb: T.VerbSelectionComplete, dontRepeatTense: boolean): T.VerbSelectionComplete {
function randomizeTense(
verb: T.VerbSelectionComplete,
dontRepeatTense: boolean
): T.VerbSelectionComplete {
return {
...verb,
tense: getRandomTense(
dontRepeatTense ? verb.tense : undefined,
),
tense: getRandomTense(dontRepeatTense ? verb.tense : undefined),
};
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

View File

@ -81,7 +81,11 @@ export function renderVP(VP: T.VPSelectionComplete): T.VPRendered {
kids: getVPKids(hasBa, VP.blocks, VP.form, king),
englishBase: renderEnglishVPBase({
subjectPerson,
object: VP.verb.isCompound === "dynamic" ? "none" : object,
object:
VP.verb.isCompound === "dynamic" ||
VP.verb.isCompound === "generative stative"
? "none"
: object,
vs: VP.verb,
}),
form: VP.form,