refactor and improve quiz

This commit is contained in:
lingdocs 2022-04-13 14:37:04 +05:00
parent 68633089f9
commit 39996bc92c
6 changed files with 309 additions and 259 deletions

View File

@ -1,6 +1,6 @@
{
"name": "@lingdocs/pashto-inflector",
"version": "1.9.8",
"version": "1.9.9",
"author": "lingdocs.com",
"description": "A Pashto inflection and verb conjugation engine, inculding React components for displaying Pashto text, inflections, and conjugations",
"homepage": "https://verbs.lingdocs.com",

View File

@ -68,11 +68,10 @@ export function getRandomTense(type: "basic" | "modal" | "perfect", o?: T.Perfec
return tns;
}
function TensePicker({ onChange, vps, mode, locked }: {
function TensePicker({ onChange, vps, mode }: {
vps: T.VPSelection,
onChange: (p: T.VPSelection) => void,
mode: "charts" | "phrases" | "quiz",
locked: boolean,
}) {
function onTenseSelect(o: { value: T.VerbTense | T.PerfectTense } | null) {
const value = o?.value ? o.value : undefined;
@ -167,7 +166,7 @@ function TensePicker({ onChange, vps, mode, locked }: {
label: "Modal",
value: "modal",
}]}
handleChange={onTenseCategorySelect}
handleChange={mode === "quiz" ? onTenseCategorySelect : () => null}
/>
</div>}
</div>
@ -180,11 +179,11 @@ function TensePicker({ onChange, vps, mode, locked }: {
options={tOptions}
{...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")}>
<i className="fas fa-chevron-left" />
</div>
{mode !== "charts" && <ButtonSelect
{mode === "phrases" && <ButtonSelect
small
value={vps.verb.negative.toString()}
options={[{

View File

@ -4,10 +4,20 @@ import InlinePs from "../InlinePs";
import AbbreviationFormSelector from "./AbbreviationFormSelector";
import { isPastTense } from "../../lib/phrase-building/vp-tools";
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 [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 });
return <div className="text-center mt-2">
{VP.verb.transitivity === "transitive" && <div className="form-check mb-2">

View File

@ -1,9 +1,9 @@
import NPPicker from "../np-picker/NPPicker";
import VerbPicker from "./VerbPicker";
import TensePicker, { getRandomTense } from "./TensePicker";
import TensePicker from "./TensePicker";
import VPDisplay from "./VPDisplay";
import ButtonSelect from "../ButtonSelect";
import { renderVP, compileVP } from "../../lib/phrase-building/index";
import {
isInvalidSubjObjCombo,
} from "../../lib/phrase-building/vp-tools";
@ -11,32 +11,15 @@ import * as T from "../../types";
import ChartDisplay from "./ChartDisplay";
import useStickyState from "../../lib/useStickyState";
import { makeVPSelectionState } from "./verb-selection";
import { CSSProperties, useEffect, useState } 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 { useEffect } from "react";
import { getKingAndServant } from "../../lib/phrase-building/render-vp";
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 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
// add energy drinks?
@ -54,18 +37,6 @@ const answerFeedback: CSSProperties = {
// 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: {
verb: T.VerbEntry,
opts: T.TextOptions,
@ -90,33 +61,16 @@ export function VPExplorer(props: {
},
"verbExplorerMode",
);
const [quizState, setQuizState] = useState<QuizState | undefined>(undefined);
const [showCheck, setShowCheck] = useState<boolean>(false);
const [currentCorrectEmoji, setCurrentCorrectEmoji] = useState<string>(randFromArray(correctEmoji));
useEffect(() => {
setVps(o => {
setVps(oldVps => {
if (mode === "quiz") {
setMode("phrases");
}
return makeVPSelectionState(props.verb, o);
return makeVPSelectionState(props.verb, oldVps);
});
// eslint-disable-next-line
}, [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) {
if (!skipPronounConflictCheck && hasPronounConflict(subject, vps.verb?.object)) {
alert("That combination of pronouns is not allowed");
@ -153,31 +107,6 @@ export function VPExplorer(props: {
}
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"}}>
<VerbPicker
{..."getNounByTs" in props ? {
@ -198,7 +127,7 @@ export function VPExplorer(props: {
{ label: "Phrases", value: "phrases" },
{ label: "Quiz", value: "quiz" },
]}
handleChange={handleChangeMode}
handleChange={setMode}
/>
</div>
{(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
</button>
</div>}
<div className="d-flex flex-row justify-content-around flex-wrap" style={{ marginLeft: "-0.5rem", marginRight: "-0.5rem" }}>
{mode !== "charts" && <>
{mode !== "quiz" && <div className="d-flex flex-row justify-content-around flex-wrap" style={{ marginLeft: "-0.5rem", marginRight: "-0.5rem" }}>
{mode === "phrases" && <>
<div className="my-2">
<div className="h5 text-center">Subject {showRole(vps, "subject")}</div>
<NPPicker
@ -223,9 +152,8 @@ export function VPExplorer(props: {
}}
np={vps.subject}
counterPart={vps.verb ? vps.verb.object : undefined}
onChange={quizLock(handleSubjectChange)}
onChange={handleSubjectChange}
opts={props.opts}
cantClear={mode === "quiz"}
/>
</div>
{vps.verb && (vps.verb.object !== "none") && <div className="my-2">
@ -245,9 +173,8 @@ export function VPExplorer(props: {
asObject
np={vps.verb.object}
counterPart={vps.subject}
onChange={quizLock(handleObjectChange)}
onChange={handleObjectChange}
opts={props.opts}
cantClear={mode === "quiz"}
/>}
</div>}
</>}
@ -256,40 +183,12 @@ export function VPExplorer(props: {
vps={vps}
onChange={quizLock(setVps)}
mode={mode}
locked={!!(mode === "quiz" && quizState)}
/>
</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>}
{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>
}
@ -313,138 +212,3 @@ function showRole(VP: T.VPSelection, member: "subject" | "object") {
</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,
),
};
}

View File

@ -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;

View File

@ -154,3 +154,16 @@ export function removeDuplicates(psv: T.PsString[]): T.PsString[] {
))
));
}
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,
}
};
}